Skip to content

Commit

Permalink
Merge pull request #298 from fghdotio/feat/offline-utxo
Browse files Browse the repository at this point in the history
feat: implement getRgbppSpvProof for OfflineBtcAssetsDataSource
  • Loading branch information
duanyytop authored Jan 24, 2025
2 parents 8c6705a + 214a4ec commit 777db68
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .changeset/nervous-doors-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rgbpp-sdk/service': minor
---

Implement `getRgbppSpvProof` for `OfflineBtcAssetsDataSource`, enabling support for offline unlocking of BTC time lock cells.

- Add an example demonstrating how to unlock BTC time lock cells offline.
10 changes: 7 additions & 3 deletions examples/rgbpp/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
systemScripts,
} from '@nervosnetwork/ckb-sdk-utils';
import { NetworkType, AddressType, DataSource } from 'rgbpp/btc';
import { BtcAssetsApi, OfflineBtcAssetsDataSource, OfflineBtcUtxo, BtcApiUtxo } from 'rgbpp/service';
import { BtcAssetsApi, OfflineBtcAssetsDataSource, OfflineBtcUtxo, BtcApiUtxo, SpvProofEntry } from 'rgbpp/service';
import {
BTCTestnetType,
Collector,
Expand Down Expand Up @@ -96,7 +96,11 @@ export const initOfflineCkbCollector = async (
};
};

