Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 75 additions & 32 deletions modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'assert';

import {
BaseCoin,
HalfSignedUtxoTransaction,
IInscriptionBuilder,
IWallet,
Expand All @@ -9,9 +10,8 @@ import {
PreparedInscriptionRevealData,
SubmitTransactionResponse,
xprvToRawPrv,
xpubToCompressedPub,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import { bip32 } from '@bitgo/secp256k1';
import {
createPsbtForSingleInscriptionPassingTransaction,
DefaultInscriptionConstraints,
Expand All @@ -23,10 +23,24 @@ import {
findOutputLayoutForWalletUnspents,
MAX_UNSPENTS_FOR_OUTPUT_LAYOUT,
SatPoint,
WalletUnspent,
type TapLeafScript,
} from '@bitgo/utxo-ord';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { AbstractUtxoCoin, RootWalletKeys } from '../../abstractUtxoCoin';
import { getWalletKeys } from '../../recovery/crossChainRecovery';
import { AbstractUtxoCoin } from '../../abstractUtxoCoin';
import { fetchWasmRootWalletKeys } from '../../keychains';

/** Key identifier for signing */
type SignerKey = 'user' | 'backup' | 'bitgo';

/** Unspent from wallet API (value may be number or bigint) */
type WalletUnspentLike = {
id: string;
value: number | bigint;
chain: number;
index: number;
};

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

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

const derived = this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() });
const compressedPublicKey = xpubToCompressedPub(derived.key);
const xOnlyPublicKey = utxolib.bitgo.outputScripts.toXOnlyPublicKey(Buffer.from(compressedPublicKey, 'hex'));
const userKey = bip32.fromBase58(user.pub);
const { key: derivedKey } = BaseCoin.deriveKeyWithSeedBip32(userKey, inscriptionData.toString());

const result = inscriptions.createInscriptionRevealData(
derivedKey.publicKey,
contentType,
inscriptionData,
this.coin.name
);

return inscriptions.createInscriptionRevealData(xOnlyPublicKey, contentType, inscriptionData, this.coin.network);
// Convert TapLeafScript to utxolib format for backwards compatibility
return {
address: result.address,
revealTransactionVSize: result.revealTransactionVSize,
tapLeafScript: {
controlBlock: Buffer.from(result.tapLeafScript.controlBlock),
script: Buffer.from(result.tapLeafScript.script),
leafVersion: result.tapLeafScript.leafVersion,
},
};
}

