diff --git a/sdk/examples/gateway.ts b/sdk/examples/gateway.ts new file mode 100644 index 00000000..1898032b --- /dev/null +++ b/sdk/examples/gateway.ts @@ -0,0 +1,63 @@ +import { GatewayApiClient } from "../src/gateway"; +import * as bitcoin from "bitcoinjs-lib"; +import { AddressType, getAddressInfo } from "bitcoin-address-validation"; +import { createTransfer } from "../src/wallet/utxo"; +import { hex } from '@scure/base'; +import { Transaction as SigTx } from '@scure/btc-signer'; + +const BOB_TBTC_V2_TOKEN_ADDRESS = "0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2"; + +export async function swapBtcForToken(evmAddress: string) { + const gatewayClient = new GatewayApiClient("https://onramp-api-mainnet.gobob.xyz"); + + const amount = 10000; // 0.0001 BTC + const { fee: _fee, onramp_address: onrampAddress, bitcoin_address: bitcoinAddress, gratuity: _gratuity } = await gatewayClient.getQuote( + BOB_TBTC_V2_TOKEN_ADDRESS, + amount + ); + + const orderId = await gatewayClient.createOrder(onrampAddress, evmAddress, amount); + + const tx = await createTxWithOpReturn("bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d", bitcoinAddress, amount, evmAddress); + + // NOTE: relayer should broadcast the tx + await gatewayClient.updateOrder(orderId, tx.toHex()); + +} + +async function createTxWithOpReturn(fromAddress: string, toAddress: string, amount: number, opReturn: string, fromPubKey?: string): Promise { + const addressType = getAddressInfo(fromAddress).type; + + // Ensure this is not the P2TR address for ordinals (we don't want to spend from it) + if (addressType === AddressType.p2tr) { + throw new Error('Cannot transfer using Taproot (P2TR) address. Please use another address type.'); + } + + // We need the public key to generate the redeem and witness script to spend the scripts + if (addressType === (AddressType.p2sh || AddressType.p2wsh)) { + if (!fromPubKey) { + throw new Error('Public key is required to spend from the selected address type'); + } + } + + const unsignedTx = await createTransfer( + 'mainnet', + addressType, + fromAddress, + toAddress, + amount, + fromPubKey, + opReturn, + ); + + const psbt = unsignedTx.toPSBT(0); + const psbtHex = hex.encode(psbt); + + // TODO: sign PSBT + const signedPsbtHex = psbtHex; + + const signedTx = SigTx.fromPSBT(bitcoin.Psbt.fromHex(signedPsbtHex).toBuffer()); + signedTx.finalize(); + + return bitcoin.Transaction.fromBuffer(Buffer.from(signedTx.extract())); +} diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 9c3e12ee..1167a5ea 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -8,7 +8,10 @@ "name": "@gobob/bob-sdk", "version": "1.2.0", "dependencies": { - "bitcoinjs-lib": "^6.1.6" + "@scure/base": "^1.1.6", + "@scure/btc-signer": "^1.3.1", + "bitcoin-address-validation": "^2.2.3", + "bitcoinjs-lib": "^6.1.5" }, "devDependencies": { "@types/node": "^20.14.8", @@ -93,6 +96,17 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -133,6 +147,28 @@ "linux" ] }, + "node_modules/@scure/base": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/btc-signer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.3.2.tgz", + "integrity": "sha512-BmcQHvxaaShKwgbFC0vDk0xzqbMhNtNmgXm6u7cz07FNtGsVItUuHow6NbgHmc+oJSBZJRym5dz8+Uu0JoEJhQ==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6", + "micro-packed": "~0.6.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -385,6 +421,14 @@ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", "license": "MIT" }, + "node_modules/base58-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", + "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/bech32": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", @@ -413,6 +457,16 @@ "node": ">=8.0.0" } }, + "node_modules/bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "dependencies": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, "node_modules/bitcoinjs-lib": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", @@ -440,13 +494,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -870,11 +923,10 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -916,6 +968,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1109,7 +1175,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1273,6 +1338,17 @@ "dev": true, "license": "MIT" }, + "node_modules/micro-packed": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.6.3.tgz", + "integrity": "sha512-VmVkyc7lIzAq/XCPFuLc/CwQ7Ehs5XDK3IwqsZHiBIDttAI9Gs7go6Lv4lNRuAIKrGKcRTtthFKUNyHS0S4wJQ==", + "dependencies": { + "@scure/base": "~1.1.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -1813,6 +1889,11 @@ "sha.js": "bin.js" } }, + "node_modules/sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2018,7 +2099,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/sdk/package.json b/sdk/package.json index d42ce326..e0438840 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "1.2.0", + "version": "2.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { @@ -29,6 +29,9 @@ "yargs": "^17.5.1" }, "dependencies": { - "bitcoinjs-lib": "^6.1.6" + "@scure/base": "^1.1.6", + "@scure/btc-signer": "^1.3.1", + "bitcoin-address-validation": "^2.2.3", + "bitcoinjs-lib": "^6.1.5" } } \ No newline at end of file diff --git a/sdk/scripts/relay-genesis.ts b/sdk/scripts/relay-genesis.ts index 713d4206..f1b23726 100644 --- a/sdk/scripts/relay-genesis.ts +++ b/sdk/scripts/relay-genesis.ts @@ -1,4 +1,4 @@ -import { DefaultElectrsClient } from "../src/electrs"; +import { DefaultEsploraClient } from "../src/esplora"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { exec } from "child_process"; @@ -50,18 +50,18 @@ function range(size: number, startAt = 0) { return [...Array(size).keys()].map(i => i + startAt); } -async function getRetargetHeaders(electrs: DefaultElectrsClient, nextRetargetHeight: number, proofLength: number) { - const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => electrs.getBlockHeaderAt(height))); - const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => electrs.getBlockHeaderAt(height))); +async function getRetargetHeaders(esploraClient: DefaultEsploraClient, nextRetargetHeight: number, proofLength: number) { + const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => esploraClient.getBlockHeaderAt(height))); + const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => esploraClient.getBlockHeaderAt(height))); return beforeRetarget.concat(afterRetarget).join(""); } async function main(): Promise { - const electrs = new DefaultElectrsClient(args["network"]); + const esploraClient = new DefaultEsploraClient(args["network"]); let initHeight = args["init-height"]; if (initHeight == "latest") { - const currentHeight = await electrs.getLatestHeight(); + const currentHeight = await esploraClient.getLatestHeight(); initHeight = currentHeight - (currentHeight % 2016) - 2016; console.log(`Using block ${initHeight}`) } @@ -69,13 +69,13 @@ async function main(): Promise { throw new Error("Invalid genesis height: must be multiple of 2016"); } - const genesis = await electrs.getBlockHeaderAt(initHeight); + const genesis = await esploraClient.getBlockHeaderAt(initHeight); const proofLength = args["proof-length"]; const nextRetargetHeight = initHeight + 2016; console.log(`Next retarget height: ${nextRetargetHeight}`); - const retargetHeaders = await getRetargetHeaders(electrs, nextRetargetHeight, proofLength); + const retargetHeaders = await getRetargetHeaders(esploraClient, nextRetargetHeight, proofLength); let rpcUrl: string; let verifyOpts: string | undefined; diff --git a/sdk/scripts/relay-retarget.ts b/sdk/scripts/relay-retarget.ts index 2fb60ca9..5f0915d9 100644 --- a/sdk/scripts/relay-retarget.ts +++ b/sdk/scripts/relay-retarget.ts @@ -1,4 +1,4 @@ -import { DefaultElectrsClient } from "../src/electrs"; +import { DefaultEsploraClient } from "../src/esplora"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { exec } from "child_process"; @@ -38,14 +38,14 @@ function range(size: number, startAt = 0) { return [...Array(size).keys()].map(i => i + startAt); } -async function getRetargetHeaders(electrs: DefaultElectrsClient, nextRetargetHeight: number, proofLength: number) { - const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => electrs.getBlockHeaderAt(height))); - const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => electrs.getBlockHeaderAt(height))); +async function getRetargetHeaders(esploraClient: DefaultEsploraClient, nextRetargetHeight: number, proofLength: number) { + const beforeRetarget = await Promise.all(range(proofLength, nextRetargetHeight - proofLength).map(height => esploraClient.getBlockHeaderAt(height))); + const afterRetarget = await Promise.all(range(proofLength, nextRetargetHeight).map(height => esploraClient.getBlockHeaderAt(height))); return beforeRetarget.concat(afterRetarget).join(""); } async function main(): Promise { - const electrs = new DefaultElectrsClient(args["network"]); + const esploraClient = new DefaultEsploraClient(args["network"]); let privateKey: string; if (args["private-key"]) { @@ -94,12 +94,12 @@ async function main(): Promise { console.log(`Next retarget height: ${nextRetargetHeight}`); try { - await electrs.getBlockHash(nextRetargetHeight + proofLength); + await esploraClient.getBlockHash(nextRetargetHeight + proofLength); } catch (e) { throw new Error(`Cannot retarget without ${proofLength} headers after ${nextRetargetHeight}`); } - const retargetHeaders = await getRetargetHeaders(electrs, nextRetargetHeight, proofLength); + const retargetHeaders = await getRetargetHeaders(esploraClient, nextRetargetHeight, proofLength); let env = [ `RELAY_ADDRESS=${relayAddress}`, diff --git a/sdk/src/electrs.ts b/sdk/src/esplora.ts similarity index 90% rename from sdk/src/electrs.ts rename to sdk/src/esplora.ts index 92ecf726..dc95f9d2 100644 --- a/sdk/src/electrs.ts +++ b/sdk/src/esplora.ts @@ -66,7 +66,7 @@ export interface Transaction { scriptpubkey: string scriptpubkey_asm?: string scriptpubkey_type?: string - scriptpubkey_address?: string + scriptpubkey_address?: string value: number }> status: { @@ -98,11 +98,11 @@ export interface Block { /** * - * The `ElectrsClient` interface provides a set of methods for interacting with an Esplora API + * The `EsploraClient` interface provides a set of methods for interacting with an Esplora API * for Bitcoin network data retrieval. * See https://github.com/blockstream/esplora/blob/master/API.md for more information. */ -export interface ElectrsClient { +export interface EsploraClient { /** * Get the latest block height of the Bitcoin chain. * @@ -119,9 +119,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const blockHash = 'your_block_hash_here'; - * electrsClient.getBlock(blockHash) + * esploraClient.getBlock(blockHash) * .then((block) => { * console.log(`Block data for block with hash ${blockHash}: ${JSON.stringify(block)}`); * }) @@ -143,9 +143,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const blockHeight = 123456; - * electrsClient.getBlockHash(blockHeight) + * esploraClient.getBlockHash(blockHeight) * .then((blockHash) => { * console.log(`Block hash at height ${blockHeight}: ${blockHash}`); * }) @@ -165,9 +165,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const blockHash = 'your_block_hash_here'; - * electrsClient.getBlockHeader(blockHash) + * esploraClient.getBlockHeader(blockHash) * .then((blockHeader) => { * console.log(`Raw block header for block with hash ${blockHash}: ${blockHeader}`); * }) @@ -187,9 +187,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const transactionId = 'your_transaction_id_here'; - * electrsClient.getTransaction(transactionId) + * esploraClient.getTransaction(transactionId) * .then((transaction) => { * console.log(`Transaction data for transaction with ID ${transactionId}: ${JSON.stringify(transaction)}`); * }) @@ -209,9 +209,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const transactionId = 'your_transaction_id_here'; - * electrsClient.getTransactionHex(transactionId) + * esploraClient.getTransactionHex(transactionId) * .then((transactionHex) => { * console.log(`Transaction hex for transaction with ID ${transactionId}: ${transactionHex}`); * }) @@ -231,9 +231,9 @@ export interface ElectrsClient { * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const transactionId = 'your_transaction_id_here'; - * electrsClient.getMerkleProof(transactionId) + * esploraClient.getMerkleProof(transactionId) * .then((merkleProof) => { * console.log(`Merkle inclusion proof for transaction with ID ${transactionId}: ${merkleProof}`); * }) @@ -272,33 +272,33 @@ export interface ElectrsClient { /** * @ignore */ -function encodeElectrsMerkleProof(merkle: string[]): string { +function encodeEsploraMerkleProof(merkle: string[]): string { // convert to little-endian return merkle.map(item => Buffer.from(item, "hex").reverse().toString("hex")).join(''); } /** - * The `DefaultElectrsClient` class provides a client for interacting with an Esplora API + * The `DefaultEsploraClient` class provides a client for interacting with an Esplora API * for Bitcoin network data retrieval. */ -export class DefaultElectrsClient implements ElectrsClient { +export class DefaultEsploraClient implements EsploraClient { private basePath: string; /** - * Create an instance of the `DefaultElectrsClient` with the specified network or URL. + * Create an instance of the `DefaultEsploraClient` with the specified network or URL. * If the `networkOrUrl` parameter is omitted, it defaults to "mainnet." * * @param networkOrUrl The Bitcoin network (e.g., "mainnet," "testnet," "regtest") * - * @returns An instance of the `DefaultElectrsClient` configured for the specified network or URL. + * @returns An instance of the `DefaultEsploraClient` configured for the specified network or URL. * * @example * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * * @example * // Create a client for the mainnet using the default URL. - * const electrsClientMainnet = new DefaultElectrsClient(); + * const esploraClientMainnet = new DefaultEsploraClient(); */ constructor(networkOrUrl: string = "mainnet") { switch (networkOrUrl) { @@ -374,7 +374,7 @@ export class DefaultElectrsClient implements ElectrsClient { }>(`${this.basePath}/tx/${txId}/merkle-proof`); return { blockHeight: response.block_height, - merkle: encodeElectrsMerkleProof(response.merkle), + merkle: encodeEsploraMerkleProof(response.merkle), pos: response.pos, }; } diff --git a/sdk/src/gateway.ts b/sdk/src/gateway.ts new file mode 100644 index 00000000..d07fdcd6 --- /dev/null +++ b/sdk/src/gateway.ts @@ -0,0 +1,88 @@ +export type EvmAddress = string; + +type GatewayQuote = { + onramp_address: EvmAddress; + dust_threshold: number; + satoshis: number; + fee: number; + gratuity: string; + bitcoin_address: string; + tx_proof_difficulty_factor: number; +}; + +type GatewayOrderResponse = { + onramp_address: EvmAddress; + token_address: EvmAddress; + txid: string; + status: boolean; + timestamp: number; + tokens: string; + satoshis: number; + fee: number; + tx_proof_difficulty_factor: number; +}; + +export class GatewayApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + async getQuote(address: string, atomicAmount?: number | string): Promise { + const response = await fetch(`${this.baseUrl}/quote/${address}/${atomicAmount || ''}`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + return await response.json(); + } + + // TODO: add error handling + async createOrder(contractAddress: string, userAddress: EvmAddress, atomicAmount: number | string): Promise { + const response = await fetch(`${this.baseUrl}/order`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ onramp_address: contractAddress, user_address: userAddress, satoshis: atomicAmount }) + }); + + if (!response.ok) { + throw new Error('Failed to create order'); + } + + return await response.json(); + } + + async updateOrder(id: string, tx: string) { + const response = await fetch(`${this.baseUrl}/order/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ bitcoin_tx: tx }) + }); + + if (!response.ok) { + throw new Error('Failed to update order'); + } + } + + async getOrders(userAddress: EvmAddress): Promise { + const response = await fetch(`${this.baseUrl}/orders/${userAddress}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + return response.json(); + } +} + diff --git a/sdk/src/helpers.ts b/sdk/src/helpers.ts index 5647427e..1fe8789c 100644 --- a/sdk/src/helpers.ts +++ b/sdk/src/helpers.ts @@ -1,11 +1,11 @@ -import { ElectrsClient, UTXO } from "./electrs"; +import { EsploraClient, UTXO } from "./esplora"; import { parseInscriptions } from "./inscription"; import { InscriptionId, OrdinalsClient } from "./ordinal-api"; import * as bitcoin from "bitcoinjs-lib"; // Get the (encoded) inscription IDs for the address -export async function getInscriptionIds(electrsClient: ElectrsClient, ordinalsClient: OrdinalsClient, bitcoinAddress: string) { - const utxos = await electrsClient.getAddressUtxos(bitcoinAddress); +export async function getInscriptionIds(esploraClient: EsploraClient, ordinalsClient: OrdinalsClient, bitcoinAddress: string) { + const utxos = await esploraClient.getAddressUtxos(bitcoinAddress); const inscriptionIds = await Promise.all( utxos.sort((a, b) => { // force large number if height is not available (as expected for unconfirmed utxo) @@ -13,20 +13,20 @@ export async function getInscriptionIds(electrsClient: ElectrsClient, ordinalsCl const heightB = b.height || Number.MAX_SAFE_INTEGER; return heightA - heightB; - }).map(utxo => getInscriptionIdsForUtxo(electrsClient, ordinalsClient, utxo)) + }).map(utxo => getInscriptionIdsForUtxo(esploraClient, ordinalsClient, utxo)) ); return inscriptionIds.flat(); } // Get the (encoded) inscription IDs for the UTXO -async function getInscriptionIdsForUtxo(electrsClient: ElectrsClient, ordinalsClient: OrdinalsClient, utxo: UTXO) { +async function getInscriptionIdsForUtxo(esploraClient: EsploraClient, ordinalsClient: OrdinalsClient, utxo: UTXO) { if (utxo.confirmed) { // use ord api if the tx has been included in a block const outputJson = await ordinalsClient.getInscriptionsFromOutPoint(utxo); return outputJson.inscriptions; } - const txHex = await electrsClient.getTransactionHex(utxo.txid); + const txHex = await esploraClient.getTransactionHex(utxo.txid); const tx = bitcoin.Transaction.fromHex(txHex); // FIXME: assumes inscriptions are always sent to the first output diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 80edeaf9..a7720e77 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,5 +1,7 @@ -export * from "./electrs"; +export * from "./esplora"; export * from "./relay"; export * from "./utils"; export * from "./ordinals"; export * from "./helpers"; +export * from "./wallet"; +export * from "./gateway"; \ No newline at end of file diff --git a/sdk/src/inscription.ts b/sdk/src/inscription.ts index 543e71ed..d1f97591 100644 --- a/sdk/src/inscription.ts +++ b/sdk/src/inscription.ts @@ -1,5 +1,5 @@ import * as bitcoin from "bitcoinjs-lib"; -import { ElectrsClient } from "./electrs"; +import { EsploraClient } from "./esplora"; import { InscriptionId } from "./ordinal-api"; const textEncoder = new TextEncoder(); @@ -84,7 +84,7 @@ export class Inscription { PROTOCOL_ID, ...this.getTags().map(([key, value]) => [ 1, - key, + key, value, ]).flat(), bitcoin.opcodes.OP_0, @@ -101,8 +101,8 @@ export module Inscription { export function createTextInscription(text: string): Inscription { return Inscription.createInscription( "text/plain;charset=utf-8", - Buffer.from(textEncoder.encode(text) - )); + Buffer.from(textEncoder.encode(text)) + ); } /** @@ -111,7 +111,7 @@ export module Inscription { export function createInscription(contentType: string, content: Buffer): Inscription { const inscription = new Inscription; // e.g. `image/png` - inscription.setContentType(contentType), + inscription.setContentType(contentType); inscription.body = content; return inscription; } @@ -187,14 +187,14 @@ export function parseInscriptions(tx: bitcoin.Transaction) { return inscriptions; } -export async function getTxInscriptions(electrsClient: ElectrsClient, txid: string) { - const txHex = await electrsClient.getTransactionHex(txid); +export async function getTxInscriptions(esploraClient: EsploraClient, txid: string) { + const txHex = await esploraClient.getTransactionHex(txid); const tx = bitcoin.Transaction.fromHex(txHex); return parseInscriptions(tx); } -export async function getInscriptionFromId(electrsClient: ElectrsClient, inscriptionId: string) { +export async function getInscriptionFromId(esploraClient: EsploraClient, inscriptionId: string) { const { txid, index } = InscriptionId.fromString(inscriptionId); - const inscriptions = await getTxInscriptions(electrsClient, txid); + const inscriptions = await getTxInscriptions(esploraClient, txid); return inscriptions[index]; } \ No newline at end of file diff --git a/sdk/src/ordinal-api/index.ts b/sdk/src/ordinal-api/index.ts index d90c4e3d..d60d2fc4 100644 --- a/sdk/src/ordinal-api/index.ts +++ b/sdk/src/ordinal-api/index.ts @@ -85,7 +85,7 @@ export module SatPoint { /** * @ignore */ -// https://github.com/ordinals/ord/blob/2badb82a8f4b2bb23a90b88a6d711b3475eb6c92/src/api.rs#L117-L121 +// https://github.com/ordinals/ord/blob/0.18.5/src/api.rs#L117-L121 export interface InscriptionsJson { /** * An array of inscription ids. @@ -106,7 +106,7 @@ export interface InscriptionsJson { /** * @ignore */ -// https://github.com/ordinals/ord/blob/2badb82a8f4b2bb23a90b88a6d711b3475eb6c92/src/api.rs#L124-L134 +// https://github.com/ordinals/ord/blob/0.18.5/src/api.rs#L124-L134 export interface OutputJson { /** * The address associated with the UTXO. @@ -126,7 +126,13 @@ export interface OutputJson { /** * A map of runes. */ - runes: [string, number][]; + runes: { + [key: string]: { + amount: number, + divisibility: number, + symbol: string | null, + } + }; /** * The SAT ranges. @@ -157,7 +163,7 @@ export interface OutputJson { /** * @ignore */ -// https://github.com/ordinals/ord/blob/4d29e078535bb8e630133a17cd3e9af22c631ebd/src/api.rs#L165-L180 +// https://github.com/ordinals/ord/blob/0.18.5/src/api.rs#L165-L180 export interface SatJson { /** * The number of the ordinal. @@ -184,6 +190,8 @@ export interface SatJson { */ block: number; + charms: number[], + /** * The cycle associated with the ordinal. */ @@ -233,7 +241,7 @@ export interface SatJson { /** * @ignore */ -// https://github.com/ordinals/ord/blob/2badb82a8f4b2bb23a90b88a6d711b3475eb6c92/src/api.rs#L80-L98 +// https://github.com/ordinals/ord/blob/0.18.5/src/api.rs#L80-L99 export interface InscriptionJson { /** * The address associated with the inscription. @@ -257,6 +265,8 @@ export interface InscriptionJson { */ content_type: string | null; + effective_content_type: String | null, + /** * The genesis fee of the inscription. */ @@ -283,10 +293,15 @@ export interface InscriptionJson { number: number; /** - * The parent inscription ID. + * The parent inscription IDs. */ parent: InscriptionId | null; + /** + * The parent inscription IDs. + */ + parents: string[]; + /** * The previous inscription ID. */ @@ -433,6 +448,7 @@ export class DefaultOrdinalsClient implements OrdinalsClient { * @ignore */ async getInscriptionFromId(id: InscriptionId): Promise> { + console.log(`${this.basePath}/inscription/${InscriptionId.toString(id)}`) const inscriptionJson = await this.getJson>(`${this.basePath}/inscription/${InscriptionId.toString(id)}`); return { ...inscriptionJson, diff --git a/sdk/src/relay.ts b/sdk/src/relay.ts index 40f716bf..1be8c9ec 100644 --- a/sdk/src/relay.ts +++ b/sdk/src/relay.ts @@ -7,7 +7,7 @@ import { Transaction } from "bitcoinjs-lib"; /** * @ignore */ -import { ElectrsClient } from "./electrs"; +import { EsploraClient } from "./esplora"; /** * @ignore */ @@ -42,23 +42,23 @@ export interface BitcoinTxInfo { /** * Retrieves information about a Bitcoin transaction, such as version, input vector, output vector, and locktime. * - * @param electrsClient - An ElectrsClient instance for interacting with the Electrum server. + * @param esploraClient - The EsploraClient instance for interacting with the Esplora server. * @param txId - The ID of the Bitcoin transaction. * @returns A promise that resolves to a BitcoinTxInfo object. * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const txId = "279121610d9575d132c95312c032116d6b8a58a3a31f69adf9736b493de96a16"; //enter the transaction id here - * const info = await getBitcoinTxInfo(electrsClient, txId); + * const info = await getBitcoinTxInfo(esploraClient, txId); * ``` */ export async function getBitcoinTxInfo( - electrsClient: ElectrsClient, + esploraClient: EsploraClient, txId: string, forWitness?: boolean, ): Promise { - const txHex = await electrsClient.getTransactionHex(txId); + const txHex = await esploraClient.getTransactionHex(txId); const tx = Transaction.fromHex(txHex); const versionBuffer = Buffer.allocUnsafe(4); @@ -98,25 +98,25 @@ export interface BitcoinTxProof { /** * Retrieves a proof for a Bitcoin transaction, including the merkle proof, transaction index in the block, and Bitcoin headers. * - * @param electrsClient - An ElectrsClient instance for interacting with the Electrum server. + * @param esploraClient - The EsploraClient instance for interacting with the Esplora server. * @param txId - The ID of the Bitcoin transaction. * @param txProofDifficultyFactor - The number of block headers to retrieve for proof verification. * @example * ```typescript * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const txId = "279121610d9575d132c95312c032116d6b8a58a3a31f69adf9736b493de96a16";//enter the transaction id here * const txProofDifficultyFactor = "1";//enter the difficulty factor - * const info = await getBitcoinTxProof(electrsClient, txId, txProofDifficultyFactor); + * const info = await getBitcoinTxProof(esploraClient, txId, txProofDifficultyFactor); * ``` */ export async function getBitcoinTxProof( - electrsClient: ElectrsClient, + esploraClient: EsploraClient, txId: string, txProofDifficultyFactor: number, ): Promise { - const merkleProof = await electrsClient.getMerkleProof(txId); - const bitcoinHeaders = await getBitcoinHeaders(electrsClient, merkleProof.blockHeight, txProofDifficultyFactor); + const merkleProof = await esploraClient.getMerkleProof(txId); + const bitcoinHeaders = await getBitcoinHeaders(esploraClient, merkleProof.blockHeight, txProofDifficultyFactor); return { merkleProof: merkleProof.merkle, @@ -126,9 +126,9 @@ export async function getBitcoinTxProof( } /** - * Retrieves Bitcoin block headers using an Electrs client. + * Retrieves Bitcoin block headers using the Esplora client. * - * @param electrsClient - The ElectrsClient instance used to interact with the Esplora API. + * @param esploraClient - The EsploraClient instance used to interact with the Esplora API. * @param startHeight - The starting block height from which to fetch headers. * @param numBlocks - The number of consecutive block headers to retrieve. * @returns A Promise that resolves to a concatenated string of Bitcoin block headers. @@ -137,11 +137,11 @@ export async function getBitcoinTxProof( * * @example * const BITCOIN_NETWORK = "regtest"; - * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const esploraClient = new DefaultEsploraClient(BITCOIN_NETWORK); * const startHeight = 0; * const numBlocks = 10; * - * getBitcoinHeaders(electrsClient, startHeight, numBlocks) + * getBitcoinHeaders(esploraClient, startHeight, numBlocks) * .then(headers => { * console.log(headers); // Concatenated block headers as a string. * }) @@ -150,7 +150,7 @@ export async function getBitcoinTxProof( * }); */ export async function getBitcoinHeaders( - electrsClient: ElectrsClient, + esploraClient: EsploraClient, startHeight: number, numBlocks: number, ): Promise { @@ -158,8 +158,8 @@ export async function getBitcoinHeaders( const blockHeights = range(startHeight, startHeight + numBlocks); const bitcoinHeaders = await Promise.all(blockHeights.map(async height => { - const hash = await electrsClient.getBlockHash(height); - return electrsClient.getBlockHeader(hash); + const hash = await esploraClient.getBlockHash(height); + return esploraClient.getBlockHeader(hash); })); return bitcoinHeaders.join(''); diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 04bd6c9a..bc9a6924 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -18,6 +18,11 @@ import { hash256 } from "bitcoinjs-lib/src/crypto"; * @ignore */ import { Output, Transaction } from "bitcoinjs-lib/src/transaction"; +//@ts-nocheck +/** + * @ignore + */ +import * as bitcoin from "bitcoinjs-lib"; /** * @ignore @@ -175,3 +180,25 @@ export function getMerkleProof(block: Block, txHash: string, forWitness?: boolea root: merkleAndRoot.root.toString("hex"), }; } + +/** + * Estimate the tx inclusion fee for N P2WPKH inputs and 3 P2WPKH outputs. + * + * @param feeRate - The current rate for inclusion, satoshi per byte. + * @param numInputs - The number of inputs to estimate for. + * @returns The estimated fee for transaction inclusion. + */ +export function estimateTxFee(feeRate: number, numInputs: number = 1) { + const tx = new bitcoin.Transaction(); + for (let i = 0; i < numInputs; i++) { + tx.addInput(Buffer.alloc(32, 0), 0, 0xfffffffd, Buffer.alloc(0)); + } + // https://github.com/interlay/interbtc-clients/blob/6bd3e81d695b93180c5aeae4f33910ad4395ff1a/bitcoin/src/light/wallet.rs#L80 + tx.ins.map(tx_input => (tx_input.witness = [Buffer.alloc(33 + 32 + 7, 0), Buffer.alloc(33, 0)])); + tx.addOutput(Buffer.alloc(22, 0), 1000); // P2WPKH + tx.addOutput(Buffer.alloc(22, 0), 1000); // P2WPKH (change) + tx.addOutput(bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, Buffer.alloc(20, 0)]), 0); + const vsize = tx.virtualSize(); + const satoshis = feeRate * vsize; + return satoshis; +} diff --git a/sdk/src/wallet/address.ts b/sdk/src/wallet/address.ts new file mode 100644 index 00000000..aaa3f7f5 --- /dev/null +++ b/sdk/src/wallet/address.ts @@ -0,0 +1,18 @@ +import { BitcoinNetworkName } from "./utxo"; + +const BTC_MAINNET_REGEX = /\b([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[ac-hj-np-zAC-HJ-NP-Z02-9]{11,71})\b/; +const BTC_TESTNET_REGEX = /\b([2mn][a-km-zA-HJ-NP-Z1-9]{25,34}|tb1[ac-hj-np-zAC-HJ-NP-Z02-9]{11,71})\b/; + +const mainnetRegex = new RegExp(BTC_MAINNET_REGEX); +const testnetRegex = new RegExp(BTC_TESTNET_REGEX); + +export const isValidBtcAddress = (network: BitcoinNetworkName, address: string): boolean => { + switch (network) { + case 'mainnet': + return mainnetRegex.test(address); + case 'testnet': + return testnetRegex.test(address); + default: + throw new Error(`Invalid bitcoin network configured: ${network}. Valid values are: mainnet | testnet.`); + } +}; diff --git a/sdk/src/wallet/index.ts b/sdk/src/wallet/index.ts new file mode 100644 index 00000000..43b97889 --- /dev/null +++ b/sdk/src/wallet/index.ts @@ -0,0 +1,2 @@ +export * from "./address"; +export * from "./utxo"; \ No newline at end of file diff --git a/sdk/src/wallet/inscriptions.ts b/sdk/src/wallet/inscriptions.ts new file mode 100644 index 00000000..1c789af6 --- /dev/null +++ b/sdk/src/wallet/inscriptions.ts @@ -0,0 +1,55 @@ +import { EsploraClient, UTXO } from "../esplora"; +import { getTxInscriptions } from "../inscription"; +import { DefaultOrdinalsClient, InscriptionId, OrdinalsClient } from "../ordinal-api"; + +export async function findUtxoForInscriptionId( + esploraClient: EsploraClient, + ordinalsClient: OrdinalsClient, + utxos: UTXO[], + inscriptionId: string +): Promise { + // TODO: can we get the current UTXO of the inscription from ord? + // we can use the satpoint for this + const { txid, index } = InscriptionId.fromString(inscriptionId) + + for (const utxo of utxos) { + if (utxo.confirmed) { + const inscriptionUtxo = await ordinalsClient.getInscriptionsFromOutPoint(utxo) + if (inscriptionUtxo.inscriptions && inscriptionUtxo.inscriptions.includes(inscriptionId)) { + return utxo; + } + } else if (txid == utxo.txid) { + const inscriptions = await getTxInscriptions(esploraClient, utxo.txid); + + if (typeof inscriptions[index] !== 'undefined') { + return utxo; + } + } + } + + return undefined; +} + +export async function findUtxosWithoutInscriptions(network: string, utxos: UTXO[]): Promise { + const ordinalsClient = new DefaultOrdinalsClient(network); + + const safeUtxos: UTXO[] = []; + + // Exclude UTXOs that are uncomfirmed or have inscriptions + await Promise.all([ + utxos.map(async (utxo) => { + if (utxo.confirmed) { + const inscription = await ordinalsClient.getInscriptionsFromOutPoint({ + txid: utxo.txid, + vout: utxo.vout + }); + + if (inscription.inscriptions.length === 0) { + safeUtxos.push(utxo); + } + } + }) + ]); + + return safeUtxos; +} diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts new file mode 100644 index 00000000..3b4d425e --- /dev/null +++ b/sdk/src/wallet/utxo.ts @@ -0,0 +1,159 @@ +import { Transaction, Script, selectUTXO, TEST_NETWORK, NETWORK, p2wpkh, p2sh } from '@scure/btc-signer'; +import { hex } from "@scure/base"; +import { AddressType } from 'bitcoin-address-validation'; + +import { DefaultEsploraClient, UTXO } from '../esplora'; + +export type BitcoinNetworkName = 'mainnet' | 'testnet'; + +const bitcoinNetworks: Record = { + mainnet: NETWORK, + testnet: TEST_NETWORK +}; + +export const getBtcNetwork = (name: BitcoinNetworkName) => { + return bitcoinNetworks[name]; +}; + +type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint }; + +export interface Input { + txid: string; + index: number; + witness_script?: Uint8Array; + redeemScript?: Uint8Array; + witnessUtxo?: { + script: Uint8Array; + amount: bigint; + }; + nonWitnessUtxo?: Uint8Array; +} + +export async function createTransfer( + network: BitcoinNetworkName, + addressType: AddressType, + fromAddress: string, + toAddress: string, + amount: number, + publicKey?: string, + opReturnData?: string, + confirmationTarget: number = 3, +): Promise { + const esploraClient = new DefaultEsploraClient(network); + + // NOTE: esplora only returns the 25 most recent UTXOs + // TODO: change this to use the pagination API and return all UTXOs + const [confirmedUtxos, feeRate] = await Promise.all([ + esploraClient.getAddressUtxos(fromAddress), + esploraClient.getFeeEstimate(confirmationTarget) + ]); + + if (confirmedUtxos.length === 0) { + throw new Error('No confirmed UTXOs'); + } + + // To construct the spending transaction and estimate the fee, we need the transactions for the UTXOs + let possibleInputs: Input[] = []; + + await Promise.all( + confirmedUtxos.map(async (utxo) => { + const hex = await esploraClient.getTransactionHex(utxo.txid); + + const transaction = Transaction.fromRaw(Buffer.from(hex, 'hex'), { allowUnknownOutputs: true }); + + const input = getInputFromUtxoAndTx(network, utxo, transaction, addressType, publicKey); + + possibleInputs.push(input); + }) + ); + + const outputs: Output[] = [ + { + address: toAddress, + amount: BigInt(amount) + } + ]; + + if (opReturnData) { + // Strip 0x prefix from opReturn + if (opReturnData.startsWith('0x')) { + opReturnData = opReturnData.slice(2); + } + outputs.push({ + // OP_RETURN https://github.com/paulmillr/scure-btc-signer/issues/26 + script: Script.encode(['RETURN', hex.decode(opReturnData)]), + amount: BigInt(0) + }) + } + + // Outsource UTXO selection to btc-signer + // https://github.com/paulmillr/scure-btc-signer?tab=readme-ov-file#utxo-selection + // default = exactBiggest/accumBiggest creates tx with smallest fees, but it breaks + // big outputs to small ones, which in the end will create a lot of outputs close to dust. + const transaction = selectUTXO(possibleInputs, outputs, 'default', { + changeAddress: fromAddress, // Refund surplus to the payment address + feePerByte: BigInt(Math.ceil(feeRate)), // round up to the nearest integer + bip69: true, // Sort inputs and outputs according to BIP69 + createTx: true, // Create the transaction + network: getBtcNetwork(network), + allowUnknownOutputs: true, // Required for OP_RETURN + allowLegacyWitnessUtxo: true // Required for P2SH-P2WPKH + }); + + if (!transaction || !transaction.tx) { + throw new Error('Failed to create transaction. Do you have enough funds?'); + } + + return transaction.tx; +} + +// Using the UTXO and the transaction, we can construct the input for the transaction +export function getInputFromUtxoAndTx( + network: BitcoinNetworkName, + utxo: UTXO, + transaction: Transaction, + addressType: AddressType, + pubKey?: string +): Input { + // The output containts the necessary details to spend the UTXO based on the script type + // Under the hood, @scure/btc-signer parses the output and extracts the script and amount + const output = transaction.getOutput(utxo.vout); + + // For p2sh, we additionally need the redeem script. This cannot be extracted from the transaction itself + // We only support P2SH-P2WPKH + // TODO: add support for P2WSH + // TODO: add support for P2SH-P2PKH + let redeemScript = {}; + + if (addressType === AddressType.p2sh) { + const inner = p2wpkh(Buffer.from(pubKey!, 'hex'), getBtcNetwork(network)); + redeemScript = p2sh(inner); + } + + // For the redeem and witness script, we need to construct the script mixin + const scriptMixin = { + ...redeemScript + }; + + + const nonWitnessUtxo = { + nonWitnessUtxo: Buffer.from(transaction.hex, 'hex') + }; + const witnessUtxo = { + witnessUtxo: { + script: output.script!, + amount: output.amount! + } + }; + const witnessMixin = transaction.hasWitnesses ? witnessUtxo : nonWitnessUtxo; + + // Construct inputs based on the script type + const input = { + txid: utxo.txid, + index: utxo.vout, + ...scriptMixin, // Maybe adds the redeemScript and/or witnessScript + ...witnessMixin // Adds the witnessUtxo or nonWitnessUtxo + }; + + return input; +} diff --git a/sdk/test/electrs.test.ts b/sdk/test/esplora.test.ts similarity index 53% rename from sdk/test/electrs.test.ts rename to sdk/test/esplora.test.ts index d12d975a..f23c403b 100644 --- a/sdk/test/electrs.test.ts +++ b/sdk/test/esplora.test.ts @@ -1,15 +1,15 @@ import { assert, describe, it } from "vitest"; -import { DefaultElectrsClient, Transaction, Block } from "../src/electrs"; +import { DefaultEsploraClient, Transaction, Block } from "../src/esplora"; -describe("Electrs Tests", () => { +describe("Esplora Tests", () => { it("should get block height", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const height = await client.getLatestHeight(); assert(height > 0); }); it("should get block", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const block = await client.getBlock("000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); const expectedBlock: Block = { id: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', @@ -33,71 +33,71 @@ describe("Electrs Tests", () => { }); it("should get block hash", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const blockHash = await client.getBlockHash(0); - assert.equal(blockHash,"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); + assert.equal(blockHash, "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); }); it("should get block header", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const blockHeader = await client.getBlockHeader("000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); - assert.equal(blockHeader,"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18"); + assert.equal(blockHeader, "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18"); }); it("should get block header at certain height", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const blockHeader = await client.getBlockHeaderAt(10); - assert.equal(blockHeader,"010000001e93aa99c8ff9749037d74a2207f299502fa81d56a4ea2ad5330ff50000000002ec2266c3249ce2e079059e0aec01a2d8d8306a468ad3f18f06051f2c3b1645435e9494dffff001d008918cf"); + assert.equal(blockHeader, "010000001e93aa99c8ff9749037d74a2207f299502fa81d56a4ea2ad5330ff50000000002ec2266c3249ce2e079059e0aec01a2d8d8306a468ad3f18f06051f2c3b1645435e9494dffff001d008918cf"); }); it("should get transaction", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const tx = await client.getTransaction("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"); const expectedTransaction: Transaction = { txid: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', version: 1, locktime: 0, vin: [ - { - txid: '0000000000000000000000000000000000000000000000000000000000000000', - vout: 4294967295, - prevout: null, - scriptsig: '04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73', - scriptsig_asm: 'OP_PUSHBYTES_4 ffff001d OP_PUSHBYTES_1 04 OP_PUSHBYTES_69 5468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73', - is_coinbase: true, - sequence: 4294967295 - } + { + txid: '0000000000000000000000000000000000000000000000000000000000000000', + vout: 4294967295, + prevout: null, + scriptsig: '04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73', + scriptsig_asm: 'OP_PUSHBYTES_4 ffff001d OP_PUSHBYTES_1 04 OP_PUSHBYTES_69 5468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73', + is_coinbase: true, + sequence: 4294967295 + } ], vout: [ - { - scriptpubkey: '4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac', - scriptpubkey_asm: 'OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG', - scriptpubkey_type: 'p2pk', - value: 5000000000 - } + { + scriptpubkey: '4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac', + scriptpubkey_asm: 'OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG', + scriptpubkey_type: 'p2pk', + value: 5000000000 + } ], size: 204, weight: 816, fee: 0, status: { - confirmed: true, - block_height: 0, - block_hash: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', - block_time: 1296688602 + confirmed: true, + block_height: 0, + block_hash: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', + block_time: 1296688602 } }; assert.deepEqual(tx, expectedTransaction); }); it("should get tx hex", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const txHex = await client.getTransactionHex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"); - assert.equal(txHex,"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"); + assert.equal(txHex, "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"); }); it("should serialize merkle proof", async () => { - const client = new DefaultElectrsClient(); + const client = new DefaultEsploraClient(); const proof = await client.getMerkleProof("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735"); assert.equal(proof.merkle, "ace8423f874c95f5f9042d7cda6b9f0727251f3059ef827f373a56831cc621a371db6dfce8daed1d809275" + @@ -111,10 +111,10 @@ describe("Electrs Tests", () => { }); it("should get fee rate", async () => { - const client = new DefaultElectrsClient("testnet"); + const client = new DefaultEsploraClient("testnet"); const feeRate = await client.getFeeEstimate(1); assert.isAbove(feeRate, 1); }); - + }); diff --git a/sdk/test/inscription.test.ts b/sdk/test/inscription.test.ts index 4ffcf90b..81e5479a 100644 --- a/sdk/test/inscription.test.ts +++ b/sdk/test/inscription.test.ts @@ -1,5 +1,5 @@ import * as bitcoin from "bitcoinjs-lib"; -import { DefaultElectrsClient } from "../src/electrs"; +import { DefaultEsploraClient } from "../src/esplora"; import { Inscription, PROTOCOL_ID, parseInscriptions } from "../src/inscription"; import { assert, describe, it } from "vitest"; @@ -23,9 +23,9 @@ function createOrdinalTransaction(outputScript: Buffer) { describe("Inscription Tests", () => { it("should parse text inscription", async () => { - const electrsClient = new DefaultElectrsClient("mainnet"); + const esploraClient = new DefaultEsploraClient("mainnet"); - const txHex = await electrsClient.getTransactionHex("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735"); + const txHex = await esploraClient.getTransactionHex("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735"); const tx = bitcoin.Transaction.fromHex(txHex); const inscriptions = parseInscriptions(tx); @@ -48,9 +48,9 @@ describe("Inscription Tests", () => { }); it("should parse image inscription", async () => { - const electrsClient = new DefaultElectrsClient("mainnet"); + const esploraClient = new DefaultEsploraClient("mainnet"); - const txHex = await electrsClient.getTransactionHex("79ddcce9b4aaa4d2c3ba512a1dfd9bf2dd1f840eab98101c41bf8b801bcb3e0c"); + const txHex = await esploraClient.getTransactionHex("79ddcce9b4aaa4d2c3ba512a1dfd9bf2dd1f840eab98101c41bf8b801bcb3e0c"); const tx = bitcoin.Transaction.fromHex(txHex); const inscriptions = parseInscriptions(tx); diff --git a/sdk/test/ordinal-api.test.ts b/sdk/test/ordinal-api.test.ts index 455d12de..a543a448 100644 --- a/sdk/test/ordinal-api.test.ts +++ b/sdk/test/ordinal-api.test.ts @@ -8,38 +8,40 @@ import { import { assert, describe, it } from "vitest"; describe("Ordinal API Tests", () => { - // TODO: change to use ordi it("should get inscription from id", async () => { - const client = new DefaultOrdinalsClient("testnet"); + const client = new DefaultOrdinalsClient("mainnet"); + // Deploy ORDI - BRC20 const inscriptionJson = await client.getInscriptionFromId({ - txid: "74c86592f75716a14a534898913e6077fb5d7650cfc17600868964bbe2b7e512", + txid: "b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735", index: 0, }); const expectedInscriptionJson: InscriptionJson = { - address: 'tb1qn50zg73kl8f8wkn8358n4z2drvwraxhl7zdzly', + address: 'bc1pxaneaf3w4d27hl2y93fuft2xk6m4u3wc4rafevc6slgd7f5tq2dqyfgy06', charms: [], children: [], - content_length: 868, - content_type: 'text/javascript', - fee: 395, - height: 2537128, - id: InscriptionId.fromString('74c86592f75716a14a534898913e6077fb5d7650cfc17600868964bbe2b7e512i0'), - number: 560474, - next: InscriptionId.fromString('dd90d8222da2a6f3260109b1e4d1a2c341d999fce4707b1d77e49956a51a0305i0'), + content_length: 94, + content_type: 'text/plain;charset=utf-8', + effective_content_type: "text/plain;charset=utf-8", + fee: 4830, + height: 779832, + id: InscriptionId.fromString('b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735i0'), + number: 348020, + next: InscriptionId.fromString('693bd98380ad6e58f83de6068c236c6eb9d629c825cc3342c2d93f24c6762c6di0'), parent: null, - previous: InscriptionId.fromString('332d3fae125de51de29e97cd9e80aab7c63025d5094944a3dceb117c556c41cci0'), + parents: [], + previous: InscriptionId.fromString('4f0ff6259efa9d56b16664e6c5c9755c148818dc6bbca98f7f9166b277e4b7c0i0'), rune: null, sat: null, - satpoint: SatPoint.fromString('a4f11b32041419829b56fe456a976efef0c3ba557cf6041918e81e5d3265b884:2:96181932'), - timestamp: 1699246476, - value: 156405502, + satpoint: SatPoint.fromString('b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735:0:0'), + timestamp: 1678248991, + value: 10000, }; - assert.deepStrictEqual(expectedInscriptionJson, inscriptionJson); + assert.deepStrictEqual(inscriptionJson, expectedInscriptionJson); }); - it("should get inscriptions", async () => { + it("should get inscriptions", { timeout: 10000 }, async () => { const client = new DefaultOrdinalsClient("testnet"); const inscriptionsJson = await client.getInscriptions(); // assert that inscriptionsJson is not null, undefined or empty @@ -59,7 +61,7 @@ describe("Ordinal API Tests", () => { more: false, page_index: 0, }; - assert.deepStrictEqual(expectedInscriptionsJson, inscriptionsJson); + assert.deepStrictEqual(inscriptionsJson, expectedInscriptionsJson); }); it("should get inscriptions from UTXO", async () => { @@ -75,11 +77,11 @@ describe("Ordinal API Tests", () => { transaction: 'd370be1b6bf74677c82226d7a0d65743cbe3846b9216e0ad207a7b03a5230ec3', sat_ranges: null, inscriptions: [], - runes: [], + runes: {}, indexed: false, spent: true }; - assert.deepStrictEqual(expectedOutputJson, outputJson); + assert.deepStrictEqual(outputJson, expectedOutputJson); }); it("should get inscriptions from Sat", async () => { @@ -92,6 +94,7 @@ describe("Ordinal API Tests", () => { degree: '0°0′0″100‴', name: 'nvtdijuwxht', block: 0, + charms: [], cycle: 0, epoch: 0, period: 0, @@ -102,10 +105,10 @@ describe("Ordinal API Tests", () => { timestamp: 1296688602, inscriptions: [] }; - assert.deepStrictEqual(expectedSatJson, satJson); + assert.deepStrictEqual(satJson, expectedSatJson); }); - it("should get inscriptions from start block", async () => { + it("should get inscriptions from start block", { timeout: 10000 }, async () => { const client = new DefaultOrdinalsClient("testnet"); const startBlock: number = 2537138; const inscriptions = await client.getInscriptionsFromStartBlock(startBlock); diff --git a/sdk/test/relay.test.ts b/sdk/test/relay.test.ts index f3fda613..3fb50830 100644 --- a/sdk/test/relay.test.ts +++ b/sdk/test/relay.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "vitest"; -import { DefaultElectrsClient } from "../src/electrs"; +import { DefaultEsploraClient } from "../src/esplora"; import { getBitcoinTxInfo, getBitcoinTxProof } from "../src/relay"; import * as bitcoin from "bitcoinjs-lib"; import { encodeRawOutput } from "../src/utils"; @@ -19,7 +19,7 @@ describe("Relay Tests", () => { }); it("should get tx info", async () => { - const client = new DefaultElectrsClient(); + const client = new DefaultEsploraClient(); const txId = "2ef69769cc0ee81141c79552de6b91f372ff886216dbfa84e5497a16b0173e79"; const txInfo = await getBitcoinTxInfo(client, txId); assert.deepEqual(txInfo, { @@ -32,7 +32,7 @@ describe("Relay Tests", () => { }); it("should get tx proof", async () => { - const client = new DefaultElectrsClient(); + const client = new DefaultEsploraClient(); const txId = "2ef69769cc0ee81141c79552de6b91f372ff886216dbfa84e5497a16b0173e79"; const txProof = await getBitcoinTxProof(client, txId, 2); assert.equal(txProof.txIndexInBlock, 1); diff --git a/sdk/test/utils.test.ts b/sdk/test/utils.test.ts index eeee2da7..1cb2e329 100644 --- a/sdk/test/utils.test.ts +++ b/sdk/test/utils.test.ts @@ -1,11 +1,11 @@ import { assert, describe, it } from "vitest"; -import { MAINNET_ESPLORA_BASE_PATH } from "../src/electrs"; +import { MAINNET_ESPLORA_BASE_PATH } from "../src/esplora"; import { Block } from "bitcoinjs-lib"; -import { getMerkleProof } from "../src/utils"; +import { estimateTxFee, getMerkleProof } from "../src/utils"; describe("Utils Tests", () => { // NOTE: this is a bit flaky due to slow response times from electrs - it("should construct witness merkle proof from block", async () => { + it("should construct witness merkle proof from block", { timeout: 20000, skip: true }, async () => { const hash = "000000000000000000015712838394aeb93f5d45d0e5bec197382c08b375016e"; const response = await fetch(`${MAINNET_ESPLORA_BASE_PATH}/block/${hash}/raw`); const blob = await response.blob() @@ -18,5 +18,9 @@ describe("Utils Tests", () => { proof: '6034ddf453f5dd20de449b29b1221dede67ccae56f00528e0767e2ab506db31c4d2946e88f7efa3e94bb17bbd10f3f44172b59c48f2eb6bd7f67a88d149373ee4082c8b474ccf00906a1e61694fdf0b717790ac3bdf850b36afb8df107aca93b7c3c4f91ddf49c7f74244336c5833377d40760ae09dd1fba83063ace480f94cca3920a489b23f9133fc84d7987d990acc7c2569a81b547a5f65385856d90100e84878b4f305a3909a9420293cdc741109864c9338ea326449a7a303b227f2b10490bc4343355e1a391f51c42918a894c2980012cca5ffd4b56a6702abd98497802de83f5889b2ad5bd157762a58505948f32f42b9fa886c93bf30fef6144a64666843a28ef13184f9e7ac3c34b5741f58c8895a0167f496e0157e7d0a97f4041f97b8df4d8aee81d20d0d062ed3ee0f9b0afb196bdf5373712883cacdfd8349b739c0e6e41d650d05727ea5faec197bfa563d19b0150fba718ba1981aea9ef90', root: '7cee5e99c8f0fc25fb115b7d7d00befca61f59a8544adaf3980f52132baf61ae' }); - }, { timeout: 20000, skip: true }); + }); + + it("should estimate fee", async () => { + assert.equal(estimateTxFee(1), 172); + }); }); diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts new file mode 100644 index 00000000..f9d1d5a6 --- /dev/null +++ b/sdk/test/utxo.test.ts @@ -0,0 +1,192 @@ +import { describe, it, assert } from 'vitest'; +import { AddressType, getAddressInfo } from 'bitcoin-address-validation'; +import { Address, NETWORK, OutScript, Script, Transaction } from '@scure/btc-signer'; +import { hex } from '@scure/base'; + +import { createTransfer, getInputFromUtxoAndTx } from '../src/wallet/utxo'; +import { TransactionOutput } from '@scure/btc-signer/psbt'; + +// TODO: Add more tests using https://github.com/paulmillr/scure-btc-signer/tree/5ead71ea9a873d8ba1882a9cd6aa561ad410d0d1/test/bitcoinjs-test/fixtures/bitcoinjs +// TODO: Ensure that the paymentAddresses have sufficient funds to create the transaction +describe('UTXO Tests', () => { + it('should spend from address to create a transaction with an OP return output', { timeout: 10000 }, async () => { + const network = 'mainnet'; + // Addresses where randomly picked from blockstream.info + const paymentAddresses = [ + // P2WPKH: https://blockstream.info/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq + 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + // P2SH-P2WPKH: https://blockstream.info/address/3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr + '3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr', + // P2PKH: https://blockstream.info/address/1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g + '1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g' + ]; + + const toAddresses = [ + // P2SH + '35iMHbUZeTssxBodiHwEEkb32jpBfVueEL', + // P2WSH + 'bc1q6rgl33d3s9dugudw7n68yrryajkr3ha9q8q24j20zs62se4q9tsqdy0t2q', + // P2WPKH + 'bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d', + // P2PKH + '1Pr4Y216BpyGxj1Qa9GUzLQU6uUuzE61YS', + // P2TR + 'bc1peqr5a5kfufvsl66444jm9y8qq0s87ph0zv4lfkcs7h40ew02uvsqkhjav0' + ]; + const amount = 1000; + + // EVM address for OP return + let opReturn = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + + // Refactor to execute in parallel + await Promise.all( + toAddresses.map(async (toAddress) => { + await Promise.all( + paymentAddresses.map(async (paymentAddress) => { + const paymentAddressType = getAddressInfo(paymentAddress).type; + + let pubkey: string | undefined; + + if (paymentAddressType === AddressType.p2sh) { + // Use a random public key for P2SH-P2WPKH + pubkey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; + } + const transaction = await createTransfer( + network, + paymentAddressType, + paymentAddress, + toAddress, + amount, + pubkey, + opReturn, + ); + + assert(transaction); + + // Check that output conditions are correct + const addressType = getAddressInfo(toAddress).type; + + // Get all outputs and add them to array + const outputs: TransactionOutput[] = []; + + for (var i = 0; i < transaction.outputsLength; i++) { + const output = transaction.getOutput(i); + + outputs.push(output); + } + + for (const output of outputs) { + // All outputs should have an amount and a script + assert.exists(output.amount); + assert.exists(output.script); + // Check OP_RETURN + if (opReturn.startsWith('0x')) { + opReturn = opReturn.slice(2); + } + if (output.amount! === BigInt(0)) { + const parsedScript = Script.decode(output.script!); + + assert.equal(parsedScript.length, 2); + assert.equal(parsedScript[0], 'RETURN'); + assert.deepEqual(parsedScript[1], hex.decode(opReturn)); + + // Check the transfer script to the toAddress + } else if (output.amount === BigInt(amount)) { + const scriptDecoded = OutScript.decode(output.script!) as any; + + // Remove "p2" from the address type as it's exluced in the OutScript type + assert.equal(scriptDecoded.type, addressType.slice(2)); + + const address = Address(NETWORK).decode(toAddress) as any; + + assert.deepEqual(scriptDecoded.hash, address.hash); + + // Check the possible change output + } else { + const scriptDecoded = OutScript.decode(output.script!) as any; + + // Remove "p2" from the address type as it's exluced in the OutScript type + assert.equal(scriptDecoded.type, paymentAddressType.slice(2)); + } + } + }) + ); + }) + ); + }); + + it('should get input from an UTXO and its transaction', async () => { + const testset = [ + // - P2WPKH (Unisat) + { + utxo: { + // https://blockstream.info/tx/47ba2a950608cc1cba218b98d18ee5bc74e4f80023f2ecd1d81e87a88557eec4 + txid: '47ba2a950608cc1cba218b98d18ee5bc74e4f80023f2ecd1d81e87a88557eec4', + vout: 2, + value: 100000, + confirmed: true, + height: 821504 + }, + transaction: Transaction.fromRaw( + Buffer.from( + '0100000000010192488f51132fc73b22bb4d1fd81b77537fe7cf51136a8440c271f84959d5cb590a00000000ffffffff0b37050a0000000000160014e8df018c7e326cc253faac7e46cdc51e68542c423703f9000000000016001476b266d98da6faba506dabc3b00677c64f188a5840420f0000000000160014d756a6f5d39388f233152e46cc6a158b4b6b09aa0ae70300000000001600141c68092871e29a51e89cb47e9c4cb9094e8601340d242b0000000000160014de9ddc9f2dac708171028a51191b07797b1e2232f56d1200000000001600140a92a0fff2025bd29a7850887539652882509c4c389c1c00000000001600147ab5220da0a05704f05877e7cff9bfb6c251205b93ef0300000000001600142c73866d315405c14943ba3da0b187aa3d7cd9441cd305000000000016001486b28a10dd335f089b9e6f1700f0375f0aebf80e7802e50000000000160014b3b58eafe9cfbbf56657cc89dec1d6c334829fe95124470d000000001600147ce3e33d3d4a123ca358399f5c0925a85dbe905d02473044022024d21268f3b3e4150962e2826612b2302db21e8e12bbe58f21f7460035205ef7022028375980ea8cebd8127565ef04ce2aa844a64be5ccc84789839a87beb866795f0121030c49ba250eaf17bc57bc07d07665647c72b81b9c420c8f65c1f5df2a0fbffad000000000', + 'hex' + ) + ), + addressType: AddressType.p2wpkh, + publicKey: undefined + }, + // - P2SH-P2WPKH (Xverse, Unisat) + { + utxo: { + // https://blockstream.info/tx/10f746b824fcd844f4615daf5faa10105ef0a3ad24d583c00e350f6e981515dc + txid: '10f746b824fcd844f4615daf5faa10105ef0a3ad24d583c00e350f6e981515dc', + vout: 4, + value: 6000, + confirmed: true, + height: 841314 + }, + transaction: Transaction.fromRaw( + Buffer.from( + '02000000000104f6ffa6acd416ee27c6381598f9d1ace8f57bd5f997205fe9c10ca717a81208e20100000017160014d32ae97372f0b2549d26fa40e33f0699cfe86d55fffffffff6ffa6acd416ee27c6381598f9d1ace8f57bd5f997205fe9c10ca717a81208e20400000017160014d32ae97372f0b2549d26fa40e33f0699cfe86d55ffffffff500f4b6891bd86538937ca96508b6b34fd1eb8869e61a63448c1c7a5307c32fdbf00000000ffffffff248a0badd48bdc94ab7438cd8df43915af9e3ce334e4f3217d18564343ccbf510500000017160014d32ae97372f0b2549d26fa40e33f0699cfe86d55ffffffff07b00400000000000017a9147ecd91afdcadf6f1b9e8e026a312e4cce61e63ea872202000000000000225120173c179ec304311da634604e70958ac92f39325915ca5c0778061155c7735eb59a9d000000000000225120dca54ace6e977c3071e81ce4ee359c1c6ee003586434a29fca3632b346006e6ce80300000000000017a914ea6b832a05c6ca578baa3836f3f25553d41068a587580200000000000017a9147ecd91afdcadf6f1b9e8e026a312e4cce61e63ea87580200000000000017a9147ecd91afdcadf6f1b9e8e026a312e4cce61e63ea87a88f00000000000017a9147ecd91afdcadf6f1b9e8e026a312e4cce61e63ea870247304402200534ee640f73a3d2c03cdd67b3e55a8e9af73132bbfef97cdb6e240011e51892022075f2de763e423465659c741f9c73baccb158ae1fec5782075664383c87a79b4e012103b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe102473044022066cbcc462d3d30faebb927496ae21945d9a9cca3d26a9908809ff90bab1ad89802201877bad7327af6d2c8ebd6afe94990215285fe5dbf057604bb4a4cdabdfe1af7012103b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe10141a942ec1a6d845dbe9d0fa117cebcee4968458f400ccc0ef2f9a872aae043c068a7158637c3e372d5312f2bcaab2405438b48ceb56cf16ada3bf5d087d1d3dbf08302483045022100e5538a760f9cc8dd5d369f4dc48a4b4c858962da55c6f70b2c10fae90f052fef02204c3f5105f4c0b5c2bfb01d41a465d840e8e8863fff3eb37999c8ee2997efb30e012103b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe100000000', + 'hex' + ) + ), + addressType: AddressType.p2sh, + publicKey: '030000000000000000000000000000000000000000000000000000000000000001' // use a random public key + }, + // - P2PKH (Unisat) + // https://github.com/paulmillr/scure-btc-signer/blob/5ead71ea9a873d8ba1882a9cd6aa561ad410d0d1/test/basic.test.js#L48 + { + utxo: { + // https://blockstream.info/tx/c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e?expand + txid: '1c261c03d93e03d72a65606719a5eaf6fac6d87d55b5db34e30a74e0c4a6da3e', + vout: 0, + value: 550, + confirmed: true, + height: 609815 + }, + transaction: Transaction.fromRaw( + Buffer.from( + '010000000160a941de6f9989628da24ede7730bb68990eac96da30379cec4da3cd3f1097cd010000008b4830450221009ee3ca47753ae5e4991358787ae660508222979493bd7d9b7191deff85d5e96102206a2d4b42a9d4afb3817d0e7a5b37c4d639eb16a52ad8c2d8c519f8802fff25920141049875dbe0f1f8ff9331b46630ad3fcb1c37c6516cf4b5e29d2ce0b0ae64935b40ba29e0669e9545768513a70bee36b699ca80cf37e245e24fb89907cb9a06f087ffffffff0226020000000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988acaa360b00000000001976a914a7158108fd0a615539a37122ed320784295ed1f388ac00000000', + 'hex' + ) + ), + addressType: AddressType.p2pkh, + publicKey: undefined + } + ]; + + for (const test of testset) { + const input = getInputFromUtxoAndTx( + 'testnet', + test.utxo, + test.transaction, + test.addressType, + test.publicKey + ); + + assert(input); + } + }); +}); diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index 4807292c..39829fda 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -15,6 +15,10 @@ "rootDir": "./src", "outDir": "./dist" }, - "include": ["src/**/*"], - "exclude": ["./node_modules/*"] -} + "include": [ + "src/**/*" + ], + "exclude": [ + "./node_modules/*" + ] +} \ No newline at end of file