@bsv/sdk
Version:
BSV Blockchain Software Development Kit
1,124 lines (1,056 loc) • 60.3 kB
text/typescript
import BigNumber from '../../primitives/BigNumber'
import TransactionSignature from '../../primitives/TransactionSignature'
import { toHex, toArray, Writer } from '../../primitives/utils'
import Script from '../../script/Script'
import UnlockingScript from '../../script/UnlockingScript'
import LockingScript from '../../script/LockingScript'
import Transaction from '../../transaction/Transaction'
import { hash256, hash160 } from '../../primitives/Hash'
import PrivateKey from '../../primitives/PrivateKey'
import Curve from '../../primitives/Curve'
import P2PKH from '../../script/templates/P2PKH'
import fromUtxo from '../../compat/Utxo'
import MerklePath from '../../transaction/MerklePath'
import { BEEF_V1 } from '../../transaction/Beef'
import SatoshisPerKilobyte from '../../transaction/fee-models/SatoshisPerKilobyte'
import sighashVectors from '../../primitives/__tests/sighash.vectors'
import invalidTransactions from './tx.invalid.vectors'
import validTransactions from './tx.valid.vectors'
import bigTX from './bigtx.vectors'
import { BroadcastResponse } from '../../transaction/Broadcaster'
const BRC62Hex =
'0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
const MerkleRootFromBEEF =
'bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00'
const testPrivateKey = new PrivateKey(11)
const testP2PKHScript = new P2PKH().lock(testPrivateKey.toPublicKey().toHash())
describe('Transaction', () => {
const txhex =
'000000000100000000000000000000000000000000000000000000000000000000000000000000000001ae0000000001050000000000000001ae00000000'
const txbuf = toArray(txhex, 'hex')
const tx2idhex =
'8c9aa966d35bfeaf031409e0001b90ccdafd8d859799eb945a3c515b8260bcf2'
const tx2hex =
'01000000029e8d016a7b0dc49a325922d05da1f916d1e4d4f0cb840c9727f3d22ce8d1363f000000008c493046022100e9318720bee5425378b4763b0427158b1051eec8b08442ce3fbfbf7b30202a44022100d4172239ebd701dae2fbaaccd9f038e7ca166707333427e3fb2a2865b19a7f27014104510c67f46d2cbb29476d1f0b794be4cb549ea59ab9cc1e731969a7bf5be95f7ad5e7f904e5ccf50a9dc1714df00fbeb794aa27aaff33260c1032d931a75c56f2ffffffffa3195e7a1ab665473ff717814f6881485dc8759bebe97e31c301ffe7933a656f020000008b48304502201c282f35f3e02a1f32d2089265ad4b561f07ea3c288169dedcf2f785e6065efa022100e8db18aadacb382eed13ee04708f00ba0a9c40e3b21cf91da8859d0f7d99e0c50141042b409e1ebbb43875be5edde9c452c82c01e3903d38fa4fd89f3887a52cb8aea9dc8aec7e2c9d5b3609c03eb16259a2537135a1bf0f9c5fbbcbdbaf83ba402442ffffffff02206b1000000000001976a91420bb5c3bfaef0231dc05190e7f1c8e22e098991e88acf0ca0100000000001976a9149e3e2d23973a04ec1b02be97c30ab9f2f27c3b2c88ac00000000'
const tx2buf = toArray(tx2hex, 'hex')
it('should make a new transaction', () => {
let tx = new Transaction()
expect(tx).toBeDefined()
tx = new Transaction()
expect(tx).toBeDefined()
expect(Transaction.fromBinary(txbuf).toHex()).toEqual(txhex)
// should set known defaults
expect(tx.version).toEqual(1)
expect(tx.inputs.length).toEqual(0)
expect(tx.outputs.length).toEqual(0)
expect(tx.lockTime).toEqual(0)
})
describe('#constructor', () => {
it('should set these known defaults', () => {
const tx = new Transaction()
expect(tx.version).toEqual(1)
expect(tx.inputs.length).toEqual(0)
expect(tx.outputs.length).toEqual(0)
expect(tx.lockTime).toEqual(0)
})
})
describe('#fromHex', () => {
it('should recover from this known tx', () => {
expect(Transaction.fromHex(txhex).toHex()).toEqual(txhex)
})
it('should recover from this known tx from the blockchain', () => {
expect(Transaction.fromHex(tx2hex).toHex()).toEqual(tx2hex)
})
})
describe('#fromBinary', () => {
it('should recover from this known tx', () => {
expect(toHex(Transaction.fromBinary(txbuf).toBinary())).toEqual(txhex)
})
it('should recover from this known tx from the blockchain', () => {
expect(toHex(Transaction.fromBinary(tx2buf).toBinary())).toEqual(tx2hex)
})
})
describe('#parseScriptOffsets', () => {
it('should match sliced scripts to parsed scripts', async () => {
const tx = Transaction.fromBinary(tx2buf)
expect(tx.id('hex')).toBe(tx2idhex)
const r = Transaction.parseScriptOffsets(tx2buf)
expect(r.inputs.length).toBe(2)
expect(r.outputs.length).toBe(2)
for (let vin = 0; vin < 2; vin++) {
const i = r.inputs[vin]
const script = tx2buf.slice(i.offset, i.length + i.offset)
expect(script).toEqual(tx.inputs[vin].unlockingScript?.toBinary())
}
for (let vout = 0; vout < 2; vout++) {
const o = r.outputs[vout]
const script = tx2buf.slice(o.offset, o.length + o.offset)
expect(script).toEqual(tx.outputs[vout].lockingScript?.toBinary())
}
})
})
describe('#toHex', () => {
it('should produce this known tx', () => {
expect(Transaction.fromHex(txhex).toHex()).toEqual(txhex)
})
})
describe('#toBinary', () => {
it('should produce this known tx', () => {
expect(toHex(Transaction.fromBinary(txbuf).toBinary())).toEqual(txhex)
})
})
describe('#hash', () => {
it('should correctly calculate the hash of this known transaction', () => {
const tx = Transaction.fromBinary(tx2buf)
const hash = tx.hash()
const reversedHash = Array.isArray(hash)
? hash.reverse()
: toArray(hash, 'hex').reverse()
expect(toHex(reversedHash)).toEqual(tx2idhex)
})
})
describe('#id', () => {
it('should correctly calculate the txid of this known transaction', () => {
const tx = Transaction.fromBinary(tx2buf)
expect(tx.id('hex')).toEqual(tx2idhex)
})
})
describe('#addInput', () => {
it('should add an input', () => {
const txIn = {
sourceTXID:
'0000000000000000000000000000000000000000000000000000000000000000',
sourceOutputIndex: 0,
unlockingScript: new UnlockingScript(),
sequence: 0xffffffff
}
const tx = new Transaction()
expect(tx.inputs.length).toEqual(0)
tx.addInput(txIn)
expect(tx.inputs.length).toEqual(1)
})
})
describe('#addOutput', () => {
it('should add an output', () => {
const txOut = {
lockingScript: new LockingScript(),
satoshis: 1
}
const tx = new Transaction()
expect(tx.outputs.length).toEqual(0)
tx.addOutput(txOut)
expect(tx.outputs.length).toEqual(1)
})
})
describe('Signing', () => {
it('Signs unlocking script templates, hydrating the scripts', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.inputs[0].unlockingScript).not.toBeDefined()
await spendTx.fee()
await spendTx.sign()
expect(spendTx.inputs[0].unlockingScript).toBeDefined()
// P2PKH unlocking scripts have two chunks (the signature and public key)
expect(spendTx.inputs[0].unlockingScript?.chunks.length).toBe(2)
})
it('Signs a large number of unlocking script templates in a timely manner', async () => {
const privateKey = new PrivateKey(134)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const spendCount = 30
const output = {
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
const manyOutputs = [output]
for (let i = 1; i < spendCount; i++) {
manyOutputs[i] = {
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
}
const sourceTx = new Transaction(1, [], manyOutputs, 0)
const input = {
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
const manyInputs = [input]
for (let i = 1; i < spendCount; i++) {
manyInputs[i] = {
sourceTransaction: sourceTx,
sourceOutputIndex: i,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
}
const spendTx = new Transaction(
1,
manyInputs,
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.inputs[0].unlockingScript).not.toBeDefined()
await spendTx.fee()
await spendTx.sign()
expect(spendTx.inputs[0].unlockingScript).toBeDefined()
// P2PKH unlocking scripts have two chunks (the signature and public key)
expect(spendTx.inputs[0].unlockingScript?.chunks.length).toBe(2)
})
it('Throws an Error if signing before the fee is computed', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(
publicKey.encode(true),
'hex'
) as unknown as number[]
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
await expect(spendTx.sign()).rejects.toThrow()
})
})
describe('Fees', () => {
it('Computes fees with the default fee model', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
await spendTx.fee()
// Transaction size is 225 bytes for one-input two-output P2PKH.
// Default fee rate is 1 sat/kb = 0.225 sats (round up to 1).
// 4000 sats in - 1000 sats out - 3 sats fee = expected 2999 sats change.
expect(spendTx.outputs[1].satoshis).toEqual(2999)
})
it('Computes fees with a custom fee model', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
await spendTx.fee({
// Our custom fee model will always charge 1033 sats for a tx.
computeFee: async () => 1033
})
// 4000 sats in - 1000 sats out - 1033 sats fee = expected 1967 sats change
expect(spendTx.outputs[1].satoshis).toEqual(1967)
})
it('Computes fee using FixedFee model', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
await spendTx.fee(1033)
// 4000 sats in - 1000 sats out - 1033 sats fee = expected 1967 sats change
expect(spendTx.outputs[1].satoshis).toEqual(1967)
})
it('Distributes change equally among multiple change outputs', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 4000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1000,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
expect(spendTx.outputs[2].satoshis).not.toBeDefined()
expect(spendTx.outputs[3].satoshis).not.toBeDefined()
expect(spendTx.outputs[4].satoshis).not.toBeDefined()
await spendTx.fee({
// Our custom fee model will always charge 1033 sats for a tx.
computeFee: async () => 1032
})
// 4000 sats in - 1000 sats out - 1033 sats fee = expected 1967 sats change
// Divide by 2 (no remainder) = 983 sats per change output
expect(spendTx.outputs[0].satoshis).toEqual(1000)
expect(spendTx.outputs[1].satoshis).toEqual(492)
expect(spendTx.outputs[2].satoshis).toEqual(492)
expect(spendTx.outputs[3].satoshis).toEqual(492)
expect(spendTx.outputs[4].satoshis).toEqual(492)
})
it('Distributes change randomly among multiple change outputs', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 900
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
expect(spendTx.outputs[2].satoshis).not.toBeDefined()
expect(spendTx.outputs[3].satoshis).not.toBeDefined()
expect(spendTx.outputs[4].satoshis).not.toBeDefined()
expect(spendTx.outputs[5].satoshis).not.toBeDefined()
expect(spendTx.outputs[6].satoshis).not.toBeDefined()
await spendTx.fee(
{
computeFee: async () => 3
},
'random'
)
expect(spendTx.outputs[0].satoshis).toEqual(1)
expect(spendTx.outputs.reduce((a, b) => a + (b.satoshis ?? 0), 0)).toEqual(897)
})
it('Distributes change randomly among multiple change outputs, with one set output', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 9
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 1,
lockingScript: p2pkh.lock(publicKeyHash)
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
},
{
lockingScript: p2pkh.lock(publicKeyHash),
change: true
}
],
0
)
expect(spendTx.outputs[1].satoshis).not.toBeDefined()
expect(spendTx.outputs[2].satoshis).not.toBeDefined()
expect(spendTx.outputs[3].satoshis).not.toBeDefined()
expect(spendTx.outputs[4].satoshis).not.toBeDefined()
expect(spendTx.outputs[5].satoshis).not.toBeDefined()
expect(spendTx.outputs[6].satoshis).not.toBeDefined()
await spendTx.fee(
{
computeFee: async () => 1
},
'random'
)
expect(spendTx.outputs.reduce((a, b) => a + (b.satoshis ?? 0), 0)).toEqual(8)
})
it('Distributes change randomly among multiple change outputs, thinnly spread', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 46
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
Array(21)
.fill(null)
.map(() => ({
lockingScript: p2pkh.lock(publicKeyHash),
change: true
})),
0
)
await spendTx.fee(
{
computeFee: async () => 1
},
'random'
)
expect(spendTx.outputs.reduce((a, b) => a + (b.satoshis ?? 0), 0)).toEqual(45)
})
it('Calculates fee for utxo based transaction', async () => {
const utxos = [
// WoC format utxos
{
height: 1600000,
tx_pos: 0,
tx_hash:
'672dd6a93fa5d7ba6794e0bdf8b479440b95a55ec10ad3d9e03585ecb5628d8d',
value: 10000
},
{
height: 1600000,
tx_pos: 0,
tx_hash:
'f33505acf37a7726cc37d391bc6f889b8684ac2a2d581c4be2a4b1c8b46609bc',
value: 10000
}
]
const tx = new Transaction()
utxos.forEach(utxo => {
const u = {
txid: utxo.tx_hash,
vout: utxo.tx_pos,
script: testP2PKHScript.toHex(),
satoshis: utxo.value
}
tx.addInput(fromUtxo(u, new P2PKH().unlock(testPrivateKey)))
})
tx.addOutput({
lockingScript: testP2PKHScript,
change: true
})
await tx.fee({ computeFee: async () => 10 })
expect(tx.outputs[0].satoshis).toEqual(20000 - 10)
expect(tx.getFee()).toEqual(10)
})
})
describe('Broadcast', () => {
it('Broadcasts with the default Broadcaster instance', async () => {
const mockedFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
headers: {
get(key: string) {
if (key === 'Content-Type') {
return 'application/json'
}
}
},
json: async () => ({
txid: 'mocked_txid',
txStatus: 'success',
extraInfo: 'received'
})
})
; (global as any).window = { fetch: mockedFetch } as any
const tx = new Transaction()
const rv = await tx.broadcast()
expect(mockedFetch).toHaveBeenCalled()
const url = (mockedFetch as jest.Mock).mock.calls[0][0] as string
expect(url).toEqual('https://arc.gorillapool.io/v1/tx')
expect(rv).toEqual({
status: 'success',
txid: 'mocked_txid',
message: 'success received'
})
})
it('Broadcasts with the provided Broadcaster instance', async () => {
const mockBroadcast = jest.fn(async (): Promise<BroadcastResponse> => {
return {
status: 'success', // Explicitly matches the literal type "success"
txid: 'mock_txid',
message: 'Transaction successfully broadcasted.'
}
})
const tx = new Transaction()
const rv = await tx.broadcast({
broadcast: mockBroadcast
})
// Ensure the mock function was called with the correct argument
expect(mockBroadcast).toHaveBeenCalledWith(tx)
// Verify the return value matches the mocked response
expect(rv).toEqual({
status: 'success',
txid: 'mock_txid',
message: 'Transaction successfully broadcasted.'
})
})
describe('BEEF', () => {
it('Serialization and deserialization', async () => {
const tx = Transaction.fromBEEF(toArray(BRC62Hex, 'hex'))
expect(tx.inputs[0].sourceTransaction?.merklePath?.blockHeight).toEqual(
814435
)
const beef = toHex(tx.toBEEF())
expect(beef).toEqual(BRC62Hex)
})
it('Does not double-encode transactions', () => {
// Source: https://github.com/bsv-blockchain/ts-sdk/issues/77
const incorrect =
'0100beef01fe76eb0c000e02c402deff5437203e0b5cb22646cbada24a60349bf45c8b280ffb755868f2955c3111c500f4076b7f48031fc467f87d5e99d9c3c0b59e4dca5e3049f58b735c59b413a8b6016300bad9c2d948e8a2ca647fdb50f2fd36641c4adf937b41134405a3e7f734b8beb201300053604a579558b5f7030e618d5c726a19229e0ff677f6edf109f41c5cfdafc93e0119005f8465c2a8d1558afbfa80c2395f3f8866a2fa5015e54fab778b0149da58376c010d00cd452b4e74f57d199cdb81b8a0e4a62dcdaf89504d6c63a5a65d5b866912b8c0010700d2ae7e2ce76da560509172066f1a1cf81faf81d73f9c0f6fd5af0904973dcfb10102006e5e077bcaa35c0240d61c1f3bba8d789223711ec035ef88b0911fc569d2b95a010000c961038959b9d404297a180c066816562dd2a34986c0960121a87ba91a51262f010100a50e381b4e8812479ea561e5bab7dcaa80078652b1b39ee5410966c515a3442b010100383ce8891ca7bf1ddefa5e0d8a1ba9ab01cb4e18046e9d7d0d438b5aaecc38b2010100c694be322b4e74acca8a5ef7703afedb708281321fd674f1221eebc743b0e01c0101000f3cc61f2b3d762cfecdd977ba768a5cbb0a4b402ad4f0d1bd3a98a582794c35010100094ad56eeea3b47edb2b298775f2efabe48172612cb3419962632251d8cdb78e010100de84bf9dd8873f37decbda1b5188e24ead978b147a63c809691702d19c47e8cb050200000001b67f1b6a6c3e70742a39b82ba45d51c983f598963ebf237101cc372da1144b83020000006b483045022100d14c3eb0c1438747c124f099bc664bf945cd27cbd96915027057e508bbc9e03302203c73f79d4e00f8018783e1008ce0fbb8e8c58bff6bd8042ab7e3966a66c8788c41210356762156ee9347c3e2ceca17d13713028d5446f453e9cbcb0ea170b4ca7fab52ffffffff037c660000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac75330100000000001976a914eb645f9ea7e4e232e54b9651474c100026698c3088acf2458005000000001976a914802737e30c85b6fe86e26fb28e03140058aca65e88ac0000000001000100000001deff5437203e0b5cb22646cbada24a60349bf45c8b280ffb755868f2955c3111000000006a473044022076da9f61380c208f43652587c219b4452a7b803a0407c2c7c0f3bc27612c4e88022021a9eb02da5529873a5986933f9c35965aa78537b9e2aef9382de33cfb1ab4bb41210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff023c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac3c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac00000000000200000001b67f1b6a6c3e70742a39b82ba45d51c983f598963ebf237101cc372da1144b83020000006b483045022100d14c3eb0c1438747c124f099bc664bf945cd27cbd96915027057e508bbc9e03302203c73f79d4e00f8018783e1008ce0fbb8e8c58bff6bd8042ab7e3966a66c8788c41210356762156ee9347c3e2ceca17d13713028d5446f453e9cbcb0ea170b4ca7fab52ffffffff037c660000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac75330100000000001976a914eb645f9ea7e4e232e54b9651474c100026698c3088acf2458005000000001976a914802737e30c85b6fe86e26fb28e03140058aca65e88ac0000000001000100000001deff5437203e0b5cb22646cbada24a60349bf45c8b280ffb755868f2955c3111000000006a473044022076da9f61380c208f43652587c219b4452a7b803a0407c2c7c0f3bc27612c4e88022021a9eb02da5529873a5986933f9c35965aa78537b9e2aef9382de33cfb1ab4bb41210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff023c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac3c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac000000000001000000022e7f69f3e1e17e22cfb8818577b3c83a4fbbbc1bab55c70ffcdd994ae30ea48b000000006b483045022100d9a2d1efea4896b36b2eb5af42cf52009982c7c31b446213fe37f26835d9d72202203e4dee0ceb068a4936e79b0bf69f72203906a00a4256cb1a7b30a40764616e8441210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff2e7f69f3e1e17e22cfb8818577b3c83a4fbbbc1bab55c70ffcdd994ae30ea48b010000006b483045022100b57a09145c57b7b5efb4b546f1b0bfb7adbc5e64d35d9d6989345d4c60c483940220280998a210a49a6efaacda6fb73670001bb7269d069be80eb14ea2227a73e82241210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff0174660000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac0000000000'
const correct =
'0100beef01fe76eb0c000e02c402deff5437203e0b5cb22646cbada24a60349bf45c8b280ffb755868f2955c3111c500f4076b7f48031fc467f87d5e99d9c3c0b59e4dca5e3049f58b735c59b413a8b6016300bad9c2d948e8a2ca647fdb50f2fd36641c4adf937b41134405a3e7f734b8beb201300053604a579558b5f7030e618d5c726a19229e0ff677f6edf109f41c5cfdafc93e0119005f8465c2a8d1558afbfa80c2395f3f8866a2fa5015e54fab778b0149da58376c010d00cd452b4e74f57d199cdb81b8a0e4a62dcdaf89504d6c63a5a65d5b866912b8c0010700d2ae7e2ce76da560509172066f1a1cf81faf81d73f9c0f6fd5af0904973dcfb10102006e5e077bcaa35c0240d61c1f3bba8d789223711ec035ef88b0911fc569d2b95a010000c961038959b9d404297a180c066816562dd2a34986c0960121a87ba91a51262f010100a50e381b4e8812479ea561e5bab7dcaa80078652b1b39ee5410966c515a3442b010100383ce8891ca7bf1ddefa5e0d8a1ba9ab01cb4e18046e9d7d0d438b5aaecc38b2010100c694be322b4e74acca8a5ef7703afedb708281321fd674f1221eebc743b0e01c0101000f3cc61f2b3d762cfecdd977ba768a5cbb0a4b402ad4f0d1bd3a98a582794c35010100094ad56eeea3b47edb2b298775f2efabe48172612cb3419962632251d8cdb78e010100de84bf9dd8873f37decbda1b5188e24ead978b147a63c809691702d19c47e8cb030200000001b67f1b6a6c3e70742a39b82ba45d51c983f598963ebf237101cc372da1144b83020000006b483045022100d14c3eb0c1438747c124f099bc664bf945cd27cbd96915027057e508bbc9e03302203c73f79d4e00f8018783e1008ce0fbb8e8c58bff6bd8042ab7e3966a66c8788c41210356762156ee9347c3e2ceca17d13713028d5446f453e9cbcb0ea170b4ca7fab52ffffffff037c660000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac75330100000000001976a914eb645f9ea7e4e232e54b9651474c100026698c3088acf2458005000000001976a914802737e30c85b6fe86e26fb28e03140058aca65e88ac0000000001000100000001deff5437203e0b5cb22646cbada24a60349bf45c8b280ffb755868f2955c3111000000006a473044022076da9f61380c208f43652587c219b4452a7b803a0407c2c7c0f3bc27612c4e88022021a9eb02da5529873a5986933f9c35965aa78537b9e2aef9382de33cfb1ab4bb41210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff023c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac3c330000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac000000000001000000022e7f69f3e1e17e22cfb8818577b3c83a4fbbbc1bab55c70ffcdd994ae30ea48b000000006b483045022100d9a2d1efea4896b36b2eb5af42cf52009982c7c31b446213fe37f26835d9d72202203e4dee0ceb068a4936e79b0bf69f72203906a00a4256cb1a7b30a40764616e8441210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff2e7f69f3e1e17e22cfb8818577b3c83a4fbbbc1bab55c70ffcdd994ae30ea48b010000006b483045022100b57a09145c57b7b5efb4b546f1b0bfb7adbc5e64d35d9d6989345d4c60c483940220280998a210a49a6efaacda6fb73670001bb7269d069be80eb14ea2227a73e82241210314793e1758db3caa7d2bce97b347ae3ced2f8a402b797ed986be63473d4644a0ffffffff0174660000000000001976a91417c85798ff61f7ec8af257f672d973b6ec6d88fd88ac0000000000'
const tx1 = Transaction.fromHexBEEF(incorrect)
expect(tx1.toHexBEEF()).toEqual(correct)
})
})
describe('EF', () => {
it('Serialization and deserialization', async () => {
const tx = Transaction.fromBEEF(toArray(BRC62Hex, 'hex'))
const ef = toHex(tx.toEF())
expect(ef).toEqual(
'010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000'
)
})
})
describe('Verification', () => {
it('Verifies the transaction from the BEEF spec', async () => {
const tx = Transaction.fromHexBEEF(BRC62Hex)
const alwaysYesChainTracker = {
currentHeight: async () => 1631619, // Mocked current height
isValidRootForHeight: async () => true // Always returns true
}
const verified = await tx.verify(alwaysYesChainTracker)
expect(verified).toBe(true)
})
})
it('Verifies the transaction from the BEEF spec with a default chain tracker', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
headers: {
get(key: string) {
if (key === 'Content-Type') {
return 'application/json'
}
}
},
json: async () => ({
merkleroot: MerkleRootFromBEEF
})
})
; (global as any).window = { fetch: mockFetch }
const tx = Transaction.fromHexBEEF(BRC62Hex)
const verified = await tx.verify()
expect(mockFetch).toHaveBeenCalled()
expect(verified).toBe(true)
})
it('Verifies the transaction from the BEEF spec with a scripts only', async () => {
const BEEF =
'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'
const tx = Transaction.fromBEEF(toArray(BEEF, 'base64'))
// Verifies transaction with scripts only
const verified = await tx.verify('scripts only')
expect(verified).toBe(true)
})
it('Verifies tx scripts only when our input has no MerklePath.', async () => {
const sourceTransaction = Transaction.fromHex(
'01000000013834506e4af751dc3a0d73e9bdd067709ade5248860967fb3eb72854be6011dc020000006b4830450221008abf2ce1afbcc3c4895265b0966ac69e2275fb35a738f9873b5416cd3bb68cc102202c906f9771d2d6868f81a34d6a3af4cd7de070969286db7097c5e0f3a009b638412102f6a81c442dcd903c7808a75abde9217268bb70417ad538c18fb597b4c553c354ffffffff0301000000000000007c21039bf52152665699e24c1018bf5b48e575de316f844962a79db32b93b7e328bf71ac1062656e63686d61726b546f6b656e5f36463044022034ca3cc49163e4d5bc24f41c5ea4631efd4ae7c243f904c90e1db6e6a55e8f0e02206bb031baca2fc6d64da5ad722ce2e07137718e5e58092e5ecf54e52fcdd664ab6dc8000000000000001976a91470763a553d615a8144fa0889a0e1f93d1d8da8b088acaf430000000000001976a9145aa12f9be0121eee54e7331d8f995fe4dded72f588ac00000000'
)
const tx = Transaction.fromHex(
'01000000015c22e8d34a95a761a0685b8b5a99a0845e9f03d87d5716a7351d08939db5cd92020000006a47304402205101f2bdc40098cbde1a270071a5bfdd5a6fbefc8a38a8b4a1adaca0a6ff55e702200ec5c0a57cb65dfc6aa22e6856bb4c62b8987a7b9fa32cb601c94b579ba67d83412103c8be6b217fc55939851e28239fbbf731f997e026ad344aa9d4b739181fe96743ffffffff0301000000000000007c21039bf52152665699e24c1018bf5b48e575de316f844962a79db32b93b7e328bf71ac1062656e63686d61726b546f6b656e5f374630440220026808e9453f75ca1fc3c7bb0f7be1efaca5cea6b1eda96ab7db56c519b6793202207d1fa5e7d54254d2142256947f93a061781280a6ed2464052470633a7c888d1f6dc8000000000000001976a91428bb6f207ac8f4b7f6c1c5914e143a6165586b2488ace5420000000000001976a914197cbde77abbcb985ca60a85c2988b2ac32c541a88ac00000000'
)
// Create a mock MerklePath
const mockMerklePath = new MerklePath(0, [
[{ offset: 0, hash: 'dummyHash' }]
])
sourceTransaction.merklePath = mockMerklePath
tx.inputs[0].sourceTransaction = sourceTransaction
const verified = await tx.verify('scripts only', undefined)
expect(verified).toBe(true)
})
describe('vectors: a 1mb transaction', () => {
it('should find the correct id of this (valid, on the blockchain) 1 mb transaction', () => {
const txidhex = bigTX.txidhex
const txhex = bigTX.txhex
const tx = Transaction.fromHex(txhex)
const txid = tx.id('hex')
expect(txid).toEqual(txidhex)
})
})
describe('vectors: sighash and serialization', () => {
sighashVectors.forEach((vector, i) => {
if (i === 0) {
return
}
it(`should pass bitcoin-abc sighash test vector ${i}`, () => {
const txbuf = toArray(vector[0], 'hex')
const scriptbuf = toArray(vector[1], 'hex')
const subScript = Script.fromBinary(scriptbuf)
const nIn = vector[2] as number
const nHashType = vector[3] as number
const sighashBuf = toArray(vector[4], 'hex')
const tx = Transaction.fromBinary(txbuf)
// make sure transacion to/from buffer is isomorphic
expect(toHex(tx.toBinary())).toEqual(toHex(txbuf))
// sighash ought to be correct
const valueBn = new BigNumber(0).toNumber()
const otherInputs = [...tx.inputs]
const [input] = otherInputs.splice(nIn, 1)
const preimage = TransactionSignature.format({
sourceTXID: input.sourceTXID ?? '',
sourceOutputIndex: input.sourceOutputIndex,
sourceSatoshis: valueBn,
transactionVersion: tx.version,
otherInputs,
outputs: tx.outputs,
inputIndex: nIn,
subscript: subScript,
inputSequence: input.sequence ?? 0xffffffff,
lockTime: tx.lockTime,
scope: nHashType
})
const hash = hash256(preimage)
hash.reverse()
expect(toHex(hash)).toEqual(toHex(sighashBuf))
})
})
validTransactions.forEach((vector, i) => {
if (vector.length === 1) {
return
}
it(`should correctly serialized/deserialize tx_valid test vector ${i}`, () => {
const expectedHex = vector[1]
const expectedBin = toArray(vector[1], 'hex')
const actualTX = Transaction.fromBinary(expectedBin)
const actualBin = actualTX.toBinary()
const actualHex = toHex(actualBin)
expect(actualHex).toEqual(expectedHex)
})
})
invalidTransactions.forEach((vector, i) => {
if (vector.length === 1) {
return
}
// 151, 142 and 25 have invalid Satoshi amounts that exceed 53 bits in length, causing exceptions that make serialization and deserialization impossible.
if (i === 151 || i === 142 || i === 25) {
return
}
it(`should correctly serialized/deserialize tx_invalid test vector ${i}`, () => {
const expectedHex = vector[1]
const expectedBin = toArray(vector[1], 'hex')
const actualTX = Transaction.fromBinary(expectedBin)
const actualBin = actualTX.toBinary()
const actualHex = toHex(actualBin)
expect(actualHex).toEqual(expectedHex)
})
})
})
describe('Atomic BEEF', () => {
it('should serialize a transaction to Atomic BEEF format correctly', async () => {
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true))
const p2pkh = new P2PKH()
// Create a simple transaction
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 10000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 9000,
lockingScript: p2pkh.lock(publicKeyHash)
}
],
0
)
// Sign the transaction
await spendTx.fee()
await spendTx.sign()
// Assign a MerklePath to the source transaction to simulate mined transaction
const sourceTxID = sourceTx.id('hex')
const merklePath = new MerklePath(1000, [
[
{ offset: 0, hash: sourceTxID, txid: true },
{ offset: 1, duplicate: true }
]
])
sourceTx.merklePath = merklePath
// Serialize to Atomic BEEF
const atomicBEEF = spendTx.toAtomicBEEF()
expect(atomicBEEF).toBeDefined()
// Verify that the Atomic BEEF starts with the correct prefix and TXID
const expectedPrefix = [0x01, 0x01, 0x01, 0x01]
expect(atomicBEEF.slice(0, 4)).toEqual(expectedPrefix)
const txid = spendTx.hash()
expect(atomicBEEF.slice(4, 36)).toEqual(txid)
// Deserialize from Atomic BEEF
const deserializedTx = Transaction.fromAtomicBEEF(atomicBEEF)
expect(deserializedTx.toHex()).toEqual(spendTx.toHex())
})
it('should throw an error when deserializing Atomic BEEF if subject transaction is missing', () => {
// Create Atomic BEEF data with missing subject transaction
const writer = new Writer()
// Write Atomic BEEF prefix
writer.writeUInt32LE(0x01010101)
// Write subject TXID
const fakeTXID = toArray('00'.repeat(32), 'hex')
writer.writeReverse(fakeTXID)
// Write empty BEEF data
writer.writeUInt32LE(BEEF_V1) // BEEF version
writer.writeVarIntNum(0) // No BUMPs
writer.writeVarIntNum(0) // No transactions
const atomicBEEFData = writer.toArray()
expect(() => {
Transaction.fromAtomicBEEF(atomicBEEFData)
}).toThrow('beef must include at least one transaction.')
})
it('should allow selecting a specific TXID from BEEF data', async () => {
// Create two transactions, one depending on the other
const privateKey = new PrivateKey(1)
const publicKey = new Curve().g.mul(privateKey)
const publicKeyHash = hash160(publicKey.encode(true)) as number[]
const p2pkh = new P2PKH()
const sourceTx = new Transaction(
1,
[],
[
{
lockingScript: p2pkh.lock(publicKeyHash),
satoshis: 10000
}
],
0
)
const spendTx = new Transaction(
1,
[
{
sourceTransaction: sourceTx,
sourceOutputIndex: 0,
unlockingScriptTemplate: p2pkh.unlock(privateKey),
sequence: 0xffffffff
}
],
[
{
satoshis: 9000,
lockingScript: p2pkh.lock(publicKeyHash)
}
],
0
)
// Sign transactions
await spendTx.fee()
await spendTx.sign()
// Assign merkle path to source transaction
const sourceTxID = sourceTx.id('hex')
sourceTx.merklePath = new MerklePath(1000, [
[
{ offset: 0, hash: sourceTxID, txid: true },
{ offset: 1, duplicate: true }
]
])
// Serialize to BEEF
const beefData = spendTx.toBEEF()
// Get TXIDs
const spendTxID = spendTx.id('hex')
// Deserialize the source transaction from BEEF