diff --git a/.changeset/lazy-shrimps-roll.md b/.changeset/lazy-shrimps-roll.md new file mode 100644 index 00000000..0800e8dd --- /dev/null +++ b/.changeset/lazy-shrimps-roll.md @@ -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 diff --git a/examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts b/examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts new file mode 100644 index 00000000..3908f40b --- /dev/null +++ b/examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts @@ -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 +*/ diff --git a/examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts b/examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts new file mode 100644 index 00000000..b83c13b8 --- /dev/null +++ b/examples/rgbpp/xudt/offline/compatible-xudt/2-btc-transfer.ts @@ -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(); + 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 +*/ diff --git a/examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts b/examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts new file mode 100644 index 00000000..e695e2d4 --- /dev/null +++ b/examples/rgbpp/xudt/offline/compatible-xudt/3-btc-leap-ckb.ts @@ -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(); + 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 +*/ diff --git a/examples/rgbpp/xudt/offline/compatible-xudt/4-unlock-btc-time-cell.ts b/examples/rgbpp/xudt/offline/compatible-xudt/4-unlock-btc-time-cell.ts new file mode 100644 index 00000000..4ce313d5 --- /dev/null +++ b/examples/rgbpp/xudt/offline/compatible-xudt/4-unlock-btc-time-cell.ts @@ -0,0 +1,94 @@ +import { BtcAssetsApiError, buildBtcTimeCellsSpentTx } from 'rgbpp'; +import { + sendCkbTx, + getBtcTimeLockScript, + btcTxIdAndAfterFromBtcTimeLockArgs, + prepareBtcTimeCellSpentUnsignedTx, + addressToScriptHash, + signCkbTransaction, +} from 'rgbpp/ckb'; +import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../../env'; +import { OfflineBtcAssetsDataSource, SpvProofEntry } from 'rgbpp/service'; + +const unlockRusdBtcTimeCell = 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[] = await Promise.all( + btcTimeCells.map(async (btcTimeCell) => { + const { btcTxId, after } = btcTxIdAndAfterFromBtcTimeLockArgs(btcTimeCell.output.lock.args); + let proof = null; + let attempts = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + console.log(`Attempt ${attempts + 1}: Waiting for SPV proof for txId ${btcTxId}...`); + proof = await btcService.getRgbppSpvProof(btcTxId, after); + if (proof) { + break; + } + } catch (error) { + if (!(error instanceof BtcAssetsApiError)) { + console.error(error); + throw error; + } + console.log('BtcAssetsApiError', error.message); + } + await new Promise((resolve) => setTimeout(resolve, 10 * 1000)); + attempts++; + } + + return { + txid: btcTxId, + confirmations: after, + proof, + }; + }), + ); + + const offlineBtcAssetsDataSource = new OfflineBtcAssetsDataSource({ + txs: [], + utxos: [], + rgbppSpvProofs: spvProofs, + }); + + const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ + btcTimeCells, + btcAssetsApi: offlineBtcAssetsDataSource, + isMainnet, + btcTestnetType: BTC_TESTNET_TYPE, + }); + + const { ckbRawTx: unsignedTx, inputCells } = await prepareBtcTimeCellSpentUnsignedTx({ + collector, + masterCkbAddress: ckbAddress, + ckbRawTx, + isMainnet, + }); + + const keyMap = new Map(); + keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY); + const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true); + + 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 +unlockRusdBtcTimeCell({ + btcTimeCellArgs: + '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c0600000086c0f54823abebbd966c5110cbdbc72cc6f6b32b81b4254b9f49788a090bcfab', +}); + +/* +npx tsx examples/rgbpp/xudt/offline/compatible-xudt/4-unlock-btc-time-cell.ts +*/ diff --git a/packages/ckb/src/rgbpp/btc-jump-ckb.ts b/packages/ckb/src/rgbpp/btc-jump-ckb.ts index 1588b6ac..52ca2885 100644 --- a/packages/ckb/src/rgbpp/btc-jump-ckb.ts +++ b/packages/ckb/src/rgbpp/btc-jump-ckb.ts @@ -20,6 +20,7 @@ import { throwErrorWhenRgbppCellsInvalid, isRgbppCapacitySufficientForChange, isStandardUDTTypeSupported, + isOfflineMode, } from '../utils'; import { Hex, IndexerCell } from '../types'; import { RGBPP_WITNESS_PLACEHOLDER, getSecp256k1CellDep } from '../constants'; @@ -52,8 +53,9 @@ export const genBtcJumpCkbVirtualTx = async ({ }: BtcJumpCkbVirtualTxParams): Promise => { const isMainnet = toCkbAddress.startsWith('ckb'); const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script; + const isOffline = isOfflineMode(vendorCellDeps); - if (!isUDTTypeSupported(xudtType, isMainnet)) { + if (!isUDTTypeSupported(xudtType, isMainnet, isOffline)) { throw new TypeAssetNotSupportedError('The type script asset is not supported now'); } @@ -65,7 +67,7 @@ export const genBtcJumpCkbVirtualTx = async ({ for await (const rgbppLock of rgbppLocks) { const cells = await collector.getCells({ lock: rgbppLock, isDataMustBeEmpty: false }); - throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet); + throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet, isOffline); const targetCells = cells!.filter((cell) => isScriptEqual(cell.output.type!, xudtTypeBytes)); const otherTypeCells = cells!.filter((cell) => !isScriptEqual(cell.output.type!, xudtTypeBytes)); diff --git a/packages/ckb/src/rgbpp/btc-time.ts b/packages/ckb/src/rgbpp/btc-time.ts index 58a89676..e16df16e 100644 --- a/packages/ckb/src/rgbpp/btc-time.ts +++ b/packages/ckb/src/rgbpp/btc-time.ts @@ -27,6 +27,7 @@ import { isCompatibleUDTTypesSupported, signCkbTransaction, addressToScriptHash, + isOfflineMode, } from '../utils'; export const buildBtcTimeUnlockWitness = (btcTxProof: Hex): Hex => { @@ -65,7 +66,7 @@ export const buildBtcTimeCellsSpentTx = async ({ const hasStandardUDT = outputs.some((output) => isStandardUDTTypeSupported(output.type!, isMainnet)); const compatibleXudtCodeHashes = outputs - .filter((output) => isCompatibleUDTTypesSupported(output.type!)) + .filter((output) => isCompatibleUDTTypesSupported(output.type!, isOfflineMode(vendorCellDeps))) .map((output) => output.type!.codeHash); const cellDeps = await fetchTypeIdCellDeps( isMainnet, diff --git a/packages/ckb/src/rgbpp/btc-transfer.ts b/packages/ckb/src/rgbpp/btc-transfer.ts index 78a081ef..84e3d6a9 100644 --- a/packages/ckb/src/rgbpp/btc-transfer.ts +++ b/packages/ckb/src/rgbpp/btc-transfer.ts @@ -27,6 +27,7 @@ import { isStandardUDTTypeSupported, signCkbTransaction, addressToScriptHash, + isOfflineMode, } from '../utils'; import { Hex, IndexerCell } from '../types'; import { @@ -65,8 +66,9 @@ export const genBtcTransferCkbVirtualTx = async ({ vendorCellDeps, }: BtcTransferVirtualTxParams): Promise => { const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script; + const isOffline = isOfflineMode(vendorCellDeps); - if (!isUDTTypeSupported(xudtType, isMainnet)) { + if (!isUDTTypeSupported(xudtType, isMainnet, isOffline)) { throw new TypeAssetNotSupportedError('The type script asset is not supported now'); } @@ -78,7 +80,7 @@ export const genBtcTransferCkbVirtualTx = async ({ for await (const rgbppLock of rgbppLocks) { const cells = await collector.getCells({ lock: rgbppLock, isDataMustBeEmpty: false }); - throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet); + throwErrorWhenRgbppCellsInvalid(cells, xudtTypeBytes, isMainnet, isOffline); const targetCells = cells!.filter((cell) => isScriptEqual(cell.output.type!, xudtTypeBytes)); const otherTypeCells = cells!.filter((cell) => !isScriptEqual(cell.output.type!, xudtTypeBytes)); @@ -245,7 +247,7 @@ export const genBtcBatchTransferCkbVirtualTx = async ({ }: BtcBatchTransferVirtualTxParams): Promise => { const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script; - if (!isUDTTypeSupported(xudtType, isMainnet)) { + if (!isUDTTypeSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) { throw new TypeAssetNotSupportedError('The type script asset is not supported now'); } diff --git a/packages/ckb/src/rgbpp/ckb-jump-btc.ts b/packages/ckb/src/rgbpp/ckb-jump-btc.ts index 1ed80209..dd3f103b 100644 --- a/packages/ckb/src/rgbpp/ckb-jump-btc.ts +++ b/packages/ckb/src/rgbpp/ckb-jump-btc.ts @@ -10,6 +10,7 @@ import { u128ToLe, genRgbppLockScript, isStandardUDTTypeSupported, + isOfflineMode, } from '../utils'; import { MAX_FEE, MIN_CAPACITY, RGBPP_TX_WITNESS_MAX_SIZE } from '../constants'; import { blockchain } from '@ckb-lumos/base'; @@ -39,7 +40,7 @@ export const genCkbJumpBtcVirtualTx = async ({ }: CkbJumpBtcVirtualTxParams): Promise => { const isMainnet = fromCkbAddress.startsWith('ckb'); const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script; - if (!isTypeAssetSupported(xudtType, isMainnet)) { + if (!isTypeAssetSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) { throw new TypeAssetNotSupportedError('The type script asset is not supported now'); } @@ -156,7 +157,7 @@ export const genCkbBatchJumpBtcVirtualTx = async ({ }: CkbBatchJumpBtcVirtualTxParams): Promise => { const isMainnet = fromCkbAddress.startsWith('ckb'); const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script; - if (!isTypeAssetSupported(xudtType, isMainnet)) { + if (!isTypeAssetSupported(xudtType, isMainnet, isOfflineMode(vendorCellDeps))) { throw new TypeAssetNotSupportedError('The type script asset is not supported now'); } diff --git a/packages/ckb/src/utils/cell-dep.ts b/packages/ckb/src/utils/cell-dep.ts index 6163d609..fad89082 100644 --- a/packages/ckb/src/utils/cell-dep.ts +++ b/packages/ckb/src/utils/cell-dep.ts @@ -210,6 +210,7 @@ const GITHUB_STATIC_COMPATIBLE_XUDT_URL = /** * The `CompatibleXUDTRegistry` class is responsible for managing a cache of compatible XUDT (eXtensible User-Defined Token) scripts. * It fetches and caches the compatible tokens from specified URLs and refreshes the cache periodically. + * Alternatively, the compatible tokens can also be fetched from the static list only when offline mode is enabled. */ export class CompatibleXUDTRegistry { private static cache: CKBComponents.Script[] = []; @@ -218,7 +219,11 @@ export class CompatibleXUDTRegistry { private static xudtUrl = VERCEL_STATIC_COMPATIBLE_XUDT_URL; // If you want to get the latest compatible xUDT list, CompatibleXUDTRegistry.refreshCache should be called first - static getCompatibleTokens(): CKBComponents.Script[] { + static getCompatibleTokens(offline?: boolean): CKBComponents.Script[] { + if (offline) { + return COMPATIBLE_XUDT_TYPE_SCRIPTS; + } + const now = Date.now(); if (this.cache.length === 0 || now - this.lastFetchTime > this.CACHE_DURATION) { this.refreshCache(this.xudtUrl); diff --git a/packages/ckb/src/utils/ckb-tx.ts b/packages/ckb/src/utils/ckb-tx.ts index d205751b..43ae2a38 100644 --- a/packages/ckb/src/utils/ckb-tx.ts +++ b/packages/ckb/src/utils/ckb-tx.ts @@ -61,10 +61,11 @@ export const isTokenMetadataType = (type: CKBComponents.Script, isMainnet: boole * If you want to get the latest compatible xUDT list, CompatibleXUDTRegistry.refreshCache should be called before the isCompatibleUDTTypesSupported * * @param type - The UDT type script to check for compatibility. + * @param offline - Whether to use the offline mode. * @returns A boolean indicating whether the provided UDT type script is supported. */ -export const isCompatibleUDTTypesSupported = (type: CKBComponents.Script): boolean => { - const compatibleList = CompatibleXUDTRegistry.getCompatibleTokens(); +export const isCompatibleUDTTypesSupported = (type: CKBComponents.Script, offline?: boolean): boolean => { + const compatibleList = CompatibleXUDTRegistry.getCompatibleTokens(offline); const compatibleXudtTypeBytes = compatibleList.map((script) => serializeScript(script)); const typeAsset = serializeScript({ ...type, @@ -82,8 +83,8 @@ export const isStandardUDTTypeSupported = (type: CKBComponents.Script, isMainnet return xudtType === typeAsset; }; -export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { - return isStandardUDTTypeSupported(type, isMainnet) || isCompatibleUDTTypesSupported(type); +export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean, offline?: boolean): boolean => { + return isStandardUDTTypeSupported(type, isMainnet) || isCompatibleUDTTypesSupported(type, offline); }; export const isSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { @@ -104,8 +105,8 @@ export const isClusterSporeTypeSupported = (type: CKBComponents.Script, isMainne return isSporeTypeSupported(type, isMainnet) || clusterType === typeAsset; }; -export const isTypeAssetSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { - return isUDTTypeSupported(type, isMainnet) || isClusterSporeTypeSupported(type, isMainnet); +export const isTypeAssetSupported = (type: CKBComponents.Script, isMainnet: boolean, offline?: boolean): boolean => { + return isUDTTypeSupported(type, isMainnet, offline) || isClusterSporeTypeSupported(type, isMainnet); }; const CELL_CAPACITY_SIZE = 8; diff --git a/packages/ckb/src/utils/rgbpp.ts b/packages/ckb/src/utils/rgbpp.ts index 8cf2496b..7a96a4a9 100644 --- a/packages/ckb/src/utils/rgbpp.ts +++ b/packages/ckb/src/utils/rgbpp.ts @@ -31,6 +31,7 @@ import { serializeScript, } from '@nervosnetwork/ckb-sdk-utils'; import { HashType } from '../schemas/customized'; +import { CellDepsObject } from './cell-dep'; export const genRgbppLockScript = (rgbppLockArgs: Hex, isMainnet: boolean, btcTestnetType?: BTCTestnetType) => { return { @@ -268,6 +269,7 @@ export const throwErrorWhenRgbppCellsInvalid = ( cells: IndexerCell[] | undefined, xudtTypeBytes: Hex, isMainnet: boolean, + isOffline?: boolean, ) => { if (!cells || cells.length === 0) { throw new NoRgbppLiveCellError('No rgbpp cells found with the rgbpp lock args'); @@ -278,7 +280,7 @@ export const throwErrorWhenRgbppCellsInvalid = ( } const isUDTTypeNotSupported = typeCells.some( - (cell) => cell.output.type && !isUDTTypeSupported(cell.output.type, isMainnet), + (cell) => cell.output.type && !isUDTTypeSupported(cell.output.type, isMainnet, isOffline), ); if (isUDTTypeNotSupported) { throw new RgbppUtxoBindMultiTypeAssetsError( @@ -302,3 +304,10 @@ export const isRgbppCapacitySufficientForChange = ( const rgbppOccupiedCapacity = calculateRgbppCellCapacity(); return sumUdtInputsCapacity > receiverOutputCapacity + rgbppOccupiedCapacity; }; + +/** + * When vendorCellDeps is provided, this indicates offline mode, which means cellDeps and compatible xUDT Type Scripts will not be fetched through network requests + */ +export const isOfflineMode = (vendorCellDeps: CellDepsObject | undefined) => { + return vendorCellDeps === undefined; +}; diff --git a/packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts b/packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts index 83a0ed72..691df8b2 100644 --- a/packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts +++ b/packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts @@ -35,7 +35,7 @@ export class AssetSummarizer { constructor(public isMainnet: boolean) {} - addGroup(utxo: Utxo, cells: Cell[]): AssetGroupSummary { + addGroup(utxo: Utxo, cells: Cell[], offline: boolean = false): AssetGroupSummary { const utxoId = encodeUtxoId(utxo.txid, utxo.vout); const cellIds: string[] = []; @@ -45,7 +45,7 @@ export class AssetSummarizer { const cellId = encodeCellId(cell.outPoint!.txHash, cell.outPoint!.index); cellIds.push(cellId); - const isXudt = !!cell.cellOutput.type && isUDTTypeSupported(cell.cellOutput.type, this.isMainnet); + const isXudt = !!cell.cellOutput.type && isUDTTypeSupported(cell.cellOutput.type, this.isMainnet, offline); if (isXudt) { // If the cell type is a supported xUDT type, record its asset information const xudtTypeArgs = cell.cellOutput.type?.args ?? 'empty'; @@ -77,8 +77,8 @@ export class AssetSummarizer { return result; } - addGroups(groups: AssetGroup[]): TransactionGroupSummary { - const groupResults = groups.map((group) => this.addGroup(group.utxo, group.cells)); + addGroups(groups: AssetGroup[], offline: boolean = false): TransactionGroupSummary { + const groupResults = groups.map((group) => this.addGroup(group.utxo, group.cells, offline)); return this.summarizeGroups(groupResults); } diff --git a/packages/rgbpp/src/rgbpp/types/xudt.ts b/packages/rgbpp/src/rgbpp/types/xudt.ts index d02fd10e..c359a85a 100644 --- a/packages/rgbpp/src/rgbpp/types/xudt.ts +++ b/packages/rgbpp/src/rgbpp/types/xudt.ts @@ -1,4 +1,11 @@ -import { BaseCkbVirtualTxResult, BTCTestnetType, BtcTransferVirtualTxResult, Collector, Hex } from '@rgbpp-sdk/ckb'; +import { + BaseCkbVirtualTxResult, + BTCTestnetType, + BtcTransferVirtualTxResult, + Collector, + Hex, + CellDepsObject, +} from '@rgbpp-sdk/ckb'; import { AddressToPubkeyMap, DataSource } from '@rgbpp-sdk/btc'; import { TransactionGroupSummary } from '../summary/asset-summarizer'; @@ -55,6 +62,14 @@ export interface RgbppTransferAllTxsParams { feeRate?: bigint; // If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required compatibleXudtTypeScript?: CKBComponents.Script; + + /* + * Vendor cell deps provided by the caller. + * These cell deps belong to scripts that may be upgraded in the future. + * Please ensure the cell dep information is up to date. The latest cell dep information is maintained at: + * https://raw.githubusercontent.com/utxostack/typeid-contract-cell-deps/main/deployment/cell-deps.json. + */ + vendorCellDeps?: CellDepsObject; }; btc: { // The list of BTC addresses to provide RGB++ xUDT assets diff --git a/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts b/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts index 1af0e8aa..24c7582c 100644 --- a/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts +++ b/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts @@ -7,6 +7,7 @@ import { unpackRgbppLockArgs, genBtcTransferCkbVirtualTx, RGBPP_TX_INPUTS_MAX_LENGTH, + isOfflineMode, } from '@rgbpp-sdk/ckb'; import { Utxo, @@ -32,6 +33,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams // Prepare base props const maxRgbppCellsPerCkbTx = RGBPP_TX_INPUTS_MAX_LENGTH; const isMainnet = params.isMainnet; + const isOffline = isOfflineMode(params.ckb.vendorCellDeps); const btcSource = params.btc.dataSource; const btcService = btcSource.service; const ckbCollector = params.ckb.collector; @@ -97,7 +99,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams } const utxo = utxoMap.get(utxoId); const hasUnsupportedTypeCell = cells.some((cell) => { - return cell.cellOutput.type && !isUDTTypeSupported(cell.cellOutput.type, isMainnet); + return cell.cellOutput.type && !isUDTTypeSupported(cell.cellOutput.type, isMainnet, isOffline); }); if (!utxo || !cells || cells.length > maxRgbppCellsPerCkbTx || hasUnsupportedTypeCell) { invalidUtxoIds.add(utxoId); @@ -134,6 +136,7 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams utxo: utxoMap.get(group.id)!, cells: cellsMap.get(group.id)!, })), + isOffline, ); // Props for constructing CKB_VTX