Skip to content

Commit ae648c8

Browse files
Merge pull request #127 from BitGo/BTC-2866.more-descriptor-wallet-support
feat(wasm-utxo): port descriptor wallet functionality from utxo-core
2 parents 41607f8 + 1ba55fa commit ae648c8

32 files changed

Lines changed: 2657 additions & 5 deletions

packages/wasm-utxo/eslint.config.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default tseslint.config(
77
{
88
languageOptions: {
99
parserOptions: {
10-
projectService: true,
10+
project: ["./tsconfig.json", "./tsconfig.test.json"],
1111
tsconfigRootDir: import.meta.dirname,
1212
},
1313
},
@@ -25,4 +25,29 @@ export default tseslint.config(
2525
"*.config.js",
2626
],
2727
},
28+
// Ban Node.js globals in production code
29+
{
30+
files: ["js/**/*.ts"],
31+
rules: {
32+
"no-restricted-globals": [
33+
"error",
34+
{
35+
name: "Buffer",
36+
message: "Use Uint8Array instead of Buffer for ESM compatibility.",
37+
},
38+
{
39+
name: "process",
40+
message: "Avoid Node.js process global for ESM compatibility.",
41+
},
42+
{
43+
name: "__dirname",
44+
message: "Use import.meta.url instead of __dirname for ESM.",
45+
},
46+
{
47+
name: "__filename",
48+
message: "Use import.meta.url instead of __filename for ESM.",
49+
},
50+
],
51+
},
52+
},
2853
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* DescriptorMap type and utilities.
3+
* Moved from @bitgo/utxo-core.
4+
*/
5+
import { Descriptor } from "../index.js";
6+
7+
/** Map from descriptor name to descriptor (TypeScript Map) */
8+
export type DescriptorMap = Map<string, Descriptor>;
9+
10+
/** Convert an array of descriptor name-value pairs to a descriptor map */
11+
export function toDescriptorMap(
12+
descriptors: { name: string; value: Descriptor | string }[],
13+
): DescriptorMap {
14+
return new Map(
15+
descriptors.map((d) => [
16+
d.name,
17+
d.value instanceof Descriptor ? d.value : Descriptor.fromStringDetectType(d.value),
18+
]),
19+
);
20+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Descriptor output types and utilities.
3+
* Moved from @bitgo/utxo-core.
4+
*/
5+
import { Descriptor } from "../index.js";
6+
7+
import { getFixedOutputSum, MaxOutput, Output, PrevOutput } from "./Output.js";
8+
import { DescriptorMap } from "./DescriptorMap.js";
9+
import { getDescriptorAtIndexCheckScript } from "./derive.js";
10+
11+
export type WithDescriptor<T> = T & {
12+
descriptor: Descriptor;
13+
};
14+
15+
export type WithOptDescriptor<T> = T & {
16+
descriptor?: Descriptor;
17+
};
18+
19+
export function isInternalOutput<T extends object>(
20+
output: T | WithDescriptor<T>,
21+
): output is WithDescriptor<T> {
22+
return "descriptor" in output && output.descriptor !== undefined;
23+
}
24+
25+
export function isExternalOutput<T extends object>(output: T | WithDescriptor<T>): output is T {
26+
return !isInternalOutput(output);
27+
}
28+
29+
/**
30+
* @return the sum of the external outputs that are not 'max'
31+
* @param outputs
32+
*/
33+
export function getExternalFixedAmount(outputs: WithOptDescriptor<Output | MaxOutput>[]): bigint {
34+
return getFixedOutputSum(outputs.filter(isExternalOutput));
35+
}
36+
37+
export type DescriptorWalletOutput = PrevOutput & {
38+
descriptorName: string;
39+
descriptorIndex: number | undefined;
40+
};
41+
42+
export type DerivedDescriptorWalletOutput = WithDescriptor<PrevOutput>;
43+
44+
export function toDerivedDescriptorWalletOutput(
45+
output: DescriptorWalletOutput,
46+
descriptorMap: DescriptorMap,
47+
): DerivedDescriptorWalletOutput {
48+
const descriptor = descriptorMap.get(output.descriptorName);
49+
if (!descriptor) {
50+
throw new Error(`Descriptor not found: ${output.descriptorName}`);
51+
}
52+
if (!(descriptor instanceof Descriptor)) {
53+
throw new Error(`Expected Descriptor instance for ${output.descriptorName}`);
54+
}
55+
const descriptorAtIndex = getDescriptorAtIndexCheckScript(
56+
descriptor,
57+
output.descriptorIndex,
58+
output.witnessUtxo.script,
59+
output.descriptorName,
60+
);
61+
return {
62+
hash: output.hash,
63+
index: output.index,
64+
witnessUtxo: output.witnessUtxo,
65+
descriptor: descriptorAtIndex,
66+
};
67+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Migration Guide: utxo-core/descriptor to wasm-utxo/descriptorWallet
2+
3+
This module provides descriptor wallet functionality that was previously in `@bitgo/utxo-core`.
4+
5+
## Import Changes
6+
7+
### Before (utxo-core)
8+
9+
```typescript
10+
import {
11+
DescriptorMap,
12+
toDescriptorMap,
13+
findDescriptorForInput,
14+
createPsbt,
15+
parse,
16+
getDescriptorAtIndex,
17+
createScriptPubKeyFromDescriptor,
18+
getVirtualSize,
19+
} from "@bitgo/utxo-core/descriptor";
20+
```
21+
22+
### After (wasm-utxo)
23+
24+
```typescript
25+
import { descriptorWallet } from "@bitgo/wasm-utxo";
26+
27+
const {
28+
toDescriptorMap,
29+
findDescriptorForInput,
30+
createPsbt,
31+
parse,
32+
getDescriptorAtIndex,
33+
createScriptPubKeyFromDescriptor,
34+
getVirtualSize,
35+
} = descriptorWallet;
36+
```
37+
38+
## API Changes
39+
40+
### PSBT Creation
41+
42+
The `createPsbt` function returns a `wasm-utxo.Psbt` instead of `utxolib.bitgo.UtxoPsbt`.
43+
44+
```typescript
45+
// Before: Returns utxolib.bitgo.UtxoPsbt
46+
const psbt = createPsbt(params, inputs, outputs);
47+
48+
// After: Returns wasm-utxo Psbt
49+
const psbt = descriptorWallet.createPsbt(params, inputs, outputs);
50+
```
51+
52+
### Address Creation
53+
54+
The `createAddressFromDescriptor` function takes a `CoinName` instead of `utxolib.Network`:
55+
56+
```typescript
57+
// Before
58+
createAddressFromDescriptor(descriptor, index, utxolib.networks.bitcoin);
59+
60+
// After
61+
descriptorWallet.createAddressFromDescriptor(descriptor, index, "Bitcoin");
62+
```
63+
64+
### Signing
65+
66+
Use `signWithKey` from the descriptorWallet module:
67+
68+
```typescript
69+
// Before
70+
tx.signInputHD(vin, signerKeychain);
71+
72+
// After
73+
descriptorWallet.signWithKey(psbt, signerKeychain);
74+
```
75+
76+
## Not Ported
77+
78+
The following are intentionally **not** included in this migration:
79+
80+
- `fromFixedScriptWallet` - Converting fixed-script wallets to descriptors should remain in utxo-core or abstract-utxo
81+
82+
## Network Support
83+
84+
Descriptor wallets are currently only supported for Bitcoin mainnet and testnet.
85+
Altcoin descriptor wallets should continue using the fixed-script wallet approach.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Output types and utilities for descriptor wallets.
3+
* Moved from @bitgo/utxo-core.
4+
*/
5+
6+
export type Output<TValue = bigint> = {
7+
script: Uint8Array;
8+
value: TValue;
9+
};
10+
export type MaxOutput = Output<"max">;
11+
type ValueBigInt = { value: bigint };
12+
type ValueMax = { value: "max" };
13+
14+
/**
15+
* @return true if the output is a max output
16+
*/
17+
export function isMaxOutput<A extends ValueBigInt, B extends ValueMax>(output: A | B): output is B {
18+
return output.value === "max";
19+
}
20+
21+
/**
22+
* @return the max output if there is one
23+
* @throws if there are multiple max outputs
24+
*/
25+
export function getMaxOutput<A extends ValueBigInt, B extends ValueMax>(
26+
outputs: (A | B)[],
27+
): B | undefined {
28+
const max = outputs.filter(isMaxOutput<A, B>);
29+
if (max.length === 0) {
30+
return undefined;
31+
}
32+
if (max.length > 1) {
33+
throw new Error("Multiple max outputs");
34+
}
35+
return max[0];
36+
}
37+
38+
/**
39+
* @return the sum of the outputs
40+
*/
41+
export function getOutputSum(outputs: ValueBigInt[]): bigint {
42+
return outputs.reduce((sum, output) => sum + output.value, 0n);
43+
}
44+
45+
/**
46+
* @return the sum of the outputs that are not 'max'
47+
*/
48+
export function getFixedOutputSum(outputs: (ValueBigInt | ValueMax)[]): bigint {
49+
return getOutputSum(outputs.filter((o): o is Output => !isMaxOutput(o)));
50+
}
51+
52+
/**
53+
* @param outputs
54+
* @param params
55+
* @return the outputs with the 'max' output replaced with the max amount
56+
*/
57+
export function toFixedOutputs<A extends ValueBigInt, B extends ValueMax>(
58+
outputs: (A | B)[],
59+
params: { maxAmount: bigint },
60+
): A[] {
61+
// assert that there is at most one max output
62+
const maxOutput = getMaxOutput<A, B>(outputs);
63+
return outputs.map((output): A => {
64+
if (isMaxOutput(output)) {
65+
if (output !== maxOutput) {
66+
throw new Error("illegal state");
67+
}
68+
return { ...output, value: params.maxAmount };
69+
} else {
70+
return output;
71+
}
72+
});
73+
}
74+
75+
export type PrevOutput = {
76+
hash: string;
77+
index: number;
78+
witnessUtxo: Output;
79+
};

0 commit comments

Comments
 (0)