private async prepareTransferWithExtraInputs(
Expand All @@ -59,36 +88,41 @@ export class InscriptionBuilder implements IInscriptionBuilder {
inscriptionConstraints,
txFormat,
}: {
signer: utxolib.bitgo.KeyName;
cosigner: utxolib.bitgo.KeyName;
signer: SignerKey;
cosigner: SignerKey;
inscriptionConstraints: {
minChangeOutput?: bigint;
minInscriptionOutput?: bigint;
maxInscriptionOutput?: bigint;
};
txFormat?: 'psbt' | 'legacy';
},
rootWalletKeys: RootWalletKeys,
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
outputs: InscriptionOutputs,
inscriptionUnspents: utxolib.bitgo.WalletUnspent<bigint>[],
inscriptionUnspents: WalletUnspent[],
supplementaryUnspentsMinValue: number
): Promise<PrebuildTransactionResult> {
let supplementaryUnspents: utxolib.bitgo.WalletUnspent<bigint>[] = [];
let supplementaryUnspents: WalletUnspent[] = [];
if (supplementaryUnspentsMinValue > 0) {
const response = await this.wallet.unspents({
minValue: supplementaryUnspentsMinValue,
});
// Filter out the inscription unspent from the supplementary unspents
supplementaryUnspents = response.unspents
.filter((unspent) => unspent.id !== inscriptionUnspents[0].id)
.filter((unspent: { id: string }) => unspent.id !== inscriptionUnspents[0].id)
.slice(0, MAX_UNSPENTS_FOR_OUTPUT_LAYOUT - 1)
.map((unspent) => {
unspent.value = BigInt(unspent.value);
return unspent;
});
.map(
(unspent: WalletUnspentLike): WalletUnspent => ({
id: unspent.id,
value: BigInt(unspent.value),
chain: unspent.chain,
index: unspent.index,
})
);
}

const psbt = createPsbtForSingleInscriptionPassingTransaction(
this.coin.network,
this.coin.name,
{
walletKeys: rootWalletKeys,
signer,
Expand Down Expand Up @@ -117,7 +151,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
}
return {
walletId: this.wallet.id(),
txHex: txFormat === 'psbt' ? psbt.toHex() : psbt.getUnsignedTx().toHex(),
txHex: Buffer.from(psbt.serialize()).toString('hex'),
txInfo: { unspents: allUnspents },
feeInfo: { fee: Number(outputLayout.layout.feeOutput), feeString: outputLayout.layout.feeOutput.toString() },
};
Expand Down Expand Up @@ -146,27 +180,36 @@ export class InscriptionBuilder implements IInscriptionBuilder {
changeAddressType = 'p2wsh',
txFormat = 'psbt',
}: {
signer?: utxolib.bitgo.KeyName;
cosigner?: utxolib.bitgo.KeyName;
signer?: SignerKey;
cosigner?: SignerKey;
inscriptionConstraints?: {
minChangeOutput?: bigint;
minInscriptionOutput?: bigint;
maxInscriptionOutput?: bigint;
};
changeAddressType?: utxolib.bitgo.outputScripts.ScriptType2Of3;
changeAddressType?: 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2';
txFormat?: 'psbt' | 'legacy';
}
): Promise<PrebuildTransactionResult> {
assert(isSatPoint(satPoint));

const rootWalletKeys = await getWalletKeys(this.coin, this.wallet);
const rootWalletKeys = await fetchWasmRootWalletKeys(this.coin, this.wallet);
const parsedSatPoint = parseSatPoint(satPoint);
const transaction = await this.wallet.getTransaction({ txHash: parsedSatPoint.txid });
const unspents: utxolib.bitgo.WalletUnspent<bigint>[] = [transaction.outputs[parsedSatPoint.vout]];
unspents[0].value = BigInt(unspents[0].value);
const output = transaction.outputs[parsedSatPoint.vout];
const unspents: WalletUnspent[] = [
{
id: `${parsedSatPoint.txid}:${parsedSatPoint.vout}`,
value: BigInt(output.value),
chain: output.chain,
index: output.index,
},
];

const changeChain = fixedScriptWallet.ChainCode.value(changeAddressType, 'internal');

const changeAddress = await this.wallet.createAddress({
chain: utxolib.bitgo.getInternalChainCode(changeAddressType),
chain: changeChain,
});
const outputs: InscriptionOutputs = {
inscriptionRecipient: recipient,
Expand Down Expand Up @@ -209,10 +252,10 @@ export class InscriptionBuilder implements IInscriptionBuilder {
*/
async signAndSendReveal(
walletPassphrase: string,
tapLeafScript: utxolib.bitgo.TapLeafScript,
tapLeafScript: TapLeafScript,
commitAddress: string,
unsignedCommitTx: Buffer,
commitTransactionUnspents: utxolib.bitgo.WalletUnspent[],
commitTransactionUnspents: WalletUnspentLike[],
recipientAddress: string,
inscriptionData: Buffer
): Promise<SubmitTransactionResponse> {
Expand All @@ -230,19 +273,19 @@ export class InscriptionBuilder implements IInscriptionBuilder {
const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
const prv = xprvToRawPrv(derived.key);

const fullySignedRevealTransaction = await inscriptions.signRevealTransaction(
const fullySignedRevealTransaction = inscriptions.signRevealTransaction(
Buffer.from(prv, 'hex'),
tapLeafScript,
commitAddress,
recipientAddress,
Buffer.from(halfSignedCommitTransaction.txHex, 'hex'),
this.coin.network
this.coin.name
);

return this.wallet.submitTransaction({
halfSigned: {
txHex: halfSignedCommitTransaction.txHex,
signedChildPsbt: fullySignedRevealTransaction.toHex(),
signedChildPsbt: Buffer.from(fullySignedRevealTransaction).toString('hex'),
},
});
}
Expand Down
13 changes: 13 additions & 0 deletions modules/abstract-utxo/src/keychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as t from 'io-ts';
import { bitgo } from '@bitgo/utxo-lib';
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { AbstractUtxoCoin } from './abstractUtxoCoin';
import { UtxoWallet } from './wallet';
Expand Down Expand Up @@ -108,6 +109,18 @@ export async function fetchKeychains(
return result;
}

/**
* Fetch wallet keys as wasm-utxo RootWalletKeys
*/
export async function fetchWasmRootWalletKeys(
coin: AbstractUtxoCoin,
wallet: IWallet,
reqId?: IRequestTracer
): Promise<fixedScriptWallet.RootWalletKeys> {
const keychains = await fetchKeychains(coin, wallet, reqId);
return fixedScriptWallet.RootWalletKeys.from([keychains.user.pub, keychains.backup.pub, keychains.bitgo.pub]);
}

export const KeySignatures = t.partial({
backupPub: t.string,
bitgoPub: t.string,
Expand Down
5 changes: 3 additions & 2 deletions modules/utxo-ord/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"directory": "modules/utxo-ord"
},
"dependencies": {
"@bitgo/sdk-core": "^36.27.0",
"@bitgo/unspents": "^0.50.14",
"@bitgo/wasm-utxo": "^1.27.0"
},
"devDependencies": {
"@bitgo/utxo-lib": "^11.19.1"
},
"lint-staged": {
Expand Down
23 changes: 21 additions & 2 deletions modules/utxo-ord/src/SatPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,29 @@ https://github.com/casey/ord/blob/master/bip.mediawiki#terminology-and-notation
> `680df1e4d43016571e504b0b142ee43c5c0b83398a97bdcfd94ea6f287322d22:0:6`

*/
import { bitgo } from '@bitgo/utxo-lib';

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

/**
* Parse an output ID (txid:vout) into its components.
*/
export function parseOutputId(outputId: string): { txid: string; vout: number } {
const colonIndex = outputId.lastIndexOf(':');
if (colonIndex === -1) {
throw new Error(`Invalid output id format: missing colon`);
}
const txid = outputId.slice(0, colonIndex);
const voutStr = outputId.slice(colonIndex + 1);
if (txid.length !== 64 || !/^[0-9a-fA-F]+$/.test(txid)) {
throw new Error(`Invalid txid: must be 64 hex characters`);
}
const vout = parseInt(voutStr, 10);
if (isNaN(vout) || vout < 0) {
throw new Error(`Invalid vout: must be non-negative integer`);
}
return { txid, vout };
}

export function parseSatPoint(p: SatPoint): { txid: string; vout: number; offset: bigint } {
const parts = p.split(':');
if (parts.length !== 3) {
Expand All @@ -27,7 +46,7 @@ export function parseSatPoint(p: SatPoint): { txid: string; vout: number; offset
throw new Error(`SatPoint offset must be positive`);
}
return {
...bitgo.parseOutputId([txid, vout].join(':')),
...parseOutputId([txid, vout].join(':')),
offset,
};
}
Expand Down
2 changes: 2 additions & 0 deletions modules/utxo-ord/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './OutputLayout';
export * from './SatPoint';
export * from './psbt';
export * as inscriptions from './inscriptions';
export type { TapLeafScript, PreparedInscriptionRevealData } from './inscriptions';
export type { WalletUnspent } from './psbt';
Loading
Loading