@arkade-os/sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
179 lines (178 loc) • 5.86 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BIP322 = void 0;
exports.craftToSpendTx = craftToSpendTx;
const btc_signer_1 = require("@scure/btc-signer");
const errors_1 = require("./errors");
const secp256k1_1 = require("@noble/curves/secp256k1");
const base_1 = require("@scure/base");
/**
* BIP-322 signature implementation for Bitcoin message signing.
*
* BIP-322 defines a standard for signing Bitcoin messages as well as proving
* ownership of coins. This namespace provides utilities for creating and
* validating BIP-322.
*
* @see https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
*
* @example
* ```typescript
* // Create a BIP-322 proof
* const proof = BIP322.create(
* "Hello Bitcoin!",
* [input],
* [output]
* );
*
* // Sign the proof
* const signedProof = await identity.sign(proof);
*
* // Extract the signature
* const signature = BIP322.signature(signedProof);
* ```
*/
var BIP322;
(function (BIP322) {
/**
* Creates a new BIP-322 "full" proof of funds 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 BIP-322 message to be signed
* @param inputs - Array of transaction inputs to prove ownership of
* @param outputs - Optional array of transaction outputs
* @returns An unsigned BIP-322 proof transaction
*/
function create(message, inputs, outputs = []) {
if (inputs.length == 0)
throw errors_1.ErrMissingInputs;
if (!validateInputs(inputs))
throw errors_1.ErrMissingData;
if (!validateOutputs(outputs))
throw errors_1.ErrMissingData;
// create the initial transaction to spend
const toSpend = craftToSpendTx(message, inputs[0].witnessUtxo.script);
// create the transaction to sign
return craftToSignTx(toSpend, inputs, outputs);
}
BIP322.create = create;
/**
* Finalizes and extracts the FullProof transaction into a BIP-322 signature.
*
* This function takes a signed proof transaction and converts it into a
* base64-encoded signature string. If the proof's inputs have special
* spending conditions, a custom finalizer can be provided.
*
* @param signedProof - The signed BIP-322 proof transaction
* @param finalizer - Optional custom finalizer function
* @returns Base64-encoded BIP-322 signature
*/
function signature(signedProof, finalizer = (tx) => tx.finalize()) {
finalizer(signedProof);
return base_1.base64.encode(signedProof.extract());
}
BIP322.signature = signature;
})(BIP322 || (exports.BIP322 = BIP322 = {}));
const OP_RETURN_EMPTY_PKSCRIPT = new Uint8Array([btc_signer_1.OP.RETURN]);
const ZERO_32 = new Uint8Array(32).fill(0);
const MAX_INDEX = 0xffffffff;
const TAG_BIP322 = "BIP0322-signed-message";
function validateInput(input) {
if (input.index === undefined)
throw errors_1.ErrMissingData;
if (input.txid === undefined)
throw errors_1.ErrMissingData;
if (input.witnessUtxo === undefined)
throw errors_1.ErrMissingWitnessUtxo;
return true;
}
function validateInputs(inputs) {
inputs.forEach(validateInput);
return true;
}
function validateOutput(output) {
if (output.amount === undefined)
throw errors_1.ErrMissingData;
if (output.script === undefined)
throw errors_1.ErrMissingData;
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 btc_signer_1.Transaction({
version: 0,
allowUnknownOutputs: true,
allowUnknown: true,
allowUnknownInputs: true,
});
// 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: btc_signer_1.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 tx = new btc_signer_1.Transaction({
version: 2,
allowUnknownOutputs: outputs.length === 0,
allowUnknown: true,
allowUnknownInputs: true,
lockTime: 0,
});
// add the first "toSpend" input
tx.addInput({
...firstInput,
txid: toSpend.id,
index: 0,
witnessUtxo: {
script: firstInput.witnessUtxo.script,
amount: 0n,
},
sighashType: btc_signer_1.SigHash.ALL,
});
// add other inputs
for (const input of inputs) {
tx.addInput({
...input,
sighashType: btc_signer_1.SigHash.ALL,
});
}
// 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 secp256k1_1.schnorr.utils.taggedHash(TAG_BIP322, new TextEncoder().encode(message));
}