@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,91 @@
1
+ import * as assert from "assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { Descriptor } from "../js/index.js";
4
+ import { getDescriptorForScriptType } from "./descriptorUtil.js";
5
+ const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
6
+ const scriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"];
7
+ const scope = ["external", "internal"];
8
+ const index = [0, 1, 2];
9
+ /** Get the expected max weight to satisfy the descriptor */
10
+ function getExpectedMaxWeightToSatisfy(scriptType) {
11
+ switch (scriptType) {
12
+ case "p2sh":
13
+ return 256;
14
+ case "p2shP2wsh":
15
+ return 99;
16
+ case "p2wsh":
17
+ return 64;
18
+ default:
19
+ throw new Error("unexpected script type");
20
+ }
21
+ }
22
+ /** Compute the total size of the input, including overhead */
23
+ function getTotalInputSize(vSize) {
24
+ const sizeOpPushData1 = 1;
25
+ const sizeOpPushData2 = 2;
26
+ return (32 /* txid */ +
27
+ 4 /* vout */ +
28
+ 4 /* nSequence */ +
29
+ (vSize < 255 ? sizeOpPushData1 : sizeOpPushData2) +
30
+ vSize);
31
+ }
32
+ /** Get the full expected vSize of the input including overhead */
33
+ function getExpectedVSize(scriptType) {
34
+ // https://github.com/BitGo/BitGoJS/blob/master/modules/unspents/docs/input-costs.md
35
+ switch (scriptType) {
36
+ case "p2sh":
37
+ return 298;
38
+ case "p2shP2wsh":
39
+ return 140;
40
+ case "p2wsh":
41
+ return 105;
42
+ default:
43
+ throw new Error("unexpected script type");
44
+ }
45
+ }
46
+ function runTest(scriptType, index, scope) {
47
+ describe(`scriptType=${scriptType}, index=${index}, scope=${scope}`, function () {
48
+ const chainCode = scope === "external"
49
+ ? utxolib.bitgo.getExternalChainCode(scriptType)
50
+ : utxolib.bitgo.getInternalChainCode(scriptType);
51
+ const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chainCode, index);
52
+ const scriptUtxolib = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys.publicKeys, scriptType).scriptPubKey;
53
+ let descriptor;
54
+ before(function () {
55
+ descriptor = Descriptor.fromString(getDescriptorForScriptType(rootWalletKeys, scriptType, scope), "derivable");
56
+ });
57
+ it("descriptor should have expected format", function () {
58
+ const [x1, x2, x3] = rootWalletKeys.triple.map((xpub) => xpub.neutered().toBase58());
59
+ if (scriptType === "p2sh" && scope === "external") {
60
+ // spot check
61
+ assert.ok(descriptor
62
+ .toString()
63
+ .startsWith(`sh(multi(2,${x1}/0/0/0/*,${x2}/0/0/0/*,${x3}/0/0/0/*))`));
64
+ }
65
+ if (scriptType === "p2shP2wsh" && scope === "internal") {
66
+ // spot check
67
+ assert.ok(descriptor
68
+ .toString()
69
+ .startsWith(`sh(wsh(multi(2,${x1}/0/0/11/*,${x2}/0/0/11/*,${x3}/0/0/11/*)))`));
70
+ }
71
+ });
72
+ it("address should match descriptor", function () {
73
+ const scriptFromDescriptor = Buffer.from(descriptor.atDerivationIndex(index).scriptPubkey());
74
+ assert.deepStrictEqual(scriptUtxolib.toString("hex"), scriptFromDescriptor.toString("hex"));
75
+ });
76
+ it("should have expected weights", function () {
77
+ assert.ok(Number.isInteger(descriptor.maxWeightToSatisfy()));
78
+ const vSize = Math.ceil(descriptor.maxWeightToSatisfy() / 4);
79
+ console.log(scriptType, "scriptLength", descriptor.atDerivationIndex(0).encode().length, "vSize", vSize);
80
+ assert.equal(vSize, getExpectedMaxWeightToSatisfy(scriptType));
81
+ assert.equal(getTotalInputSize(vSize), getExpectedVSize(scriptType));
82
+ });
83
+ });
84
+ }
85
+ scriptTypes.forEach((scriptType) => {
86
+ index.forEach((index) => {
87
+ scope.forEach((scope) => {
88
+ runTest(scriptType, index, scope);
89
+ });
90
+ });
91
+ });
@@ -0,0 +1 @@
1
+ export declare function getFixture(path: string, defaultValue: unknown): Promise<unknown>;
@@ -0,0 +1,12 @@
1
+ import * as fs from "fs/promises";
2
+ export async function getFixture(path, defaultValue) {
3
+ try {
4
+ return JSON.parse(await fs.readFile(path, "utf8"));
5
+ }
6
+ catch (e) {
7
+ if (e.code === "ENOENT") {
8
+ await fs.writeFile(path, JSON.stringify(defaultValue, null, 2));
9
+ throw new Error(`Fixture not found at ${path}, created a new one`);
10
+ }
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import * as assert from "assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { Descriptor } from "../js/index.js";
4
+ import { finalizePsbt, updateInputWithDescriptor } from "./psbt.util.js";
5
+ import { getFixture } from "./fixtures.js";
6
+ const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
7
+ function getDescriptorOpDropP2ms(locktime, keys) {
8
+ const xpubs = keys.map((key) => key.toBase58() + "/*");
9
+ // the `r:` prefix is a custom BitGo modification of miniscript to allow OP_DROP
10
+ return `wsh(and_v(r:after(${locktime}),multi(2,${xpubs.join(",")})))`;
11
+ }
12
+ describe("CLV with OP_DROP", function () {
13
+ const locktime = 1024;
14
+ const descriptor = Descriptor.fromString(getDescriptorOpDropP2ms(locktime, rootWalletKeys.triple), "derivable");
15
+ it("has expected AST", () => {
16
+ assert.deepStrictEqual(descriptor.node(), {
17
+ Wsh: {
18
+ Ms: {
19
+ AndV: [
20
+ {
21
+ Drop: {
22
+ After: {
23
+ absLockTime: 1024,
24
+ },
25
+ },
26
+ },
27
+ {
28
+ Multi: [
29
+ 2,
30
+ {
31
+ XPub: "xpub661MyMwAqRbcFNusVUbSN3nbanHMtJjLgZGrs1wxH6f77kKQd6Vq4HfkZQNPC1vSbN6RTiBWJJV6FwJtCfBon2SgaT2J3MSkydukstKjwbJ/*",
32
+ },
33
+ {
34
+ XPub: "xpub661MyMwAqRbcFo3t7PUqvbgvAcEuuoeVib5aapsg52inrG6KGF5aNtR5ey1FNCt1zJpMQiNec5XpofQmLNRhHvQRbhkc8UsWwwMwsXW6ogU/*",
35
+ },
36
+ {
37
+ XPub: "xpub661MyMwAqRbcGg7f22Kcg2gy1F4jBjWR3xQTECVeJPHmxvhg5gUAZC6EYFtnyi6aMDQir1kV8HzCqC2FzTowGgEZqRh7rinqUCDeNDdmYzH/*",
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ });
45
+ });
46
+ it("has expected asm", () => {
47
+ assert.deepStrictEqual(descriptor.atDerivationIndex(0).toAsmString().split(" "), [
48
+ "OP_PUSHBYTES_2",
49
+ "0004",
50
+ "OP_CLTV",
51
+ "OP_DROP",
52
+ "OP_PUSHNUM_2",
53
+ "OP_PUSHBYTES_33",
54
+ "02ae7c3c0ebc315a33151a1985ebb1fdcae72b3b91c38e3193c40ebabfffe9c343",
55
+ "OP_PUSHBYTES_33",
56
+ "0260ba2407f7c75d525db9f171e9b2f3cf5ba3f0d7fc6067b20d4b91585432f974",
57
+ "OP_PUSHBYTES_33",
58
+ "03eadd6e4300dac62f1d4cf1131a06c5e140911f04245c64934c27510e93dbe843",
59
+ "OP_PUSHNUM_3",
60
+ "OP_CHECKMULTISIG",
61
+ ]);
62
+ });
63
+ it("can be signed", async function () {
64
+ const psbt = Object.assign(new utxolib.Psbt({ network: utxolib.networks.bitcoin }), {
65
+ locktime,
66
+ });
67
+ const signers = rootWalletKeys.triple.slice(0, 2);
68
+ const descriptorAt0 = descriptor.atDerivationIndex(0);
69
+ const script = Buffer.from(descriptorAt0.scriptPubkey());
70
+ psbt.addInput({
71
+ hash: Buffer.alloc(32),
72
+ index: 0,
73
+ sequence: 0xfffffffe,
74
+ witnessUtxo: { script, value: BigInt(1e8) },
75
+ });
76
+ psbt.addOutput({ script, value: BigInt(1e8) });
77
+ updateInputWithDescriptor(psbt, 0, descriptorAt0);
78
+ for (const signer of signers) {
79
+ psbt.signAllInputsHD(signer);
80
+ }
81
+ finalizePsbt(psbt);
82
+ const signedTx = psbt.extractTransaction().toBuffer();
83
+ assert.strictEqual(signedTx.toString("hex"), await getFixture("test/fixtures/opdrop.json", signedTx.toString("hex")));
84
+ });
85
+ });
@@ -0,0 +1,8 @@
1
+ import * as utxolib from "@bitgo/utxo-lib";
2
+ import { Descriptor, Psbt } from "../js/index.js";
3
+ export declare function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | utxolib.Psbt | Buffer | Uint8Array): Psbt;
4
+ export declare function toUtxoPsbt(psbt: Psbt | Buffer | Uint8Array): utxolib.bitgo.UtxoPsbt<utxolib.bitgo.UtxoTransaction<bigint>>;
5
+ export declare function updateInputWithDescriptor(psbt: utxolib.Psbt, inputIndex: number, descriptor: Descriptor): void;
6
+ export declare function updateOutputWithDescriptor(psbt: utxolib.Psbt, outputIndex: number, descriptor: Descriptor): void;
7
+ export declare function finalizePsbt(psbt: utxolib.Psbt): void;
8
+ export declare function assertEqualPsbt(a: utxolib.Psbt, b: utxolib.Psbt): void;
@@ -0,0 +1,116 @@
1
+ import * as assert from "node:assert";
2
+ import * as utxolib from "@bitgo/utxo-lib";
3
+ import { Psbt } from "../js/index.js";
4
+ function toAddress(descriptor, network) {
5
+ utxolib.address.fromOutputScript(Buffer.from(descriptor.scriptPubkey()), network);
6
+ }
7
+ export function toWrappedPsbt(psbt) {
8
+ if (psbt instanceof utxolib.bitgo.UtxoPsbt || psbt instanceof utxolib.Psbt) {
9
+ psbt = psbt.toBuffer();
10
+ }
11
+ if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
12
+ return Psbt.deserialize(psbt);
13
+ }
14
+ throw new Error("Invalid input");
15
+ }
16
+ export function toUtxoPsbt(psbt) {
17
+ if (psbt instanceof Psbt) {
18
+ psbt = psbt.serialize();
19
+ }
20
+ if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
21
+ return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), {
22
+ network: utxolib.networks.bitcoin,
23
+ });
24
+ }
25
+ throw new Error("Invalid input");
26
+ }
27
+ export function updateInputWithDescriptor(psbt, inputIndex, descriptor) {
28
+ const wrappedPsbt = toWrappedPsbt(psbt);
29
+ wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor);
30
+ psbt.data.inputs[inputIndex] = toUtxoPsbt(wrappedPsbt).data.inputs[inputIndex];
31
+ }
32
+ export function updateOutputWithDescriptor(psbt, outputIndex, descriptor) {
33
+ const wrappedPsbt = toWrappedPsbt(psbt);
34
+ wrappedPsbt.updateOutputWithDescriptor(outputIndex, descriptor);
35
+ psbt.data.outputs[outputIndex] = toUtxoPsbt(wrappedPsbt).data.outputs[outputIndex];
36
+ }
37
+ export function finalizePsbt(psbt) {
38
+ const wrappedPsbt = toWrappedPsbt(psbt);
39
+ wrappedPsbt.finalize();
40
+ const unwrappedPsbt = toUtxoPsbt(wrappedPsbt);
41
+ for (let i = 0; i < psbt.data.inputs.length; i++) {
42
+ psbt.data.inputs[i] = unwrappedPsbt.data.inputs[i];
43
+ }
44
+ }
45
+ function toEntries(k, v, path) {
46
+ if (matchPath(path, ["data", "inputs", any, "sighashType"])) {
47
+ return [];
48
+ }
49
+ if (matchPath(path.slice(-1), ["unknownKeyVals"])) {
50
+ if (Array.isArray(v) && v.length === 0) {
51
+ return [];
52
+ }
53
+ }
54
+ return [[k, toPlainObject(v, path)]];
55
+ }
56
+ const any = Symbol("any");
57
+ function matchPath(path, pattern) {
58
+ if (path.length !== pattern.length) {
59
+ return false;
60
+ }
61
+ for (let i = 0; i < path.length; i++) {
62
+ if (pattern[i] !== any && path[i] !== pattern[i]) {
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
68
+ function normalizeBip32Derivation(v) {
69
+ if (!Array.isArray(v)) {
70
+ throw new Error("Expected bip32Derivation to be an array");
71
+ }
72
+ return [...v]
73
+ .map((e) => {
74
+ let { path } = e;
75
+ if (path.startsWith("m/")) {
76
+ path = path.slice(2);
77
+ }
78
+ return {
79
+ ...e,
80
+ path,
81
+ };
82
+ })
83
+ .sort((a, b) => a.masterFingerprint.toString().localeCompare(b.masterFingerprint.toString()));
84
+ }
85
+ function toPlainObject(v, path) {
86
+ // psbts have fun getters and other types of irregular properties that we mash into shape here
87
+ if (v === null || v === undefined) {
88
+ return v;
89
+ }
90
+ if (matchPath(path, ["data", "inputs", any, "bip32Derivation"]) ||
91
+ matchPath(path, ["data", "outputs", any, "bip32Derivation"])) {
92
+ v = normalizeBip32Derivation(v);
93
+ }
94
+ switch (typeof v) {
95
+ case "number":
96
+ case "bigint":
97
+ case "string":
98
+ case "boolean":
99
+ return v;
100
+ case "object":
101
+ if (v instanceof Buffer || v instanceof Uint8Array) {
102
+ return v.toString("hex");
103
+ }
104
+ if (Array.isArray(v)) {
105
+ return v.map((v, i) => toPlainObject(v, [...path, i]));
106
+ }
107
+ return Object.fromEntries(Object.entries(v)
108
+ .flatMap(([k, v]) => toEntries(k, v, [...path, k]))
109
+ .sort(([a], [b]) => a.localeCompare(b)));
110
+ default:
111
+ throw new Error(`Unsupported type: ${typeof v}`);
112
+ }
113
+ }
114
+ export function assertEqualPsbt(a, b) {
115
+ assert.deepStrictEqual(toPlainObject(a, []), toPlainObject(b, []));
116
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import * as utxolib from "@bitgo/utxo-lib";
2
+ import * as assert from "node:assert";
3
+ import { getPsbtFixtures } from "./psbtFixedScriptCompatFixtures.js";
4
+ import { Descriptor } from "../js/index.js";
5
+ import { getDescriptorForScriptType } from "./descriptorUtil.js";
6
+ import { assertEqualPsbt, toUtxoPsbt, toWrappedPsbt, updateInputWithDescriptor, } from "./psbt.util.js";
7
+ import { getKey } from "@bitgo/utxo-lib/dist/src/testutil";
8
+ const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
9
+ function assertEqualBuffer(a, b, message) {
10
+ assert.strictEqual(Buffer.from(a).toString("hex"), Buffer.from(b).toString("hex"), message);
11
+ }
12
+ const fixtures = getPsbtFixtures(rootWalletKeys);
13
+ function describeUpdateInputWithDescriptor(psbt, scriptType) {
14
+ function getFixtureAtStage(stage) {
15
+ const f = fixtures.find((f) => f.scriptType === scriptType && f.stage === stage);
16
+ if (!f) {
17
+ throw new Error(`Could not find fixture for scriptType ${scriptType} and stage ${stage}`);
18
+ }
19
+ return f;
20
+ }
21
+ const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
22
+ const index = 0;
23
+ const descriptor = Descriptor.fromString(descriptorStr, "derivable");
24
+ function getWrappedPsbt() {
25
+ return toWrappedPsbt(psbt);
26
+ }
27
+ function getWrappedPsbtWithDescriptorInfo() {
28
+ const wrappedPsbt = getWrappedPsbt();
29
+ const descriptorAtDerivation = descriptor.atDerivationIndex(index);
30
+ wrappedPsbt.updateInputWithDescriptor(0, descriptorAtDerivation);
31
+ wrappedPsbt.updateOutputWithDescriptor(0, descriptorAtDerivation);
32
+ return wrappedPsbt;
33
+ }
34
+ describe("Wrapped PSBT updateInputWithDescriptor", function () {
35
+ it("should update the input with the descriptor", function () {
36
+ const wrappedPsbt = getWrappedPsbtWithDescriptorInfo();
37
+ const updatedPsbt = toUtxoPsbt(wrappedPsbt);
38
+ assertEqualPsbt(updatedPsbt, getFixtureAtStage("unsigned").psbt);
39
+ updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
40
+ updatedPsbt.signAllInputsHD(rootWalletKeys.triple[2]);
41
+ const wrappedSignedPsbt = toWrappedPsbt(updatedPsbt);
42
+ updatedPsbt.finalizeAllInputs();
43
+ wrappedSignedPsbt.finalize();
44
+ assertEqualBuffer(updatedPsbt.toBuffer(), wrappedSignedPsbt.serialize());
45
+ assertEqualBuffer(getFixtureAtStage("fullsigned")
46
+ .psbt.clone()
47
+ .finalizeAllInputs()
48
+ .extractTransaction()
49
+ .toBuffer(), updatedPsbt.extractTransaction().toBuffer());
50
+ });
51
+ });
52
+ describe("updateInputWithDescriptor util", function () {
53
+ it("should update the input with the descriptor", function () {
54
+ const cloned = psbt.clone();
55
+ updateInputWithDescriptor(cloned, 0, descriptor.atDerivationIndex(index));
56
+ cloned.signAllInputsHD(rootWalletKeys.triple[0]);
57
+ cloned.signAllInputsHD(rootWalletKeys.triple[2]);
58
+ cloned.finalizeAllInputs();
59
+ assertEqualBuffer(getFixtureAtStage("fullsigned")
60
+ .psbt.clone()
61
+ .finalizeAllInputs()
62
+ .extractTransaction()
63
+ .toBuffer(), cloned.extractTransaction().toBuffer());
64
+ });
65
+ });
66
+ describe("psbt signWithXprv", function () {
67
+ function signWithKey(keys, { checkFinalized = false } = {}) {
68
+ it(`signs the input with keys ${keys}`, function () {
69
+ const psbt = getWrappedPsbtWithDescriptorInfo();
70
+ keys.forEach((keyName) => {
71
+ const key = keyName === "unrelated" ? getKey(keyName) : rootWalletKeys[keyName];
72
+ const derivationPaths = toUtxoPsbt(psbt).data.inputs[0].bip32Derivation.map((d) => d.path);
73
+ assert.ok(derivationPaths.every((p) => p === derivationPaths[0]));
74
+ const derived = key.derivePath(derivationPaths[0]);
75
+ assert.deepStrictEqual(psbt.signWithXprv(key.toBase58()), {
76
+ // map: input index -> pubkey array
77
+ 0: { Ecdsa: keyName === "unrelated" ? [] : [derived.publicKey.toString("hex")] },
78
+ });
79
+ });
80
+ if (checkFinalized) {
81
+ psbt.finalize();
82
+ assertEqualBuffer(toUtxoPsbt(psbt).extractTransaction().toBuffer(), getFixtureAtStage("fullsigned")
83
+ .psbt.finalizeAllInputs()
84
+ .extractTransaction()
85
+ .toBuffer());
86
+ }
87
+ });
88
+ }
89
+ signWithKey(["user"]);
90
+ signWithKey(["backup"]);
91
+ signWithKey(["bitgo"]);
92
+ signWithKey(["unrelated"]);
93
+ signWithKey(["user", "bitgo"], { checkFinalized: true });
94
+ });
95
+ }
96
+ fixtures.forEach(({ psbt, scriptType, stage }) => {
97
+ describe(`PSBT fixture ${scriptType} ${stage}`, function () {
98
+ let buf;
99
+ let wrappedPsbt;
100
+ before(function () {
101
+ buf = psbt.toBuffer();
102
+ wrappedPsbt = toWrappedPsbt(buf);
103
+ });
104
+ it("should map to same hex", function () {
105
+ assertEqualBuffer(buf, wrappedPsbt.serialize());
106
+ });
107
+ it("should round-trip utxolib -> ms -> utxolib", function () {
108
+ assertEqualBuffer(buf, toUtxoPsbt(wrappedPsbt).toBuffer());
109
+ });
110
+ if (stage === "bare") {
111
+ describeUpdateInputWithDescriptor(psbt, scriptType);
112
+ }
113
+ });
114
+ });
@@ -0,0 +1,10 @@
1
+ import * as utxolib from "@bitgo/utxo-lib";
2
+ import { RootWalletKeys } from "@bitgo/utxo-lib/dist/src/bitgo";
3
+ export type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";
4
+ export declare function toPsbtWithPrevOutOnly(psbt: utxolib.bitgo.UtxoPsbt): utxolib.bitgo.UtxoPsbt<utxolib.bitgo.UtxoTransaction<bigint>>;
5
+ export type PsbtFixture = {
6
+ psbt: utxolib.bitgo.UtxoPsbt;
7
+ scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3;
8
+ stage: PsbtStage;
9
+ };
10
+ export declare function getPsbtFixtures(keys: RootWalletKeys): PsbtFixture[];
@@ -0,0 +1,53 @@
1
+ import * as utxolib from "@bitgo/utxo-lib";
2
+ export function toPsbtWithPrevOutOnly(psbt) {
3
+ const psbtCopy = utxolib.bitgo.UtxoPsbt.createPsbt({
4
+ network: utxolib.networks.bitcoin,
5
+ });
6
+ psbtCopy.setVersion(psbt.version);
7
+ psbtCopy.setLocktime(psbt.locktime);
8
+ psbt.txInputs.forEach((input, vin) => {
9
+ const { witnessUtxo, nonWitnessUtxo } = psbt.data.inputs[vin];
10
+ psbtCopy.addInput({
11
+ hash: input.hash,
12
+ index: input.index,
13
+ sequence: input.sequence,
14
+ ...(witnessUtxo ? { witnessUtxo } : { nonWitnessUtxo }),
15
+ });
16
+ });
17
+ psbt.txOutputs.forEach((output, vout) => {
18
+ psbtCopy.addOutput(output);
19
+ });
20
+ return psbtCopy;
21
+ }
22
+ function getPsbtWithScriptTypeAndStage(keys, scriptType, stage) {
23
+ if (stage === "bare") {
24
+ const psbt = getPsbtWithScriptTypeAndStage(keys, scriptType, "unsigned");
25
+ return toPsbtWithPrevOutOnly(psbt);
26
+ }
27
+ return utxolib.testutil.constructPsbt([
28
+ {
29
+ scriptType,
30
+ value: BigInt(1e8),
31
+ },
32
+ ], [
33
+ {
34
+ value: BigInt(1e8 - 1000),
35
+ scriptType,
36
+ isInternalAddress: true,
37
+ },
38
+ ], utxolib.networks.bitcoin, keys, stage);
39
+ }
40
+ export function getPsbtFixtures(keys) {
41
+ const testMatrixScriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"];
42
+ const testMatrixStages = ["bare", "unsigned", "halfsigned", "fullsigned"];
43
+ return testMatrixStages.flatMap((stage) => {
44
+ return testMatrixScriptTypes.map((scriptType) => {
45
+ return {
46
+ psbt: getPsbtWithScriptTypeAndStage(keys, scriptType, stage),
47
+ name: `${scriptType}-${stage}`,
48
+ scriptType,
49
+ stage,
50
+ };
51
+ });
52
+ });
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ import assert from "node:assert";
2
+ import { ECPair } from "@bitgo/utxo-lib";
3
+ import { getKey } from "@bitgo/utxo-lib/dist/src/testutil";
4
+ import { formatNode } from "../js/ast/index.js";
5
+ import { mockPsbtDefault } from "./psbtFromDescriptor.util.js";
6
+ import { Descriptor } from "../js/index.js";
7
+ import { toWrappedPsbt } from "./psbt.util.js";
8
+ function toKeyWithPath(k, path = "*") {
9
+ return k.neutered().toBase58() + "/" + path;
10
+ }
11
+ function toKeyPlain(k) {
12
+ return k.toString("hex");
13
+ }
14
+ function toECPair(k) {
15
+ assert(k.privateKey);
16
+ return ECPair.fromPrivateKey(k.privateKey);
17
+ }
18
+ function toKeyPlainXOnly(k) {
19
+ return k.subarray(1).toString("hex");
20
+ }
21
+ const external = getKey("external");
22
+ const a = getKey("a");
23
+ const b = getKey("b");
24
+ const c = getKey("c");
25
+ const keys = { external, a, b, c };
26
+ function getKeyName(k) {
27
+ const objKeys = Object.keys(keys);
28
+ return objKeys.find((key) => keys[key] === k || toECPair(keys[key]).publicKey.equals(k.publicKey));
29
+ }
30
+ function describeSignDescriptor(name, descriptor, { signBip32 = [], signECPair = [], }) {
31
+ describe(`psbt with descriptor ${name}`, function () {
32
+ const isTaproot = Object.keys(descriptor.node())[0] === "Tr";
33
+ const psbt = mockPsbtDefault({
34
+ descriptorSelf: descriptor,
35
+ descriptorOther: Descriptor.fromString(formatNode({ wpkh: toKeyWithPath(external) }), "derivable"),
36
+ });
37
+ function getSigResult(keys) {
38
+ return {
39
+ [isTaproot ? "Schnorr" : "Ecdsa"]: keys.map((key) => key.publicKey.subarray(isTaproot ? 1 : 0).toString("hex")),
40
+ };
41
+ }
42
+ signBip32.forEach((signSeq, i) => {
43
+ it(`should sign ${signSeq.map((k) => getKeyName(k))} xprv`, function () {
44
+ const wrappedPsbt = toWrappedPsbt(psbt);
45
+ signSeq.forEach((key) => {
46
+ assert.deepStrictEqual(wrappedPsbt.signWithXprv(key.toBase58()), {
47
+ 0: getSigResult([key.derive(0)]),
48
+ 1: getSigResult([key.derive(1)]),
49
+ });
50
+ });
51
+ wrappedPsbt.finalize();
52
+ });
53
+ it(`should sign ${signSeq.map((k) => getKeyName(k))} prv buffer`, function () {
54
+ const wrappedPsbt = toWrappedPsbt(psbt);
55
+ signSeq.forEach((key) => {
56
+ assert.deepStrictEqual(wrappedPsbt.signWithPrv(key.derive(0).privateKey), {
57
+ // NOTE: signing with a plain derived key does not work for taproot
58
+ // see SingleKeySigner implementation in psbt.rs for details
59
+ 0: getSigResult(isTaproot ? [] : [key.derive(0)]),
60
+ 1: getSigResult([]),
61
+ });
62
+ });
63
+ });
64
+ });
65
+ signECPair.forEach((signSeq, i) => {
66
+ it(`should sign ${signSeq.map((k) => getKeyName(k))} ec pair`, function () {
67
+ const wrappedPsbt = toWrappedPsbt(psbt);
68
+ signSeq.forEach((key) => {
69
+ assert(key.privateKey);
70
+ assert.deepStrictEqual(wrappedPsbt.signWithPrv(key.privateKey), {
71
+ 0: getSigResult([key]),
72
+ 1: getSigResult([key]),
73
+ });
74
+ });
75
+ wrappedPsbt.finalize();
76
+ });
77
+ });
78
+ });
79
+ }
80
+ function fromNodes(node, type) {
81
+ return Descriptor.fromString(formatNode(node), type);
82
+ }
83
+ describeSignDescriptor("Wsh2Of3", fromNodes({
84
+ wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] },
85
+ }, "derivable"), {
86
+ signBip32: [
87
+ [a, b],
88
+ [b, a],
89
+ ],
90
+ });
91
+ describeSignDescriptor("Tr1Of3", fromNodes({
92
+ tr: [toKeyWithPath(a), [{ pk: toKeyWithPath(b) }, { pk: toKeyWithPath(c) }]],
93
+ }, "derivable"), { signBip32: [[a], [b], [c]] });
94
+ describeSignDescriptor("TrWithExternalPlain", fromNodes({
95
+ tr: [
96
+ toKeyPlainXOnly(external.publicKey),
97
+ [
98
+ { pk: toKeyPlainXOnly(external.publicKey) },
99
+ {
100
+ or_b: [
101
+ { pk: toKeyPlainXOnly(external.publicKey) },
102
+ { "s:pk": toKeyPlainXOnly(a.publicKey) },
103
+ ],
104
+ },
105
+ ],
106
+ ],
107
+ }, "definite"), { signECPair: [[toECPair(a)]] });