UNPKG

@arklabs/wallet-sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

158 lines (157 loc) 5.57 kB
import * as bip68 from "bip68"; import { ScriptNum, Transaction } from "@scure/btc-signer"; import { sha256x2 } from "@scure/btc-signer/utils"; import { base64, hex } from "@scure/base"; export class TxTreeError extends Error { constructor(message) { super(message); this.name = "TxTreeError"; } } export const ErrLeafNotFound = new TxTreeError("leaf not found in tx tree"); export const ErrParentNotFound = new TxTreeError("parent not found"); // TxTree is represented as a matrix of Node objects // the first level of the matrix is the root of the tree export class TxTree { constructor(tree) { this.tree = tree; } get levels() { return this.tree; } // Returns the root node of the vtxo tree root() { if (this.tree.length <= 0 || this.tree[0].length <= 0) { throw new TxTreeError("empty vtxo tree"); } return this.tree[0][0]; } // Returns the leaves of the vtxo tree leaves() { const leaves = [...this.tree[this.tree.length - 1]]; // Check other levels for leaf nodes for (let i = 0; i < this.tree.length - 1; i++) { for (const node of this.tree[i]) { if (node.leaf) { leaves.push(node); } } } return leaves; } // Returns all nodes that have the given node as parent children(nodeTxid) { const children = []; for (const level of this.tree) { for (const node of level) { if (node.parentTxid === nodeTxid) { children.push(node); } } } return children; } // Returns the total number of nodes in the vtxo tree numberOfNodes() { return this.tree.reduce((count, level) => count + level.length, 0); } // Returns the branch of the given vtxo txid from root to leaf branch(vtxoTxid) { const branch = []; const leaves = this.leaves(); // Check if the vtxo is a leaf const leaf = leaves.find((leaf) => leaf.txid === vtxoTxid); if (!leaf) { throw ErrLeafNotFound; } branch.push(leaf); const rootTxid = this.root().txid; while (branch[0].txid !== rootTxid) { const parent = this.findParent(branch[0]); branch.unshift(parent); } return branch; } // Helper method to find parent of a node findParent(node) { for (const level of this.tree) { for (const potentialParent of level) { if (potentialParent.txid === node.parentTxid) { return potentialParent; } } } throw ErrParentNotFound; } // Validates that the tree is coherent by checking txids and parent relationships validate() { // Skip the root level, validate from level 1 onwards for (let i = 1; i < this.tree.length; i++) { for (const node of this.tree[i]) { // Verify that the node's transaction matches its claimed txid const tx = Transaction.fromPSBT(base64.decode(node.tx)); const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse()); if (txid !== node.txid) { throw new TxTreeError(`node ${node.txid} has txid ${node.txid}, but computed txid is ${txid}`); } // Verify that the node has a valid parent try { this.findParent(node); } catch (err) { throw new TxTreeError(`node ${node.txid} has no parent: ${err instanceof Error ? err.message : String(err)}`); } } } } } const COSIGNER_KEY_PREFIX = new Uint8Array("cosigner".split("").map((c) => c.charCodeAt(0))); const VTXO_TREE_EXPIRY_PSBT_KEY = new Uint8Array("expiry".split("").map((c) => c.charCodeAt(0))); export function getVtxoTreeExpiry(input) { if (!input.unknown) return null; for (const u of input.unknown) { // Check if key contains the VTXO tree expiry key if (u.key.length < VTXO_TREE_EXPIRY_PSBT_KEY.length) continue; let found = true; for (let i = 0; i < VTXO_TREE_EXPIRY_PSBT_KEY.length; i++) { if (u.key[i] !== VTXO_TREE_EXPIRY_PSBT_KEY[i]) { found = false; break; } } if (found) { const value = ScriptNum(6, true).decode(u.value); const { blocks, seconds } = bip68.decode(Number(value)); return { type: blocks ? "blocks" : "seconds", value: BigInt(blocks ?? seconds ?? 0), }; } } return null; } function parsePrefixedCosignerKey(key) { if (key.length < COSIGNER_KEY_PREFIX.length) return false; for (let i = 0; i < COSIGNER_KEY_PREFIX.length; i++) { if (key[i] !== COSIGNER_KEY_PREFIX[i]) return false; } return true; } export function getCosignerKeys(tx) { const keys = []; const input = tx.getInput(0); if (!input.unknown) return keys; for (const unknown of input.unknown) { const ok = parsePrefixedCosignerKey(new Uint8Array([unknown[0].type, ...unknown[0].key])); if (!ok) continue; // Assuming the value is already a valid public key in compressed format keys.push(unknown[1]); } return keys; }