@bitgo/wasm-utxo 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/{esm → cjs/js}/fixedScriptWallet.d.ts +19 -0
  2. package/dist/cjs/{fixedScriptWallet.js → js/fixedScriptWallet.js} +25 -0
  3. package/dist/{esm → cjs/js}/wasm/wasm_utxo.d.ts +29 -0
  4. package/dist/cjs/{wasm → js/wasm}/wasm_utxo.js +77 -0
  5. package/dist/{esm → cjs/js}/wasm/wasm_utxo_bg.wasm +0 -0
  6. package/dist/cjs/{wasm → js/wasm}/wasm_utxo_bg.wasm.d.ts +19 -16
  7. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  8. package/dist/{cjs → esm/js}/fixedScriptWallet.d.ts +19 -0
  9. package/dist/esm/{fixedScriptWallet.js → js/fixedScriptWallet.js} +25 -0
  10. package/dist/{cjs → esm/js}/wasm/wasm_utxo.d.ts +29 -0
  11. package/dist/esm/{wasm → js/wasm}/wasm_utxo_bg.js +77 -0
  12. package/dist/{cjs → esm/js}/wasm/wasm_utxo_bg.wasm +0 -0
  13. package/dist/esm/{wasm → js/wasm}/wasm_utxo_bg.wasm.d.ts +19 -16
  14. package/dist/esm/test/address/utxolibCompat.d.ts +1 -0
  15. package/dist/esm/test/address/utxolibCompat.js +105 -0
  16. package/dist/esm/test/ast/formatNode.d.ts +1 -0
  17. package/dist/esm/test/ast/formatNode.js +15 -0
  18. package/dist/esm/test/descriptorFixtures.d.ts +25 -0
  19. package/dist/esm/test/descriptorFixtures.js +605 -0
  20. package/dist/esm/test/descriptorUtil.d.ts +13 -0
  21. package/dist/esm/test/descriptorUtil.js +52 -0
  22. package/dist/esm/test/fixedScript/address.d.ts +1 -0
  23. package/dist/esm/test/fixedScript/address.js +64 -0
  24. package/dist/esm/test/fixedScript/finalizeExtract.d.ts +1 -0
  25. package/dist/esm/test/fixedScript/finalizeExtract.js +66 -0
  26. package/dist/esm/test/fixedScript/fixtureUtil.d.ts +93 -0
  27. package/dist/esm/test/fixedScript/fixtureUtil.js +44 -0
  28. package/dist/esm/test/fixedScript/parseTransactionWithWalletKeys.d.ts +1 -0
  29. package/dist/esm/test/fixedScript/parseTransactionWithWalletKeys.js +140 -0
  30. package/dist/esm/test/fixedScript/verifySignature.d.ts +1 -0
  31. package/dist/esm/test/fixedScript/verifySignature.js +141 -0
  32. package/dist/esm/test/fixedScriptToDescriptor.d.ts +1 -0
  33. package/dist/esm/test/fixedScriptToDescriptor.js +91 -0
  34. package/dist/esm/test/fixtures.d.ts +1 -0
  35. package/dist/esm/test/fixtures.js +12 -0
  36. package/dist/esm/test/opdrop.d.ts +1 -0
  37. package/dist/esm/test/opdrop.js +85 -0
  38. package/dist/esm/test/psbt.util.d.ts +8 -0
  39. package/dist/esm/test/psbt.util.js +116 -0
  40. package/dist/esm/test/psbtFixedScriptCompat.d.ts +1 -0
  41. package/dist/esm/test/psbtFixedScriptCompat.js +114 -0
  42. package/dist/esm/test/psbtFixedScriptCompatFixtures.d.ts +10 -0
  43. package/dist/esm/test/psbtFixedScriptCompatFixtures.js +53 -0
  44. package/dist/esm/test/psbtFromDescriptor.d.ts +1 -0
  45. package/dist/esm/test/psbtFromDescriptor.js +107 -0
  46. package/dist/esm/test/psbtFromDescriptor.util.d.ts +63 -0
  47. package/dist/esm/test/psbtFromDescriptor.util.js +101 -0
  48. package/dist/esm/test/test.d.ts +1 -0
  49. package/dist/esm/test/test.js +122 -0
  50. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  51. package/package.json +12 -12
  52. /package/dist/cjs/{address.d.ts → js/address.d.ts} +0 -0
  53. /package/dist/cjs/{address.js → js/address.js} +0 -0
  54. /package/dist/cjs/{ast → js/ast}/formatNode.d.ts +0 -0
  55. /package/dist/cjs/{ast → js/ast}/formatNode.js +0 -0
  56. /package/dist/cjs/{ast → js/ast}/fromWasmNode.d.ts +0 -0
  57. /package/dist/cjs/{ast → js/ast}/fromWasmNode.js +0 -0
  58. /package/dist/cjs/{ast → js/ast}/index.d.ts +0 -0
  59. /package/dist/cjs/{ast → js/ast}/index.js +0 -0
  60. /package/dist/cjs/{coinName.d.ts → js/coinName.d.ts} +0 -0
  61. /package/dist/cjs/{coinName.js → js/coinName.js} +0 -0
  62. /package/dist/cjs/{index.d.ts → js/index.d.ts} +0 -0
  63. /package/dist/cjs/{index.js → js/index.js} +0 -0
  64. /package/dist/cjs/{triple.d.ts → js/triple.d.ts} +0 -0
  65. /package/dist/cjs/{triple.js → js/triple.js} +0 -0
  66. /package/dist/cjs/{utxolibCompat.d.ts → js/utxolibCompat.d.ts} +0 -0
  67. /package/dist/cjs/{utxolibCompat.js → js/utxolibCompat.js} +0 -0
  68. /package/dist/esm/{address.d.ts → js/address.d.ts} +0 -0
  69. /package/dist/esm/{address.js → js/address.js} +0 -0
  70. /package/dist/esm/{ast → js/ast}/formatNode.d.ts +0 -0
  71. /package/dist/esm/{ast → js/ast}/formatNode.js +0 -0
  72. /package/dist/esm/{ast → js/ast}/fromWasmNode.d.ts +0 -0
  73. /package/dist/esm/{ast → js/ast}/fromWasmNode.js +0 -0
  74. /package/dist/esm/{ast → js/ast}/index.d.ts +0 -0
  75. /package/dist/esm/{ast → js/ast}/index.js +0 -0
  76. /package/dist/esm/{coinName.d.ts → js/coinName.d.ts} +0 -0
  77. /package/dist/esm/{coinName.js → js/coinName.js} +0 -0
  78. /package/dist/esm/{index.d.ts → js/index.d.ts} +0 -0
  79. /package/dist/esm/{index.js → js/index.js} +0 -0
  80. /package/dist/esm/{triple.d.ts → js/triple.d.ts} +0 -0
  81. /package/dist/esm/{triple.js → js/triple.js} +0 -0
  82. /package/dist/esm/{utxolibCompat.d.ts → js/utxolibCompat.d.ts} +0 -0
  83. /package/dist/esm/{utxolibCompat.js → js/utxolibCompat.js} +0 -0
  84. /package/dist/esm/{wasm → js/wasm}/wasm_utxo.js +0 -0
