diff --git a/src/index.ts b/src/index.ts index 2f55f86..f625bb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,3 +27,16 @@ export { MerklePath } from './merklePath' export { Shift10 } from './shift10' export { ArrayUtils } from './arrayUtils' export { Schnorr } from './schnorr' +export { + TxParser, + Output, + Input, + MAX_TX_INPUT_COUNT, + MAX_TX_OUTPUT_COUNT, +} from './txParser' +export { + TxParserBTC, + MAX_BTC_TX_INPUT_COUNT, + MAX_BTC_TX_OUTPUT_COUNT, + MAX_WITNESS_ITEM_COUNT, +} from './txParserBTC' diff --git a/src/txParser.ts b/src/txParser.ts new file mode 100644 index 0000000..4847d73 --- /dev/null +++ b/src/txParser.ts @@ -0,0 +1,187 @@ +import { + prop, + method, + SmartContractLib, + FixedArray, + assert, + ByteString, + toByteString, + slice, + fill, + Utils, +} from 'scrypt-ts' + +export interface Output { + satoshis: bigint + script: ByteString +} + +export interface Input { + txId: ByteString + vout: bigint + scriptSig: ByteString + nSequence: bigint +} + +export const MAX_TX_INPUT_COUNT = 5 + +export const MAX_TX_OUTPUT_COUNT = 5 + +export class TxParser extends SmartContractLib { + @prop() + buf: ByteString + + @prop() + pos: bigint + + @prop() + inputs: FixedArray + + @prop() + outputs: FixedArray + + @prop() + inputsCount: bigint + + @prop() + outputsCount: bigint + + @prop() + version: bigint + + @prop() + nLockTime: bigint + + constructor(raw: ByteString) { + super(raw) + this.buf = raw + this.pos = 0n + this.inputsCount = 0n + this.outputsCount = 0n + this.version = 0n + this.nLockTime = 0n + + this.inputs = fill( + { + txId: toByteString(''), + vout: 0n, + scriptSig: toByteString(''), + nSequence: 1n, + }, + MAX_TX_INPUT_COUNT + ) + this.outputs = fill( + { satoshis: 0n, script: toByteString('') }, + MAX_TX_OUTPUT_COUNT + ) + } + + @method() + readVarInt(): bigint { + const n: bigint = Utils.fromLEUnsigned( + slice(this.buf, this.pos, this.pos + 1n) + ) + this.pos++ + + let ret = 0n + + if (n < 0xfdn) { + ret = n + } else if (n == 0xfdn) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 2n)) + this.pos += 2n + } else if (n == 0xfen) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 4n)) + this.pos += 4n + } else if (n == 0xffn) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 8n)) + this.pos += 8n + } + + return ret + } + + @method() + readBytes(l: bigint): ByteString { + const ret = slice(this.buf, this.pos, this.pos + l) + this.pos += l + return ret + } + + @method() + readUInt32(): bigint { + return Utils.fromLEUnsigned(this.readBytes(4n)) + } + + @method() + readInput(): Input { + // Previous Transaction hash, 32 bytes + const txid: ByteString = this.readBytes(32n) + // Previous Txout-index, 4 bytes + const vout: bigint = Utils.fromLEUnsigned(this.readBytes(4n)) + // Txin-script length, 1 - 9 bytes + const scriptSigLen: bigint = this.readVarInt() + // Txin-script / scriptSig, -many bytes + const scriptSig: ByteString = + scriptSigLen > 0n ? this.readBytes(scriptSigLen) : toByteString('') + // Sequence_no, 4 bytes + const nSequence: bigint = Utils.fromLEUnsigned(this.readBytes(4n)) + return { + txId: txid, + vout: vout, + scriptSig: scriptSig, + nSequence: nSequence, + } + } + + @method() + readOutput(): Output { + // Satoshi value, 8 bytes + const satoshis: bigint = Utils.fromLEUnsigned(this.readBytes(8n)) + // Txout-script length, 1 - 9 bytes + const scriptLen: bigint = this.readVarInt() + // Txout-script / scriptPubKey, -many bytes + const script: ByteString = this.readBytes(scriptLen) + return { + script: script, + satoshis: satoshis, + } + } + + @method() + parse(): boolean { + this.version = this.readUInt32() + + this.inputsCount = this.readVarInt() + assert(this.inputsCount < MAX_TX_INPUT_COUNT, 'too much inputs') + + for (let i = 0; i < MAX_TX_INPUT_COUNT; i++) { + if (i < this.inputsCount) { + this.inputs[i] = this.readInput() + } + } + + this.outputsCount = this.readVarInt() + assert(this.outputsCount < MAX_TX_OUTPUT_COUNT, 'too much outputs') + + for (let i = 0; i < MAX_TX_OUTPUT_COUNT; i++) { + if (i < this.outputsCount) { + this.outputs[i] = this.readOutput() + } + } + + this.nLockTime = this.readUInt32() + + return true + } + + @method() + getInputs(): FixedArray { + return this.inputs + } + + @method() + getOutputs(): FixedArray { + return this.outputs + } +} diff --git a/src/txParserBTC.ts b/src/txParserBTC.ts new file mode 100644 index 0000000..5c6e474 --- /dev/null +++ b/src/txParserBTC.ts @@ -0,0 +1,235 @@ +import { + prop, + method, + SmartContractLib, + FixedArray, + assert, + ByteString, + toByteString, + slice, + Utils, + fill, +} from 'scrypt-ts' + +import { Output, Input } from './txParser' + +export const MAX_BTC_TX_INPUT_COUNT = 5 + +export const MAX_BTC_TX_OUTPUT_COUNT = 5 + +export const MAX_WITNESS_ITEM_COUNT = 5 + +export class TxParserBTC extends SmartContractLib { + @prop() + buf: ByteString + + @prop() + pos: bigint + + @prop() + inputs: FixedArray + + @prop() + outputs: FixedArray + + @prop() + witness: FixedArray< + FixedArray, + typeof MAX_BTC_TX_INPUT_COUNT + > + + @prop() + inputsCount: bigint + + @prop() + outputsCount: bigint + + @prop() + version: bigint + + @prop() + nLockTime: bigint + + @prop() + isWitness: boolean + + constructor(raw: ByteString) { + super(raw) + this.buf = raw + this.pos = 0n + this.inputsCount = 0n + this.outputsCount = 0n + this.version = 0n + this.nLockTime = 0n + this.isWitness = false + this.inputs = fill( + { + txId: toByteString(''), + vout: 0n, + scriptSig: toByteString(''), + nSequence: 1n, + }, + MAX_BTC_TX_INPUT_COUNT + ) + this.outputs = fill( + { satoshis: 0n, script: toByteString('') }, + MAX_BTC_TX_OUTPUT_COUNT + ) + this.witness = fill( + fill(toByteString(''), MAX_WITNESS_ITEM_COUNT), + MAX_BTC_TX_INPUT_COUNT + ) + } + + @method() + readVarInt(): bigint { + const n: bigint = Utils.fromLEUnsigned( + slice(this.buf, this.pos, this.pos + 1n) + ) + this.pos++ + + let ret = 0n + + if (n < 0xfdn) { + ret = n + } else if (n == 0xfdn) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 2n)) + this.pos += 2n + } else if (n == 0xfen) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 4n)) + this.pos += 4n + } else if (n == 0xffn) { + ret = Utils.fromLEUnsigned(slice(this.buf, this.pos, this.pos + 8n)) + this.pos += 8n + } + + return ret + } + + @method() + readBytes(l: bigint): ByteString { + const ret = slice(this.buf, this.pos, this.pos + l) + this.pos += l + return ret + } + + @method() + readUInt32(): bigint { + return Utils.fromLEUnsigned(this.readBytes(4n)) + } + + @method() + tryReadFlag(): boolean { + const flag = slice(this.buf, this.pos, this.pos + 2n) + const ret = flag == toByteString('0001') + if (ret) { + this.pos += 2n + } + return ret + } + + @method() + readInput(): Input { + // Previous Transaction hash, 32 bytes + const txid: ByteString = this.readBytes(32n) + // Previous Txout-index, 4 bytes + const vout: bigint = Utils.fromLEUnsigned(this.readBytes(4n)) + // Txin-script length, 1 - 9 bytes + const scriptSigLen: bigint = this.readVarInt() + // Txin-script / scriptSig, -many bytes + const scriptSig: ByteString = + scriptSigLen > 0n ? this.readBytes(scriptSigLen) : toByteString('') + // Sequence_no, 4 bytes + const nSequence: bigint = Utils.fromLEUnsigned(this.readBytes(4n)) + return { + txId: txid, + vout: vout, + scriptSig: scriptSig, + nSequence: nSequence, + } + } + + @method() + readOutput(): Output { + // Satoshi value, 8 bytes + const satoshis: bigint = Utils.fromLEUnsigned(this.readBytes(8n)) + // Txout-script length, 1 - 9 bytes + const scriptLen: bigint = this.readVarInt() + // Txout-script / scriptPubKey, -many bytes + const script: ByteString = this.readBytes(scriptLen) + return { + script: script, + satoshis: satoshis, + } + } + + @method() + readWitness(): void { + for (let i = 0; i < MAX_BTC_TX_INPUT_COUNT; i++) { + if (i < this.inputsCount) { + const n = this.readVarInt() + if (n > 0n) { + for (let j = 0; j < MAX_WITNESS_ITEM_COUNT; j++) { + if (j < n) { + const itemLen = this.readVarInt() + if (itemLen > 0n) { + this.witness[i][j] = this.readBytes(itemLen) + } + } + } + } + } + } + } + + @method() + parse(): boolean { + this.version = this.readUInt32() + + this.isWitness = this.tryReadFlag() + + this.inputsCount = this.readVarInt() + assert(this.inputsCount < MAX_BTC_TX_INPUT_COUNT, 'too much inputs') + + for (let i = 0; i < MAX_BTC_TX_INPUT_COUNT; i++) { + if (i < this.inputsCount) { + this.inputs[i] = this.readInput() + } + } + + this.outputsCount = this.readVarInt() + assert(this.outputsCount < MAX_BTC_TX_OUTPUT_COUNT, 'too much outputs') + + for (let i = 0; i < MAX_BTC_TX_OUTPUT_COUNT; i++) { + if (i < this.outputsCount) { + this.outputs[i] = this.readOutput() + } + } + + if (this.isWitness) { + this.readWitness() + } + + this.nLockTime = this.readUInt32() + + return true + } + + @method() + getInputs(): FixedArray { + return this.inputs + } + + @method() + getOutputs(): FixedArray { + return this.outputs + } + + @method() + getWitness(): FixedArray< + FixedArray, + typeof MAX_BTC_TX_INPUT_COUNT + > { + return this.witness + } +} diff --git a/tests/contracts/txParserBTCTest.ts b/tests/contracts/txParserBTCTest.ts new file mode 100644 index 0000000..9ef7817 --- /dev/null +++ b/tests/contracts/txParserBTCTest.ts @@ -0,0 +1,65 @@ +import { + TxParserBTC, + Output, + Input, + MAX_WITNESS_ITEM_COUNT, + MAX_BTC_TX_INPUT_COUNT, +} from '../scrypt-ts-lib' +import { + method, + assert, + SmartContract, + ByteString, + equals, + FixedArray, +} from 'scrypt-ts' + +export class TransactionParserTest extends SmartContract { + @method() + public unlock( + rawTx: ByteString, + input: Input, + output: Output, + version: bigint, + nLockTime: bigint + ) { + const parser = new TxParserBTC(rawTx) + + const result = parser.parse() + + assert(result == true, 'parser failed') + + assert(equals(parser.getInputs()[0], input), 'input not expected') + + assert(equals(parser.getOutputs()[0], output), 'output not expected') + + assert(parser.version == version, 'version not expected') + assert(parser.nLockTime == nLockTime, 'nLockTime not expected') + assert(parser.isWitness == false, 'isWitness not expected') + } + + @method() + public unlockWitness( + rawTx: ByteString, + input: Input, + output: Output, + version: bigint, + nLockTime: bigint, + witness: FixedArray + ) { + const parser = new TxParserBTC(rawTx) + + const result = parser.parse() + + assert(result == true, 'parser failed') + + assert(equals(parser.getInputs()[0], input), 'input not expected') + + assert(equals(parser.getOutputs()[0], output), 'output not expected') + + assert(parser.version == version, 'version not expected') + assert(parser.nLockTime == nLockTime, 'nLockTime not expected') + assert(parser.isWitness == true, 'isWitness not expected') + assert(equals(parser.getWitness()[0], witness), 'witness not expected') + } +} diff --git a/tests/contracts/txParserTest.ts b/tests/contracts/txParserTest.ts new file mode 100644 index 0000000..4a7a5c5 --- /dev/null +++ b/tests/contracts/txParserTest.ts @@ -0,0 +1,26 @@ +import { TxParser, Output, Input } from '../scrypt-ts-lib' +import { method, assert, SmartContract, ByteString, equals } from 'scrypt-ts' + +export class TransactionParserTest extends SmartContract { + @method() + public unlock( + rawTx: ByteString, + input: Input, + output: Output, + version: bigint, + nLockTime: bigint + ) { + const parser = new TxParser(rawTx) + + const result = parser.parse() + + assert(result == true, 'parser failed') + + assert(equals(parser.getInputs()[0], input), 'input not expected') + + assert(equals(parser.getOutputs()[0], output), 'output not expected') + + assert(parser.version == version, 'version not expected') + assert(parser.nLockTime == nLockTime, 'nLockTime not expected') + } +} diff --git a/tests/specs/txParser.test.ts b/tests/specs/txParser.test.ts new file mode 100644 index 0000000..572c31b --- /dev/null +++ b/tests/specs/txParser.test.ts @@ -0,0 +1,71 @@ +import { expect } from 'chai' + +import { TransactionParserTest } from '../contracts/txParserTest' +import { toByteString } from 'scrypt-ts' + +describe('Test TransactionParserTest', () => { + let parserTest: TransactionParserTest + + before(async () => { + await TransactionParserTest.compile() + parserTest = new TransactionParserTest() + }) + + it('should pass', () => { + const result = parserTest.verify((self) => { + self.unlock( + toByteString( + '010000000134506e6c42b7d3b00b730d9589e63368e91b9ddad2822d32977b5cf91233f6cc010000006a473044022016fffa4d938d729edf195eb2c87a9d35ee3c55bc44413baf8ba3f83ff12b3b5602201cc7e51b139e52e42d5697a8354f657e78e3389a79e0d07c8ccba3b419a2f2d5412102d93fcc370b05b31b9e78e37d38448572b6813e55b54bbbe1772a03882529f402ffffffff020000000000000000fd1d07006a076d423a646f63314d1007a56a63697068657274657874d8405904f300ef08d144e7af2a94bd897c65f5b1b15b7dfd3865b252f748cbd119f166a5e7376902996a41fc0086cbfc7bf476feaecb2ac9e577206a059b75403bf6359db9f55191887507674512513d7918e1b0853f9f5ced3949ab8605ccded93b9782056a74e5b8287c71b097c18a680abda46e67f519746f02a4cf66a855441007803e776cd7e5bc9c490d46056d039c8506926ed6faf1c48b530e2a4a4f4f416aad866a2b566a4f468ca0e2d2b2ce612c5078e6181da1fac8410a6ae2dcd685764d0f428fb3fa5da44e799296104716359ba3e527121d05c680423a77aa517f0dcd16ffa24bd2ba402f08b2b36aa062b024269561d2e20e9e217a1b922a31208a88ba1a42437a62999cb907bbd97d6b353998d5d40b8f493eb4be6c94a78512d34e76f6ca06d5401075206a1c1edc9555564631e902eb26056ebd1c4fa4f67512c1c0e5c4511b286b1a3296817fd29d1aec91e34c09da527029d2db4e02a4bceb17bf8ad6263f9c212f5ba6e9bbab43519c3080cc1000e47ca4ebdad7e67ff66aa6d85f83bfb47f532b3d5c5f12318dabf9cba0888a92c6c445734d116eb3eff1dae52615a2301a7f3dc90f72e3b19def56d5cf5ffe2a5e3278596f3bdef5b1f0d04584dfb0bdc7c18e232f29a94dc998e8fc03c502a81f09c752cb559335701d9b18085860b9ee9a48e7f455a2417ffce7e949c0707cba89bd1a56a8415eaab5570ca7bad80fd9115176db32edca251b9a6d3a159267384ad8228fd849365e0d9229f2adc0d40a0c54ff77986beb14bceb06ad5224ff07f909735a4dc513b2e01dada96b4a399985be501a8994f1005722fe6db630082eef066ee28b3d59bee0569af1f73a3945263be7db061d42afe4b8565714d45938f7a241a4830595acabfc11a1b059b4ab6ce42855b72a18068d6d2d3f561422e056b1135b92169edd0a809c7baf975a8fc183430d7e0db6c200cfd9475a2adc9197b436e03f5809817b36f53c9066adcd61410e68efd6bcc775867ef162cd2caf5eaaba90f412472ee60a624d28172b1779777acca61637a7d0a2fa2f00e6a828c2c402caec8993fb4c41e45e309f872f4c5ea946359f57c4a0762bfd0fba266d68fe64977ec6f3e5ca7928cdef7f071ddcd885f6e2c3c7ed72c4b0d0a6a58f3801cda571dab809d15ffe5d6b62bc38f2af9f2954ef33154b1e7945a310f34af1491a26e2f2257bf64b8c26568eec2f793cc7d8c61c3e8c4f67de4d76386c1835ecaba31393db460d52dcf70545aee31a3d4065e4da4a3459310877d73b78f6d212dbf2ab5505d9953bfd5f254f8dc9030fd5a376280539786d1c31333f3726e80680ae28626fb9c8bdae98b80e8b6d320084e10e3f07b7b6307904a9b441985f0ee7e6d1f140e24efb1a9c92cfcdfa024068e732d69583a6021f0796a74587811c56414d8049ad6509fffb384c16b0db116cada3d0dab24cf1b9115a2295b44f64e7dffb543e7f1f94c21d71da27848d6bc834fc149ce734604fb401af4349639a5f870cf4a70fb248db61059678299acf9ad258abf77daa9d9a4b5235c757b137f4fbfd4b5d6ea3580069b616a8b3506a28da798eac0a787391c4c3ecf691ced51d69d691cd6f4b32c19a504abdd85bfc99c2d4a49a66dce62acbc76ad8a347d27a1107853a6f85f1bccdd9f4009539e46d2036c0a27af14f35a37494802177f38caace94e6ba0fba04c728db7394142854eb1282c4f49943987d504e69cf0e45221a57a1d26d1b457573935347b7d67f0b492d91361366f984df4b8287626976d8404c00128fc302fd27daa3d2fb2d6a726563697069656e747382a26d656e637279707465645f6b6579782b6b473170656f6c377654664b515946636f724c78684f5956755147595a4273477254703953656568497a4566686561646572a463616c67694132353647434d4b57636b6964782b5334616c74325433646832484536515a6b4958636a384c61417037536f4a395135755852692d554f2d6e59626976704368535644664e3362494b526458327363746167765f64417357474c626a787935777733485f6771664777a26d656e637279707465645f6b6579783633755531516941696d394e456c7279755570464e454546676e4d3059656459506a64537a6977555671694e50424144496a325673765166686561646572a363616c676e454344482d45532b413235364b57636b6964782b6f553345374f74674d4335546a554c3234487151646430396b5546534255674d4d35526d43375541736c346365706ba46178782b4d5278383849615a4e357873682d526472586a313676794872666f43734e6473656a36456243586f7756736363727665502d323536636b74796245436179782b664867466c436a37435f7744345636636d323153317757563261574533764c50754e44573041537068745563746167d840503e827075bf0cea08195bd06836dd400c6970726f746563746564d840517b22656e63223a224132353647434d227dd2060000000000001976a91442df33690dc6bc346339bcd8e77c9a828af6eaf388ac00000000' + ), + { + txId: toByteString( + '34506e6c42b7d3b00b730d9589e63368e91b9ddad2822d32977b5cf91233f6cc' + ), + vout: 1n, + scriptSig: toByteString( + '473044022016fffa4d938d729edf195eb2c87a9d35ee3c55bc44413baf8ba3f83ff12b3b5602201cc7e51b139e52e42d5697a8354f657e78e3389a79e0d07c8ccba3b419a2f2d5412102d93fcc370b05b31b9e78e37d38448572b6813e55b54bbbe1772a03882529f402' + ), + nSequence: 4294967295n, + }, + { + script: toByteString( + '006a076d423a646f63314d1007a56a63697068657274657874d8405904f300ef08d144e7af2a94bd897c65f5b1b15b7dfd3865b252f748cbd119f166a5e7376902996a41fc0086cbfc7bf476feaecb2ac9e577206a059b75403bf6359db9f55191887507674512513d7918e1b0853f9f5ced3949ab8605ccded93b9782056a74e5b8287c71b097c18a680abda46e67f519746f02a4cf66a855441007803e776cd7e5bc9c490d46056d039c8506926ed6faf1c48b530e2a4a4f4f416aad866a2b566a4f468ca0e2d2b2ce612c5078e6181da1fac8410a6ae2dcd685764d0f428fb3fa5da44e799296104716359ba3e527121d05c680423a77aa517f0dcd16ffa24bd2ba402f08b2b36aa062b024269561d2e20e9e217a1b922a31208a88ba1a42437a62999cb907bbd97d6b353998d5d40b8f493eb4be6c94a78512d34e76f6ca06d5401075206a1c1edc9555564631e902eb26056ebd1c4fa4f67512c1c0e5c4511b286b1a3296817fd29d1aec91e34c09da527029d2db4e02a4bceb17bf8ad6263f9c212f5ba6e9bbab43519c3080cc1000e47ca4ebdad7e67ff66aa6d85f83bfb47f532b3d5c5f12318dabf9cba0888a92c6c445734d116eb3eff1dae52615a2301a7f3dc90f72e3b19def56d5cf5ffe2a5e3278596f3bdef5b1f0d04584dfb0bdc7c18e232f29a94dc998e8fc03c502a81f09c752cb559335701d9b18085860b9ee9a48e7f455a2417ffce7e949c0707cba89bd1a56a8415eaab5570ca7bad80fd9115176db32edca251b9a6d3a159267384ad8228fd849365e0d9229f2adc0d40a0c54ff77986beb14bceb06ad5224ff07f909735a4dc513b2e01dada96b4a399985be501a8994f1005722fe6db630082eef066ee28b3d59bee0569af1f73a3945263be7db061d42afe4b8565714d45938f7a241a4830595acabfc11a1b059b4ab6ce42855b72a18068d6d2d3f561422e056b1135b92169edd0a809c7baf975a8fc183430d7e0db6c200cfd9475a2adc9197b436e03f5809817b36f53c9066adcd61410e68efd6bcc775867ef162cd2caf5eaaba90f412472ee60a624d28172b1779777acca61637a7d0a2fa2f00e6a828c2c402caec8993fb4c41e45e309f872f4c5ea946359f57c4a0762bfd0fba266d68fe64977ec6f3e5ca7928cdef7f071ddcd885f6e2c3c7ed72c4b0d0a6a58f3801cda571dab809d15ffe5d6b62bc38f2af9f2954ef33154b1e7945a310f34af1491a26e2f2257bf64b8c26568eec2f793cc7d8c61c3e8c4f67de4d76386c1835ecaba31393db460d52dcf70545aee31a3d4065e4da4a3459310877d73b78f6d212dbf2ab5505d9953bfd5f254f8dc9030fd5a376280539786d1c31333f3726e80680ae28626fb9c8bdae98b80e8b6d320084e10e3f07b7b6307904a9b441985f0ee7e6d1f140e24efb1a9c92cfcdfa024068e732d69583a6021f0796a74587811c56414d8049ad6509fffb384c16b0db116cada3d0dab24cf1b9115a2295b44f64e7dffb543e7f1f94c21d71da27848d6bc834fc149ce734604fb401af4349639a5f870cf4a70fb248db61059678299acf9ad258abf77daa9d9a4b5235c757b137f4fbfd4b5d6ea3580069b616a8b3506a28da798eac0a787391c4c3ecf691ced51d69d691cd6f4b32c19a504abdd85bfc99c2d4a49a66dce62acbc76ad8a347d27a1107853a6f85f1bccdd9f4009539e46d2036c0a27af14f35a37494802177f38caace94e6ba0fba04c728db7394142854eb1282c4f49943987d504e69cf0e45221a57a1d26d1b457573935347b7d67f0b492d91361366f984df4b8287626976d8404c00128fc302fd27daa3d2fb2d6a726563697069656e747382a26d656e637279707465645f6b6579782b6b473170656f6c377654664b515946636f724c78684f5956755147595a4273477254703953656568497a4566686561646572a463616c67694132353647434d4b57636b6964782b5334616c74325433646832484536515a6b4958636a384c61417037536f4a395135755852692d554f2d6e59626976704368535644664e3362494b526458327363746167765f64417357474c626a787935777733485f6771664777a26d656e637279707465645f6b6579783633755531516941696d394e456c7279755570464e454546676e4d3059656459506a64537a6977555671694e50424144496a325673765166686561646572a363616c676e454344482d45532b413235364b57636b6964782b6f553345374f74674d4335546a554c3234487151646430396b5546534255674d4d35526d43375541736c346365706ba46178782b4d5278383849615a4e357873682d526472586a313676794872666f43734e6473656a36456243586f7756736363727665502d323536636b74796245436179782b664867466c436a37435f7744345636636d323153317757563261574533764c50754e44573041537068745563746167d840503e827075bf0cea08195bd06836dd400c6970726f746563746564d840517b22656e63223a224132353647434d227d' + ), + satoshis: 0n, + }, + 1n, + 0n + ) + }) + expect(result.success, result.error).to.be.true + }) + + it('should pass witt coinbase tx', () => { + const result = parserTest.verify((self) => { + self.unlock( + toByteString( + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1a03afa00c2f7461616c2e636f6d2f9f3e331b9dbd6ababf680d00ffffffff0177f04225000000001976a914522cf9e7626d9bd8729e5a1398ece40dad1b6a2f88ac00000000' + ), + { + txId: toByteString( + '0000000000000000000000000000000000000000000000000000000000000000' + ), + vout: 4294967295n, + scriptSig: toByteString( + '03afa00c2f7461616c2e636f6d2f9f3e331b9dbd6ababf680d00' + ), + nSequence: 4294967295n, + }, + { + script: toByteString( + '76a914522cf9e7626d9bd8729e5a1398ece40dad1b6a2f88ac' + ), + satoshis: 625143927n, + }, + 1n, + 0n + ) + }) + expect(result.success, result.error).to.be.true + }) +}) diff --git a/tests/specs/txParserBTC.test.ts b/tests/specs/txParserBTC.test.ts new file mode 100644 index 0000000..6707be6 --- /dev/null +++ b/tests/specs/txParserBTC.test.ts @@ -0,0 +1,205 @@ +import { expect } from 'chai' + +import { TransactionParserTest } from '../contracts/txParserBTCTest' +import { FixedArray, toByteString, ByteString } from 'scrypt-ts' +import { MAX_WITNESS_ITEM_COUNT } from '../scrypt-ts-lib' + +describe('Test TransactionParserTest', () => { + let parserTest: TransactionParserTest + + before(async () => { + await TransactionParserTest.compile() + parserTest = new TransactionParserTest() + }) + + it('should pass', () => { + const input = { + txId: toByteString( + '34506e6c42b7d3b00b730d9589e63368e91b9ddad2822d32977b5cf91233f6cc' + ), + vout: 1n, + scriptSig: toByteString( + '473044022016fffa4d938d729edf195eb2c87a9d35ee3c55bc44413baf8ba3f83ff12b3b5602201cc7e51b139e52e42d5697a8354f657e78e3389a79e0d07c8ccba3b419a2f2d5412102d93fcc370b05b31b9e78e37d38448572b6813e55b54bbbe1772a03882529f402' + ), + nSequence: 4294967295n, + } + + const output = { + script: toByteString( + '006a076d423a646f63314d1007a56a63697068657274657874d8405904f300ef08d144e7af2a94bd897c65f5b1b15b7dfd3865b252f748cbd119f166a5e7376902996a41fc0086cbfc7bf476feaecb2ac9e577206a059b75403bf6359db9f55191887507674512513d7918e1b0853f9f5ced3949ab8605ccded93b9782056a74e5b8287c71b097c18a680abda46e67f519746f02a4cf66a855441007803e776cd7e5bc9c490d46056d039c8506926ed6faf1c48b530e2a4a4f4f416aad866a2b566a4f468ca0e2d2b2ce612c5078e6181da1fac8410a6ae2dcd685764d0f428fb3fa5da44e799296104716359ba3e527121d05c680423a77aa517f0dcd16ffa24bd2ba402f08b2b36aa062b024269561d2e20e9e217a1b922a31208a88ba1a42437a62999cb907bbd97d6b353998d5d40b8f493eb4be6c94a78512d34e76f6ca06d5401075206a1c1edc9555564631e902eb26056ebd1c4fa4f67512c1c0e5c4511b286b1a3296817fd29d1aec91e34c09da527029d2db4e02a4bceb17bf8ad6263f9c212f5ba6e9bbab43519c3080cc1000e47ca4ebdad7e67ff66aa6d85f83bfb47f532b3d5c5f12318dabf9cba0888a92c6c445734d116eb3eff1dae52615a2301a7f3dc90f72e3b19def56d5cf5ffe2a5e3278596f3bdef5b1f0d04584dfb0bdc7c18e232f29a94dc998e8fc03c502a81f09c752cb559335701d9b18085860b9ee9a48e7f455a2417ffce7e949c0707cba89bd1a56a8415eaab5570ca7bad80fd9115176db32edca251b9a6d3a159267384ad8228fd849365e0d9229f2adc0d40a0c54ff77986beb14bceb06ad5224ff07f909735a4dc513b2e01dada96b4a399985be501a8994f1005722fe6db630082eef066ee28b3d59bee0569af1f73a3945263be7db061d42afe4b8565714d45938f7a241a4830595acabfc11a1b059b4ab6ce42855b72a18068d6d2d3f561422e056b1135b92169edd0a809c7baf975a8fc183430d7e0db6c200cfd9475a2adc9197b436e03f5809817b36f53c9066adcd61410e68efd6bcc775867ef162cd2caf5eaaba90f412472ee60a624d28172b1779777acca61637a7d0a2fa2f00e6a828c2c402caec8993fb4c41e45e309f872f4c5ea946359f57c4a0762bfd0fba266d68fe64977ec6f3e5ca7928cdef7f071ddcd885f6e2c3c7ed72c4b0d0a6a58f3801cda571dab809d15ffe5d6b62bc38f2af9f2954ef33154b1e7945a310f34af1491a26e2f2257bf64b8c26568eec2f793cc7d8c61c3e8c4f67de4d76386c1835ecaba31393db460d52dcf70545aee31a3d4065e4da4a3459310877d73b78f6d212dbf2ab5505d9953bfd5f254f8dc9030fd5a376280539786d1c31333f3726e80680ae28626fb9c8bdae98b80e8b6d320084e10e3f07b7b6307904a9b441985f0ee7e6d1f140e24efb1a9c92cfcdfa024068e732d69583a6021f0796a74587811c56414d8049ad6509fffb384c16b0db116cada3d0dab24cf1b9115a2295b44f64e7dffb543e7f1f94c21d71da27848d6bc834fc149ce734604fb401af4349639a5f870cf4a70fb248db61059678299acf9ad258abf77daa9d9a4b5235c757b137f4fbfd4b5d6ea3580069b616a8b3506a28da798eac0a787391c4c3ecf691ced51d69d691cd6f4b32c19a504abdd85bfc99c2d4a49a66dce62acbc76ad8a347d27a1107853a6f85f1bccdd9f4009539e46d2036c0a27af14f35a37494802177f38caace94e6ba0fba04c728db7394142854eb1282c4f49943987d504e69cf0e45221a57a1d26d1b457573935347b7d67f0b492d91361366f984df4b8287626976d8404c00128fc302fd27daa3d2fb2d6a726563697069656e747382a26d656e637279707465645f6b6579782b6b473170656f6c377654664b515946636f724c78684f5956755147595a4273477254703953656568497a4566686561646572a463616c67694132353647434d4b57636b6964782b5334616c74325433646832484536515a6b4958636a384c61417037536f4a395135755852692d554f2d6e59626976704368535644664e3362494b526458327363746167765f64417357474c626a787935777733485f6771664777a26d656e637279707465645f6b6579783633755531516941696d394e456c7279755570464e454546676e4d3059656459506a64537a6977555671694e50424144496a325673765166686561646572a363616c676e454344482d45532b413235364b57636b6964782b6f553345374f74674d4335546a554c3234487151646430396b5546534255674d4d35526d43375541736c346365706ba46178782b4d5278383849615a4e357873682d526472586a313676794872666f43734e6473656a36456243586f7756736363727665502d323536636b74796245436179782b664867466c436a37435f7744345636636d323153317757563261574533764c50754e44573041537068745563746167d840503e827075bf0cea08195bd06836dd400c6970726f746563746564d840517b22656e63223a224132353647434d227d' + ), + satoshis: 0n, + } + + const result = parserTest.verify((self) => { + self.unlock( + toByteString( + '010000000134506e6c42b7d3b00b730d9589e63368e91b9ddad2822d32977b5cf91233f6cc010000006a473044022016fffa4d938d729edf195eb2c87a9d35ee3c55bc44413baf8ba3f83ff12b3b5602201cc7e51b139e52e42d5697a8354f657e78e3389a79e0d07c8ccba3b419a2f2d5412102d93fcc370b05b31b9e78e37d38448572b6813e55b54bbbe1772a03882529f402ffffffff020000000000000000fd1d07006a076d423a646f63314d1007a56a63697068657274657874d8405904f300ef08d144e7af2a94bd897c65f5b1b15b7dfd3865b252f748cbd119f166a5e7376902996a41fc0086cbfc7bf476feaecb2ac9e577206a059b75403bf6359db9f55191887507674512513d7918e1b0853f9f5ced3949ab8605ccded93b9782056a74e5b8287c71b097c18a680abda46e67f519746f02a4cf66a855441007803e776cd7e5bc9c490d46056d039c8506926ed6faf1c48b530e2a4a4f4f416aad866a2b566a4f468ca0e2d2b2ce612c5078e6181da1fac8410a6ae2dcd685764d0f428fb3fa5da44e799296104716359ba3e527121d05c680423a77aa517f0dcd16ffa24bd2ba402f08b2b36aa062b024269561d2e20e9e217a1b922a31208a88ba1a42437a62999cb907bbd97d6b353998d5d40b8f493eb4be6c94a78512d34e76f6ca06d5401075206a1c1edc9555564631e902eb26056ebd1c4fa4f67512c1c0e5c4511b286b1a3296817fd29d1aec91e34c09da527029d2db4e02a4bceb17bf8ad6263f9c212f5ba6e9bbab43519c3080cc1000e47ca4ebdad7e67ff66aa6d85f83bfb47f532b3d5c5f12318dabf9cba0888a92c6c445734d116eb3eff1dae52615a2301a7f3dc90f72e3b19def56d5cf5ffe2a5e3278596f3bdef5b1f0d04584dfb0bdc7c18e232f29a94dc998e8fc03c502a81f09c752cb559335701d9b18085860b9ee9a48e7f455a2417ffce7e949c0707cba89bd1a56a8415eaab5570ca7bad80fd9115176db32edca251b9a6d3a159267384ad8228fd849365e0d9229f2adc0d40a0c54ff77986beb14bceb06ad5224ff07f909735a4dc513b2e01dada96b4a399985be501a8994f1005722fe6db630082eef066ee28b3d59bee0569af1f73a3945263be7db061d42afe4b8565714d45938f7a241a4830595acabfc11a1b059b4ab6ce42855b72a18068d6d2d3f561422e056b1135b92169edd0a809c7baf975a8fc183430d7e0db6c200cfd9475a2adc9197b436e03f5809817b36f53c9066adcd61410e68efd6bcc775867ef162cd2caf5eaaba90f412472ee60a624d28172b1779777acca61637a7d0a2fa2f00e6a828c2c402caec8993fb4c41e45e309f872f4c5ea946359f57c4a0762bfd0fba266d68fe64977ec6f3e5ca7928cdef7f071ddcd885f6e2c3c7ed72c4b0d0a6a58f3801cda571dab809d15ffe5d6b62bc38f2af9f2954ef33154b1e7945a310f34af1491a26e2f2257bf64b8c26568eec2f793cc7d8c61c3e8c4f67de4d76386c1835ecaba31393db460d52dcf70545aee31a3d4065e4da4a3459310877d73b78f6d212dbf2ab5505d9953bfd5f254f8dc9030fd5a376280539786d1c31333f3726e80680ae28626fb9c8bdae98b80e8b6d320084e10e3f07b7b6307904a9b441985f0ee7e6d1f140e24efb1a9c92cfcdfa024068e732d69583a6021f0796a74587811c56414d8049ad6509fffb384c16b0db116cada3d0dab24cf1b9115a2295b44f64e7dffb543e7f1f94c21d71da27848d6bc834fc149ce734604fb401af4349639a5f870cf4a70fb248db61059678299acf9ad258abf77daa9d9a4b5235c757b137f4fbfd4b5d6ea3580069b616a8b3506a28da798eac0a787391c4c3ecf691ced51d69d691cd6f4b32c19a504abdd85bfc99c2d4a49a66dce62acbc76ad8a347d27a1107853a6f85f1bccdd9f4009539e46d2036c0a27af14f35a37494802177f38caace94e6ba0fba04c728db7394142854eb1282c4f49943987d504e69cf0e45221a57a1d26d1b457573935347b7d67f0b492d91361366f984df4b8287626976d8404c00128fc302fd27daa3d2fb2d6a726563697069656e747382a26d656e637279707465645f6b6579782b6b473170656f6c377654664b515946636f724c78684f5956755147595a4273477254703953656568497a4566686561646572a463616c67694132353647434d4b57636b6964782b5334616c74325433646832484536515a6b4958636a384c61417037536f4a395135755852692d554f2d6e59626976704368535644664e3362494b526458327363746167765f64417357474c626a787935777733485f6771664777a26d656e637279707465645f6b6579783633755531516941696d394e456c7279755570464e454546676e4d3059656459506a64537a6977555671694e50424144496a325673765166686561646572a363616c676e454344482d45532b413235364b57636b6964782b6f553345374f74674d4335546a554c3234487151646430396b5546534255674d4d35526d43375541736c346365706ba46178782b4d5278383849615a4e357873682d526472586a313676794872666f43734e6473656a36456243586f7756736363727665502d323536636b74796245436179782b664867466c436a37435f7744345636636d323153317757563261574533764c50754e44573041537068745563746167d840503e827075bf0cea08195bd06836dd400c6970726f746563746564d840517b22656e63223a224132353647434d227dd2060000000000001976a91442df33690dc6bc346339bcd8e77c9a828af6eaf388ac00000000' + ), + input, + output, + 1n, + 0n + ) + }) + expect(result.success, result.error).to.be.true + }) + + it('should pass', () => { + const input = { + txId: toByteString( + 'e382d005c73c32290d7aa837527ec4898d173a9e4cbe1cadca1f92fb5be53d22' + ), + vout: 1n, + scriptSig: toByteString( + '48304502210088ad4d891047bf71e765207427679f8e616dc8b440d4b717f8ea729ee8f11c7702206153b7eafbe9ea2ac0cbae6a37c43a5218b6690c5b3348aa0baf02f0c792119a012102efa06f8449fbd773f4fea0132e3cf9735babe293bcc47683083cddf0b07e7ddc' + ), + nSequence: 4294967295n, + } + + const output = { + script: toByteString( + 'a9148c2fcd82592bcf8e2c937af42eb734b4a95f9eac87' + ), + satoshis: 291652n, + } + + const result = parserTest.verify((self) => { + self.unlock( + toByteString( + '0200000001e382d005c73c32290d7aa837527ec4898d173a9e4cbe1cadca1f92fb5be53d22010000006b48304502210088ad4d891047bf71e765207427679f8e616dc8b440d4b717f8ea729ee8f11c7702206153b7eafbe9ea2ac0cbae6a37c43a5218b6690c5b3348aa0baf02f0c792119a012102efa06f8449fbd773f4fea0132e3cf9735babe293bcc47683083cddf0b07e7ddcffffffff02447304000000000017a9148c2fcd82592bcf8e2c937af42eb734b4a95f9eac877adc0a00000000001976a914f4d0c99900ea0ce58a822f26a615aff0d7b2eeac88ac00000000' + ), + input, + output, + 2n, + 0n + ) + }) + expect(result.success, result.error).to.be.true + }) + + it('witness: should pass', () => { + const input = { + txId: toByteString( + '972e47c018acd3c313bd1e022c684d83b039d6e938eba6810d582ad7c24a8831' + ), + vout: 0n, + scriptSig: toByteString(''), + nSequence: 4294967293n, + } + + const output = { + script: toByteString( + '76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac' + ), + satoshis: 546n, + } + + const witness: FixedArray = [ + toByteString( + 'dfa1ff00b53d254f73f116c28ff9fc01a093b5cffb938467e4f9e6a2b6920027224071e9e4117b83fcfd5c28e224b1d56fe3bd8052ee9fe62b4f70c002ff0bf9' + ), + toByteString( + '20117f692257b2331233b5705ce9c682be8719ff1b2b64cbca290bd6faeb54423eac060409e7d58701750063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800387b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f726469222c22616d74223a22313030227d68' + ), + toByteString( + 'c0117f692257b2331233b5705ce9c682be8719ff1b2b64cbca290bd6faeb54423e' + ), + toByteString(''), + toByteString(''), + ] + + const result = parserTest.verify((self) => { + self.unlockWitness( + toByteString( + '02000000000101972e47c018acd3c313bd1e022c684d83b039d6e938eba6810d582ad7c24a88310000000000fdffffff0222020000000000001976a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888acbe08000000000000160014d53e49ce18549fcfd591d95b97c0cad1a8fbc49c0340dfa1ff00b53d254f73f116c28ff9fc01a093b5cffb938467e4f9e6a2b6920027224071e9e4117b83fcfd5c28e224b1d56fe3bd8052ee9fe62b4f70c002ff0bf98620117f692257b2331233b5705ce9c682be8719ff1b2b64cbca290bd6faeb54423eac060409e7d58701750063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800387b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f726469222c22616d74223a22313030227d6821c0117f692257b2331233b5705ce9c682be8719ff1b2b64cbca290bd6faeb54423e00000000' + ), + input, + output, + 2n, + 0n, + witness + ) + }) + expect(result.success, result.error).to.be.true + }) + + it('witness: should pass', () => { + const input = { + txId: toByteString( + '6c03855e9413477c4dbce40d89bf8e2bc5cd14d55a7c8622a06db724be402a62' + ), + vout: 0n, + scriptSig: toByteString(''), + nSequence: 4294967293n, + } + + const output = { + script: toByteString( + '51205b86d87bc9fcdb73032cc6f71015cfef404ddeefce906e1da24654c4f63f25d1' + ), + satoshis: 546n, + } + + const witness: FixedArray = [ + toByteString( + 'e12c728511e0fe90656ed67c2ebca1d6ff3163687a218c62fa498dff9e11acf6d3143d415fdb2a5cc43c04c8e404b9b703f0815b1a725757b2559f0536e93542' + ), + toByteString( + '20fb87bcef501c992689b3b7101fd7a04e76bdaa96456c89caadc43707483e4aa6ac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38003a7b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a2244524147222c22616d74223a223530303030227d68' + ), + toByteString( + 'c0fb87bcef501c992689b3b7101fd7a04e76bdaa96456c89caadc43707483e4aa6' + ), + toByteString(''), + toByteString(''), + ] + + const result = parserTest.verify((self) => { + self.unlockWitness( + toByteString( + '020000000001016c03855e9413477c4dbce40d89bf8e2bc5cd14d55a7c8622a06db724be402a620000000000fdffffff0122020000000000002251205b86d87bc9fcdb73032cc6f71015cfef404ddeefce906e1da24654c4f63f25d10340e12c728511e0fe90656ed67c2ebca1d6ff3163687a218c62fa498dff9e11acf6d3143d415fdb2a5cc43c04c8e404b9b703f0815b1a725757b2559f0536e935428020fb87bcef501c992689b3b7101fd7a04e76bdaa96456c89caadc43707483e4aa6ac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38003a7b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a2244524147222c22616d74223a223530303030227d6821c0fb87bcef501c992689b3b7101fd7a04e76bdaa96456c89caadc43707483e4aa600000000' + ), + input, + output, + 2n, + 0n, + witness + ) + }) + expect(result.success, result.error).to.be.true + }) + + it('should pass with coinbase tx', () => { + const input = { + txId: toByteString( + '0000000000000000000000000000000000000000000000000000000000000000' + ), + vout: 4294967295n, + scriptSig: toByteString( + '03afa00c2f7461616c2e636f6d2f9f3e331b9dbd6ababf680d00' + ), + nSequence: 4294967295n, + } + + const output = { + script: toByteString( + '76a914522cf9e7626d9bd8729e5a1398ece40dad1b6a2f88ac' + ), + satoshis: 625143927n, + } + + const result = parserTest.verify((self) => { + self.unlock( + toByteString( + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1a03afa00c2f7461616c2e636f6d2f9f3e331b9dbd6ababf680d00ffffffff0177f04225000000001976a914522cf9e7626d9bd8729e5a1398ece40dad1b6a2f88ac00000000' + ), + input, + output, + 1n, + 0n + ) + }) + expect(result.success, result.error).to.be.true + }) +})