Skip to content
This repository was archived by the owner on May 27, 2025. It is now read-only.
Merged
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
11 changes: 11 additions & 0 deletions .changeset/lazy-shrimps-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'rgbpp': minor
'@rgbpp-sdk/ckb': minor
---

Add offline mode support for compatible xUDT type scripts:
- Introduce an optional `offline` boolean parameter to the following methods:
- `isUDTTypeSupported`
- `isCompatibleUDTTypesSupported`
- `CompatibleXUDTRegistry.getCompatibleTokens`
- Add examples demonstrating compatible xUDT asset management in offline mode
72 changes: 72 additions & 0 deletions examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
import { genCkbJumpBtcVirtualTx } from 'rgbpp';
import { getSecp256k1CellDep, buildRgbppLockArgs, signCkbTransaction } from 'rgbpp/ckb';
import {
CKB_PRIVATE_KEY,
isMainnet,
collector,
ckbAddress,
BTC_TESTNET_TYPE,
initOfflineCkbCollector,
vendorCellDeps,
} from '../../../env';

interface LeapToBtcParams {
outIndex: number;
btcTxId: string;
transferAmount: bigint;
compatibleXudtTypeScript: CKBComponents.Script;
}

const leapRusdFromCkbToBtc = async ({
outIndex,
btcTxId,
transferAmount,
compatibleXudtTypeScript,
}: LeapToBtcParams) => {
const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId);

const { collector: offlineCollector } = await initOfflineCkbCollector([
{ lock: addressToScript(ckbAddress), type: compatibleXudtTypeScript },
{ lock: addressToScript(ckbAddress) },
]);

const ckbRawTx = await genCkbJumpBtcVirtualTx({
collector: offlineCollector,
fromCkbAddress: ckbAddress,
toRgbppLockArgs,
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
transferAmount,
btcTestnetType: BTC_TESTNET_TYPE,
vendorCellDeps,
});

const emptyWitness = { lock: '', inputType: '', outputType: '' };
const unsignedTx: CKBComponents.RawTransactionToSign = {
...ckbRawTx,
cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)],
witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)],
};

const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx);
const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough');
console.info(`Rgbpp compatible xUDT asset has been leaped from CKB to BTC and CKB tx hash is ${txHash}`);
};

// Please use your real BTC UTXO information on the BTC Testnet
// BTC Testnet3: https://mempool.space/testnet
// BTC Signet: https://mempool.space/signet
leapRusdFromCkbToBtc({
outIndex: 2,
btcTxId: '4239d2f9fe566513b0604e4dfe10f3b85b6bebe25096cf426559a89c87c68d1a',
compatibleXudtTypeScript: {
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
hashType: 'type',
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
},
transferAmount: BigInt(200_0000),
});

/*
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts
*/
136 changes: 136 additions & 0 deletions examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
import { BtcAssetsApiError, genBtcTransferCkbVirtualTx, sendRgbppUtxos } from 'rgbpp';

import {
isMainnet,
collector,
btcService,
CKB_PRIVATE_KEY,
ckbAddress,
btcAccount,
BTC_TESTNET_TYPE,
initOfflineCkbCollector,
initOfflineBtcDataSource,
vendorCellDeps,
} from '../../../env';
import {
appendCkbTxWitnesses,
buildRgbppLockArgs,
sendCkbTx,
updateCkbTxWithRealBtcTxId,
genRgbppLockScript,
appendIssuerCellToBtcBatchTransferToSign,
addressToScriptHash,
signCkbTransaction,
} from 'rgbpp/ckb';
import { saveCkbVirtualTxResult } from '../../../shared/utils';
import { signAndSendPsbt } from '../../../shared/btc-account';

interface RgbppTransferParams {
rgbppLockArgsList: string[];
toBtcAddress: string;
transferAmount: bigint;
compatibleXudtTypeScript: CKBComponents.Script;
}