export const initOfflineBtcDataSource = async (rgbppLockArgsList: string[], address: string) => {
export const initOfflineBtcDataSource = async (
rgbppLockArgsList: string[],
address: string,
spvProofs: SpvProofEntry[] = [],
) => {
const btcTxIds = rgbppLockArgsList.map((rgbppLockArgs) => remove0x(unpackRgbppLockArgs(rgbppLockArgs).btcTxId));
const btcTxs = await Promise.all(
btcTxIds.map(async (btcTxId) => {
Expand Down Expand Up @@ -130,7 +134,7 @@ export const initOfflineBtcDataSource = async (rgbppLockArgsList: string[], addr
});

return new DataSource(
new OfflineBtcAssetsDataSource({ txs: btcTxs, utxos: Array.from(utxoMap.values()) }),
new OfflineBtcAssetsDataSource({ txs: btcTxs, utxos: Array.from(utxoMap.values()), rgbppSpvProofs: spvProofs }),
networkType,
);
};
Expand Down
57 changes: 57 additions & 0 deletions examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp';
import { sendCkbTx, getBtcTimeLockScript, btcTxIdAndAfterFromBtcTimeLockArgs } from 'rgbpp/ckb';
import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../env';
import { OfflineBtcAssetsDataSource, SpvProofEntry } from 'rgbpp/service';

const unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => {
const btcTimeCells = await collector.getCells({
lock: {
...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE),
args: btcTimeCellArgs,
},
isDataMustBeEmpty: false,
});
if (!btcTimeCells || btcTimeCells.length === 0) {
throw new Error('No btc time cell found');
}

const spvProofs: SpvProofEntry[] = [];
for (const btcTimeCell of btcTimeCells) {
const { btcTxId, after } = btcTxIdAndAfterFromBtcTimeLockArgs(btcTimeCell.output.lock.args);
spvProofs.push({
txid: btcTxId,
confirmations: after,
proof: await btcService.getRgbppSpvProof(btcTxId, after),
});
}

const offlineBtcAssetsDataSource = new OfflineBtcAssetsDataSource({
txs: [],
utxos: [],
rgbppSpvProofs: spvProofs,
});

const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({
btcTimeCells,
btcAssetsApi: offlineBtcAssetsDataSource,
isMainnet,
btcTestnetType: BTC_TESTNET_TYPE,
});

const signedTx = await signBtcTimeCellSpentTx({
secp256k1PrivateKey: CKB_PRIVATE_KEY,
collector,
masterCkbAddress: ckbAddress,
ckbRawTx,
isMainnet,
});

const txHash = await sendCkbTx({ collector, signedTx });
console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`);
};

// The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction
unlockBtcTimeCell({
btcTimeCellArgs:
'0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c0600000045241651e435d786d0ba8a1280d4bb3283eca10db728e2ba0a3978c136f5bb19',
});
2 changes: 1 addition & 1 deletion packages/ckb/src/utils/cell-dep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const fetchCellDepsJsonFromStaticSource = async () => {
}
};

const fetchCellDepsJson = async () => {
export const fetchCellDepsJson = async () => {
try {
const response = await request(VERCEL_SERVER_CELL_DEPS_JSON_URL);
if (response && response.data) {
Expand Down
2 changes: 2 additions & 0 deletions packages/service/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ErrorCodes {
ASSETS_API_RESPONSE_DECODE_ERROR,

OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE,
OFFLINE_DATA_SOURCE_SPV_PROOF_NOT_FOUND,
}

export const ErrorMessages = {
Expand All @@ -22,6 +23,7 @@ export const ErrorMessages = {
[ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR]: 'Failed to decode the response of BtcAssetsAPI',

[ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE]: 'Method not available for offline data source',
[ErrorCodes.OFFLINE_DATA_SOURCE_SPV_PROOF_NOT_FOUND]: 'SPV proof not found for the given txid and confirmations',
};

export class BtcAssetsApiError extends Error {
Expand Down
37 changes: 33 additions & 4 deletions packages/service/src/service/offline-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ import {
RgbppApiRetryCkbTransactionPayload,
OfflineBtcUtxo,
BtcApiRecommendedFeeRates,
RgbppApiSpvProof,
} from '../types';

export interface OfflineBtcData {
txs: BtcApiTransaction[];
utxos: OfflineBtcUtxo[];
rgbppSpvProofs: SpvProofEntry[];
}

export interface SpvProofEntry {
txid: string;
confirmations: number;
proof: RgbppApiSpvProof;
}

/*
Expand All @@ -35,6 +43,8 @@ export class OfflineBtcAssetsDataSource extends BtcAssetsApi {
private txs: Record<string, BtcApiTransaction>;
// address -> utxos
private utxos: Record<string, OfflineBtcUtxo[]>;
// txid:confirmations -> spv proof
private rgbppSpvProofs: Record<string, RgbppApiSpvProof>;

private defaultFee = 1;

Expand All @@ -56,6 +66,18 @@ export class OfflineBtcAssetsDataSource extends BtcAssetsApi {
},
{} as Record<string, OfflineBtcUtxo[]>,
);

this.rgbppSpvProofs = offlineData.rgbppSpvProofs.reduce(
(acc, proof) => {
acc[this.spvKey(proof.txid, proof.confirmations)] = proof.proof;
return acc;
},
{} as Record<string, RgbppApiSpvProof>,
);
}

private spvKey(txid: string, confirmations: number) {
return `${txid}:${confirmations}`;
}

getBtcTransaction(txId: string): Promise<BtcApiTransaction> {
Expand Down Expand Up @@ -86,6 +108,17 @@ export class OfflineBtcAssetsDataSource extends BtcAssetsApi {
});
}

getRgbppSpvProof(btcTxId: string, confirmations: number) {
const proof = this.rgbppSpvProofs[this.spvKey(btcTxId, confirmations)];
if (!proof) {
throw new OfflineBtcAssetsDataSourceError(
ErrorCodes.OFFLINE_DATA_SOURCE_SPV_PROOF_NOT_FOUND,
`SPV proof not found for txid ${btcTxId} with ${confirmations} confirmations`,
);
}
return Promise.resolve(proof);
}

/*
* The following methods are not available in offline mode.
*/
Expand Down Expand Up @@ -160,10 +193,6 @@ export class OfflineBtcAssetsDataSource extends BtcAssetsApi {
return Promise.reject(new OfflineBtcAssetsDataSourceError(ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE));
}

getRgbppSpvProof(btcTxId: string, confirmations: number) {
return Promise.reject(new OfflineBtcAssetsDataSourceError(ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE));
}

sendRgbppCkbTransaction(payload: RgbppApiSendCkbTransactionPayload) {
return Promise.reject(new OfflineBtcAssetsDataSourceError(ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE));
}
Expand Down

1 comment on commit 777db68

@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-20250124052302
@rgbpp-sdk/ckb 0.0.0-snap-20250124052302
rgbpp 0.0.0-snap-20250124052302
@rgbpp-sdk/service 0.0.0-snap-20250124052302

Please sign in to comment.