UNPKG

@arkade-os/sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

184 lines (183 loc) 6.05 kB
import { OP, Script, SigHash } from "@scure/btc-signer"; import { schnorr } from "@noble/curves/secp256k1.js"; import { Transaction } from '../utils/transaction.js'; /** * Intent proof implementation for Bitcoin message signing. * * Intent proof defines a standard for signing Bitcoin messages as well as proving * ownership of coins. This namespace provides utilities for creating and * validating Intent proof. * * it is greatly inspired by BIP322. * @see https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki * * @example * ```typescript * // Create a Intent proof * const proof = Intent.create( * "Hello Bitcoin!", * [input], * [output] * ); * * // Sign the proof * const signedProof = await identity.sign(proof); * */ export var Intent; (function (Intent) { /** * Creates a new Intent proof unsigned transaction. * * This function constructs a special transaction that can be signed to prove * ownership of VTXOs and UTXOs. The proof includes the message to be * signed and the inputs/outputs that demonstrate ownership. * * @param message - The Intent message to be signed, either raw string of Message object * @param inputs - Array of transaction inputs to prove ownership of * @param outputs - Optional array of transaction outputs * @returns An unsigned Intent proof transaction */ function create(message, inputs, outputs = []) { if (typeof message !== "string") { message = encodeMessage(message); } if (inputs.length == 0) throw new Error("intent proof requires at least one input"); if (!validateInputs(inputs)) throw new Error("invalid inputs"); if (!validateOutputs(outputs)) throw new Error("invalid outputs"); // create the initial transaction to spend const toSpend = craftToSpendTx(message, inputs[0].witnessUtxo.script); // create the transaction to sign return craftToSignTx(toSpend, inputs, outputs); } Intent.create = create; function encodeMessage(message) { switch (message.type) { case "register": return JSON.stringify({ type: "register", onchain_output_indexes: message.onchain_output_indexes, valid_at: message.valid_at, expire_at: message.expire_at, cosigners_public_keys: message.cosigners_public_keys, }); case "delete": return JSON.stringify({ type: "delete", expire_at: message.expire_at, }); case "get-pending-tx": return JSON.stringify({ type: "get-pending-tx", expire_at: message.expire_at, }); } } Intent.encodeMessage = encodeMessage; })(Intent || (Intent = {})); const OP_RETURN_EMPTY_PKSCRIPT = new Uint8Array([OP.RETURN]); const ZERO_32 = new Uint8Array(32).fill(0); const MAX_INDEX = 0xffffffff; const TAG_INTENT_PROOF = "ark-intent-proof-message"; function validateInput(input) { if (input.index === undefined) throw new Error("intent proof input requires index"); if (input.txid === undefined) throw new Error("intent proof input requires txid"); if (input.witnessUtxo === undefined) throw new Error("intent proof input requires witness utxo"); return true; } function validateInputs(inputs) { inputs.forEach(validateInput); return true; } function validateOutput(output) { if (output.amount === undefined) throw new Error("intent proof output requires amount"); if (output.script === undefined) throw new Error("intent proof output requires script"); return true; } function validateOutputs(outputs) { outputs.forEach(validateOutput); return true; } // craftToSpendTx creates the initial transaction that will be spent in the proof function craftToSpendTx(message, pkScript) { const messageHash = hashMessage(message); const tx = new Transaction({ version: 0, }); // add input with zero hash and max index tx.addInput({ txid: ZERO_32, // zero hash index: MAX_INDEX, sequence: 0, }); // add output with zero value and provided pkScript tx.addOutput({ amount: 0n, script: pkScript, }); tx.updateInput(0, { finalScriptSig: Script.encode(["OP_0", messageHash]), }); return tx; } // craftToSignTx creates the transaction that will be signed for the proof function craftToSignTx(toSpend, inputs, outputs) { const firstInput = inputs[0]; const lockTime = inputs .map((input) => input.sequence || 0) .reduce((a, b) => Math.max(a, b), 0); const tx = new Transaction({ version: 2, lockTime, }); // add the first "toSpend" input tx.addInput({ ...firstInput, txid: toSpend.id, index: 0, witnessUtxo: { script: firstInput.witnessUtxo.script, amount: 0n, }, sighashType: SigHash.ALL, }); // add other inputs for (const [i, input] of inputs.entries()) { tx.addInput({ ...input, sighashType: SigHash.ALL, }); if (input.unknown?.length) { tx.updateInput(i + 1, { unknown: input.unknown, }); } } // add the special OP_RETURN output if no outputs are provided if (outputs.length === 0) { outputs = [ { amount: 0n, script: OP_RETURN_EMPTY_PKSCRIPT, }, ]; } for (const output of outputs) { tx.addOutput({ amount: output.amount, script: output.script, }); } return tx; } function hashMessage(message) { return schnorr.utils.taggedHash(TAG_INTENT_PROOF, new TextEncoder().encode(message)); }