Skip to content

Commit

Permalink
Add support for coinbase inputs on block reward transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
samholmes committed Nov 5, 2024
1 parent d17a38e commit b166a70
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- fixed: Improper handling of WebSocket message processing errors, causing sync-halting.
- fixed: Add support for processing coinbase inputs on block reward transactions.

## 3.4.3 (2024-10-15)

Expand Down
47 changes: 32 additions & 15 deletions src/common/utxobased/engine/UtxoEngineProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1257,43 +1257,60 @@ const processTransactionResponse = (
args: { txResponse: TransactionResponse }
): TransactionData => {
const { txResponse } = args
const inputs = txResponse.vin.map(vin => {

let outputTotalValue = '0'
const outputs: TransactionData['outputs'] = txResponse.vout.map(vout => {
const scriptPubkey =
// Note: Blockbook has empirically not sent a hex value as the
// scriptPubkey for vins. If we discover this to be changed for some
// cases, we may want to use the `hex` field as an optimization.
vout.hex ??
asMaybe(
raw => validScriptPubkeyFromAddress(raw),
'unknown'
)({
address: vin.addresses[0],
address: vout.addresses[0],
coin: common.pluginInfo.coinInfo.name
})
outputTotalValue = add(outputTotalValue, vout.value)
return {
txId: vin.txid,
outputIndex: vin.vout,
n: vin.n,
n: vout.n,
scriptPubkey,
sequence: vin.sequence,
amount: vin.value
amount: vout.value
}
})
const outputs = txResponse.vout.map(vout => {

const inputs: TransactionData['inputs'] = txResponse.vin.map(vin => {
// Handle coinbase input
if (!vin.isAddress) {
return {
amount: outputTotalValue,
n: 0,
outputIndex: 4294967295,
scriptPubkey: '', // Maybe there's a bogus scriptPubkey we can use here?
sequence: vin.sequence,
txId: '0000000000000000000000000000000000000000000000000000000000000000'
}
}

const scriptPubkey =
vout.hex ??
// Note: Blockbook has empirically not sent a hex value as the
// scriptPubkey for vins. If we discover this to be changed for some
// cases, we may want to use the `hex` field as an optimization.
asMaybe(
raw => validScriptPubkeyFromAddress(raw),
'unknown'
)({
address: vout.addresses[0],
address: vin.addresses[0],
coin: common.pluginInfo.coinInfo.name
})
return {
n: vout.n,
txId: vin.txid,
outputIndex: vin.vout,
n: vin.n,
scriptPubkey,
amount: vout.value
sequence: vin.sequence,
amount: vin.value
}
})

return {
txid: txResponse.txid,
hex: txResponse.hex,
Expand Down
76 changes: 50 additions & 26 deletions src/common/utxobased/network/blockbookApi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
asArray,
asBoolean,
asEither,
asMaybe,
asNumber,
asObject,
asOptional,
asString,
asUnknown,
asValue,
Cleaner,
uncleaner
} from 'cleaners'
Expand Down Expand Up @@ -57,16 +59,24 @@ export interface BlockbookTransaction {
confirmations: number
blockTime: number
fees: string
vin: Array<{
txid: string
sequence: number
n: number
vout: number
addresses: string[]
isAddress: boolean
value: string
hex?: string
}>
vin: Array<
| {
addresses: string[]
hex?: string
isAddress: true
n: number
sequence: number
txid: string
value: string
vout: number
}
| {
coinbase: string
isAddress: false
n: number
sequence: number
}
>
vout: Array<{
n: number
value: string
Expand All @@ -85,22 +95,36 @@ export const asBlockbookTransaction = (
blockTime: asNumber,
fees: asString,
vin: asArray(
asObject({
txid: asString,
// Empirically observed omitted sequence is possible for when sequence is zero.
// Is the case for tx `19ecc679cfc7e71ad616a22bbee96fd5abe8616e4f408f1f5daaf137400ae091`.
sequence: asOptional(asNumber, 0),
n: asNumber,
// If Blockbook doesn't provide vout, assume 0. Empirically observed
// case for tx `fefac8c22ba1178df5d7c90b78cc1c203d1a9f5f5506f7b8f6f469fa821c2674`
// which has no `vout` for input in WebSocket response payload but block
// will show the input's vout value to be `0`.
vout: asOptional(asNumber, 0),
addresses: asArray(asAddress),
isAddress: asBoolean,
value: asString,
hex: asOptional(asString)
})
asEither(
// Address input:
asObject({
addresses: asArray(asAddress),
// `isAddress` is a boolean flag that indicates whether the input is an address.
// And therefore has `addresses` field. If `isAddress` is false, then the input is likely a coinbase input.
isAddress: asValue(true),
// This is the index of the input. Not to be confused with the index of the previous output (vout).
n: asNumber,
// Empirically observed omitted sequence is possible for when sequence is zero.
// Is the case for tx `19ecc679cfc7e71ad616a22bbee96fd5abe8616e4f408f1f5daaf137400ae091`.
sequence: asOptional(asNumber, 0),
txid: asString,
value: asString,
// If Blockbook doesn't provide vout, assume 0. Empirically observed
// case for tx `fefac8c22ba1178df5d7c90b78cc1c203d1a9f5f5506f7b8f6f469fa821c2674`
// which has no `vout` for input in WebSocket response payload but block
// will show the input's vout value to be `0`.
vout: asOptional(asNumber, 0),
hex: asOptional(asString)
}),
// Coinbase input:
asObject({
// Coinbase input is a string of hex data (see example c6e617656b7b6d9fdcf8800fb5370479e5aceea4b6fe2fd74bd7bb0f3f2c64db)
coinbase: asString,
isAddress: asValue(false),
n: asNumber,
sequence: asNumber
})
)
),
vout: asArray(
asObject({
Expand Down
28 changes: 25 additions & 3 deletions test/common/utxobased/network/Blockbook.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,16 @@ describe('Blockbook', function () {
})

describe('fetchTransaction', function () {
const satoshiHash =
'3ed86f1b0a0a6fe180195bc1f93fd9d0801aea8c8ad5018de82c026dc21e2b15'
it('should fetch details from a transaction hash', async function () {
const satoshiHash =
'3ed86f1b0a0a6fe180195bc1f93fd9d0801aea8c8ad5018de82c026dc21e2b15'
const tx = await blockbook.fetchTransaction(satoshiHash)

tx.txid.should.equal(satoshiHash)
tx.fees.should.equal('226')
tx.blockHeight.should.equal(651329)
tx.vin[0].isAddress.should.equal(true)
if (!tx.vin[0].isAddress) throw new Error('Type assertion failed')
tx.vin[0].value.should.equal('97373')
tx.vin[0].txid.should.equal(
'fac5994d454817db2daec796cfa79cce670a372e7505fdef2a259289d5df0814'
Expand All @@ -289,12 +291,32 @@ describe('Blockbook', function () {
tx.vin[0].addresses.should.eqls([
'bc1qg6lwu6c8yqhhw7rrq69akknepxcft09agkkuqv'
])
tx.vin[0].isAddress.should.equal(true)
tx.vin[0].value.should.equal('97373')

tx.vout[1].value.should.equal('95000')
tx.should.have.property('confirmations')
tx.should.have.property('blockTime')
})

it('should fetch details from a coinbase transaction hash', async function () {
const blockRewardTxid =
'c6e617656b7b6d9fdcf8800fb5370479e5aceea4b6fe2fd74bd7bb0f3f2c64db'
const tx = await blockbook.fetchTransaction(blockRewardTxid)

tx.txid.should.equal(blockRewardTxid)
tx.fees.should.equal('0')
tx.blockHeight.should.equal(868078)
tx.vin[0].isAddress.should.equal(false)
if (tx.vin[0].isAddress) throw new Error('Type assertion failed')
tx.vin[0].coinbase.should.equal(
'03ee3e0d1e3c204f4345414e2e58595a203e0f456c656b74726f6e20456e657267790007155cb4e4ff33420eb387b0ccf1ee1567020000000000'
)
expect(tx.vin[0].sequence).to.equal(4294967295)
tx.vin[0].n.should.equal(0)

tx.vout[1].value.should.equal('135549329')
tx.should.have.property('confirmations')
tx.should.have.property('blockTime')
})
})
})

0 comments on commit b166a70

Please sign in to comment.