@@ -0,0 +1,52 @@
1
+ import * as assert from "node:assert";
2
+ import * as fs from "fs/promises";
3
+ import * as utxolib from "@bitgo/utxo-lib";
4
+ import { formatNode } from "../js/ast/index.js";
5
+ async function assertEqualJSON(path, value) {
6
+ try {
7
+ const data = JSON.parse(await fs.readFile(path, "utf8"));
8
+ assert.deepStrictEqual(data, value);
9
+ }
10
+ catch (e) {
11
+ if (e.code === "ENOENT") {
12
+ await fs.writeFile(path, JSON.stringify(value, null, 2));
13
+ throw new Error("Expected file not found, wrote it instead");
14
+ }
15
+ throw e;
16
+ }
17
+ }
18
+ export async function assertEqualFixture(path, content) {
19
+ await assertEqualJSON(path, content);
20
+ }
21
+ /** Expand a template with the given root wallet keys and chain code */
22
+ function expand(rootWalletKeys, keyIndex, chainCode) {
23
+ if (keyIndex !== 0 && keyIndex !== 1 && keyIndex !== 2) {
24
+ throw new Error("Invalid key index");
25
+ }
26
+ const xpub = rootWalletKeys.triple[keyIndex].neutered().toBase58();
27
+ const prefix = rootWalletKeys.derivationPrefixes[keyIndex];
28
+ return xpub + "/" + prefix + "/" + chainCode + "/*";
29
+ }
30
+ /**
31
+ * Get a standard output descriptor that corresponds to the proprietary HD wallet setup
32
+ * used in BitGo wallets.
33
+ * Only supports a subset of script types.
34
+ */
35
+ export function getDescriptorForScriptType(rootWalletKeys, scriptType, scope) {
36
+ const chain = scope === "external"
37
+ ? utxolib.bitgo.getExternalChainCode(scriptType)
38
+ : utxolib.bitgo.getInternalChainCode(scriptType);
39
+ const multi = {
40
+ multi: [2, ...rootWalletKeys.triple.map((_, i) => expand(rootWalletKeys, i, chain))],
41
+ };
42
+ switch (scriptType) {
43
+ case "p2sh":
44
+ return formatNode({ sh: multi });
45
+ case "p2shP2wsh":
46
+ return formatNode({ sh: { wsh: multi } });
47
+ case "p2wsh":
48
+ return formatNode({ wsh: multi });
49
+ default:
50
+ throw new Error(`Unsupported script type ${scriptType}`);
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { fixedScriptWallet } from "../../js/index.js";
4
+ function getAddressUtxoLib(keys, chain, index, network, addressFormat) {
5
+ if (!utxolib.bitgo.isChainCode(chain)) {
6
+ throw new Error(`Invalid chain code: ${chain}`);
7
+ }
8
+ const derived = keys.deriveForChainAndIndex(chain, index);
9
+ const script = utxolib.bitgo.outputScripts.createOutputScript2of3(derived.publicKeys, utxolib.bitgo.outputScripts.scriptTypeForChain(chain));
10
+ const address = utxolib.addressFormat.fromOutputScriptWithFormat(script.scriptPubKey, addressFormat, network);
11
+ return address;
12
+ }
13
+ function runTest(network, { derivationPrefixes, addressFormat, } = {}) {
14
+ describe(`address for network ${utxolib.getNetworkName(network)}, derivationPrefixes=${Boolean(derivationPrefixes)}`, function () {
15
+ const keyTriple = utxolib.testutil.getKeyTriple("wasm");
16
+ const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(keyTriple.map((k) => k.neutered()), derivationPrefixes);
17
+ const supportedChainCodes = utxolib.bitgo.chainCodes.filter((chainCode) => {
18
+ const scriptType = utxolib.bitgo.outputScripts.scriptTypeForChain(chainCode);
19
+ return utxolib.bitgo.outputScripts.isSupportedScriptType(network, scriptType);
20
+ });
21
+ it(`can recreate address from wallet keys for chain codes ${supportedChainCodes.join(", ")}`, function () {
22
+ for (const chainCode of supportedChainCodes) {
23
+ for (let index = 0; index < 2; index++) {
24
+ const utxolibAddress = getAddressUtxoLib(rootWalletKeys, chainCode, index, network, addressFormat ?? "default");
25
+ const wasmAddress = fixedScriptWallet.address(rootWalletKeys, chainCode, index, network, addressFormat);
26
+ assert.strictEqual(utxolibAddress, wasmAddress);
27
+ }
28
+ }
29
+ });
30
+ const unsupportedChainCodes = utxolib.bitgo.chainCodes.filter((chainCode) => {
31
+ const scriptType = utxolib.bitgo.outputScripts.scriptTypeForChain(chainCode);
32
+ return !utxolib.bitgo.outputScripts.isSupportedScriptType(network, scriptType);
33
+ });
34
+ if (unsupportedChainCodes.length > 0) {
35
+ it(`throws error for unsupported chain codes ${unsupportedChainCodes.join(", ")}`, function () {
36
+ for (const chainCode of unsupportedChainCodes) {
37
+ const scriptType = utxolib.bitgo.outputScripts.scriptTypeForChain(chainCode);
38
+ assert.throws(() => {
39
+ fixedScriptWallet.address(rootWalletKeys, chainCode, 0, network, addressFormat);
40
+ }, (error) => {
41
+ const errorMessage = error.message.toLowerCase();
42
+ const isSegwitError = scriptType === "p2shP2wsh" || scriptType === "p2wsh";
43
+ const isTaprootError = scriptType === "p2tr" || scriptType === "p2trMusig2";
44
+ if (isSegwitError) {
45
+ return errorMessage.includes("does not support segwit");
46
+ }
47
+ else if (isTaprootError) {
48
+ return errorMessage.includes("does not support taproot");
49
+ }
50
+ return false;
51
+ }, `Expected error for unsupported script type ${scriptType} on network ${utxolib.getNetworkName(network)}`);
52
+ }
53
+ });
54
+ }
55
+ });
56
+ }
57
+ utxolib.getNetworkList().forEach((network) => {
58
+ runTest(network);
59
+ runTest(network, { derivationPrefixes: ["m/1/2", "m/0/0", "m/0/0"] });
60
+ if (utxolib.getMainnet(network) === utxolib.networks.bitcoincash ||
61
+ utxolib.getMainnet(network) === utxolib.networks.ecash) {
62
+ runTest(network, { addressFormat: "cashaddr" });
63
+ }
64
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { fixedScriptWallet } from "../../js/index.js";
4
+ import { loadPsbtFixture, getPsbtBuffer, getExtractedTransactionHex, } from "./fixtureUtil.js";
5
+ describe("finalize and extract transaction", function () {
6
+ const supportedNetworks = utxolib.getNetworkList().filter((network) => {
7
+ return (utxolib.isMainnet(network) &&
8
+ network !== utxolib.networks.bitcoincash &&
9
+ network !== utxolib.networks.bitcoingold &&
10
+ network !== utxolib.networks.bitcoinsv &&
11
+ network !== utxolib.networks.ecash &&
12
+ network !== utxolib.networks.zcash);
13
+ });
14
+ supportedNetworks.forEach((network) => {
15
+ const networkName = utxolib.getNetworkName(network);
16
+ describe(`network: ${networkName}`, function () {
17
+ let fullsignedFixture;
18
+ let fullsignedPsbtBuffer;
19
+ let fullsignedBitgoPsbt;
20
+ before(function () {
21
+ fullsignedFixture = loadPsbtFixture(networkName, "fullsigned");
22
+ fullsignedPsbtBuffer = getPsbtBuffer(fullsignedFixture);
23
+ fullsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBuffer, networkName);
24
+ });
25
+ it("should serialize and deserialize PSBT (round-trip)", function () {
26
+ const serialized = fullsignedBitgoPsbt.serialize();
27
+ // Verify we can deserialize what we serialized (functional round-trip)
28
+ const deserialized = fixedScriptWallet.BitGoPsbt.fromBytes(serialized, networkName);
29
+ // Verify the deserialized PSBT has the same unsigned txid
30
+ assert.strictEqual(deserialized.unsignedTxid(), fullsignedBitgoPsbt.unsignedTxid(), "Deserialized PSBT should have same unsigned txid after round-trip");
31
+ // Verify the re-deserialized PSBT can be serialized back to bytes
32
+ const reserialized = deserialized.serialize();
33
+ // Verify functional equivalence by deserializing again and checking txid
34
+ const redeserialized = fixedScriptWallet.BitGoPsbt.fromBytes(reserialized, networkName);
35
+ assert.strictEqual(redeserialized.unsignedTxid(), fullsignedBitgoPsbt.unsignedTxid(), "PSBT should maintain consistency through multiple serialize/deserialize cycles");
36
+ });
37
+ it("should finalize all inputs and be extractable", function () {
38
+ // Create a fresh instance for finalization
39
+ const psbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBuffer, networkName);
40
+ // Finalize all inputs
41
+ psbt.finalizeAllInputs();
42
+ // Serialize the finalized PSBT
43
+ const serialized = psbt.serialize();
44
+ // Verify we can deserialize the finalized PSBT
45
+ const deserialized = fixedScriptWallet.BitGoPsbt.fromBytes(serialized, networkName);
46
+ // Verify it can be extracted (which confirms finalization worked)
47
+ const extractedTx = deserialized.extractTransaction();
48
+ const extractedTxHex = Buffer.from(extractedTx).toString("hex");
49
+ const expectedTxHex = getExtractedTransactionHex(fullsignedFixture);
50
+ assert.strictEqual(extractedTxHex, expectedTxHex, "Extracted transaction from finalized PSBT should match expected transaction");
51
+ });
52
+ it("should extract transaction from finalized PSBT", function () {
53
+ // Create a fresh instance for extraction
54
+ const psbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBuffer, networkName);
55
+ // Finalize all inputs
56
+ psbt.finalizeAllInputs();
57
+ // Extract transaction
58
+ const extractedTx = psbt.extractTransaction();
59
+ const extractedTxHex = Buffer.from(extractedTx).toString("hex");
60
+ // Get expected transaction hex from fixture
61
+ const expectedTxHex = getExtractedTransactionHex(fullsignedFixture);
62
+ assert.strictEqual(extractedTxHex, expectedTxHex, "Extracted transaction should match expected transaction");
63
+ });
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,93 @@
1
+ import * as utxolib from "@bitgo/utxo-lib";
2
+ export type SignatureState = "unsigned" | "halfsigned" | "fullsigned";
3
+ export type Triple<T> = [T, T, T];
4
+ export type Bip32Derivation = {
5
+ masterFingerprint: string;
6
+ pubkey: string;
7
+ path: string;
8
+ };
9
+ export type TapBip32Derivation = Bip32Derivation & {
10
+ leafHashes: string[];
11
+ };
12
+ export type WitnessUtxo = {
13
+ value: string;
14
+ script: string;
15
+ };
16
+ export type TapLeafScript = {
17
+ controlBlock: string;
18
+ script: string;
19
+ leafVersion: number;
20
+ };
21
+ export type PsbtInput = {
22
+ type: string;
23
+ sighashType: number;
24
+ redeemScript?: string;
25
+ witnessScript?: string;
26
+ bip32Derivation?: Bip32Derivation[];
27
+ tapBip32Derivation?: TapBip32Derivation[];
28
+ witnessUtxo?: WitnessUtxo;
29
+ tapLeafScript?: TapLeafScript[];
30
+ tapInternalKey?: string;
31
+ tapMerkleRoot?: string;
32
+ musig2Participants?: {
33
+ tapOutputKey: string;
34
+ tapInternalKey: string;
35
+ participantPubKeys: string[];
36
+ };
37
+ unknownKeyVals?: Array<{
38
+ key: string;
39
+ value: string;
40
+ }>;
41
+ };
42
+ export type Input = {
43
+ hash: string;
44
+ index: number;
45
+ sequence: number;
46
+ };
47
+ export type Output = {
48
+ script: string;
49
+ value: string;
50
+ address?: string;
51
+ };
52
+ export type TapTreeLeaf = {
53
+ depth: number;
54
+ leafVersion: number;
55
+ script: string;
56
+ };
57
+ export type PsbtOutput = {
58
+ redeemScript?: string;
59
+ witnessScript?: string;
60
+ bip32Derivation?: Bip32Derivation[];
61
+ tapBip32Derivation?: TapBip32Derivation[];
62
+ tapInternalKey?: string;
63
+ tapTree?: {
64
+ leaves: TapTreeLeaf[];
65
+ };
66
+ };
67
+ export type Fixture = {
68
+ walletKeys: [string, string, string];
69
+ psbtBase64: string;
70
+ psbtBase64Finalized: string | null;
71
+ inputs: Input[];
72
+ psbtInputs: PsbtInput[];
73
+ psbtInputsFinalized: PsbtInput[] | null;
74
+ outputs: Output[];
75
+ psbtOutputs: PsbtOutput[];
76
+ extractedTransaction: any | null;
77
+ };
78
+ /**
79
+ * Get PSBT buffer from a fixture
80
+ */
81
+ export declare function getPsbtBuffer(fixture: Fixture): Buffer;
82
+ /**
83
+ * Load a PSBT fixture from JSON file
84
+ */
85
+ export declare function loadPsbtFixture(network: string, signatureState: string): Fixture;
86
+ /**
87
+ * Load wallet keys from fixture
88
+ */
89
+ export declare function loadWalletKeysFromFixture(network: string): utxolib.bitgo.RootWalletKeys;
90
+ /**
91
+ * Get extracted transaction hex from fixture
92
+ */
93
+ export declare function getExtractedTransactionHex(fixture: Fixture): string;
@@ -0,0 +1,44 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname } from "node:path";
5
+ import * as utxolib from "@bitgo/utxo-lib";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ /**
9
+ * Get PSBT buffer from a fixture
10
+ */
11
+ export function getPsbtBuffer(fixture) {
12
+ return Buffer.from(fixture.psbtBase64, "base64");
13
+ }
14
+ /**
15
+ * Load a PSBT fixture from JSON file
16
+ */
17
+ export function loadPsbtFixture(network, signatureState) {
18
+ const fixturePath = path.join(__dirname, "..", "fixtures", "fixed-script", `psbt-lite.${network}.${signatureState}.json`);
19
+ const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
20
+ return JSON.parse(fixtureContent);
21
+ }
22
+ /**
23
+ * Load wallet keys from fixture
24
+ */
25
+ export function loadWalletKeysFromFixture(network) {
26
+ const fixturePath = path.join(__dirname, "..", "fixtures", "fixed-script", `psbt-lite.${network}.fullsigned.json`);
27
+ const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
28
+ const fixture = JSON.parse(fixtureContent);
29
+ // Parse xprvs and convert to xpubs
30
+ const xpubs = fixture.walletKeys.map((xprv) => {
31
+ const key = utxolib.bip32.fromBase58(xprv);
32
+ return key.neutered();
33
+ });
34
+ return new utxolib.bitgo.RootWalletKeys(xpubs);
35
+ }
36
+ /**
37
+ * Get extracted transaction hex from fixture
38
+ */
39
+ export function getExtractedTransactionHex(fixture) {
40
+ if (fixture.extractedTransaction === null) {
41
+ throw new Error("Fixture does not have an extracted transaction");
42
+ }
43
+ return fixture.extractedTransaction;
44
+ }
@@ -0,0 +1,140 @@
1
+ import assert from "node:assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { fixedScriptWallet } from "../../js/index.js";
4
+ import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer } from "./fixtureUtil.js";
5
+ function getOtherWalletKeys() {
6
+ const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets");
7
+ return new utxolib.bitgo.RootWalletKeys(otherWalletKeys);
8
+ }
9
+ describe("parseTransactionWithWalletKeys", function () {
10
+ // Replay protection script that matches Rust tests
11
+ const replayProtectionScript = Buffer.from("a91420b37094d82a513451ff0ccd9db23aba05bc5ef387", "hex");
12
+ const supportedNetworks = utxolib.getNetworkList().filter((network) => {
13
+ return (utxolib.isMainnet(network) &&
14
+ network !== utxolib.networks.bitcoincash &&
15
+ network !== utxolib.networks.bitcoingold &&
16
+ network !== utxolib.networks.bitcoinsv &&
17
+ network !== utxolib.networks.ecash &&
18
+ network !== utxolib.networks.zcash);
19
+ });
20
+ function hasReplayProtection(network) {
21
+ const mainnet = utxolib.getMainnet(network);
22
+ return mainnet === utxolib.networks.bitcoincash;
23
+ }
24
+ supportedNetworks.forEach((network) => {
25
+ const networkName = utxolib.getNetworkName(network);
26
+ describe(`network: ${networkName}`, function () {
27
+ let fullsignedPsbtBytes;
28
+ let bitgoPsbt;
29
+ let rootWalletKeys;
30
+ before(function () {
31
+ fullsignedPsbtBytes = getPsbtBuffer(loadPsbtFixture(networkName, "fullsigned"));
32
+ bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
33
+ rootWalletKeys = loadWalletKeysFromFixture(networkName);
34
+ });
35
+ it("should have matching unsigned transaction ID", function () {
36
+ const unsignedTxid = bitgoPsbt.unsignedTxid();
37
+ const expectedUnsignedTxid = utxolib.bitgo
38
+ .createPsbtFromBuffer(fullsignedPsbtBytes, network)
39
+ .getUnsignedTx()
40
+ .getId();
41
+ assert.strictEqual(unsignedTxid, expectedUnsignedTxid);
42
+ });
43
+ it("should parse transaction and identify internal/external outputs", function () {
44
+ const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
45
+ outputScripts: [replayProtectionScript],
46
+ });
47
+ // Verify all inputs have addresses and values
48
+ parsed.inputs.forEach((input, i) => {
49
+ assert.ok(input.address, `Input ${i} should have an address`);
50
+ assert.ok(typeof input.value === "bigint", `Input ${i} value should be bigint`);
51
+ assert.ok(input.value > 0n, `Input ${i} value should be > 0`);
52
+ });
53
+ // Validate outputs
54
+ assert.ok(parsed.outputs.length > 0, "Should have at least one output");
55
+ // Count internal outputs (scriptId is defined and not null)
56
+ const internalOutputs = parsed.outputs.filter((o) => o.scriptId);
57
+ // Count external outputs (scriptId is null or undefined)
58
+ const externalOutputs = parsed.outputs.filter((o) => o.scriptId === null);
59
+ assert.ok(externalOutputs.every((o) => o.address || o.script));
60
+ const nonAddressOutputs = externalOutputs.filter((o) => o.address === null);
61
+ assert.strictEqual(nonAddressOutputs.length, 1);
62
+ const [opReturnOutput] = nonAddressOutputs;
63
+ const expectedOpReturn = utxolib.payments.embed({
64
+ data: [Buffer.from("setec astronomy")],
65
+ }).output;
66
+ assert.strictEqual(Buffer.from(opReturnOutput.script).toString("hex"), expectedOpReturn.toString("hex"));
67
+ // Fixtures now have 3 external outputs
68
+ assert.ok(internalOutputs.length > 0, "Should have internal outputs (have scriptId)");
69
+ assert.strictEqual(externalOutputs.length, 3, "Should have 3 external outputs in test fixture");
70
+ // Verify all outputs have proper structure
71
+ parsed.outputs.forEach((output, i) => {
72
+ assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
73
+ assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
74
+ assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
75
+ // Address is optional for non-standard scripts
76
+ });
77
+ // Verify spend amount (should be > 0 since there are external outputs)
78
+ assert.strictEqual(parsed.spendAmount, 900n * 3n);
79
+ // Verify miner fee calculation
80
+ const totalInputValue = parsed.inputs.reduce((sum, i) => sum + i.value, 0n);
81
+ const totalOutputValue = parsed.outputs.reduce((sum, o) => sum + o.value, 0n);
82
+ assert.strictEqual(parsed.minerFee, totalInputValue - totalOutputValue, "Miner fee should equal inputs minus outputs");
83
+ assert.ok(parsed.minerFee > 0n, "Miner fee should be > 0");
84
+ // Verify virtual size
85
+ assert.ok(typeof parsed.virtualSize === "number", "Virtual size should be a number");
86
+ assert.ok(parsed.virtualSize > 0, "Virtual size should be > 0");
87
+ });
88
+ it("should fail to parse with other wallet keys", function () {
89
+ assert.throws(() => {
90
+ bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), {
91
+ outputScripts: [replayProtectionScript],
92
+ });
93
+ }, (error) => {
94
+ return error.message.includes("Failed to parse transaction: Input 0: wallet validation failed");
95
+ });
96
+ });
97
+ it("should recognize output for other wallet keys", function () {
98
+ const parsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(getOtherWalletKeys());
99
+ // Should return an array of parsed outputs
100
+ assert.ok(Array.isArray(parsedOutputs), "Should return an array");
101
+ assert.ok(parsedOutputs.length > 0, "Should have at least one output");
102
+ // Verify all outputs have proper structure
103
+ parsedOutputs.forEach((output, i) => {
104
+ assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
105
+ assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
106
+ assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
107
+ // Address can be null for non-standard scripts
108
+ assert.ok(typeof output.address === "string" || output.address === null, `Output ${i} address should be string or null`);
109
+ // scriptId can be null for external outputs
110
+ assert.ok(output.scriptId === null ||
111
+ (typeof output.scriptId === "object" &&
112
+ typeof output.scriptId.chain === "number" &&
113
+ typeof output.scriptId.index === "number"), `Output ${i} scriptId should be null or an object with chain and index`);
114
+ });
115
+ // Compare with the original wallet keys to verify we get different results
116
+ const originalParsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(rootWalletKeys);
117
+ // Should have the same number of outputs
118
+ assert.strictEqual(parsedOutputs.length, originalParsedOutputs.length, "Should parse the same number of outputs");
119
+ // Find outputs that belong to the other wallet keys (scriptId !== null)
120
+ const otherWalletOutputs = parsedOutputs.filter((o) => o.scriptId !== null);
121
+ // Should have exactly one output for the other wallet keys
122
+ assert.strictEqual(otherWalletOutputs.length, 1, "Should have exactly one output belonging to the other wallet keys");
123
+ // Verify that this output is marked as external (scriptId === null) under regular wallet keys
124
+ const otherWalletOutputIndex = parsedOutputs.findIndex((o) => o.scriptId !== null);
125
+ const sameOutputWithRegularKeys = originalParsedOutputs[otherWalletOutputIndex];
126
+ assert.strictEqual(sameOutputWithRegularKeys.scriptId, null, "The output belonging to other wallet keys should be marked as external (scriptId === null) when parsed with regular wallet keys");
127
+ });
128
+ });
129
+ });
130
+ describe("error handling", function () {
131
+ it("should throw error for invalid PSBT bytes", function () {
132
+ const invalidBytes = new Uint8Array([0x00, 0x01, 0x02]);
133
+ assert.throws(() => {
134
+ fixedScriptWallet.BitGoPsbt.fromBytes(invalidBytes, "bitcoin");
135
+ }, (error) => {
136
+ return error.message.includes("Failed to deserialize PSBT");
137
+ }, "Should throw error for invalid PSBT bytes");
138
+ });
139
+ });
140
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ import assert from "node:assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { fixedScriptWallet } from "../../js/index.js";
4
+ import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer, } from "./fixtureUtil.js";
5
+ /**
6
+ * Get expected signature state for an input based on type and signing stage
7
+ * @param inputType - The type of input (e.g., "p2shP2pk", "p2trMusig2")
8
+ * @param signatureStage - The signing stage (unsigned, halfsigned, fullsigned)
9
+ * @returns Expected signature state for replay protection OR multi-key signatures
10
+ */
11
+ function getExpectedSignatures(inputType, signatureStage) {
12
+ // p2shP2pk inputs use replay protection signature verification
13
+ if (inputType === "p2shP2pk") {
14
+ return {
15
+ hasReplayProtectionSignature: signatureStage === "halfsigned" || signatureStage === "fullsigned",
16
+ };
17
+ }
18
+ switch (signatureStage) {
19
+ case "unsigned":
20
+ return { user: false, backup: false, bitgo: false };
21
+ case "halfsigned":
22
+ // User signs first
23
+ return { user: true, backup: false, bitgo: false };
24
+ case "fullsigned":
25
+ // p2trMusig2 uses user + backup for 2-of-2 MuSig2
26
+ if (inputType === "p2trMusig2") {
27
+ return { user: true, backup: true, bitgo: false };
28
+ }
29
+ // Regular multisig uses user + bitgo
30
+ return { user: true, backup: false, bitgo: true };
31
+ default:
32
+ throw new Error(`Unknown signature stage: ${signatureStage}`);
33
+ }
34
+ }
35
+ /**
36
+ * Verify signature state for a specific input in a PSBT
37
+ * @param bitgoPsbt - The PSBT to verify
38
+ * @param rootWalletKeys - Wallet keys for verification
39
+ * @param inputIndex - The input index to verify
40
+ * @param inputType - The type of input (for replay protection handling)
41
+ * @param expectedSignatures - Expected signature state for each key or replay protection
42
+ */
43
+ function verifyInputSignatures(bitgoPsbt, rootWalletKeys, inputIndex, expectedSignatures) {
44
+ // Handle replay protection inputs (P2shP2pk)
45
+ if ("hasReplayProtectionSignature" in expectedSignatures) {
46
+ const replayProtectionScript = Buffer.from("a91420b37094d82a513451ff0ccd9db23aba05bc5ef387", "hex");
47
+ const hasReplaySig = bitgoPsbt.verifyReplayProtectionSignature(inputIndex, {
48
+ outputScripts: [replayProtectionScript],
49
+ });
50
+ assert.strictEqual(hasReplaySig, expectedSignatures.hasReplayProtectionSignature, `Input ${inputIndex} replay protection signature mismatch`);
51
+ return;
52
+ }
53
+ // Handle standard multisig inputs
54
+ const xpubs = rootWalletKeys.triple;
55
+ const hasUserSig = bitgoPsbt.verifySignature(inputIndex, xpubs[0].toBase58());
56
+ const hasBackupSig = bitgoPsbt.verifySignature(inputIndex, xpubs[1].toBase58());
57
+ const hasBitGoSig = bitgoPsbt.verifySignature(inputIndex, xpubs[2].toBase58());
58
+ assert.strictEqual(hasUserSig, expectedSignatures.user, `Input ${inputIndex} user key signature mismatch`);
59
+ assert.strictEqual(hasBackupSig, expectedSignatures.backup, `Input ${inputIndex} backup key signature mismatch`);
60
+ assert.strictEqual(hasBitGoSig, expectedSignatures.bitgo, `Input ${inputIndex} BitGo key signature mismatch`);
61
+ }
62
+ describe("verifySignature", function () {
63
+ const supportedNetworks = utxolib.getNetworkList().filter((network) => {
64
+ return (utxolib.isMainnet(network) &&
65
+ network !== utxolib.networks.bitcoincash &&
66
+ network !== utxolib.networks.bitcoingold &&
67
+ network !== utxolib.networks.bitcoinsv &&
68
+ network !== utxolib.networks.ecash &&
69
+ network !== utxolib.networks.zcash);
70
+ });
71
+ supportedNetworks.forEach((network) => {
72
+ const networkName = utxolib.getNetworkName(network);
73
+ describe(`network: ${networkName}`, function () {
74
+ let rootWalletKeys;
75
+ let unsignedFixture;
76
+ let halfsignedFixture;
77
+ let fullsignedFixture;
78
+ let unsignedBitgoPsbt;
79
+ let halfsignedBitgoPsbt;
80
+ let fullsignedBitgoPsbt;
81
+ before(function () {
82
+ rootWalletKeys = loadWalletKeysFromFixture(networkName);
83
+ unsignedFixture = loadPsbtFixture(networkName, "unsigned");
84
+ halfsignedFixture = loadPsbtFixture(networkName, "halfsigned");
85
+ fullsignedFixture = loadPsbtFixture(networkName, "fullsigned");
86
+ unsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(unsignedFixture), networkName);
87
+ halfsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(halfsignedFixture), networkName);
88
+ fullsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(fullsignedFixture), networkName);
89
+ });
90
+ describe("unsigned PSBT", function () {
91
+ it("should return false for unsigned inputs", function () {
92
+ // Verify all xpubs return false for all inputs
93
+ unsignedFixture.psbtInputs.forEach((input, index) => {
94
+ verifyInputSignatures(unsignedBitgoPsbt, rootWalletKeys, index, getExpectedSignatures(input.type, "unsigned"));
95
+ });
96
+ });
97
+ });
98
+ describe("half-signed PSBT", function () {
99
+ it("should return true for signed xpubs and false for unsigned", function () {
100
+ halfsignedFixture.psbtInputs.forEach((input, index) => {
101
+ verifyInputSignatures(halfsignedBitgoPsbt, rootWalletKeys, index, getExpectedSignatures(input.type, "halfsigned"));
102
+ });
103
+ });
104
+ });
105
+ describe("fully signed PSBT", function () {
106
+ it("should have 2 signatures (2-of-3 multisig)", function () {
107
+ // In fullsigned fixtures, verify 2 signatures exist per multisig input
108
+ fullsignedFixture.psbtInputs.forEach((input, index) => {
109
+ verifyInputSignatures(fullsignedBitgoPsbt, rootWalletKeys, index, getExpectedSignatures(input.type, "fullsigned"));
110
+ });
111
+ });
112
+ });
113
+ describe("error handling", function () {
114
+ it("should throw error for out of bounds input index", function () {
115
+ const xpubs = rootWalletKeys.triple;
116
+ assert.throws(() => {
117
+ fullsignedBitgoPsbt.verifySignature(999, xpubs[0].toBase58());
118
+ }, (error) => {
119
+ return error.message.includes("Input index 999 out of bounds");
120
+ }, "Should throw error for out of bounds input index");
121
+ });
122
+ it("should throw error for invalid xpub", function () {
123
+ assert.throws(() => {
124
+ fullsignedBitgoPsbt.verifySignature(0, "invalid-xpub");
125
+ }, (error) => {
126
+ return error.message.includes("Invalid xpub");
127
+ }, "Should throw error for invalid xpub");
128
+ });
129
+ it("should return false for xpub not in derivation path", function () {
130
+ // Create a different xpub that's not in the wallet
131
+ // Use a proper 32-byte seed (256 bits)
132
+ const differentSeed = Buffer.alloc(32, 0xaa); // 32 bytes filled with 0xaa
133
+ const differentKey = utxolib.bip32.fromSeed(differentSeed, network);
134
+ const differentXpub = differentKey.neutered();
135
+ const result = fullsignedBitgoPsbt.verifySignature(0, differentXpub.toBase58());
136
+ assert.strictEqual(result, false, "Should return false for xpub not in PSBT derivation paths");
137
+ });
138
+ });
139
+ });
140
+ });
141
+ });
@@ -0,0 +1 @@
1
+ export {};