const transferRusdOnBtc = async ({
rgbppLockArgsList,
toBtcAddress,
compatibleXudtTypeScript,
transferAmount,
}: RgbppTransferParams) => {
const rgbppLocks = rgbppLockArgsList.map((args) => genRgbppLockScript(args, isMainnet, BTC_TESTNET_TYPE));
const { collector: offlineCollector } = await initOfflineCkbCollector([
...rgbppLocks.map((lock) => ({ lock, type: compatibleXudtTypeScript })),
{ lock: addressToScript(ckbAddress) },
]);

const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({
collector: offlineCollector,
rgbppLockArgsList,
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
transferAmount,
isMainnet,
btcTestnetType: BTC_TESTNET_TYPE,
vendorCellDeps,
});

// Save ckbVirtualTxResult
saveCkbVirtualTxResult(ckbVirtualTxResult, '2-compatible-xudt-btc-transfer-offline');

const { commitment, ckbRawTx, sumInputsCapacity } = ckbVirtualTxResult;

const btcOfflineDataSource = await initOfflineBtcDataSource(rgbppLockArgsList, btcAccount.from);

// Send BTC tx
const psbt = await sendRgbppUtxos({
ckbVirtualTx: ckbRawTx,
commitment,
tos: [toBtcAddress],
needPaymaster: false,
ckbCollector: offlineCollector,
from: btcAccount.from,
fromPubkey: btcAccount.fromPubkey,
source: btcOfflineDataSource,
feeRate: 128,
});

const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService);
console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`);
console.log('BTC tx bytes: ', btcTxBytes);

const interval = setInterval(async () => {
try {
console.log('Waiting for BTC tx and proof to be ready');
const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0);
clearInterval(interval);
// Update CKB transaction with the real BTC txId
const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet });
const ckbTx = await appendCkbTxWitnesses({
ckbRawTx: newCkbRawTx,
btcTxBytes,
rgbppApiSpvProof,
});

const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({
issuerAddress: ckbAddress,
ckbRawTx: ckbTx,
collector: offlineCollector,
sumInputsCapacity,
isMainnet,
});

const keyMap = new Map<string, string>();
keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY);
const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true);

const txHash = await sendCkbTx({ collector, signedTx });
console.info(`Rgbpp compatible xUDT asset has been transferred on BTC and the related CKB tx hash is ${txHash}`);
} catch (error) {
if (!(error instanceof BtcAssetsApiError)) {
console.error(error);
}
}
}, 20 * 1000);
};

// Please use your real BTC UTXO information on the BTC Testnet
// BTC Testnet3: https://mempool.space/testnet
// BTC Signet: https://mempool.space/signet

// rgbppLockArgs: outIndexU32 + btcTxId
transferRusdOnBtc({
rgbppLockArgsList: [buildRgbppLockArgs(2, '4239d2f9fe566513b0604e4dfe10f3b85b6bebe25096cf426559a89c87c68d1a')],
toBtcAddress: 'tb1qe68sv5pr5vdj2daw2v96pwvw5m9ca4ew35ewp5',
// Please use your own RGB++ compatible xudt asset's type script
compatibleXudtTypeScript: {
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
hashType: 'type',
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
},
transferAmount: BigInt(100_0000),
});

/*
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts
*/
138 changes: 138 additions & 0 deletions examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
buildRgbppLockArgs,
genRgbppLockScript,
appendIssuerCellToBtcBatchTransferToSign,
signCkbTransaction,
addressToScriptHash,
appendCkbTxWitnesses,
updateCkbTxWithRealBtcTxId,
sendCkbTx,
} from 'rgbpp/ckb';
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
import { genBtcJumpCkbVirtualTx, sendRgbppUtxos, BtcAssetsApiError } from 'rgbpp';
import {
isMainnet,
collector,
btcService,
btcAccount,
BTC_TESTNET_TYPE,
CKB_PRIVATE_KEY,
ckbAddress,
initOfflineCkbCollector,
vendorCellDeps,
initOfflineBtcDataSource,
} from '../../../env';
import { saveCkbVirtualTxResult } from '../../../shared/utils';
import { signAndSendPsbt } from '../../../shared/btc-account';

interface LeapToCkbParams {
rgbppLockArgsList: string[];
toCkbAddress: string;
transferAmount: bigint;
compatibleXudtTypeScript: CKBComponents.Script;
}

const leapRusdFromBtcToCKB = async ({
rgbppLockArgsList,
toCkbAddress,
compatibleXudtTypeScript,
transferAmount,
}: LeapToCkbParams) => {
const rgbppLocks = rgbppLockArgsList.map((args) => genRgbppLockScript(args, isMainnet, BTC_TESTNET_TYPE));
const { collector: offlineCollector } = await initOfflineCkbCollector([
...rgbppLocks.map((lock) => ({ lock, type: compatibleXudtTypeScript })),
{ lock: addressToScript(ckbAddress) },
]);

const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({
collector: offlineCollector,
rgbppLockArgsList,
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
transferAmount,
toCkbAddress,
isMainnet,
btcTestnetType: BTC_TESTNET_TYPE,
vendorCellDeps,
});

// Save ckbVirtualTxResult
saveCkbVirtualTxResult(ckbVirtualTxResult, '3-compatible-xudt-btc-leap-ckb-offline');

const { commitment, ckbRawTx, sumInputsCapacity } = ckbVirtualTxResult;

const btcOfflineDataSource = await initOfflineBtcDataSource(rgbppLockArgsList, btcAccount.from);

// Send BTC tx
const psbt = await sendRgbppUtxos({
ckbVirtualTx: ckbRawTx,
commitment,
tos: [btcAccount.from],
ckbCollector: offlineCollector,
from: btcAccount.from,
fromPubkey: btcAccount.fromPubkey,
source: btcOfflineDataSource,
needPaymaster: false,
feeRate: 128,
});

const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService);
console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`);
console.log('BTC tx bytes: ', btcTxBytes);

const interval = setInterval(async () => {
try {
console.log('Waiting for BTC tx and proof to be ready');
const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0);
clearInterval(interval);
// Update CKB transaction with the real BTC txId
const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet });
const ckbTx = await appendCkbTxWitnesses({
ckbRawTx: newCkbRawTx,
btcTxBytes,
rgbppApiSpvProof,
});

const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({
issuerAddress: ckbAddress,
ckbRawTx: ckbTx,
collector: offlineCollector,
sumInputsCapacity,
isMainnet,
});

const keyMap = new Map<string, string>();
keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY);
const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true);

const txHash = await sendCkbTx({ collector, signedTx });
console.info(
`Rgbpp compatible xUDT asset has been leaped from BTC to CKB and the related CKB tx hash is ${txHash}`,
);
} catch (error) {
if (!(error instanceof BtcAssetsApiError)) {
console.error(error);
}
}
}, 20 * 1000);
};

// Please use your real BTC UTXO information on the BTC Testnet
// BTC Testnet3: https://mempool.space/testnet
// BTC Signet: https://mempool.space/signet

// rgbppLockArgs: outIndexU32 + btcTxId
leapRusdFromBtcToCKB({
rgbppLockArgsList: [buildRgbppLockArgs(2, 'daec93a97c8b7f6fdd33696f814f0292be966dc4ea4853400d3cada816c70f5d')],
toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqfpu7pwavwf3yang8khrsklumayj6nyxhqpmh7fq',
// Please use your own RGB++ compatible xudt asset's type script
compatibleXudtTypeScript: {
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
hashType: 'type',
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
},
transferAmount: BigInt(10_0000),
});

/*
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts
*/
Loading
Loading