Skip to content

Commit 5158c6f

Browse files
Merge pull request #7918 from BitGo/BTC-2936.utxo-ord-wasm-utxo
feat(abstract-utxo): migrate inscription builder to wasm-utxo
2 parents 5bb25d9 + e33840c commit 5158c6f

11 files changed

Lines changed: 582 additions & 357 deletions

File tree

modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import assert from 'assert';
22

33
import {
4+
BaseCoin,
45
HalfSignedUtxoTransaction,
56
IInscriptionBuilder,
67
IWallet,
78
KeyIndices,
89
PrebuildTransactionResult,
910
PreparedInscriptionRevealData,
1011
SubmitTransactionResponse,
12+
Triple,
1113
xprvToRawPrv,
12-
xpubToCompressedPub,
1314
} from '@bitgo/sdk-core';
14-
import * as utxolib from '@bitgo/utxo-lib';
15+
import { bip32 } from '@bitgo/secp256k1';
1516
import {
1617
createPsbtForSingleInscriptionPassingTransaction,
1718
DefaultInscriptionConstraints,
@@ -23,10 +24,24 @@ import {
2324
findOutputLayoutForWalletUnspents,
2425
MAX_UNSPENTS_FOR_OUTPUT_LAYOUT,
2526
SatPoint,
27+
WalletUnspent,
28+
type TapLeafScript,
2629
} from '@bitgo/utxo-ord';
30+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
2731

28-
import { AbstractUtxoCoin, RootWalletKeys } from '../../abstractUtxoCoin';
29-
import { getWalletKeys } from '../../recovery/crossChainRecovery';
32+
import { AbstractUtxoCoin } from '../../abstractUtxoCoin';
33+
import { fetchKeychains } from '../../keychains';
34+
35+
/** Key identifier for signing */
36+
type SignerKey = 'user' | 'backup' | 'bitgo';
37+
38+
/** Unspent from wallet API (value may be number or bigint) */
39+
type WalletUnspentLike = {
40+
id: string;
41+
value: number | bigint;
42+
chain: number;
43+
index: number;
44+
};
3045

3146
const SUPPLEMENTARY_UNSPENTS_MIN_VALUE_SATS = [0, 20_000, 200_000];
3247

@@ -43,11 +58,26 @@ export class InscriptionBuilder implements IInscriptionBuilder {
4358
const user = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
4459
assert(user.pub);
4560

46-
const derived = this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() });
47-
const compressedPublicKey = xpubToCompressedPub(derived.key);
48-
const xOnlyPublicKey = utxolib.bitgo.outputScripts.toXOnlyPublicKey(Buffer.from(compressedPublicKey, 'hex'));
61+
const userKey = bip32.fromBase58(user.pub);
62+
const { key: derivedKey } = BaseCoin.deriveKeyWithSeedBip32(userKey, inscriptionData.toString());
63+
64+
const result = inscriptions.createInscriptionRevealData(
65+
derivedKey.publicKey,
66+
contentType,
67+
inscriptionData,
68+
this.coin.name
69+
);
4970

50-
return inscriptions.createInscriptionRevealData(xOnlyPublicKey, contentType, inscriptionData, this.coin.network);
71+
// Convert TapLeafScript to utxolib format for backwards compatibility
72+
return {
73+
address: result.address,
74+
revealTransactionVSize: result.revealTransactionVSize,
75+
tapLeafScript: {
76+
controlBlock: Buffer.from(result.tapLeafScript.controlBlock),
77+
script: Buffer.from(result.tapLeafScript.script),
78+
leafVersion: result.tapLeafScript.leafVersion,
79+
},
80+
};
5181
}
5282

