From 63a833bc58cd01185c7495cbab871d919623b0b8 Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Thu, 7 Mar 2024 23:56:45 +0800 Subject: [PATCH 1/6] chore: regroup transaction tests --- packages/btc/tests/Transaction.test.ts | 77 +++++++++++++------------- packages/btc/tests/shared/env.ts | 4 +- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/btc/tests/Transaction.test.ts b/packages/btc/tests/Transaction.test.ts index 18c21f36..c6596cbf 100644 --- a/packages/btc/tests/Transaction.test.ts +++ b/packages/btc/tests/Transaction.test.ts @@ -1,48 +1,49 @@ import { describe, expect, it } from 'vitest'; -import { accounts, networkType, service } from './shared/env'; -import { DataSource, ErrorCodes, ErrorMessages, sendBtc } from '../src'; +import { accounts, networkType, source } from './shared/env'; +import { ErrorCodes, ErrorMessages, sendBtc } from '../src'; describe('Transaction', () => { - const addresses = [ - { type: 'Native SegWit (P2WPKH)', address: accounts.charlie.p2wpkh.address }, - { type: 'Nested SegWit (P2SH)', address: '2N4gkVAQ1f6bi8BKon8MLKEV1pi85MJWcPV' }, - { type: 'Taproot (P2TR)', address: 'tb1pjew2gs9aqr2m7r8jc8car9jpwuv6wye006l4slplzcthupnldmjqpf8h5d' }, - { type: 'Legacy (P2PKH)', address: 'mqkAgjy8gfrMZh1VqV5Wm1Yi4G9KWLXA1Q' }, - ]; + describe('Transfer to various address types', () => { + const addresses = [ + { type: 'Native SegWit (P2WPKH)', address: accounts.charlie.p2wpkh.address }, + { type: 'Nested SegWit (P2SH)', address: '2N4gkVAQ1f6bi8BKon8MLKEV1pi85MJWcPV' }, + { type: 'Taproot (P2TR)', address: 'tb1pjew2gs9aqr2m7r8jc8car9jpwuv6wye006l4slplzcthupnldmjqpf8h5d' }, + { type: 'Legacy (P2PKH)', address: 'mqkAgjy8gfrMZh1VqV5Wm1Yi4G9KWLXA1Q' }, + ]; + addresses.forEach((addressInfo, index) => { + it(`Transfer BTC from P2WPKH address to ${addressInfo.type} address`, async () => { + if (index !== 0) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + } - addresses.forEach((addressInfo, index) => { - it(`Transfer BTC from P2WPKH address to ${addressInfo.type} address`, async () => { - if (index !== 0) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - } - const source = new DataSource(service, networkType); - const psbt = await sendBtc({ - from: accounts.charlie.p2wpkh.address, - tos: [ - { - address: addressInfo.address, - value: 1000, - }, - ], - networkType, - source, - }); + const psbt = await sendBtc({ + from: accounts.charlie.p2wpkh.address, + tos: [ + { + address: addressInfo.address, + value: 1000, + }, + ], + networkType, + source, + }); + + // Sign & finalize inputs + psbt.signAllInputs(accounts.charlie.keyPair); + psbt.finalizeAllInputs(); - // Sign & finalize inputs - psbt.signAllInputs(accounts.charlie.keyPair); - psbt.finalizeAllInputs(); + // Convert psbt to transaction + const tx = psbt.extractTransaction(); + console.log('ins:', tx.ins); + console.log('outs:', tx.outs); - // Broadcast transaction - const tx = psbt.extractTransaction(); - console.log('ins:', tx.ins); - console.log('outs:', tx.outs); - // const res = await service.sendTransaction(tx.toHex()); - // expect(res.txid).toMatch(/^[a-f0-9]+$/); - // console.log(`explorer: https://mempool.space/testnet/tx/${res.txid}`); - }, 10000); + // Broadcast transaction + // const res = await service.sendTransaction(tx.toHex()); + // console.log(`explorer: https://mempool.space/testnet/tx/${res.txid}`); + }, 10000); + }); }); - it('Transfer with an impossible "minimalSatoshi" filter', async () => { - const source = new DataSource(service, networkType); + it('Transfer with an impossible "minUtxoSatoshi" filter', async () => { await expect(() => sendBtc({ from: accounts.charlie.p2wpkh.address, diff --git a/packages/btc/tests/shared/env.ts b/packages/btc/tests/shared/env.ts index c19d56d7..8d33f444 100644 --- a/packages/btc/tests/shared/env.ts +++ b/packages/btc/tests/shared/env.ts @@ -1,5 +1,5 @@ import bitcoin from 'bitcoinjs-lib'; -import { BtcAssetsApi, ECPair, toNetworkType } from '../../src'; +import { BtcAssetsApi, DataSource, ECPair, toNetworkType } from '../../src'; export const network = bitcoin.networks.testnet; export const networkType = toNetworkType(network); @@ -10,6 +10,8 @@ export const service = BtcAssetsApi.fromToken( process.env.VITE_SERVICE_ORIGIN!, ); +export const source = new DataSource(service, networkType); + export const accounts = { charlie: createAccount('8d3c23d340ac0841e6c3b58a9bbccb9a28e94ab444f972cff35736fa2fcf9f3f', network), }; From 537d516af04a40645184af60a063aebc3befac26 Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Fri, 8 Mar 2024 00:45:27 +0800 Subject: [PATCH 2/6] fix: error message reading of BtcAssetsApi --- .changeset/silver-readers-melt.md | 5 +++++ packages/btc/src/query/service.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/silver-readers-melt.md diff --git a/.changeset/silver-readers-melt.md b/.changeset/silver-readers-melt.md new file mode 100644 index 00000000..eb7dc54d --- /dev/null +++ b/.changeset/silver-readers-melt.md @@ -0,0 +1,5 @@ +--- +"@rgbpp-sdk/btc": patch +--- + +Fix the error message reading from the BtcAssetsApi response diff --git a/packages/btc/src/query/service.ts b/packages/btc/src/query/service.ts index 660effe1..cfdf6e4b 100644 --- a/packages/btc/src/query/service.ts +++ b/packages/btc/src/query/service.ts @@ -179,9 +179,11 @@ export class BtcAssetsApi { } } if (json && !ok) { + const innerError = json?.error?.error ? `(${json.error.error.code}) ${json.error.error.message}` : void 0; + const message = json.message ?? innerError ?? JSON.stringify(json); throw new TxBuildError( ErrorCodes.ASSETS_API_RESPONSE_ERROR, - `${ErrorMessages[ErrorCodes.ASSETS_API_RESPONSE_ERROR]}: ${json.message}`, + `${ErrorMessages[ErrorCodes.ASSETS_API_RESPONSE_ERROR]}: ${message}`, ); } From 0aaf787fd4c89566f50d408e9251f75acc1ef570 Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Fri, 8 Mar 2024 10:18:03 +0800 Subject: [PATCH 3/6] feat: support OP_RETURN output in sendBtc API --- .changeset/gorgeous-starfishes-prove.md | 5 ++ packages/btc/src/api/sendBtc.ts | 9 ++-- packages/btc/src/error.ts | 4 ++ packages/btc/src/transaction/build.ts | 45 +++++++++++++--- packages/btc/src/transaction/embed.ts | 72 +++++++++++++++++++++++++ packages/btc/tests/Embed.test.ts | 40 ++++++++++++++ packages/btc/tests/Transaction.test.ts | 45 ++++++++++++++++ 7 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 .changeset/gorgeous-starfishes-prove.md create mode 100644 packages/btc/src/transaction/embed.ts create mode 100644 packages/btc/tests/Embed.test.ts diff --git a/.changeset/gorgeous-starfishes-prove.md b/.changeset/gorgeous-starfishes-prove.md new file mode 100644 index 00000000..9d61a108 --- /dev/null +++ b/.changeset/gorgeous-starfishes-prove.md @@ -0,0 +1,5 @@ +--- +"@rgbpp-sdk/btc": patch +--- + +Support creating OP_RETURN outputs in the sendBtc() API diff --git a/packages/btc/src/api/sendBtc.ts b/packages/btc/src/api/sendBtc.ts index d8a48b97..e9cc5e62 100644 --- a/packages/btc/src/api/sendBtc.ts +++ b/packages/btc/src/api/sendBtc.ts @@ -1,14 +1,11 @@ import bitcoin from '../bitcoin'; import { NetworkType } from '../network'; import { DataSource } from '../query/source'; -import { TxBuilder } from '../transaction/build'; +import { TxBuilder, TxTo } from '../transaction/build'; export async function sendBtc(props: { from: string; - tos: { - address: string; - value: number; - }[]; + tos: TxTo[]; source: DataSource; networkType: NetworkType; minUtxoSatoshi?: number; @@ -24,7 +21,7 @@ export async function sendBtc(props: { }); props.tos.forEach((to) => { - tx.addOutput(to.address, to.value); + tx.addTo(to); }); await tx.collectInputsAndPayFee(props.from); diff --git a/packages/btc/src/error.ts b/packages/btc/src/error.ts index b5fc1733..73be5fc4 100644 --- a/packages/btc/src/error.ts +++ b/packages/btc/src/error.ts @@ -1,7 +1,9 @@ export enum ErrorCodes { UNKNOWN, INSUFFICIENT_UTXO, + UNSUPPORTED_OUTPUT, UNSUPPORTED_ADDRESS_TYPE, + INVALID_OP_RETURN_SCRIPT, ASSETS_API_RESPONSE_ERROR, ASSETS_API_UNAUTHORIZED, ASSETS_API_INVALID_PARAM, @@ -11,7 +13,9 @@ export enum ErrorCodes { export const ErrorMessages = { [ErrorCodes.UNKNOWN]: 'Unknown error', [ErrorCodes.INSUFFICIENT_UTXO]: 'Insufficient UTXO', + [ErrorCodes.UNSUPPORTED_OUTPUT]: 'Unsupported output format', [ErrorCodes.UNSUPPORTED_ADDRESS_TYPE]: 'Unsupported address type', + [ErrorCodes.INVALID_OP_RETURN_SCRIPT]: 'Invalid OP_RETURN script format', [ErrorCodes.ASSETS_API_UNAUTHORIZED]: 'BtcAssetsAPI unauthorized, please check your token/origin', [ErrorCodes.ASSETS_API_INVALID_PARAM]: 'Invalid param(s) was provided to the BtcAssetsAPI', [ErrorCodes.ASSETS_API_RESPONSE_ERROR]: 'BtcAssetsAPI returned an error', diff --git a/packages/btc/src/transaction/build.ts b/packages/btc/src/transaction/build.ts index d91c5acb..29e22d92 100644 --- a/packages/btc/src/transaction/build.ts +++ b/packages/btc/src/transaction/build.ts @@ -6,6 +6,7 @@ import { AddressType, UnspentOutput } from '../types'; import { NetworkType, toPsbtNetwork } from '../network'; import { addressToScriptPublicKeyHex, getAddressType } from '../address'; import { MIN_COLLECTABLE_SATOSHI } from '../constants'; +import { dataToOpReturnScriptPubkey } from './embed'; import { FeeEstimator } from './fee'; interface TxInput { @@ -18,10 +19,21 @@ interface TxInput { utxo: UnspentOutput; } -interface TxOutput { +export type TxOutput = TxAddressOutput | TxScriptOutput; +export interface TxAddressOutput { address: string; value: number; } +export interface TxScriptOutput { + script: Buffer; + value: number; +} + +export type TxTo = TxAddressOutput | TxDataOutput; +export interface TxDataOutput { + data: Buffer | string; + value: number; +} export class TxBuilder { inputs: TxInput[] = []; @@ -49,14 +61,30 @@ export class TxBuilder { } addInput(utxo: UnspentOutput) { + utxo = clone(utxo); this.inputs.push(utxoToInput(utxo)); } - addOutput(address: string, value: number) { - this.outputs.push({ - address, - value, - }); + addOutput(output: TxOutput) { + output = clone(output); + this.outputs.push(output); + } + + addTo(to: TxTo) { + if ('data' in to) { + const data = typeof to.data === 'string' ? Buffer.from(to.data, 'hex') : to.data; + const scriptPubkey = dataToOpReturnScriptPubkey(data); + + return this.addOutput({ + script: scriptPubkey, + value: to.value, + }); + } + if ('address' in to) { + return this.addOutput(to); + } + + throw new TxBuildError(ErrorCodes.UNSUPPORTED_OUTPUT); } async collectInputsAndPayFee(address: string, fee?: number, extraChange?: number): Promise { @@ -92,7 +120,10 @@ export class TxBuilder { `collected satoshi: ${satoshi}, collected utxos: [${this.inputs.map((u) => u.utxo.value)}], returning change: ${changeSatoshi}`, ); if (requireChangeUtxo) { - this.addOutput(this.changedAddress, changeSatoshi); + this.addOutput({ + address: this.changedAddress, + value: changeSatoshi, + }); } const addressType = getAddressType(address); diff --git a/packages/btc/src/transaction/embed.ts b/packages/btc/src/transaction/embed.ts new file mode 100644 index 00000000..eeb59154 --- /dev/null +++ b/packages/btc/src/transaction/embed.ts @@ -0,0 +1,72 @@ +import bitcoin from 'bitcoinjs-lib'; +import { ErrorCodes, TxBuildError } from '../error'; + +/** + * Convert data to OP_RETURN script pubkey. + * The data size should be ranged in 1 to 80 bytes. + * + * @example + * const data = Buffer.from('01020304', 'hex'); + * const scriptPk = dataToOpReturnScriptPubkey(data); // + * const scriptPkHex = scriptPk.toString('hex'); // 6a0401020304 + */ +export function dataToOpReturnScriptPubkey(data: Buffer): Buffer { + const payment = bitcoin.payments.embed({ data: [data] }); + return payment.output!; +} + +/** + * Get data from a OP_RETURN script pubkey. + * + * @example + * const scriptPk = Buffer.from('6a0401020304', 'hex'); + * const data = opReturnScriptPubKeyToData(scriptPk); // + * const hex = data.toString('hex'); // 01020304 + */ +export function opReturnScriptPubKeyToData(script: Buffer): Buffer { + if (!isOpReturnScriptPubkey(script)) { + throw new TxBuildError(ErrorCodes.INVALID_OP_RETURN_SCRIPT); + } + + const [_op, data] = bitcoin.script.decompile(script)!; + return data as Buffer; +} + +/** + * Check if a script pubkey is an OP_RETURN script. + * + * A valid OP_RETURN script should have the following structure: + * - + * - + * + * @example + * // + * isOpReturnScriptPubkey(Buffer.from('6a0401020304', 'hex')); // true + * // + * isOpReturnScriptPubkey(Buffer.from('6a4c0f746573742d636f6d6d69746d656e74', 'hex')); // true + * // + * isOpReturnScriptPubkey(Buffer.from('6a4c', 'hex')); // false + * // + * isOpReturnScriptPubkey(Buffer.from('6a01', 'hex')); // false + * // ... (not an OP_RETURN script) + * isOpReturnScriptPubkey(Buffer.from('76a914a802fc56c704ce87c42d7c92eb75e7896bdc41e788ac', 'hex')); // false + */ +export function isOpReturnScriptPubkey(script: Buffer): boolean { + const scripts = bitcoin.script.decompile(script); + if (!scripts || scripts.length !== 2) { + return false; + } + + const [op, data] = scripts!; + // OP_RETURN opcode is 0x6a in hex or 106 in integer + if (op !== bitcoin.opcodes.OP_RETURN) { + return false; + } + // Standard OP_RETURN data size is up to 80 bytes + if (!(data instanceof Buffer) || data.byteLength < 1 || data.byteLength > 80) { + return false; + } + + // No false condition matched, it's an OP_RETURN script + return true; +} diff --git a/packages/btc/tests/Embed.test.ts b/packages/btc/tests/Embed.test.ts new file mode 100644 index 00000000..fd1e47af --- /dev/null +++ b/packages/btc/tests/Embed.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { dataToOpReturnScriptPubkey, opReturnScriptPubKeyToData } from '../src/transaction/embed'; + +describe('Embed', () => { + it('Encode UTF-8 data to OP_RETURN script pubkey', () => { + const data = Buffer.from('test-commitment', 'utf-8'); + const script = dataToOpReturnScriptPubkey(data); + + expect(script.toString('hex')).toEqual('6a0f746573742d636f6d6d69746d656e74'); + }); + it('Decode UTF-8 data from OP_RETURN script pubkey', () => { + const script = Buffer.from('6a0f746573742d636f6d6d69746d656e74', 'hex'); + const data = opReturnScriptPubKeyToData(script); + + expect(data.toString('utf-8')).toEqual('test-commitment'); + }); + + it('Decode 32-byte hex from OP_RETURN script pubkey', () => { + const hex = '00'.repeat(32); + const script = Buffer.from('6a20' + hex, 'hex'); + const data = opReturnScriptPubKeyToData(script); + + expect(data.toString('hex')).toEqual(hex); + }); + + it('Encode 80-byte data to OP_RETURN script pubkey', () => { + const hex = '00'.repeat(80); + const data = Buffer.from(hex, 'hex'); + const script = dataToOpReturnScriptPubkey(data); + + expect(script.toString('hex')).toEqual('6a4c50' + hex); + }); + it('Decode 80-byte hex from OP_RETURN script pubkey', () => { + const hex = '00'.repeat(80); + const script = Buffer.from('6a4c50' + hex, 'hex'); + const data = opReturnScriptPubKeyToData(script); + + expect(data.toString('hex')).toEqual(hex); + }); +}); diff --git a/packages/btc/tests/Transaction.test.ts b/packages/btc/tests/Transaction.test.ts index c6596cbf..98c9b112 100644 --- a/packages/btc/tests/Transaction.test.ts +++ b/packages/btc/tests/Transaction.test.ts @@ -1,3 +1,4 @@ +import bitcoin from 'bitcoinjs-lib'; import { describe, expect, it } from 'vitest'; import { accounts, networkType, source } from './shared/env'; import { ErrorCodes, ErrorMessages, sendBtc } from '../src'; @@ -59,4 +60,48 @@ describe('Transaction', () => { }), ).rejects.toThrow(ErrorMessages[ErrorCodes.INSUFFICIENT_UTXO]); }); + it('Transfer with an extra OP_RETURN output', async () => { + const psbt = await sendBtc({ + from: accounts.charlie.p2wpkh.address, + tos: [ + { + data: Buffer.from('00'.repeat(32), 'hex'), + value: 0, + }, + { + address: accounts.charlie.p2wpkh.address, + value: 1000, + }, + ], + networkType, + source, + }); + + const outputs = psbt.txOutputs; + expect(outputs).toHaveLength(3); + + const opReturnOutput = outputs[0]; + expect(opReturnOutput).toBeDefined(); + expect(opReturnOutput.script).toBeDefined(); + + const scripts = bitcoin.script.decompile(opReturnOutput.script); + expect(scripts).toBeDefined(); + + const op = scripts![0]; + expect(op).toBeTypeOf('number'); + expect(op).toBe(bitcoin.opcodes.OP_RETURN); + + const data = scripts![1]; + expect(data).toBeInstanceOf(Buffer); + expect((data as Buffer).toString('hex')).toEqual('00'.repeat(32)); + + // Sign & finalize inputs + psbt.signAllInputs(accounts.charlie.keyPair); + psbt.finalizeAllInputs(); + + // Broadcast transaction + // const tx = psbt.extractTransaction(); + // const res = await service.sendTransaction(tx.toHex()); + // console.log(`explorer: https://mempool.space/testnet/tx/${res.txid}`); + }); }); From 52541c8dc91f1a7b55a7184a727a5e79a25c512d Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Fri, 8 Mar 2024 20:52:11 +0800 Subject: [PATCH 4/6] feat: improve "minUtxoSatoshi" transfer test --- packages/btc/tests/Transaction.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/btc/tests/Transaction.test.ts b/packages/btc/tests/Transaction.test.ts index 98c9b112..5723dc71 100644 --- a/packages/btc/tests/Transaction.test.ts +++ b/packages/btc/tests/Transaction.test.ts @@ -1,7 +1,7 @@ import bitcoin from 'bitcoinjs-lib'; import { describe, expect, it } from 'vitest'; -import { accounts, networkType, source } from './shared/env'; -import { ErrorCodes, ErrorMessages, sendBtc } from '../src'; +import { accounts, networkType, service, source } from './shared/env'; +import { ErrorCodes, ErrorMessages, MIN_COLLECTABLE_SATOSHI, sendBtc } from '../src'; describe('Transaction', () => { describe('Transfer to various address types', () => { @@ -45,6 +45,10 @@ describe('Transaction', () => { }); }); it('Transfer with an impossible "minUtxoSatoshi" filter', async () => { + const balance = await service.getBalance(accounts.charlie.p2wpkh.address, { + min_satoshi: MIN_COLLECTABLE_SATOSHI, + }); + await expect(() => sendBtc({ from: accounts.charlie.p2wpkh.address, @@ -54,7 +58,7 @@ describe('Transaction', () => { value: 1000, }, ], - minUtxoSatoshi: 1000000000000, + minUtxoSatoshi: balance.satoshi + 1, networkType, source, }), From e10d9ac605a9bfb49d0ce2e1b877af11e3cfe019 Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Fri, 8 Mar 2024 21:02:15 +0800 Subject: [PATCH 5/6] docs: update the readme file of btc lib --- packages/btc/README.md | 44 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/btc/README.md b/packages/btc/README.md index 1f4b2950..58b5d0fb 100644 --- a/packages/btc/README.md +++ b/packages/btc/README.md @@ -70,19 +70,19 @@ console.log(res); ### Constructing transaction -Transfer BTC from a P2WPKH address: +Transfer BTC from a `P2WPKH` address: ```typescript import { sendBtc, BtcAssetsApi, DataSource, NetworkType } from '@rgbpp-sdk/btc'; -const service = BtcAssetsApi.fromToken('btc_assets_api_url', 'your_token'); - const networkType = NetworkType.TESTNET; + +const service = BtcAssetsApi.fromToken('btc_assets_api_url', 'your_token'); const source = new DataSource(service, networkType); // Create a PSBT const psbt = await sendBtc({ - from: 'from_address', // your P2WPKH address + from: account.address, // your P2WPKH address tos: [ { address: 'to_address', // destination btc address @@ -95,7 +95,41 @@ const psbt = await sendBtc({ }); // Sign & finalize inputs -psbt.signAllInputs(accounts.charlie.keyPair); +psbt.signAllInputs(account.keyPair); +psbt.finalizeAllInputs(); + +// Broadcast transaction +const tx = psbt.extractTransaction(); +const res = await service.sendTransaction(tx.toHex()); +console.log('txid:', res.txid); +``` + +Create an `OP_RETURN` output: + +```typescript +import { sendBtc, BtcAssetsApi, DataSource, NetworkType } from '@rgbpp-sdk/btc'; + +const networkType = NetworkType.TESTNET; + +const service = BtcAssetsApi.fromToken('btc_assets_api_url', 'your_token'); +const source = new DataSource(service, networkType); + +// Create a PSBT +const psbt = await sendBtc({ + from: account.address, // your address + tos: [ + { + data: Buffer.from('0x' + '00'.repeat(32), 'hex'), // any data <= 80 bytes + value: 0, // normally the value is 0 + }, + ], + feeRate: 1, // optional + networkType, + source, +}); + +// Sign & finalize inputs +psbt.signAllInputs(account.keyPair); psbt.finalizeAllInputs(); // Broadcast transaction From 6212c3011887804a5865d5c5a49b7eeb4493b5d6 Mon Sep 17 00:00:00 2001 From: ShookLyngs Date: Fri, 8 Mar 2024 21:13:33 +0800 Subject: [PATCH 6/6] fix: remove hex prefix when adding output in TxBuilder --- packages/btc/src/index.ts | 1 + packages/btc/src/transaction/build.ts | 5 +++-- packages/btc/src/utils.ts | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/btc/src/index.ts b/packages/btc/src/index.ts index b3b5ce42..bb0a8a88 100644 --- a/packages/btc/src/index.ts +++ b/packages/btc/src/index.ts @@ -11,6 +11,7 @@ export * from './query/service'; export * from './query/source'; export * from './transaction/build'; +export * from './transaction/embed'; export * from './transaction/fee'; export * from './api/sendBtc'; diff --git a/packages/btc/src/transaction/build.ts b/packages/btc/src/transaction/build.ts index 29e22d92..68cadd0d 100644 --- a/packages/btc/src/transaction/build.ts +++ b/packages/btc/src/transaction/build.ts @@ -4,8 +4,9 @@ import { DataSource } from '../query/source'; import { ErrorCodes, TxBuildError } from '../error'; import { AddressType, UnspentOutput } from '../types'; import { NetworkType, toPsbtNetwork } from '../network'; -import { addressToScriptPublicKeyHex, getAddressType } from '../address'; import { MIN_COLLECTABLE_SATOSHI } from '../constants'; +import { addressToScriptPublicKeyHex, getAddressType } from '../address'; +import { removeHexPrefix } from '../utils'; import { dataToOpReturnScriptPubkey } from './embed'; import { FeeEstimator } from './fee'; @@ -72,7 +73,7 @@ export class TxBuilder { addTo(to: TxTo) { if ('data' in to) { - const data = typeof to.data === 'string' ? Buffer.from(to.data, 'hex') : to.data; + const data = typeof to.data === 'string' ? Buffer.from(removeHexPrefix(to.data), 'hex') : to.data; const scriptPubkey = dataToOpReturnScriptPubkey(data); return this.addOutput({ diff --git a/packages/btc/src/utils.ts b/packages/btc/src/utils.ts index 16da33b8..67f2db7b 100644 --- a/packages/btc/src/utils.ts +++ b/packages/btc/src/utils.ts @@ -12,3 +12,10 @@ export function isDomain(domain: string): boolean { const regex = /^(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,}$/; return regex.test(domain); } + +/** + * Remove '0x' prefix from a hex string. + */ +export function removeHexPrefix(hex: string): string { + return hex.startsWith('0x') ? hex.slice(2) : hex; +}