Skip to content

Commit

Permalink
Merge pull request #228 from ckb-cell/feat/multi-origin-fee-estimator
Browse files Browse the repository at this point in the history
feat(btc): support including multi-origin UTXOs
  • Loading branch information
ShookLyngs authored Jun 18, 2024
2 parents 95e9190 + 2e61e73 commit 10e4e17
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 105 deletions.
9 changes: 9 additions & 0 deletions .changeset/orange-mice-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rgbpp-sdk/btc": minor
---

Support including multi-origin UTXOs in the same transaction

- Add `pubkeyMap` option in the sendUtxos(), sendRgbppUtxos() and sendRbf() API
- Rename `inputsPubkey` option to `pubkeyMap` in the sendRbf() API
- Delete `onlyProvableUtxos` option from the sendRgbppUtxos() API
11 changes: 9 additions & 2 deletions packages/btc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ interface SendUtxosProps {

// EXPERIMENTAL: the below props are unstable and can be altered at any time
skipInputsValidation?: boolean;
pubkeyMap?: AddressToPubkeyMap;
}
```

Expand Down Expand Up @@ -352,7 +353,7 @@ interface SendRgbppUtxosProps {
excludeUtxos?: BaseOutput[];

// EXPERIMENTAL: the below props are experimental and can be altered at any time
onlyProvableUtxos?: boolean;
pubkeyMap?: AddressToPubkeyMap;
}
```

Expand Down Expand Up @@ -386,7 +387,7 @@ interface SendRbfProps {
requireGreaterFeeAndRate?: boolean;

// EXPERIMENTAL: the below props are experimental and can be altered at any time
inputsPubkey?: Record<string, string>; // Record<address, pubkey>
pubkeyMap?: AddressToPubkeyMap;
}
```

Expand Down Expand Up @@ -513,3 +514,9 @@ enum NetworkType {
REGTEST,
}
```

#### AddressToPubkeyMap

```typescript
type AddressToPubkeyMap = Record<string, string>;
```
22 changes: 22 additions & 0 deletions packages/btc/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export enum AddressType {
UNKNOWN,
}

/**
* Type: Record<Address, Pubkey>
*
* The map of address and pubkey, usually for recognizing the P2TR inputs in the transaction.
*/
export type AddressToPubkeyMap = Record<string, string>;

/**
* Check weather the address is supported as a from address.
* Currently, only P2WPKH and P2TR addresses are supported.
Expand Down Expand Up @@ -210,3 +217,18 @@ function getAddressTypeDust(addressType: AddressType) {
return 546;
}
}

/**
* Add address/pubkey pair to a Record<address, pubkey> map
*/
export function addAddressToPubkeyMap(
pubkeyMap: AddressToPubkeyMap,
address: string,
pubkey?: string,
): Record<string, string> {
const newMap = { ...pubkeyMap };
if (pubkey) {
newMap[address] = pubkey;
}
return newMap;
}
17 changes: 3 additions & 14 deletions packages/btc/src/api/sendRbf.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { BaseOutput, Utxo } from '../transaction/utxo';
import { DataSource } from '../query/source';
import { AddressToPubkeyMap } from '../address';
import { ErrorCodes, TxBuildError } from '../error';
import { InitOutput, TxBuilder } from '../transaction/build';
import { isOpReturnScriptPubkey } from '../transaction/embed';
import { networkTypeToNetwork } from '../preset/network';
import { networkTypeToConfig } from '../preset/config';
import { createSendUtxosBuilder } from './sendUtxos';
import { isP2trScript } from '../script';
import { bitcoin } from '../bitcoin';

