Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5914a7c

Browse files
authoredFeb 17, 2025··
Merge pull request #303 from fghdotio/feat/offline-utxo
feat: add offline mode support for compatible xUDT type scripts
2 parents ab3d914 + 72150df commit 5914a7c

File tree

15 files changed

+512
-22
lines changed

15 files changed

+512
-22
lines changed
 

‎.changeset/lazy-shrimps-roll.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'rgbpp': minor
3+
'@rgbpp-sdk/ckb': minor
4+
---
5+
6+
Add offline mode support for compatible xUDT type scripts:
7+
- Introduce an optional `offline` boolean parameter to the following methods:
8+
- `isUDTTypeSupported`
9+
- `isCompatibleUDTTypesSupported`
10+
- `CompatibleXUDTRegistry.getCompatibleTokens`
11+
- Add examples demonstrating compatible xUDT asset management in offline mode
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
2+
import { genCkbJumpBtcVirtualTx } from 'rgbpp';
3+
import { getSecp256k1CellDep, buildRgbppLockArgs, signCkbTransaction } from 'rgbpp/ckb';
4+
import {
5+
CKB_PRIVATE_KEY,
6+
isMainnet,
7+
collector,
8+
ckbAddress,
9+
BTC_TESTNET_TYPE,
10+
initOfflineCkbCollector,
11+
vendorCellDeps,
12+
} from '../../../env';
13+
14+
interface LeapToBtcParams {
15+
outIndex: number;
16+
btcTxId: string;
17+
transferAmount: bigint;
18+
compatibleXudtTypeScript: CKBComponents.Script;
19+
}
20+
21+
const leapRusdFromCkbToBtc = async ({
22+
outIndex,
23+
btcTxId,
24+
transferAmount,
25+
compatibleXudtTypeScript,
26+
}: LeapToBtcParams) => {
27+
const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId);
28+
29+
const { collector: offlineCollector } = await initOfflineCkbCollector([
30+
{ lock: addressToScript(ckbAddress), type: compatibleXudtTypeScript },
31+
{ lock: addressToScript(ckbAddress) },
32+
]);
33+
34+
const ckbRawTx = await genCkbJumpBtcVirtualTx({
35+
collector: offlineCollector,
36+
fromCkbAddress: ckbAddress,
37+
toRgbppLockArgs,
38+
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
39+
transferAmount,
40+
btcTestnetType: BTC_TESTNET_TYPE,
41+
vendorCellDeps,
42+
});
43+
44+
const emptyWitness = { lock: '', inputType: '', outputType: '' };
45+
const unsignedTx: CKBComponents.RawTransactionToSign = {
46+
...ckbRawTx,
47+
cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)],
48+
witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)],
49+
};
50+
51+
const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx);
52+
const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough');
53+
console.info(`Rgbpp compatible xUDT asset has been leaped from CKB to BTC and CKB tx hash is ${txHash}`);
54+
};
55+
56+
// Please use your real BTC UTXO information on the BTC Testnet
57+
// BTC Testnet3: https://mempool.space/testnet
58+
// BTC Signet: https://mempool.space/signet
59+
leapRusdFromCkbToBtc({
60+
outIndex: 2,
61+
btcTxId: '4239d2f9fe566513b0604e4dfe10f3b85b6bebe25096cf426559a89c87c68d1a',
62+
compatibleXudtTypeScript: {
63+
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
64+
hashType: 'type',
65+
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
66+
},
67+
transferAmount: BigInt(200_0000),
68+
});
69+
70+
/*
71+
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts
72+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
2+
import { BtcAssetsApiError, genBtcTransferCkbVirtualTx, sendRgbppUtxos } from 'rgbpp';
3+
4+
import {
5+
isMainnet,
6+
collector,
7+
btcService,
8+
CKB_PRIVATE_KEY,
9+
ckbAddress,
10+
btcAccount,
11+
BTC_TESTNET_TYPE,
12+
initOfflineCkbCollector,
13+
initOfflineBtcDataSource,
14+
vendorCellDeps,
15+
} from '../../../env';
16+
import {
17+
appendCkbTxWitnesses,
18+
buildRgbppLockArgs,
19+
sendCkbTx,
20+
updateCkbTxWithRealBtcTxId,
21+
genRgbppLockScript,
22+
appendIssuerCellToBtcBatchTransferToSign,
23+
addressToScriptHash,
24+
signCkbTransaction,
25+
} from 'rgbpp/ckb';
26+
import { saveCkbVirtualTxResult } from '../../../shared/utils';
27+
import { signAndSendPsbt } from '../../../shared/btc-account';
28+
29+
interface RgbppTransferParams {
30+
rgbppLockArgsList: string[];
31+
toBtcAddress: string;
32+
transferAmount: bigint;
33+
compatibleXudtTypeScript: CKBComponents.Script;
34+
}
35+
36+
const transferRusdOnBtc = async ({
37+
rgbppLockArgsList,
38+
toBtcAddress,
39+
compatibleXudtTypeScript,
40+
transferAmount,
41+
}: RgbppTransferParams) => {
42+
const rgbppLocks = rgbppLockArgsList.map((args) => genRgbppLockScript(args, isMainnet, BTC_TESTNET_TYPE));
43+
const { collector: offlineCollector } = await initOfflineCkbCollector([
44+
...rgbppLocks.map((lock) => ({ lock, type: compatibleXudtTypeScript })),
45+
{ lock: addressToScript(ckbAddress) },
46+
]);
47+
48+
const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({
49+
collector: offlineCollector,
50+
rgbppLockArgsList,
51+
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
52+
transferAmount,
53+
isMainnet,
54+
btcTestnetType: BTC_TESTNET_TYPE,
55+
vendorCellDeps,
56+
});
57+
58+
// Save ckbVirtualTxResult
59+
saveCkbVirtualTxResult(ckbVirtualTxResult, '2-compatible-xudt-btc-transfer-offline');
60+
61+
const { commitment, ckbRawTx, sumInputsCapacity } = ckbVirtualTxResult;
62+
63+
const btcOfflineDataSource = await initOfflineBtcDataSource(rgbppLockArgsList, btcAccount.from);
64+
65+
// Send BTC tx
66+
const psbt = await sendRgbppUtxos({
67+
ckbVirtualTx: ckbRawTx,
68+
commitment,
69+
tos: [toBtcAddress],
70+
needPaymaster: false,
71+
ckbCollector: offlineCollector,
72+
from: btcAccount.from,
73+
fromPubkey: btcAccount.fromPubkey,
74+
source: btcOfflineDataSource,
75+
feeRate: 128,
76+
});
77+
78+
const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService);
79+
console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`);
80+
console.log('BTC tx bytes: ', btcTxBytes);
81+
82+
const interval = setInterval(async () => {
83+
try {
84+
console.log('Waiting for BTC tx and proof to be ready');
85+
const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0);
86+
clearInterval(interval);
87+
// Update CKB transaction with the real BTC txId
88+
const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet });
89+
const ckbTx = await appendCkbTxWitnesses({
90+
ckbRawTx: newCkbRawTx,
91+
btcTxBytes,
92+
rgbppApiSpvProof,
93+
});
94+
95+
const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({
96+
issuerAddress: ckbAddress,
97+
ckbRawTx: ckbTx,
98+
collector: offlineCollector,
99+
sumInputsCapacity,
100+
isMainnet,
101+
});
102+
103+
const keyMap = new Map<string, string>();
104+
keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY);
105+
const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true);
106+
107+
const txHash = await sendCkbTx({ collector, signedTx });
108+
console.info(`Rgbpp compatible xUDT asset has been transferred on BTC and the related CKB tx hash is ${txHash}`);
109+
} catch (error) {
110+
if (!(error instanceof BtcAssetsApiError)) {
111+
console.error(error);
112+
}
113+
}
114+
}, 20 * 1000);
115+
};
116+
117+
// Please use your real BTC UTXO information on the BTC Testnet
118+
// BTC Testnet3: https://mempool.space/testnet
119+
// BTC Signet: https://mempool.space/signet
120+
121+
// rgbppLockArgs: outIndexU32 + btcTxId
122+
transferRusdOnBtc({
123+
rgbppLockArgsList: [buildRgbppLockArgs(2, '4239d2f9fe566513b0604e4dfe10f3b85b6bebe25096cf426559a89c87c68d1a')],
124+
toBtcAddress: 'tb1qe68sv5pr5vdj2daw2v96pwvw5m9ca4ew35ewp5',
125+
// Please use your own RGB++ compatible xudt asset's type script
126+
compatibleXudtTypeScript: {
127+
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
128+
hashType: 'type',
129+
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
130+
},
131+
transferAmount: BigInt(100_0000),
132+
});
133+
134+
/*
135+
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts
136+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
buildRgbppLockArgs,
3+
genRgbppLockScript,
4+
appendIssuerCellToBtcBatchTransferToSign,
5+
signCkbTransaction,
6+
addressToScriptHash,
7+
appendCkbTxWitnesses,
8+
updateCkbTxWithRealBtcTxId,
9+
sendCkbTx,
10+
} from 'rgbpp/ckb';
11+
import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils';
12+
import { genBtcJumpCkbVirtualTx, sendRgbppUtxos, BtcAssetsApiError } from 'rgbpp';
13+
import {
14+
isMainnet,
15+
collector,
16+
btcService,
17+
btcAccount,
18+
BTC_TESTNET_TYPE,
19+
CKB_PRIVATE_KEY,
20+
ckbAddress,
21+
initOfflineCkbCollector,
22+
vendorCellDeps,
23+
initOfflineBtcDataSource,
24+
} from '../../../env';
25+
import { saveCkbVirtualTxResult } from '../../../shared/utils';
26+
import { signAndSendPsbt } from '../../../shared/btc-account';
27+
28+
interface LeapToCkbParams {
29+
rgbppLockArgsList: string[];
30+
toCkbAddress: string;
31+
transferAmount: bigint;
32+
compatibleXudtTypeScript: CKBComponents.Script;
33+
}
34+
35+
const leapRusdFromBtcToCKB = async ({
36+
rgbppLockArgsList,
37+
toCkbAddress,
38+
compatibleXudtTypeScript,
39+
transferAmount,
40+
}: LeapToCkbParams) => {
41+
const rgbppLocks = rgbppLockArgsList.map((args) => genRgbppLockScript(args, isMainnet, BTC_TESTNET_TYPE));
42+
const { collector: offlineCollector } = await initOfflineCkbCollector([
43+
...rgbppLocks.map((lock) => ({ lock, type: compatibleXudtTypeScript })),
44+
{ lock: addressToScript(ckbAddress) },
45+
]);
46+
47+
const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({
48+
collector: offlineCollector,
49+
rgbppLockArgsList,
50+
xudtTypeBytes: serializeScript(compatibleXudtTypeScript),
51+
transferAmount,
52+
toCkbAddress,
53+
isMainnet,
54+
btcTestnetType: BTC_TESTNET_TYPE,
55+
vendorCellDeps,
56+
});
57+
58+
// Save ckbVirtualTxResult
59+
saveCkbVirtualTxResult(ckbVirtualTxResult, '3-compatible-xudt-btc-leap-ckb-offline');
60+
61+
const { commitment, ckbRawTx, sumInputsCapacity } = ckbVirtualTxResult;
62+
63+
const btcOfflineDataSource = await initOfflineBtcDataSource(rgbppLockArgsList, btcAccount.from);
64+
65+
// Send BTC tx
66+
const psbt = await sendRgbppUtxos({
67+
ckbVirtualTx: ckbRawTx,
68+
commitment,
69+
tos: [btcAccount.from],
70+
ckbCollector: offlineCollector,
71+
from: btcAccount.from,
72+
fromPubkey: btcAccount.fromPubkey,
73+
source: btcOfflineDataSource,
74+
needPaymaster: false,
75+
feeRate: 128,
76+
});
77+
78+
const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService);
79+
console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`);
80+
console.log('BTC tx bytes: ', btcTxBytes);
81+
82+
const interval = setInterval(async () => {
83+
try {
84+
console.log('Waiting for BTC tx and proof to be ready');
85+
const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0);
86+
clearInterval(interval);
87+
// Update CKB transaction with the real BTC txId
88+
const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet });
89+
const ckbTx = await appendCkbTxWitnesses({
90+
ckbRawTx: newCkbRawTx,
91+
btcTxBytes,
92+
rgbppApiSpvProof,
93+
});
94+
95+
const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({
96+
issuerAddress: ckbAddress,
97+
ckbRawTx: ckbTx,
98+
collector: offlineCollector,
99+
sumInputsCapacity,
100+
isMainnet,
101+
});
102+
103+
const keyMap = new Map<string, string>();
104+
keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY);
105+
const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true);
106+
107+
const txHash = await sendCkbTx({ collector, signedTx });
108+
console.info(
109+
`Rgbpp compatible xUDT asset has been leaped from BTC to CKB and the related CKB tx hash is ${txHash}`,
110+
);
111+
} catch (error) {
112+
if (!(error instanceof BtcAssetsApiError)) {
113+
console.error(error);
114+
}
115+
}
116+
}, 20 * 1000);
117+
};
118+
119+
// Please use your real BTC UTXO information on the BTC Testnet
120+
// BTC Testnet3: https://mempool.space/testnet
121+
// BTC Signet: https://mempool.space/signet
122+
123+
// rgbppLockArgs: outIndexU32 + btcTxId
124+
leapRusdFromBtcToCKB({
125+
rgbppLockArgsList: [buildRgbppLockArgs(2, 'daec93a97c8b7f6fdd33696f814f0292be966dc4ea4853400d3cada816c70f5d')],
126+
toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqfpu7pwavwf3yang8khrsklumayj6nyxhqpmh7fq',
127+
// Please use your own RGB++ compatible xudt asset's type script
128+
compatibleXudtTypeScript: {
129+
codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a',
130+
hashType: 'type',
131+
args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b',
132+
},
133+
transferAmount: BigInt(10_0000),
134+
});
135+
136+
/*
137+
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts
138+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { BtcAssetsApiError, buildBtcTimeCellsSpentTx } from 'rgbpp';
2+
import {
3+
sendCkbTx,
4+
getBtcTimeLockScript,
5+
btcTxIdAndAfterFromBtcTimeLockArgs,
6+
prepareBtcTimeCellSpentUnsignedTx,
7+
addressToScriptHash,
8+
signCkbTransaction,
9+
} from 'rgbpp/ckb';
10+
import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../../env';
11+
import { OfflineBtcAssetsDataSource, SpvProofEntry } from 'rgbpp/service';
12+
13+
const unlockRusdBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => {
14+
const btcTimeCells = await collector.getCells({
15+
lock: {
16+
...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE),
17+
args: btcTimeCellArgs,
18+
},
19+
isDataMustBeEmpty: false,
20+
});
21+
if (!btcTimeCells || btcTimeCells.length === 0) {
22+
throw new Error('No btc time cell found');
23+
}
24+
25+
const spvProofs: SpvProofEntry[] = await Promise.all(
26+
btcTimeCells.map(async (btcTimeCell) => {
27+
const { btcTxId, after } = btcTxIdAndAfterFromBtcTimeLockArgs(btcTimeCell.output.lock.args);
28+
let proof = null;
29+
let attempts = 0;
30+
31+
// eslint-disable-next-line no-constant-condition
32+
while (true) {
33+
try {
34+
console.log(`Attempt ${attempts + 1}: Waiting for SPV proof for txId ${btcTxId}...`);
35+
proof = await btcService.getRgbppSpvProof(btcTxId, after);
36+
if (proof) {
37+
break;
38+
}
39+
} catch (error) {
40+
if (!(error instanceof BtcAssetsApiError)) {
41+
console.error(error);
42+
throw error;
43+
}
44+
console.log('BtcAssetsApiError', error.message);
45+
}
46+
await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
47+
attempts++;
48+
}
49+
50+
return {
51+
txid: btcTxId,
52+
confirmations: after,
53+
proof,
54+
};
55+
}),
56+
);
57+
58+
const offlineBtcAssetsDataSource = new OfflineBtcAssetsDataSource({
59+
txs: [],
60+
utxos: [],
61+
rgbppSpvProofs: spvProofs,
62+
});
63+
64+
const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({
65+
btcTimeCells,
66+
btcAssetsApi: offlineBtcAssetsDataSource,
67+
isMainnet,
68+
btcTestnetType: BTC_TESTNET_TYPE,
69+
});
70+
71+
const { ckbRawTx: unsignedTx, inputCells } = await prepareBtcTimeCellSpentUnsignedTx({
72+
collector,
73+
masterCkbAddress: ckbAddress,
74+
ckbRawTx,
75+
isMainnet,
76+
});
77+
78+
const keyMap = new Map<string, string>();
79+
keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY);
80+
const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true);
81+
82+
const txHash = await sendCkbTx({ collector, signedTx });
83+
console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`);
84+
};
85+
86+
// The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction
87+
unlockRusdBtcTimeCell({
88+
btcTimeCellArgs:
89+
'0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c0600000086c0f54823abebbd966c5110cbdbc72cc6f6b32b81b4254b9f49788a090bcfab',
90+
});
91+
92+
/*
93+
npx tsx examples/rgbpp/xudt/offline/compatible-xudt/4-unlock-btc-time-cell.ts
94+
*/

