Skip to content

Commit cc428fd

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): use wasm-utxo for inscriptionBuilder
Convert inscriptionBuilder to use @bitgo/wasm-utxo instead of utxo-lib for inscription-related functionality. Update interfaces to accommodate the new WASM-based implementation and add helper functions to handle the transition. Issue: BTC-2936 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 16e5c6b commit cc428fd

File tree

10 files changed

+575
-355
lines changed

10 files changed

+575
-355
lines changed

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

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from 'assert';
22

33
import {
4+
BaseCoin,
45
HalfSignedUtxoTransaction,
56
IInscriptionBuilder,
67
IWallet,
@@ -9,9 +10,8 @@ import {
910
PreparedInscriptionRevealData,
1011
SubmitTransactionResponse,
1112
xprvToRawPrv,
12-
xpubToCompressedPub,
1313
} from '@bitgo/sdk-core';
14-
import * as utxolib from '@bitgo/utxo-lib';
14+
import { bip32 } from '@bitgo/secp256k1';
1515
import {
1616
createPsbtForSingleInscriptionPassingTransaction,
1717
DefaultInscriptionConstraints,
@@ -23,10 +23,24 @@ import {
2323
findOutputLayoutForWalletUnspents,
2424
MAX_UNSPENTS_FOR_OUTPUT_LAYOUT,
2525
SatPoint,
26+
WalletUnspent,
27+
type TapLeafScript,
2628
} from '@bitgo/utxo-ord';
29+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
2730

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

3145
const SUPPLEMENTARY_UNSPENTS_MIN_VALUE_SATS = [0, 20_000, 200_000];
3246

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

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'));
60+
const userKey = bip32.fromBase58(user.pub);
61+
const { key: derivedKey } = BaseCoin.deriveKeyWithSeedBip32(userKey, inscriptionData.toString());
62+
63+
const result = inscriptions.createInscriptionRevealData(
64+
derivedKey.publicKey,
65+
contentType,
66+
inscriptionData,
67+
this.coin.name
68+
);
4969

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

5382
private async prepareTransferWithExtraInputs(
@@ -59,36 +88,41 @@ export class InscriptionBuilder implements IInscriptionBuilder {
5988
inscriptionConstraints,
6089
txFormat,
6190
}: {
62-
signer: utxolib.bitgo.KeyName;
63-
cosigner: utxolib.bitgo.KeyName;
91+
signer: SignerKey;
92+
cosigner: SignerKey;
6493
inscriptionConstraints: {
6594
minChangeOutput?: bigint;
6695
minInscriptionOutput?: bigint;
6796
maxInscriptionOutput?: bigint;
6897
};
6998
txFormat?: 'psbt' | 'legacy';
7099
},
71-
rootWalletKeys: RootWalletKeys,
100+
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
72101
outputs: InscriptionOutputs,
73-
inscriptionUnspents: utxolib.bitgo.WalletUnspent<bigint>[],
102+
inscriptionUnspents: WalletUnspent[],
74103
supplementaryUnspentsMinValue: number
75104
): Promise<PrebuildTransactionResult> {
76-
let supplementaryUnspents: utxolib.bitgo.WalletUnspent<bigint>[] = [];
105+
let supplementaryUnspents: WalletUnspent[] = [];
77106
if (supplementaryUnspentsMinValue > 0) {
78107
const response = await this.wallet.unspents({
79108
minValue: supplementaryUnspentsMinValue,
80109
});
81110
// Filter out the inscription unspent from the supplementary unspents
82111
supplementaryUnspents = response.unspents
83-
.filter((unspent) => unspent.id !== inscriptionUnspents[0].id)
112+
.filter((unspent: { id: string }) => unspent.id !== inscriptionUnspents[0].id)
84113
.slice(0, MAX_UNSPENTS_FOR_OUTPUT_LAYOUT - 1)
85-
.map((unspent) => {
86-
unspent.value = BigInt(unspent.value);
87-
return unspent;
88-
});
114+
.map(
115+
(unspent: WalletUnspentLike): WalletUnspent => ({
116+
id: unspent.id,
117+
value: BigInt(unspent.value),
118+
chain: unspent.chain,
119+
index: unspent.index,
120+
})
121+
);
89122
}
123+
90124
const psbt = createPsbtForSingleInscriptionPassingTransaction(
91-
this.coin.network,
125+
this.coin.name,
92126
{
93127
walletKeys: rootWalletKeys,
94128
signer,
@@ -117,7 +151,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
117151
}
118152
return {
119153
walletId: this.wallet.id(),
120-
txHex: txFormat === 'psbt' ? psbt.toHex() : psbt.getUnsignedTx().toHex(),
154+
txHex: Buffer.from(psbt.serialize()).toString('hex'),
121155
txInfo: { unspents: allUnspents },
122156
feeInfo: { fee: Number(outputLayout.layout.feeOutput), feeString: outputLayout.layout.feeOutput.toString() },
123157
};
@@ -146,27 +180,36 @@ export class InscriptionBuilder implements IInscriptionBuilder {
146180
changeAddressType = 'p2wsh',
147181
txFormat = 'psbt',
148182
}: {
149-
signer?: utxolib.bitgo.KeyName;
150-
cosigner?: utxolib.bitgo.KeyName;
183+
signer?: SignerKey;
184+
cosigner?: SignerKey;
151185
inscriptionConstraints?: {
152186
minChangeOutput?: bigint;
153187
minInscriptionOutput?: bigint;
154188
maxInscriptionOutput?: bigint;
155189
};
156-
changeAddressType?: utxolib.bitgo.outputScripts.ScriptType2Of3;
190+
changeAddressType?: 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2';
157191
txFormat?: 'psbt' | 'legacy';
158192
}
159193
): Promise<PrebuildTransactionResult> {
160194
assert(isSatPoint(satPoint));
161195

162-
const rootWalletKeys = await getWalletKeys(this.coin, this.wallet);
196+
const rootWalletKeys = await fetchWasmRootWalletKeys(this.coin, this.wallet);
163197
const parsedSatPoint = parseSatPoint(satPoint);
164198
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);
199+
const output = transaction.outputs[parsedSatPoint.vout];
200+
const unspents: WalletUnspent[] = [
201+
{
202+
id: `${parsedSatPoint.txid}:${parsedSatPoint.vout}`,
203+
value: BigInt(output.value),
204+
chain: output.chain,
205+
index: output.index,
206+
},
207+
];
208+
209+
const changeChain = fixedScriptWallet.ChainCode.value(changeAddressType, 'internal');
167210

168211
const changeAddress = await this.wallet.createAddress({
169-
chain: utxolib.bitgo.getInternalChainCode(changeAddressType),
212+
chain: changeChain,
170213
});
171214
const outputs: InscriptionOutputs = {
172215
inscriptionRecipient: recipient,
@@ -209,10 +252,10 @@ export class InscriptionBuilder implements IInscriptionBuilder {
209252
*/
210253
async signAndSendReveal(
211254
walletPassphrase: string,
212-
tapLeafScript: utxolib.bitgo.TapLeafScript,
255+
tapLeafScript: TapLeafScript,
213256
commitAddress: string,
214257
unsignedCommitTx: Buffer,
215-
commitTransactionUnspents: utxolib.bitgo.WalletUnspent[],
258+
commitTransactionUnspents: WalletUnspentLike[],
216259
recipientAddress: string,
217260
inscriptionData: Buffer
218261
): Promise<SubmitTransactionResponse> {
@@ -230,19 +273,19 @@ export class InscriptionBuilder implements IInscriptionBuilder {
230273
const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
231274
const prv = xprvToRawPrv(derived.key);
232275

233-
const fullySignedRevealTransaction = await inscriptions.signRevealTransaction(
276+
const fullySignedRevealTransaction = inscriptions.signRevealTransaction(
234277
Buffer.from(prv, 'hex'),
235278
tapLeafScript,
236279
commitAddress,
237280
recipientAddress,
238281
Buffer.from(halfSignedCommitTransaction.txHex, 'hex'),
239-
this.coin.network
282+
this.coin.name
240283
);
241284

242285
return this.wallet.submitTransaction({
243286
halfSigned: {
244287
txHex: halfSignedCommitTransaction.txHex,
245-
signedChildPsbt: fullySignedRevealTransaction.toHex(),
288+
signedChildPsbt: Buffer.from(fullySignedRevealTransaction).toString('hex'),
246289
},
247290
});
248291
}

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/utxo-ord/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
"directory": "modules/utxo-ord"
2929
},
3030
"dependencies": {
31-
"@bitgo/sdk-core": "^36.27.0",
32-
"@bitgo/unspents": "^0.50.14",
31+
"@bitgo/wasm-utxo": "^1.27.0"
32+
},
33+
"devDependencies": {
3334
"@bitgo/utxo-lib": "^11.19.1"
3435
},
3536
"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)