export interface SendRbfProps {
Expand All @@ -23,7 +23,7 @@ export interface SendRbfProps {
requireGreaterFeeAndRate?: boolean;

// EXPERIMENTAL: the below props are unstable and can be altered at any time
inputsPubkey?: Record<string, string>; // Record<address, pubkey>
pubkeyMap?: AddressToPubkeyMap;
}

export async function createSendRbfBuilder(props: SendRbfProps): Promise<{
Expand All @@ -43,18 +43,6 @@ export async function createSendRbfBuilder(props: SendRbfProps): Promise<{
if (!utxo) {
throw TxBuildError.withComment(ErrorCodes.CANNOT_FIND_UTXO, `hash: ${hash}, index: ${input.index}`);
}

// Ensure each P2TR input has a corresponding pubkey
const fromPubkey = utxo.address === props.from ? props.fromPubkey : undefined;
const inputPubkey = props.inputsPubkey?.[utxo.address];
const pubkey = inputPubkey ?? fromPubkey;
if (pubkey) {
utxo.pubkey = pubkey;
}
if (isP2trScript(utxo.scriptPk) && !utxo.pubkey) {
throw TxBuildError.withComment(ErrorCodes.MISSING_PUBKEY, utxo.address);
}

inputs.push(utxo);
}

Expand Down Expand Up @@ -151,6 +139,7 @@ export async function createSendRbfBuilder(props: SendRbfProps): Promise<{
from: props.from,
source: props.source,
feeRate: props.feeRate,
pubkeyMap: props.pubkeyMap,
fromPubkey: props.fromPubkey,
minUtxoSatoshi: props.minUtxoSatoshi,
onlyConfirmedUtxos: props.onlyConfirmedUtxos ?? true,
Expand Down
22 changes: 6 additions & 16 deletions packages/btc/src/api/sendRgbppUtxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Collector, checkCkbTxInputsCapacitySufficient } from '@rgbpp-sdk/ckb';
import { isRgbppLockCell, isBtcTimeLockCell, calculateCommitment } from '@rgbpp-sdk/ckb';
import { bitcoin } from '../bitcoin';
import { BaseOutput, Utxo } from '../transaction/utxo';
import { AddressToPubkeyMap } from '../address';
import { DataSource } from '../query/source';
import { NetworkType } from '../preset/types';
import { ErrorCodes, TxBuildError } from '../error';
Expand Down Expand Up @@ -31,7 +32,7 @@ export interface SendRgbppUtxosProps {
excludeUtxos?: BaseOutput[];

// EXPERIMENTAL: the below props are unstable and can be altered at any time
onlyProvableUtxos?: boolean;
pubkeyMap?: AddressToPubkeyMap;
}

/**
Expand All @@ -45,8 +46,6 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P
feeRate: number;
changeIndex: number;
}> {
const onlyProvableUtxos = props.onlyProvableUtxos ?? true;

const btcInputs: Utxo[] = [];
const btcOutputs: InitOutput[] = [];
let lastCkbTypeOutputIndex = -1;
Expand Down Expand Up @@ -86,33 +85,23 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P
for (let i = 0; i < ckbVirtualTx.inputs.length; i++) {
const { lockArgs, isRgbppLock } = ckbLiveCells[i];

// If input.lock == RgbppLock, add to inputs if:
// Add to inputs if all the following conditions are met:
// 1. input.lock.args can be unpacked to RgbppLockArgs
// 2. utxo can be found via the DataSource.getUtxo() API
// 3. utxo.scriptPk == addressToScriptPk(props.from)
// 4. utxo is not duplicated in the inputs
// 3. utxo is not duplicated in the inputs
if (isRgbppLock) {
const args = lockArgs!;
const utxo = btcUtxos[i];
if (!utxo) {
throw TxBuildError.withComment(ErrorCodes.CANNOT_FIND_UTXO, `hash: ${args.btcTxid}, index: ${args.outIndex}`);
}
if (onlyProvableUtxos && utxo.address !== props.from) {
throw TxBuildError.withComment(
ErrorCodes.REFERENCED_UNPROVABLE_UTXO,
`hash: ${args.btcTxid}, index: ${args.outIndex}`,
);
}

const foundInInputs = btcInputs.some((v) => v.txid === utxo.txid && v.vout === utxo.vout);
if (foundInInputs) {
continue;
}

btcInputs.push({
...utxo,
pubkey: props.fromPubkey, // For P2TR addresses, a pubkey is required
});
btcInputs.push(utxo);
}
}

Expand Down Expand Up @@ -179,6 +168,7 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P
minUtxoSatoshi: props.minUtxoSatoshi,
onlyConfirmedUtxos: props.onlyConfirmedUtxos,
excludeUtxos: props.excludeUtxos,
pubkeyMap: props.pubkeyMap,
});
}

Expand Down
24 changes: 17 additions & 7 deletions packages/btc/src/api/sendUtxos.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { bitcoin } from '../bitcoin';
import { DataSource } from '../query/source';
import { BaseOutput, Utxo } from '../transaction/utxo';
import { TxBuilder, InitOutput } from '../transaction/build';
import { BaseOutput, Utxo, prepareUtxoInputs } from '../transaction/utxo';
import { AddressToPubkeyMap, addAddressToPubkeyMap } from '../address';

export interface SendUtxosProps {
inputs: Utxo[];
Expand All @@ -17,6 +18,7 @@ export interface SendUtxosProps {

// EXPERIMENTAL: the below props are unstable and can be altered at any time
skipInputsValidation?: boolean;
pubkeyMap?: AddressToPubkeyMap;
}

export async function createSendUtxosBuilder(props: SendUtxosProps): Promise<{
Expand All @@ -32,16 +34,24 @@ export async function createSendUtxosBuilder(props: SendUtxosProps): Promise<{
onlyConfirmedUtxos: props.onlyConfirmedUtxos,
});

tx.addInputs(props.inputs);
tx.addOutputs(props.outputs);
// Prepare the UTXO inputs:
// 1. Fill pubkey for each P2TR UTXO, and throw if the corresponding pubkey is not found
// 2. Throw if unconfirmed UTXOs are found (if onlyConfirmedUtxos == true && skipInputsValidation == false)
const pubkeyMap = addAddressToPubkeyMap(props.pubkeyMap ?? {}, props.from, props.fromPubkey);
const inputs = await prepareUtxoInputs({
utxos: props.inputs,
source: props.source,
requireConfirmed: props.onlyConfirmedUtxos && !props.skipInputsValidation,
requirePubkey: true,
pubkeyMap,
});

if (props.onlyConfirmedUtxos && !props.skipInputsValidation) {
await tx.validateInputs();
}
tx.addInputs(inputs);
tx.addOutputs(props.outputs);

const paid = await tx.payFee({
address: props.from,
publicKey: props.fromPubkey,
publicKey: pubkeyMap[props.from],
changeAddress: props.changeAddress,
excludeUtxos: props.excludeUtxos,
});
Expand Down
24 changes: 10 additions & 14 deletions packages/btc/src/transaction/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { bitcoin } from '../bitcoin';
import { DataSource } from '../query/source';
import { ErrorCodes, TxBuildError } from '../error';
import { NetworkType, RgbppBtcConfig } from '../preset/types';
import { AddressType, addressToScriptPublicKeyHex, getAddressType, isSupportedFromAddress } from '../address';
import { isSupportedFromAddress } from '../address';
import { dataToOpReturnScriptPubkey, isOpReturnScriptPubkey } from './embed';
import { networkTypeToConfig } from '../preset/config';
import { BaseOutput, Utxo, utxoToInput } from './utxo';
Expand Down Expand Up @@ -213,8 +213,7 @@ export class TxBuilder {
}

// Calculate network fee
const addressType = getAddressType(address);
currentFee = await this.calculateFee(addressType, currentFeeRate);
currentFee = await this.calculateFee(currentFeeRate);

// If (fee = previousFee ±1), the fee is considered acceptable/expected.
isFeeExpected = [-1, 0, 1].includes(currentFee - previousFee);
Expand Down Expand Up @@ -471,14 +470,14 @@ export class TxBuilder {
});
}

async calculateFee(addressType: AddressType, feeRate?: number): Promise<number> {
async calculateFee(feeRate?: number): Promise<number> {
if (!feeRate && !this.feeRate) {
throw TxBuildError.withComment(ErrorCodes.INVALID_FEE_RATE, `${feeRate ?? this.feeRate}`);
}

const currentFeeRate = feeRate ?? this.feeRate!;

const psbt = await this.createEstimatedPsbt(addressType);
const psbt = await this.createEstimatedPsbt();
const tx = psbt.extractTransaction(true);

const inputs = tx.ins.length;
Expand All @@ -490,20 +489,17 @@ export class TxBuilder {
return Math.ceil(virtualSize * currentFeeRate);
}

async createEstimatedPsbt(addressType: AddressType): Promise<bitcoin.Psbt> {
const estimate = FeeEstimator.fromRandom(addressType, this.networkType);
const estimateScriptPk = addressToScriptPublicKeyHex(estimate.address, this.networkType);
async createEstimatedPsbt(): Promise<bitcoin.Psbt> {
const estimator = FeeEstimator.fromRandom(this.networkType);

const tx = this.clone();
const utxos = tx.inputs.map((input) => input.utxo);
tx.inputs = utxos.map((utxo) => {
utxo.scriptPk = estimateScriptPk;
utxo.pubkey = estimate.publicKey;
return utxoToInput(utxo);
tx.inputs = tx.inputs.map((input) => {
const replacedUtxo = estimator.replaceUtxo(input.utxo);
return utxoToInput(replacedUtxo);
});

const psbt = tx.toPsbt();
await estimate.signPsbt(psbt);
await estimator.signPsbt(psbt);
return psbt;
}

Expand Down
Loading

1 comment on commit 10e4e17

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New snapshot version of the rgbpp-sdk packages have been released:

Name Version
@rgbpp-sdk/btc 0.0.0-snap-20240618150929

Please sign in to comment.