‎packages/ckb/src/rgbpp/btc-jump-ckb.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
throwErrorWhenRgbppCellsInvalid,
2121
isRgbppCapacitySufficientForChange,
2222
isStandardUDTTypeSupported,
23+
isOfflineMode,
2324
} from '../utils';
2425
import { Hex, IndexerCell } from '../types';
2526
import { RGBPP_WITNESS_PLACEHOLDER, getSecp256k1CellDep } from '../constants';
@@ -52,8 +53,9 @@ export const genBtcJumpCkbVirtualTx = async ({
5253
}: BtcJumpCkbVirtualTxParams): Promise<BtcJumpCkbVirtualTxResult> => {
5354
const isMainnet = toCkbAddress.startsWith('ckb');
5455
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;
56+
const isOffline = isOfflineMode(vendorCellDeps);
5557

56-
if (!isUDTTypeSupported(xudtType, isMainnet)) {
58+
if (!isUDTTypeSupported(xudtType, isMainnet, isOffline)) {
5759
throw new TypeAssetNotSupportedError('The type script asset is not supported now');
5860
}
5961

@@ -65,7 +67,7 @@ export const genBtcJumpCkbVirtualTx = async ({
6567
for await (const rgbppLock of rgbppLocks) {
6668
const cells = await collector.getCells({ lock: rgbppLock, isDataMustBeEmpty: false });
6769

68-
throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet);
70+
throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet, isOffline);
6971

7072
const targetCells = cells!.filter((cell) => isScriptEqual(cell.output.type!, xudtTypeBytes));
7173
const otherTypeCells = cells!.filter((cell) => !isScriptEqual(cell.output.type!, xudtTypeBytes));

‎packages/ckb/src/rgbpp/btc-time.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
isCompatibleUDTTypesSupported,
2828
signCkbTransaction,
2929
addressToScriptHash,
30+
isOfflineMode,
3031
} from '../utils';
3132

3233
export const buildBtcTimeUnlockWitness = (btcTxProof: Hex): Hex => {
@@ -65,7 +66,7 @@ export const buildBtcTimeCellsSpentTx = async ({
6566

6667
const hasStandardUDT = outputs.some((output) => isStandardUDTTypeSupported(output.type!, isMainnet));
6768
const compatibleXudtCodeHashes = outputs
68-
.filter((output) => isCompatibleUDTTypesSupported(output.type!))
69+
.filter((output) => isCompatibleUDTTypesSupported(output.type!, isOfflineMode(vendorCellDeps)))
6970
.map((output) => output.type!.codeHash);
7071
const cellDeps = await fetchTypeIdCellDeps(
7172
isMainnet,

‎packages/ckb/src/rgbpp/btc-transfer.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
isStandardUDTTypeSupported,
2828
signCkbTransaction,
2929
addressToScriptHash,
30+
isOfflineMode,
3031
} from '../utils';
3132
import { Hex, IndexerCell } from '../types';
3233
import {
@@ -65,8 +66,9 @@ export const genBtcTransferCkbVirtualTx = async ({
6566
vendorCellDeps,
6667
}: BtcTransferVirtualTxParams): Promise<BtcTransferVirtualTxResult> => {
6768
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;
69+
const isOffline = isOfflineMode(vendorCellDeps);
6870

69-
if (!isUDTTypeSupported(xudtType, isMainnet)) {
71+
if (!isUDTTypeSupported(xudtType, isMainnet, isOffline)) {
7072
throw new TypeAssetNotSupportedError('The type script asset is not supported now');
7173
}
7274

@@ -78,7 +80,7 @@ export const genBtcTransferCkbVirtualTx = async ({
7880
for await (const rgbppLock of rgbppLocks) {
7981
const cells = await collector.getCells({ lock: rgbppLock, isDataMustBeEmpty: false });
8082

81-
throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet);
83+
throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet, isOffline);
8284

8385
const targetCells = cells!.filter((cell) => isScriptEqual(cell.output.type!, xudtTypeBytes));
8486
const otherTypeCells = cells!.filter((cell) => !isScriptEqual(cell.output.type!, xudtTypeBytes));
@@ -245,7 +247,7 @@ export const genBtcBatchTransferCkbVirtualTx = async ({
245247
}: BtcBatchTransferVirtualTxParams): Promise<BtcBatchTransferVirtualTxResult> => {
246248
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;
247249

248-
if (!isUDTTypeSupported(xudtType, isMainnet)) {
250+
if (!isUDTTypeSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) {
249251
throw new TypeAssetNotSupportedError('The type script asset is not supported now');
250252
}
251253

‎packages/ckb/src/rgbpp/ckb-jump-btc.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
u128ToLe,
1111
genRgbppLockScript,
1212
isStandardUDTTypeSupported,
13+
isOfflineMode,
1314
} from '../utils';
1415
import { MAX_FEE, MIN_CAPACITY, RGBPP_TX_WITNESS_MAX_SIZE } from '../constants';
1516
import { blockchain } from '@ckb-lumos/base';
@@ -39,7 +40,7 @@ export const genCkbJumpBtcVirtualTx = async ({
3940
}: CkbJumpBtcVirtualTxParams): Promise<CKBComponents.RawTransaction> => {
4041
const isMainnet = fromCkbAddress.startsWith('ckb');
4142
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;
42-
if (!isTypeAssetSupported(xudtType, isMainnet)) {
43+
if (!isTypeAssetSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) {
4344
throw new TypeAssetNotSupportedError('The type script asset is not supported now');
4445
}
4546

@@ -156,7 +157,7 @@ export const genCkbBatchJumpBtcVirtualTx = async ({
156157
}: CkbBatchJumpBtcVirtualTxParams): Promise<CKBComponents.RawTransaction> => {
157158
const isMainnet = fromCkbAddress.startsWith('ckb');
158159
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;
159-
if (!isTypeAssetSupported(xudtType, isMainnet)) {
160+
if (!isTypeAssetSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) {
160161
throw new TypeAssetNotSupportedError('The type script asset is not supported now');
161162
}
162163

‎packages/ckb/src/utils/cell-dep.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const GITHUB_STATIC_COMPATIBLE_XUDT_URL =
209209
/**
210210
* The `CompatibleXUDTRegistry` class is responsible for managing a cache of compatible XUDT (eXtensible User-Defined Token) scripts.
211211
* It fetches and caches the compatible tokens from specified URLs and refreshes the cache periodically.
212+
* Alternatively, the compatible tokens can also be fetched from the static list only when offline mode is enabled.
212213
*/
213214
export class CompatibleXUDTRegistry {
214215
private static cache: CKBComponents.Script[] = [];
@@ -217,7 +218,11 @@ export class CompatibleXUDTRegistry {
217218
private static xudtUrl = VERCEL_STATIC_COMPATIBLE_XUDT_URL;
218219

219220
// If you want to get the latest compatible xUDT list, CompatibleXUDTRegistry.refreshCache should be called first
220-
static getCompatibleTokens(): CKBComponents.Script[] {
221+
static getCompatibleTokens(offline?: boolean): CKBComponents.Script[] {
222+
if (offline) {
223+
return COMPATIBLE_XUDT_TYPE_SCRIPTS;
224+
}
225+
221226
const now = Date.now();
222227
if (this.cache.length === 0 || now - this.lastFetchTime > this.CACHE_DURATION) {
223228
this.refreshCache(this.xudtUrl);

‎packages/ckb/src/utils/ckb-tx.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ export const isTokenMetadataType = (type: CKBComponents.Script, isMainnet: boole
6161
* If you want to get the latest compatible xUDT list, CompatibleXUDTRegistry.refreshCache should be called before the isCompatibleUDTTypesSupported
6262
*
6363
* @param type - The UDT type script to check for compatibility.
64+
* @param offline - Whether to use the offline mode.
6465
* @returns A boolean indicating whether the provided UDT type script is supported.
6566
*/
66-
export const isCompatibleUDTTypesSupported = (type: CKBComponents.Script): boolean => {
67-
const compatibleList = CompatibleXUDTRegistry.getCompatibleTokens();
67+
export const isCompatibleUDTTypesSupported = (type: CKBComponents.Script, offline?: boolean): boolean => {
68+
const compatibleList = CompatibleXUDTRegistry.getCompatibleTokens(offline);
6869
const compatibleXudtTypeBytes = compatibleList.map((script) => serializeScript(script));
6970
const typeAsset = serializeScript({
7071
...type,
@@ -82,8 +83,8 @@ export const isStandardUDTTypeSupported = (type: CKBComponents.Script, isMainnet
8283
return xudtType === typeAsset;
8384
};
8485

85-
export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => {
86-
return isStandardUDTTypeSupported(type, isMainnet) || isCompatibleUDTTypesSupported(type);
86+
export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean, offline?: boolean): boolean => {
87+
return isStandardUDTTypeSupported(type, isMainnet) || isCompatibleUDTTypesSupported(type, offline);
8788
};
8889

8990
export const isSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => {
@@ -104,8 +105,8 @@ export const isClusterSporeTypeSupported = (type: CKBComponents.Script, isMainne
104105
return isSporeTypeSupported(type, isMainnet) || clusterType === typeAsset;
105106
};
106107

107-
export const isTypeAssetSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => {
108-
return isUDTTypeSupported(type, isMainnet) || isClusterSporeTypeSupported(type, isMainnet);
108+
export const isTypeAssetSupported = (type: CKBComponents.Script, isMainnet: boolean, offline?: boolean): boolean => {
109+
return isUDTTypeSupported(type, isMainnet, offline) || isClusterSporeTypeSupported(type, isMainnet);
109110
};
110111

111112
const CELL_CAPACITY_SIZE = 8;

‎packages/ckb/src/utils/rgbpp.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
serializeScript,
3232
} from '@nervosnetwork/ckb-sdk-utils';
3333
import { HashType } from '../schemas/customized';
34+
import { CellDepsObject } from './cell-dep';
3435

3536
export const genRgbppLockScript = (rgbppLockArgs: Hex, isMainnet: boolean, btcTestnetType?: BTCTestnetType) => {
3637
return {
@@ -268,6 +269,7 @@ export const throwErrorWhenRgbppCellsInvalid = (
268269
cells: IndexerCell[] | undefined,
269270
xudtTypeBytes: Hex,
270271
isMainnet: boolean,
272+
isOffline?: boolean,
271273
) => {
272274
if (!cells || cells.length === 0) {
273275
throw new NoRgbppLiveCellError('No rgbpp cells found with the rgbpp lock args');
@@ -278,7 +280,7 @@ export const throwErrorWhenRgbppCellsInvalid = (
278280
}
279281

280282
const isUDTTypeNotSupported = typeCells.some(
281-
(cell) => cell.output.type && !isUDTTypeSupported(cell.output.type, isMainnet),
283+
(cell) => cell.output.type && !isUDTTypeSupported(cell.output.type, isMainnet, isOffline),
282284
);
283285
if (isUDTTypeNotSupported) {
284286
throw new RgbppUtxoBindMultiTypeAssetsError(
@@ -302,3 +304,10 @@ export const isRgbppCapacitySufficientForChange = (
302304
const rgbppOccupiedCapacity = calculateRgbppCellCapacity();
303305
return sumUdtInputsCapacity > receiverOutputCapacity + rgbppOccupiedCapacity;
304306
};
307+
308+
/**
309+
* When vendorCellDeps is provided, this indicates offline mode, which means cellDeps and compatible xUDT Type Scripts will not be fetched through network requests
310+
*/
311+
export const isOfflineMode = (vendorCellDeps: CellDepsObject | undefined) => {
312+
return vendorCellDeps === undefined;
313+
};

‎packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class AssetSummarizer {
3535

3636
constructor(public isMainnet: boolean) {}
3737

38-
addGroup(utxo: Utxo, cells: Cell[]): AssetGroupSummary {
38+
addGroup(utxo: Utxo, cells: Cell[], offline: boolean = false): AssetGroupSummary {
3939
const utxoId = encodeUtxoId(utxo.txid, utxo.vout);
4040

4141
const cellIds: string[] = [];
@@ -45,7 +45,7 @@ export class AssetSummarizer {
4545
const cellId = encodeCellId(cell.outPoint!.txHash, cell.outPoint!.index);
4646
cellIds.push(cellId);
4747

48-
const isXudt = !!cell.cellOutput.type && isUDTTypeSupported(cell.cellOutput.type, this.isMainnet);
48+
const isXudt = !!cell.cellOutput.type && isUDTTypeSupported(cell.cellOutput.type, this.isMainnet, offline);
4949
if (isXudt) {
5050
// If the cell type is a supported xUDT type, record its asset information
5151
const xudtTypeArgs = cell.cellOutput.type?.args ?? 'empty';
@@ -77,8 +77,8 @@ export class AssetSummarizer {
7777
return result;
7878
}
7979

80-
addGroups(groups: AssetGroup[]): TransactionGroupSummary {
81-
const groupResults = groups.map((group) => this.addGroup(group.utxo, group.cells));
80+
addGroups(groups: AssetGroup[], offline: boolean = false): TransactionGroupSummary {
81+
const groupResults = groups.map((group) => this.addGroup(group.utxo, group.cells, offline));
8282
return this.summarizeGroups(groupResults);
8383
}
8484

‎packages/rgbpp/src/rgbpp/types/xudt.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { BaseCkbVirtualTxResult, BTCTestnetType, BtcTransferVirtualTxResult, Collector, Hex } from '@rgbpp-sdk/ckb';
1+
import {
2+
BaseCkbVirtualTxResult,
3+
BTCTestnetType,
4+
BtcTransferVirtualTxResult,
5+
Collector,
6+
Hex,
7+
CellDepsObject,
8+
} from '@rgbpp-sdk/ckb';
29
import { AddressToPubkeyMap, DataSource } from '@rgbpp-sdk/btc';
310
import { TransactionGroupSummary } from '../summary/asset-summarizer';
411

@@ -55,6 +62,14 @@ export interface RgbppTransferAllTxsParams {
5562
feeRate?: bigint;
5663
// If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required
5764
compatibleXudtTypeScript?: CKBComponents.Script;
65+
66+
/*
67+
* Vendor cell deps provided by the caller.
68+
* These cell deps belong to scripts that may be upgraded in the future.
69+
* Please ensure the cell dep information is up to date. The latest cell dep information is maintained at:
70+
* https://raw.githubusercontent.com/utxostack/typeid-contract-cell-deps/main/deployment/cell-deps.json.
71+
*/
72+
vendorCellDeps?: CellDepsObject;
5873
};
5974
btc: {
6075
// The list of BTC addresses to provide RGB++ xUDT assets

‎packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
unpackRgbppLockArgs,
88
genBtcTransferCkbVirtualTx,
99
RGBPP_TX_INPUTS_MAX_LENGTH,
10+
isOfflineMode,
1011
} from '@rgbpp-sdk/ckb';
1112
import {
1213
Utxo,
@@ -32,6 +33,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams
3233
// Prepare base props
3334
const maxRgbppCellsPerCkbTx = RGBPP_TX_INPUTS_MAX_LENGTH;
3435
const isMainnet = params.isMainnet;
36+
const isOffline = isOfflineMode(params.ckb.vendorCellDeps);
3537
const btcSource = params.btc.dataSource;
3638
const btcService = btcSource.service;
3739
const ckbCollector = params.ckb.collector;
@@ -97,7 +99,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams
9799
}
98100
const utxo = utxoMap.get(utxoId);
99101
const hasUnsupportedTypeCell = cells.some((cell) => {
100-
return cell.cellOutput.type && !isUDTTypeSupported(cell.cellOutput.type, isMainnet);
102+
return cell.cellOutput.type && !isUDTTypeSupported(cell.cellOutput.type, isMainnet, isOffline);
101103
});
102104
if (!utxo || !cells || cells.length > maxRgbppCellsPerCkbTx || hasUnsupportedTypeCell) {
103105
invalidUtxoIds.add(utxoId);
@@ -134,6 +136,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams
134136
utxo: utxoMap.get(group.id)!,
135137
cells: cellsMap.get(group.id)!,
136138
})),
139+
isOffline,
137140
);
138141

139142
// Props for constructing CKB_VTX

1 commit comments

Comments
 (1)

github-actions[bot] commented on Feb 17, 2025

@github-actions[bot]

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

Name Version
@rgbpp-sdk/btc 0.0.0-snap-20250217084517
@rgbpp-sdk/ckb 0.0.0-snap-20250217084517
rgbpp 0.0.0-snap-20250217084517
@rgbpp-sdk/service 0.0.0-snap-20250217084517
Please sign in to comment.