Skip to content

Commit cfcddc0

Browse files
Merge pull request #190 from BitGo/BTC-3049.psbt-edit-support
feat(wasm-utxo)!: add index-based PSBT/transaction mutation ops
2 parents bb5a133 + 19d0182 commit cfcddc0

15 files changed

Lines changed: 642 additions & 338 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
type PsbtOutputData,
55
type PsbtOutputDataWithAddress,
66
} from "../wasm/wasm_utxo.js";
7-
import type { IPsbtIntrospectionWithAddress } from "../psbt.js";
7+
import type { IPsbtWithAddress } from "../psbt.js";
88
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
99
import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js";
1010
import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js";
@@ -124,7 +124,7 @@ export type ParseOutputsOptions = {
124124
payGoPubkeys?: ECPairArg[];
125125
};
126126

127-
export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
127+
export class BitGoPsbt implements IPsbtWithAddress {
128128
protected constructor(protected _wasm: WasmBitGoPsbt) {}
129129

130130
/**
@@ -202,6 +202,45 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
202202
* }, outputScript);
203203
* ```
204204
*/
205+
addInputAtIndex(
206+
index: number,
207+
txid: string,
208+
vout: number,
209+
value: bigint,
210+
script: Uint8Array,
211+
sequence?: number,
212+
): number;
213+
addInputAtIndex(index: number, options: AddInputOptions, script: Uint8Array): number;
214+
addInputAtIndex(
215+
index: number,
216+
txidOrOptions: string | AddInputOptions,
217+
voutOrScript: number | Uint8Array,
218+
value?: bigint,
219+
script?: Uint8Array,
220+
sequence?: number,
221+
): number {
222+
if (typeof txidOrOptions === "string") {
223+
return this._wasm.add_input_at_index(
224+
index,
225+
txidOrOptions,
226+
voutOrScript as number,
227+
value,
228+
script,
229+
sequence,
230+
);
231+
}
232+
const options = txidOrOptions;
233+
return this._wasm.add_input_at_index(
234+
index,
235+
options.txid,
236+
options.vout,
237+
options.value,
238+
voutOrScript as Uint8Array,
239+
options.sequence,
240+
options.prevTx,
241+
);
242+
}
243+
205244
addInput(options: AddInputOptions, script: Uint8Array): number {
206245
return this._wasm.add_input(
207246
options.txid,
@@ -225,41 +264,36 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
225264
* const outputIndex = psbt.addOutput(outputScript, 50000n);
226265
* ```
227266
*/
267+
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
268+
addOutputAtIndex(index: number, address: string, value: bigint): number;
269+
addOutputAtIndex(index: number, options: AddOutputOptions): number;
270+
addOutputAtIndex(
271+
index: number,
272+
scriptOrOptions: Uint8Array | string | AddOutputOptions,
273+
value?: bigint,
274+
): number {
275+
if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") {
276+
if (value === undefined) {
277+
throw new Error("Value is required when passing a script or address");
278+
}
279+
if (scriptOrOptions instanceof Uint8Array) {
280+
return this._wasm.add_output_at_index(index, scriptOrOptions, value);
281+
}
282+
return this._wasm.add_output_with_address_at_index(index, scriptOrOptions, value);
283+
}
284+
285+
const options = scriptOrOptions;
286+
if ("script" in options) {
287+
return this._wasm.add_output_at_index(index, options.script, options.value);
288+
}
289+
if ("address" in options) {
290+
return this._wasm.add_output_with_address_at_index(index, options.address, options.value);
291+
}
292+
throw new Error("Invalid output options");
293+
}
294+
228295
addOutput(script: Uint8Array, value: bigint): number;
229-
/**
230-
* Add an output to the PSBT by address
231-
*
232-
* @param address - The destination address
233-
* @param value - Value in satoshis
234-
* @returns The index of the newly added output
235-
*
236-
* @example
237-
* ```typescript
238-
* const outputIndex = psbt.addOutput("bc1q...", 50000n);
239-
* ```
240-
*/
241296
addOutput(address: string, value: bigint): number;
242-
/**
243-
* Add an output to the PSBT
244-
*
245-
* @param options - Output options (script or address, and value)
246-
* @returns The index of the newly added output
247-
*
248-
* @example
249-
* ```typescript
250-
* // Using script
251-
* const outputIndex = psbt.addOutput({
252-
* script: outputScript,
253-
* value: 50000n,
254-
* });
255-
*
256-
* // Using address
257-
* const outputIndex = psbt.addOutput({
258-
* address: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
259-
* value: 50000n,
260-
* });
261-
* ```
262-
*/
263297
addOutput(options: AddOutputOptions): number;
264298
addOutput(scriptOrOptions: Uint8Array | string | AddOutputOptions, value?: bigint): number {
265299
if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") {
@@ -321,6 +355,28 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
321355
* );
322356
* ```
323357
*/
358+
addWalletInputAtIndex(
359+
index: number,
360+
inputOptions: AddInputOptions,
361+
walletKeys: WalletKeysArg,
362+
walletOptions: AddWalletInputOptions,
363+
): number {
364+
const keys = RootWalletKeys.from(walletKeys);
365+
return this._wasm.add_wallet_input_at_index(
366+
index,
367+
inputOptions.txid,
368+
inputOptions.vout,
369+
inputOptions.value,
370+
keys.wasm,
371+
walletOptions.scriptId.chain,
372+
walletOptions.scriptId.index,
373+
walletOptions.signPath?.signer,
374+
walletOptions.signPath?.cosigner,
375+
inputOptions.sequence,
376+
inputOptions.prevTx,
377+
);
378+
}
379+
324380
addWalletInput(
325381
inputOptions: AddInputOptions,
326382
walletKeys: WalletKeysArg,
@@ -371,6 +427,21 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
371427
* });
372428
* ```
373429
*/
430+
addWalletOutputAtIndex(
431+
index: number,
432+
walletKeys: WalletKeysArg,
433+
options: AddWalletOutputOptions,
434+
): number {
435+
const keys = RootWalletKeys.from(walletKeys);
436+
return this._wasm.add_wallet_output_at_index(
437+
index,
438+
options.chain,
439+
options.index,
440+
options.value,
441+
keys.wasm,
442+
);
443+
}
444+
374445
addWalletOutput(walletKeys: WalletKeysArg, options: AddWalletOutputOptions): number {
375446
const keys = RootWalletKeys.from(walletKeys);
376447
return this._wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm);
@@ -395,6 +466,23 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
395466
* );
396467
* ```
397468
*/
469+
addReplayProtectionInputAtIndex(
470+
index: number,
471+
inputOptions: AddInputOptions,
472+
key: ECPairArg,
473+
): number {
474+
const ecpair = ECPair.from(key);
475+
return this._wasm.add_replay_protection_input_at_index(
476+
index,
477+
ecpair.wasm,
478+
inputOptions.txid,
479+
inputOptions.vout,
480+
inputOptions.value,
481+
inputOptions.sequence,
482+
inputOptions.prevTx,
483+
);
484+
}
485+
398486
addReplayProtectionInput(inputOptions: AddInputOptions, key: ECPairArg): number {
399487
const ecpair = ECPair.from(key);
400488
return this._wasm.add_replay_protection_input(
@@ -407,27 +495,31 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
407495
);
408496
}
409497

498+
removeInput(index: number): void {
499+
this._wasm.remove_input(index);
500+
}
501+
502+
removeOutput(index: number): void {
503+
this._wasm.remove_output(index);
504+
}
505+
410506
/**
411507
* Get the unsigned transaction ID
412508
* @returns The unsigned transaction ID
413509
*/
414-
unsignedTxid(): string {
510+
unsignedTxId(): string {
415511
return this._wasm.unsigned_txid();
416512
}
417513

418514
/**
419515
* Get the transaction version
420516
* @returns The transaction version number
421517
*/
422-
get version(): number {
518+
version(): number {
423519
return this._wasm.version();
424520
}
425521

426-
/**
427-
* Get the transaction lock time
428-
* @returns The transaction lock time
429-
*/
430-
get lockTime(): number {
522+
lockTime(): number {
431523
return this._wasm.lock_time();
432524
}
433525

@@ -828,15 +920,11 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
828920
* Get the number of inputs in the PSBT
829921
* @returns The number of inputs
830922
*/
831-
get inputCount(): number {
923+
inputCount(): number {
832924
return this._wasm.input_count();
833925
}
834926

835-
/**
836-
* Get the number of outputs in the PSBT
837-
* @returns The number of outputs
838-
*/
839-
get outputCount(): number {
927+
outputCount(): number {
840928
return this._wasm.output_count();
841929
}
842930

packages/wasm-utxo/js/index.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,19 @@ declare module "./wasm/wasm_utxo.js" {
119119
// Extraction methods
120120
extractTransaction(): WasmTransaction;
121121

122+
// Mutation methods
123+
addInputAtIndex(
124+
index: number,
125+
txid: string,
126+
vout: number,
127+
value: bigint,
128+
script: Uint8Array,
129+
sequence?: number,
130+
): number;
131+
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
132+
removeInput(index: number): void;
133+
removeOutput(index: number): void;
134+
122135
// Metadata methods
123136
unsignedTxId(): string;
124137
lockTime(): number;
@@ -130,8 +143,4 @@ export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js";
130143
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js";
131144
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js";
132145
export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js";
133-
export {
134-
hasPsbtMagic,
135-
type IPsbtIntrospection,
136-
type IPsbtIntrospectionWithAddress,
137-
} from "./psbt.js";
146+
export { hasPsbtMagic, type IPsbt, type IPsbtWithAddress } from "./psbt.js";

packages/wasm-utxo/js/psbt.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import type { PsbtInputData, PsbtOutputData, PsbtOutputDataWithAddress } from "./wasm/wasm_utxo.js";
22

3-
/** Common interface for PSBT introspection methods */
4-
export interface IPsbtIntrospection {
5-
readonly inputCount: number;
6-
readonly outputCount: number;
3+
/** Common interface for PSBT types */
4+
export interface IPsbt {
5+
inputCount(): number;
6+
outputCount(): number;
77
getInputs(): PsbtInputData[];
88
getOutputs(): PsbtOutputData[];
9+
version(): number;
10+
lockTime(): number;
11+
unsignedTxId(): string;
12+
addInputAtIndex(
13+
index: number,
14+
txid: string,
15+
vout: number,
16+
value: bigint,
17+
script: Uint8Array,
18+
sequence?: number,
19+
): number;
20+
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
21+
removeInput(index: number): void;
22+
removeOutput(index: number): void;
923
}
1024

11-
/** Extended introspection with address resolution (no coin parameter needed) */
12-
export interface IPsbtIntrospectionWithAddress extends IPsbtIntrospection {
25+
/** Extended PSBT with address resolution (no coin parameter needed) */
26+
export interface IPsbtWithAddress extends IPsbt {
1327
getOutputsWithAddress(): PsbtOutputDataWithAddress[];
1428
}
1529

packages/wasm-utxo/js/transaction.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,18 @@ export class Transaction implements ITransaction {
4141
* @param sequence - Optional sequence number (default: 0xFFFFFFFF)
4242
* @returns The index of the newly added input
4343
*/
44+
addInputAtIndex(index: number, txid: string, vout: number, sequence?: number): number {
45+
return this._wasm.add_input_at_index(index, txid, vout, sequence);
46+
}
47+
4448
addInput(txid: string, vout: number, sequence?: number): number {
4549
return this._wasm.add_input(txid, vout, sequence);
4650
}
4751

48-
/**
49-
* Add an output to the transaction
50-
* @param script - Output script (scriptPubKey)
51-
* @param value - Value in satoshis
52-
* @returns The index of the newly added output
53-
*/
52+
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number {
53+
return this._wasm.add_output_at_index(index, script, value);
54+
}
55+
5456
addOutput(script: Uint8Array, value: bigint): number {
5557
return this._wasm.add_output(script, value);
5658
}

packages/wasm-utxo/package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,16 @@
7575
"build:ts": "npm run build:ts-esm && npm run build:ts-cjs",
7676
"build:package-json": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
7777
"build": "npm run build:wasm && npm run build:ts && npm run build:package-json",
78-
"check-fmt": "prettier --check . && cargo fmt -- --check",
79-
"lint": "eslint .",
80-
"lint:fix": "eslint . --fix"
78+
"check-fmt": "npm run lint:prettier && npm run lint:rustfmt",
79+
"lint:prettier": "prettier --check .",
80+
"lint:eslint": "eslint .",
81+
"lint:rustfmt": "cargo fmt -- --check",
82+
"lint:clippy": "cargo clippy --all-targets --all-features -- -D warnings",
83+
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:rustfmt && npm run lint:clippy",
84+
"lint:prettier:fix": "prettier --write .",
85+
"lint:eslint:fix": "eslint . --fix",
86+
"lint:rustfmt:fix": "cargo fmt",
87+
"lint:fix": "npm run lint:prettier:fix && npm run lint:eslint:fix && npm run lint:rustfmt:fix"
8188
},
8289
"devDependencies": {
8390
"@bitgo/unspents": "^0.50.13",

0 commit comments

Comments
 (0)