5383
private async prepareTransferWithExtraInputs(
@@ -59,38 +89,43 @@ export class InscriptionBuilder implements IInscriptionBuilder {
5989
inscriptionConstraints,
6090
txFormat,
6191
}: {
62-
signer: utxolib.bitgo.KeyName;
63-
cosigner: utxolib.bitgo.KeyName;
92+
signer: SignerKey;
93+
cosigner: SignerKey;
6494
inscriptionConstraints: {
6595
minChangeOutput?: bigint;
6696
minInscriptionOutput?: bigint;
6797
maxInscriptionOutput?: bigint;
6898
};
6999
txFormat?: 'psbt' | 'legacy';
70100
},
71-
rootWalletKeys: RootWalletKeys,
101+
walletXpubs: Triple<string>,
72102
outputs: InscriptionOutputs,
73-
inscriptionUnspents: utxolib.bitgo.WalletUnspent<bigint>[],
103+
inscriptionUnspents: WalletUnspent[],
74104
supplementaryUnspentsMinValue: number
75105
): Promise<PrebuildTransactionResult> {
76-
let supplementaryUnspents: utxolib.bitgo.WalletUnspent<bigint>[] = [];
106+
let supplementaryUnspents: WalletUnspent[] = [];
77107
if (supplementaryUnspentsMinValue > 0) {
78108
const response = await this.wallet.unspents({
79109
minValue: supplementaryUnspentsMinValue,
80110
});
81111
// Filter out the inscription unspent from the supplementary unspents
82112
supplementaryUnspents = response.unspents
83-
.filter((unspent) => unspent.id !== inscriptionUnspents[0].id)
113+
.filter((unspent: { id: string }) => unspent.id !== inscriptionUnspents[0].id)
84114
.slice(0, MAX_UNSPENTS_FOR_OUTPUT_LAYOUT - 1)
85-
.map((unspent) => {
86-
unspent.value = BigInt(unspent.value);
87-
return unspent;
88-
});
115+
.map(
116+
(unspent: WalletUnspentLike): WalletUnspent => ({
117+
id: unspent.id,
118+
value: BigInt(unspent.value),
119+
chain: unspent.chain,
120+
index: unspent.index,
121+
})
122+
);
89123
}
124+
90125
const psbt = createPsbtForSingleInscriptionPassingTransaction(
91-
this.coin.network,
126+
this.coin.name,
92127
{
93-
walletKeys: rootWalletKeys,
128+
walletKeys: walletXpubs,
94129
signer,
95130
cosigner,
96131
},
@@ -117,7 +152,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
117152
}
118153
return {
119154
walletId: this.wallet.id(),
120-
txHex: txFormat === 'psbt' ? psbt.toHex() : psbt.getUnsignedTx().toHex(),
155+
txHex: Buffer.from(psbt.serialize()).toString('hex'),
121156
txInfo: { unspents: allUnspents },
122157
feeInfo: { fee: Number(outputLayout.layout.feeOutput), feeString: outputLayout.layout.feeOutput.toString() },
123158
};
@@ -146,27 +181,37 @@ export class InscriptionBuilder implements IInscriptionBuilder {
146181
changeAddressType = 'p2wsh',
147182
txFormat = 'psbt',
148183
}: {
149-
signer?: utxolib.bitgo.KeyName;
150-
cosigner?: utxolib.bitgo.KeyName;
184+
signer?: SignerKey;
185+
cosigner?: SignerKey;
151186
inscriptionConstraints?: {
152187
minChangeOutput?: bigint;
153188
minInscriptionOutput?: bigint;
154189
maxInscriptionOutput?: bigint;
155190
};
156-
changeAddressType?: utxolib.bitgo.outputScripts.ScriptType2Of3;
191+
changeAddressType?: 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2';
157192
txFormat?: 'psbt' | 'legacy';
158193
}
159194
): Promise<PrebuildTransactionResult> {
160195
assert(isSatPoint(satPoint));
161196

162-
const rootWalletKeys = await getWalletKeys(this.coin, this.wallet);
197+
const keychains = await fetchKeychains(this.coin, this.wallet);
198+
const walletXpubs: Triple<string> = [keychains.user.pub, keychains.backup.pub, keychains.bitgo.pub];
163199
const parsedSatPoint = parseSatPoint(satPoint);
164200
const transaction = await this.wallet.getTransaction({ txHash: parsedSatPoint.txid });
165-
const unspents: utxolib.bitgo.WalletUnspent<bigint>[] = [transaction.outputs[parsedSatPoint.vout]];
166-
unspents[0].value = BigInt(unspents[0].value);
201+
const output = transaction.outputs[parsedSatPoint.vout];
202+
const unspents: WalletUnspent[] = [
203+
{
204+
id: `${parsedSatPoint.txid}:${parsedSatPoint.vout}`,
205+
value: BigInt(output.value),
206+
chain: output.chain,
207+
index: output.index,
208+
},
209+
];
210+
211+
const changeChain = fixedScriptWallet.ChainCode.value(changeAddressType, 'internal');
167212

168213
const changeAddress = await this.wallet.createAddress({
169-
chain: utxolib.bitgo.getInternalChainCode(changeAddressType),
214+
chain: changeChain,
170215
});
171216
const outputs: InscriptionOutputs = {
172217
inscriptionRecipient: recipient,
@@ -182,7 +227,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
182227
satPoint,
183228
feeRateSatKB,
184229
{ signer, cosigner, inscriptionConstraints, txFormat },
185-
rootWalletKeys,
230+
walletXpubs,
186231
outputs,
187232
unspents,
188233
supplementaryUnspentsMinValue
@@ -209,10 +254,10 @@ export class InscriptionBuilder implements IInscriptionBuilder {
209254
*/
210255
async signAndSendReveal(
211256
walletPassphrase: string,
212-
tapLeafScript: utxolib.bitgo.TapLeafScript,
257+
tapLeafScript: TapLeafScript,
213258
commitAddress: string,
214259
unsignedCommitTx: Buffer,
215-
commitTransactionUnspents: utxolib.bitgo.WalletUnspent[],
260+
commitTransactionUnspents: WalletUnspentLike[],
216261
recipientAddress: string,
217262
inscriptionData: Buffer
218263
): Promise<SubmitTransactionResponse> {
@@ -230,19 +275,19 @@ export class InscriptionBuilder implements IInscriptionBuilder {
230275
const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
231276
const prv = xprvToRawPrv(derived.key);
232277

233-
const fullySignedRevealTransaction = await inscriptions.signRevealTransaction(
278+
const fullySignedRevealTransaction = inscriptions.signRevealTransaction(
234279
Buffer.from(prv, 'hex'),
235280
tapLeafScript,
236281
commitAddress,
237282
recipientAddress,
238283
Buffer.from(halfSignedCommitTransaction.txHex, 'hex'),
239-
this.coin.network
284+
this.coin.name
240285
);
241286

242287
return this.wallet.submitTransaction({
243288
halfSigned: {
244289
txHex: halfSignedCommitTransaction.txHex,
245-
signedChildPsbt: fullySignedRevealTransaction.toHex(),
290+
signedChildPsbt: Buffer.from(fullySignedRevealTransaction).toString('hex'),
246291
},
247292
});
248293
}

modules/abstract-utxo/src/keychains.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as t from 'io-ts';
44
import { bitgo } from '@bitgo/utxo-lib';
55
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
66
import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
7+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
78

89
import { AbstractUtxoCoin } from './abstractUtxoCoin';
910
import { UtxoWallet } from './wallet';
@@ -108,6 +109,18 @@ export async function fetchKeychains(
108109
return result;
109110
}
110111

112+
/**
113+
* Fetch wallet keys as wasm-utxo RootWalletKeys
114+
*/
115+
export async function fetchWasmRootWalletKeys(
116+
coin: AbstractUtxoCoin,
117+
wallet: IWallet,
118+
reqId?: IRequestTracer
119+
): Promise<fixedScriptWallet.RootWalletKeys> {
120+
const keychains = await fetchKeychains(coin, wallet, reqId);
121+
return fixedScriptWallet.RootWalletKeys.from([keychains.user.pub, keychains.backup.pub, keychains.bitgo.pub]);
122+
}
123+
111124
export const KeySignatures = t.partial({
112125
backupPub: t.string,
113126
bitgoPub: t.string,

modules/abstract-utxo/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
},
2727
{
2828
"path": "../utxo-core"
29+
},
30+
{
31+
"path": "../utxo-ord"
2932
}
3033
]
3134
}

modules/utxo-ord/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@
4545
"directory": "modules/utxo-ord"
4646
},
4747
"dependencies": {
48-
"@bitgo/sdk-core": "^36.29.0",
49-
"@bitgo/unspents": "^0.50.14",
48+
"@bitgo/wasm-utxo": "^1.27.0"
49+
},
50+
"devDependencies": {
5051
"@bitgo/utxo-lib": "^11.19.1"
5152
},
5253
"lint-staged": {

modules/utxo-ord/src/SatPoint.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,29 @@ https://github.com/casey/ord/blob/master/bip.mediawiki#terminology-and-notation
99
> `680df1e4d43016571e504b0b142ee43c5c0b83398a97bdcfd94ea6f287322d22:0:6`
1010
1111
*/
12-
import { bitgo } from '@bitgo/utxo-lib';
1312

1413
export type SatPoint = `${string}:${number}:${bigint}`;
1514

15+
/**
16+
* Parse an output ID (txid:vout) into its components.
17+
*/
18+
export function parseOutputId(outputId: string): { txid: string; vout: number } {
19+
const colonIndex = outputId.lastIndexOf(':');
20+
if (colonIndex === -1) {
21+
throw new Error(`Invalid output id format: missing colon`);
22+
}
23+
const txid = outputId.slice(0, colonIndex);
24+
const voutStr = outputId.slice(colonIndex + 1);
25+
if (txid.length !== 64 || !/^[0-9a-fA-F]+$/.test(txid)) {
26+
throw new Error(`Invalid txid: must be 64 hex characters`);
27+
}
28+
const vout = parseInt(voutStr, 10);
29+
if (isNaN(vout) || vout < 0) {
30+
throw new Error(`Invalid vout: must be non-negative integer`);
31+
}
32+
return { txid, vout };
33+
}
34+
1635
export function parseSatPoint(p: SatPoint): { txid: string; vout: number; offset: bigint } {
1736
const parts = p.split(':');
1837
if (parts.length !== 3) {
@@ -27,7 +46,7 @@ export function parseSatPoint(p: SatPoint): { txid: string; vout: number; offset
2746
throw new Error(`SatPoint offset must be positive`);
2847
}
2948
return {
30-
...bitgo.parseOutputId([txid, vout].join(':')),
49+
...parseOutputId([txid, vout].join(':')),
3150
offset,
3251
};
3352
}

modules/utxo-ord/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export * from './OutputLayout';
88
export * from './SatPoint';
99
export * from './psbt';
1010
export * as inscriptions from './inscriptions';
11+
export type { TapLeafScript, PreparedInscriptionRevealData } from './inscriptions';
12+
export type { WalletUnspent } from './psbt';

0 commit comments

Comments
 (0)