@arkade-os/sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
575 lines (574 loc) • 22.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.TapscriptType = void 0;
exports.decodeTapscript = decodeTapscript;
const bip68 = __importStar(require("bip68"));
const btc_signer_1 = require("@scure/btc-signer");
const base_1 = require("@scure/base");
const MinimalScriptNum = (0, btc_signer_1.ScriptNum)(undefined, true);
var TapscriptType;
(function (TapscriptType) {
TapscriptType["Multisig"] = "multisig";
TapscriptType["CSVMultisig"] = "csv-multisig";
TapscriptType["ConditionCSVMultisig"] = "condition-csv-multisig";
TapscriptType["ConditionMultisig"] = "condition-multisig";
TapscriptType["CLTVMultisig"] = "cltv-multisig";
})(TapscriptType || (exports.TapscriptType = TapscriptType = {}));
/**
* decodeTapscript is a function that decodes an ark tapsript from a raw script.
*
* @throws {Error} if the script is not a valid ark tapscript
* @example
* ```typescript
* const arkTapscript = decodeTapscript(new Uint8Array(32));
* console.log("type:", arkTapscript.type);
* ```
*/
function decodeTapscript(script) {
const types = [
MultisigTapscript,
CSVMultisigTapscript,
ConditionCSVMultisigTapscript,
ConditionMultisigTapscript,
CLTVMultisigTapscript,
];
for (const type of types) {
try {
return type.decode(script);
}
catch (error) {
continue;
}
}
throw new Error(`Failed to decode: script ${base_1.hex.encode(script)} is not a valid tapscript`);
}
/**
* Implements a multi-signature tapscript.
*
* <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
*
* @example
* ```typescript
* const multisigTapscript = MultisigTapscript.encode({ pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
* ```
*/
var MultisigTapscript;
(function (MultisigTapscript) {
let MultisigType;
(function (MultisigType) {
MultisigType[MultisigType["CHECKSIG"] = 0] = "CHECKSIG";
MultisigType[MultisigType["CHECKSIGADD"] = 1] = "CHECKSIGADD";
})(MultisigType = MultisigTapscript.MultisigType || (MultisigTapscript.MultisigType = {}));
function encode(params) {
if (params.pubkeys.length === 0) {
throw new Error("At least 1 pubkey is required");
}
for (const pubkey of params.pubkeys) {
if (pubkey.length !== 32) {
throw new Error(`Invalid pubkey length: expected 32, got ${pubkey.length}`);
}
}
if (!params.type) {
params.type = MultisigType.CHECKSIG;
}
if (params.type === MultisigType.CHECKSIGADD) {
return {
type: TapscriptType.Multisig,
params,
script: (0, btc_signer_1.p2tr_ms)(params.pubkeys.length, params.pubkeys).script,
};
}
const asm = [];
for (let i = 0; i < params.pubkeys.length; i++) {
asm.push(params.pubkeys[i]);
// CHECKSIGVERIFY except the last pubkey
if (i < params.pubkeys.length - 1) {
asm.push("CHECKSIGVERIFY");
}
else {
asm.push("CHECKSIG");
}
}
return {
type: TapscriptType.Multisig,
params,
script: btc_signer_1.Script.encode(asm),
};
}
MultisigTapscript.encode = encode;
function decode(script) {
if (script.length === 0) {
throw new Error("Failed to decode: script is empty");
}
try {
// Try decoding as checksigAdd first
return decodeChecksigAdd(script);
}
catch (error) {
// If checksigAdd fails, try regular checksig
try {
return decodeChecksig(script);
}
catch (error2) {
throw new Error(`Failed to decode script: ${error2 instanceof Error ? error2.message : String(error2)}`);
}
}
}
MultisigTapscript.decode = decode;
// <pubkey> CHECKSIG <pubkey> CHECKSIGADD <len_keys> NUMEQUAL
function decodeChecksigAdd(script) {
const asm = btc_signer_1.Script.decode(script);
const pubkeys = [];
let foundNumEqual = false;
// Parse through ASM operations
for (let i = 0; i < asm.length; i++) {
const op = asm[i];
// If it's a data push, it should be a 32-byte pubkey
if (typeof op !== "string" && typeof op !== "number") {
if (op.length !== 32) {
throw new Error(`Invalid pubkey length: expected 32, got ${op.length}`);
}
pubkeys.push(op);
// Check next operation is CHECKSIGADD or CHECKSIG
if (i + 1 >= asm.length ||
(asm[i + 1] !== "CHECKSIGADD" && asm[i + 1] !== "CHECKSIG")) {
throw new Error("Expected CHECKSIGADD or CHECKSIG after pubkey");
}
i++; // Skip the CHECKSIGADD op
continue;
}
// Last operation should be NUMEQUAL
if (i === asm.length - 1) {
if (op !== "NUMEQUAL") {
throw new Error("Expected NUMEQUAL at end of script");
}
foundNumEqual = true;
}
}
if (!foundNumEqual) {
throw new Error("Missing NUMEQUAL operation");
}
if (pubkeys.length === 0) {
throw new Error("Invalid script: must have at least 1 pubkey");
}
// Verify the script by re-encoding and comparing
const reconstructed = encode({
pubkeys,
type: MultisigType.CHECKSIGADD,
});
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.Multisig,
params: { pubkeys, type: MultisigType.CHECKSIGADD },
script,
};
}
// <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
function decodeChecksig(script) {
const asm = btc_signer_1.Script.decode(script);
const pubkeys = [];
// Parse through ASM operations
for (let i = 0; i < asm.length; i++) {
const op = asm[i];
// If it's a data push, it should be a 32-byte pubkey
if (typeof op !== "string" && typeof op !== "number") {
if (op.length !== 32) {
throw new Error(`Invalid pubkey length: expected 32, got ${op.length}`);
}
pubkeys.push(op);
// Check next operation
if (i + 1 >= asm.length) {
throw new Error("Unexpected end of script");
}
const nextOp = asm[i + 1];
if (nextOp !== "CHECKSIGVERIFY" && nextOp !== "CHECKSIG") {
throw new Error("Expected CHECKSIGVERIFY or CHECKSIG after pubkey");
}
// Last operation must be CHECKSIG, not CHECKSIGVERIFY
if (i === asm.length - 2 && nextOp !== "CHECKSIG") {
throw new Error("Last operation must be CHECKSIG");
}
i++; // Skip the CHECKSIG/CHECKSIGVERIFY op
continue;
}
}
if (pubkeys.length === 0) {
throw new Error("Invalid script: must have at least 1 pubkey");
}
// Verify the script by re-encoding and comparing
const reconstructed = encode({ pubkeys, type: MultisigType.CHECKSIG });
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.Multisig,
params: { pubkeys, type: MultisigType.CHECKSIG },
script,
};
}
function is(tapscript) {
return tapscript.type === TapscriptType.Multisig;
}
MultisigTapscript.is = is;
})(MultisigTapscript || (exports.MultisigTapscript = MultisigTapscript = {}));
/**
* Implements a relative timelock script that requires all specified pubkeys to sign
* after the relative timelock has expired. The timelock can be specified in blocks or seconds.
*
* This is the standard exit closure and it is also used for the sweep closure in vtxo trees.
*
* <sequence> CHECKSEQUENCEVERIFY DROP <pubkey> CHECKSIG
*
* @example
* ```typescript
* const csvMultisigTapscript = CSVMultisigTapscript.encode({ timelock: { type: "blocks", value: 144 }, pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
* ```
*/
var CSVMultisigTapscript;
(function (CSVMultisigTapscript) {
function encode(params) {
for (const pubkey of params.pubkeys) {
if (pubkey.length !== 32) {
throw new Error(`Invalid pubkey length: expected 32, got ${pubkey.length}`);
}
}
const sequence = MinimalScriptNum.encode(BigInt(bip68.encode(params.timelock.type === "blocks"
? { blocks: Number(params.timelock.value) }
: { seconds: Number(params.timelock.value) })));
const asm = [
sequence.length === 1 ? sequence[0] : sequence,
"CHECKSEQUENCEVERIFY",
"DROP",
];
const multisigScript = MultisigTapscript.encode(params);
const script = new Uint8Array([
...btc_signer_1.Script.encode(asm),
...multisigScript.script,
]);
return {
type: TapscriptType.CSVMultisig,
params,
script,
};
}
CSVMultisigTapscript.encode = encode;
function decode(script) {
if (script.length === 0) {
throw new Error("Failed to decode: script is empty");
}
const asm = btc_signer_1.Script.decode(script);
if (asm.length < 3) {
throw new Error(`Invalid script: too short (expected at least 3)`);
}
const sequence = asm[0];
if (typeof sequence === "string") {
throw new Error("Invalid script: expected sequence number");
}
if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
throw new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
}
const multisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(3)));
let multisig;
try {
multisig = MultisigTapscript.decode(multisigScript);
}
catch (error) {
throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
}
let sequenceNum;
if (typeof sequence === "number") {
sequenceNum = sequence;
}
else {
sequenceNum = Number(MinimalScriptNum.decode(sequence));
}
const decodedTimelock = bip68.decode(sequenceNum);
const timelock = decodedTimelock.blocks !== undefined
? { type: "blocks", value: BigInt(decodedTimelock.blocks) }
: { type: "seconds", value: BigInt(decodedTimelock.seconds) };
const reconstructed = encode({
timelock,
...multisig.params,
});
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.CSVMultisig,
params: {
timelock,
...multisig.params,
},
script,
};
}
CSVMultisigTapscript.decode = decode;
function is(tapscript) {
return tapscript.type === TapscriptType.CSVMultisig;
}
CSVMultisigTapscript.is = is;
})(CSVMultisigTapscript || (exports.CSVMultisigTapscript = CSVMultisigTapscript = {}));
/**
* Combines a condition script with an exit closure. The resulting script requires
* the condition to be met, followed by the standard exit closure requirements
* (timelock and signatures).
*
* <conditionScript> VERIFY <sequence> CHECKSEQUENCEVERIFY DROP <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
*
* @example
* ```typescript
* const conditionCSVMultisigTapscript = ConditionCSVMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
* ```
*/
var ConditionCSVMultisigTapscript;
(function (ConditionCSVMultisigTapscript) {
function encode(params) {
const script = new Uint8Array([
...params.conditionScript,
...btc_signer_1.Script.encode(["VERIFY"]),
...CSVMultisigTapscript.encode(params).script,
]);
return {
type: TapscriptType.ConditionCSVMultisig,
params,
script,
};
}
ConditionCSVMultisigTapscript.encode = encode;
function decode(script) {
if (script.length === 0) {
throw new Error("Failed to decode: script is empty");
}
const asm = btc_signer_1.Script.decode(script);
if (asm.length < 1) {
throw new Error(`Invalid script: too short (expected at least 1)`);
}
let verifyIndex = -1;
for (let i = asm.length - 1; i >= 0; i--) {
if (asm[i] === "VERIFY") {
verifyIndex = i;
}
}
if (verifyIndex === -1) {
throw new Error("Invalid script: missing VERIFY operation");
}
const conditionScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(0, verifyIndex)));
const csvMultisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(verifyIndex + 1)));
let csvMultisig;
try {
csvMultisig = CSVMultisigTapscript.decode(csvMultisigScript);
}
catch (error) {
throw new Error(`Invalid CSV multisig script: ${error instanceof Error ? error.message : String(error)}`);
}
const reconstructed = encode({
conditionScript,
...csvMultisig.params,
});
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.ConditionCSVMultisig,
params: {
conditionScript,
...csvMultisig.params,
},
script,
};
}
ConditionCSVMultisigTapscript.decode = decode;
function is(tapscript) {
return tapscript.type === TapscriptType.ConditionCSVMultisig;
}
ConditionCSVMultisigTapscript.is = is;
})(ConditionCSVMultisigTapscript || (exports.ConditionCSVMultisigTapscript = ConditionCSVMultisigTapscript = {}));
/**
* Combines a condition script with a forfeit closure. The resulting script requires
* the condition to be met, followed by the standard forfeit closure requirements
* (multi-signature).
*
* <conditionScript> VERIFY <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
*
* @example
* ```typescript
* const conditionMultisigTapscript = ConditionMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
* ```
*/
var ConditionMultisigTapscript;
(function (ConditionMultisigTapscript) {
function encode(params) {
const script = new Uint8Array([
...params.conditionScript,
...btc_signer_1.Script.encode(["VERIFY"]),
...MultisigTapscript.encode(params).script,
]);
return {
type: TapscriptType.ConditionMultisig,
params,
script,
};
}
ConditionMultisigTapscript.encode = encode;
function decode(script) {
if (script.length === 0) {
throw new Error("Failed to decode: script is empty");
}
const asm = btc_signer_1.Script.decode(script);
if (asm.length < 1) {
throw new Error(`Invalid script: too short (expected at least 1)`);
}
let verifyIndex = -1;
for (let i = asm.length - 1; i >= 0; i--) {
if (asm[i] === "VERIFY") {
verifyIndex = i;
}
}
if (verifyIndex === -1) {
throw new Error("Invalid script: missing VERIFY operation");
}
const conditionScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(0, verifyIndex)));
const multisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(verifyIndex + 1)));
let multisig;
try {
multisig = MultisigTapscript.decode(multisigScript);
}
catch (error) {
throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
}
const reconstructed = encode({
conditionScript,
...multisig.params,
});
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.ConditionMultisig,
params: {
conditionScript,
...multisig.params,
},
script,
};
}
ConditionMultisigTapscript.decode = decode;
function is(tapscript) {
return tapscript.type === TapscriptType.ConditionMultisig;
}
ConditionMultisigTapscript.is = is;
})(ConditionMultisigTapscript || (exports.ConditionMultisigTapscript = ConditionMultisigTapscript = {}));
/**
* Implements an absolute timelock (CLTV) script combined with a forfeit closure.
* The script requires waiting until a specific block height/timestamp before the
* forfeit closure conditions can be met.
*
* <locktime> CHECKLOCKTIMEVERIFY DROP <pubkey> CHECKSIGVERIFY <pubkey> CHECKSIG
*
* @example
* ```typescript
* const cltvMultisigTapscript = CLTVMultisigTapscript.encode({ absoluteTimelock: 144, pubkeys: [new Uint8Array(32), new Uint8Array(32)] });
* ```
*/
var CLTVMultisigTapscript;
(function (CLTVMultisigTapscript) {
function encode(params) {
const locktime = MinimalScriptNum.encode(params.absoluteTimelock);
const asm = [
locktime.length === 1 ? locktime[0] : locktime,
"CHECKLOCKTIMEVERIFY",
"DROP",
];
const timelockedScript = btc_signer_1.Script.encode(asm);
const script = new Uint8Array([
...timelockedScript,
...MultisigTapscript.encode(params).script,
]);
return {
type: TapscriptType.CLTVMultisig,
params,
script,
};
}
CLTVMultisigTapscript.encode = encode;
function decode(script) {
if (script.length === 0) {
throw new Error("Failed to decode: script is empty");
}
const asm = btc_signer_1.Script.decode(script);
if (asm.length < 3) {
throw new Error(`Invalid script: too short (expected at least 3)`);
}
const locktime = asm[0];
if (typeof locktime === "string" || typeof locktime === "number") {
throw new Error("Invalid script: expected locktime number");
}
if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
throw new Error("Invalid script: expected CHECKLOCKTIMEVERIFY DROP");
}
const multisigScript = new Uint8Array(btc_signer_1.Script.encode(asm.slice(3)));
let multisig;
try {
multisig = MultisigTapscript.decode(multisigScript);
}
catch (error) {
throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
}
const absoluteTimelock = MinimalScriptNum.decode(locktime);
const reconstructed = encode({
absoluteTimelock,
...multisig.params,
});
if (base_1.hex.encode(reconstructed.script) !== base_1.hex.encode(script)) {
throw new Error("Invalid script format: script reconstruction mismatch");
}
return {
type: TapscriptType.CLTVMultisig,
params: {
absoluteTimelock,
...multisig.params,
},
script,
};
}
CLTVMultisigTapscript.decode = decode;
function is(tapscript) {
return tapscript.type === TapscriptType.CLTVMultisig;
}
CLTVMultisigTapscript.is = is;
})(CLTVMultisigTapscript || (exports.CLTVMultisigTapscript = CLTVMultisigTapscript = {}));