diff --git a/.changeset/gold-rules-fix.md b/.changeset/gold-rules-fix.md new file mode 100644 index 00000000..7646c2a2 --- /dev/null +++ b/.changeset/gold-rules-fix.md @@ -0,0 +1,14 @@ +--- +'@rgbpp-sdk/service': minor +'rgbpp': minor +'@rgbpp-sdk/ckb': minor +--- + +Support compatible xUDT RGB++ assets + + - Fetch compatible xUDT `cellDeps` to build CKB transactions from the `typeid-contract-cell-deps` GitHub repository + - Update the `ckb` package to support RGB++ compatible xUDT assets leaping and transferring + - Add optional parameter `compatibleXudtTypeScript` to the functions of the `rgbpp` package to transfer RGB++ compatible xUDT assets + - Add RGB++ compatible xUDT assets leaping and transferring examples + - Add RGB++ compatible xUDT assets integration tests + - Add `assets/type` API to the service package diff --git a/.changeset/odd-cheetahs-shake.md b/.changeset/odd-cheetahs-shake.md deleted file mode 100644 index 15834e16..00000000 --- a/.changeset/odd-cheetahs-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rgbpp-sdk/service": minor ---- - -Add support of /rgbpp/v1/address/{btc_address}/activity API for querying RGBPP asset activities by an BTC address diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index efc0156b..bbac17a1 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - env_set: [ xudt, spore ] + env_set: [ xudt, spore, compatible-xudt ] steps: - name: Checkout rgbpp-sdk @@ -49,9 +49,9 @@ jobs: if: ${{ matrix.env_set == 'xudt' }} run: pnpm run integration:xudt env: - VITE_SERVICE_URL: https://api.signet.rgbpp.io - VITE_SERVICE_TOKEN: ${{ secrets.SIGNET_SERVICE_TOKEN }} - VITE_SERVICE_ORIGIN: https://api.signet.rgbpp.io + VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro + VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} + VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_PRIVATE_KEY }} INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_PRIVATE_KEY }} @@ -60,8 +60,19 @@ jobs: if: ${{ matrix.env_set == 'spore' }} run: pnpm run integration:spore env: - VITE_SERVICE_URL: https://api.signet.rgbpp.io - VITE_SERVICE_TOKEN: ${{ secrets.SIGNET_SERVICE_TOKEN }} - VITE_SERVICE_ORIGIN: https://api.signet.rgbpp.io + VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro + VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} + VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_SPORE_PRIVATE_KEY }} INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_SPORE_PRIVATE_KEY }} + + - name: Run integration:compatible-xudt script + working-directory: ./tests/rgbpp + if: ${{ matrix.env_set == 'compatible-xudt' }} + run: pnpm run integration:compatible-xudt + env: + VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro + VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} + VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro + INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_compatible_xudt_PRIVATE_KEY }} + INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_compatible_xudt_PRIVATE_KEY }} diff --git a/examples/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts b/examples/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts new file mode 100644 index 00000000..218813b2 --- /dev/null +++ b/examples/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts @@ -0,0 +1,56 @@ +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genCkbJumpBtcVirtualTx } from 'rgbpp'; +import { getSecp256k1CellDep, buildRgbppLockArgs } from 'rgbpp/ckb'; +import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } 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 ckbRawTx = await genCkbJumpBtcVirtualTx({ + collector, + fromCkbAddress: ckbAddress, + toRgbppLockArgs, + xudtTypeBytes: serializeScript(compatibleXudtTypeScript), + transferAmount, + btcTestnetType: BTC_TESTNET_TYPE, + }); + + const emptyWitness = { lock: '', inputType: '', outputType: '' }; + const unsignedTx: CKBComponents.RawTransactionToSign = { + ...ckbRawTx, + cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], + witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], + }; + + const signedTx = collector.getCkb().signTransaction(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: 4, + btcTxId: '44de1b4e3ddaa95cc85cc8b1c60f3e439d343002f0c60980fb4c70841ee0c75e', + // Please use your own RGB++ compatible xUDT asset's type script + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(1000_0000), +}); diff --git a/examples/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts b/examples/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts new file mode 100644 index 00000000..8574abbf --- /dev/null +++ b/examples/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts @@ -0,0 +1,85 @@ +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { buildRgbppTransferTx } from 'rgbpp'; +import { isMainnet, collector, btcService, btcAccount, btcDataSource, BTC_TESTNET_TYPE } from '../../env'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; +import { signAndSendPsbt } from '../../shared/btc-account'; +import { bitcoin } from 'rgbpp/btc'; + +interface RgbppTransferParams { + rgbppLockArgsList: string[]; + toBtcAddress: string; + transferAmount: bigint; + compatibleXudtTypeScript: CKBComponents.Script; +} + +const transferRusdOnBtc = async ({ + rgbppLockArgsList, + toBtcAddress, + compatibleXudtTypeScript, + transferAmount, +}: RgbppTransferParams) => { + const { ckbVirtualTxResult, btcPsbtHex } = await buildRgbppTransferTx({ + ckb: { + collector, + xudtTypeArgs: compatibleXudtTypeScript.args, + rgbppLockArgsList, + transferAmount, + compatibleXudtTypeScript, + }, + btc: { + fromAddress: btcAccount.from, + toAddress: toBtcAddress, + fromPubkey: btcAccount.fromPubkey, + dataSource: btcDataSource, + testnetType: BTC_TESTNET_TYPE, + }, + isMainnet, + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-btc-transfer'); + + // Send BTC tx + const psbt = bitcoin.Psbt.fromHex(btcPsbtHex); + const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); + console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); + + await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); + + try { + const interval = setInterval(async () => { + const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); + console.log('state', state); + if (state === 'completed' || state === 'failed') { + clearInterval(interval); + if (state === 'completed') { + const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); + console.info( + `Rgbpp compatible xUDT asset has been transferred on BTC and the related CKB tx hash is ${txHash}`, + ); + } else { + console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); + } + } + }, 30 * 1000); + } catch (error) { + console.error(error); + } +}; + +// 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(4, '44de1b4e3ddaa95cc85cc8b1c60f3e439d343002f0c60980fb4c70841ee0c75e')], + toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', + // Please use your own RGB++ compatible xudt asset's type script + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(100_0000), +}); diff --git a/examples/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts b/examples/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts new file mode 100644 index 00000000..fec1724c --- /dev/null +++ b/examples/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts @@ -0,0 +1,89 @@ +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcJumpCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { isMainnet, collector, btcService, btcDataSource, btcAccount, BTC_TESTNET_TYPE } 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 ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(compatibleXudtTypeScript), + transferAmount, + toCkbAddress, + isMainnet, + btcTestnetType: BTC_TESTNET_TYPE, + // btcConfirmationBlocks: 20, // default value is 6 + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-btc-leap-ckb'); + + const { commitment, ckbRawTx } = ckbVirtualTxResult; + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [btcAccount.from], + ckbCollector: collector, + from: btcAccount.from, + fromPubkey: btcAccount.fromPubkey, + source: btcDataSource, + }); + + const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); + console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); + + await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); + + try { + const interval = setInterval(async () => { + const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); + console.log('state', state); + if (state === 'completed' || state === 'failed') { + clearInterval(interval); + if (state === 'completed') { + const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); + console.info( + `Rgbpp compatible xUDT asset has been leaped from BTC to CKB and the related CKB tx hash is ${txHash}`, + ); + } else { + console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); + } + } + }, 30 * 1000); + } catch (error) { + console.error(error); + } +}; + +// 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(1, '58ebbdec0dfd464280658e36fadc11c41945de2c4b5b59463dad6e045a7e5faf')], + toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq0e4xk4rmg5jdkn8aams492a7jlg73ue0gc0ddfj', + // Please use your own RGB++ compatible xudt asset's type script + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(100_0000), +}); diff --git a/examples/rgbpp/xudt/compatible-xudt/4-unlock-btc-time-cell.ts b/examples/rgbpp/xudt/compatible-xudt/4-unlock-btc-time-cell.ts new file mode 100644 index 00000000..8cb004d8 --- /dev/null +++ b/examples/rgbpp/xudt/compatible-xudt/4-unlock-btc-time-cell.ts @@ -0,0 +1,42 @@ +import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; +import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; +import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../env'; + +// Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 3-btc-leap-ckb.ts +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 ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ + btcTimeCells, + btcAssetsApi: btcService, + 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 +unlockRusdBtcTimeCell({ + btcTimeCellArgs: + '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000f9a9ad51ed14936d33f7bb854aaefa5f47a3ccbd0600000038036f35121682517b5f79732fc6a182e0050cfe1ad4cce0a1314c229a1ba364', +}); diff --git a/examples/rgbpp/xudt/compatible-xudt/assets-api.ts b/examples/rgbpp/xudt/compatible-xudt/assets-api.ts new file mode 100644 index 00000000..70405df4 --- /dev/null +++ b/examples/rgbpp/xudt/compatible-xudt/assets-api.ts @@ -0,0 +1,31 @@ +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { btcService } from '../../env'; + +(async () => { + const assets = await btcService.getRgbppAssetsByBtcAddress('tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', { + type_script: serializeScript({ + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }), + }); + console.log('RUSD Assets: ', JSON.stringify(assets)); + + const activities = await btcService.getRgbppActivityByBtcAddress('tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', { + type_script: serializeScript({ + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }), + }); + console.log('RUSD Activities: ', JSON.stringify(activities)); + + const info = await btcService.getRgbppAssetInfoByTypeScript( + serializeScript({ + codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', + hashType: 'type', + args: '0x661cfbe2124b3e79e50e505c406be5b2dcf9da15d8654b749ec536fa4c2eaaae', + }), + ); + console.log('Standard xUDT info: ', JSON.stringify(info)); +})(); diff --git a/packages/ckb/src/collector/collector.spec.ts b/packages/ckb/src/collector/collector.spec.ts index b71d3e66..3297654b 100644 --- a/packages/ckb/src/collector/collector.spec.ts +++ b/packages/ckb/src/collector/collector.spec.ts @@ -9,22 +9,20 @@ describe('collector', () => { it('getLiveCell', async () => { const cell = await collector.getLiveCell({ - txHash: '0xfa87db3187be8cf850117ccfcdfe2525c44f1a58f71d9714dd2ce489e9723182', + txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', index: '0x0', }); - expect(cell.output.lock.args).toBe('0x0450340178ae277261a838c89f9ccb76a190ed4b'); + expect(cell.output.lock.codeHash).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); }); it('getLiveCells', async () => { - const [cell1, cell2, cell3] = await collector.getLiveCells([ - // Cellbase - { txHash: '0xfa87db3187be8cf850117ccfcdfe2525c44f1a58f71d9714dd2ce489e9723182', index: '0x0' }, - { txHash: '0xed5adcba9bbbfe76c546264f2b8a33cbf9c95d09a88550bb0a4f98d6f36a6ed2', index: '0x0' }, + const [cell1, cell2] = await collector.getLiveCells([ + // Genesis block + { txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', index: '0x0' }, // Nervos DAO { txHash: '0x8277d74d33850581f8d843613ded0c2a1722dec0e87e748f45c115dfb14210f1', index: '0x0' }, ]); - expect(cell1.output.lock.args).toBe('0x0450340178ae277261a838c89f9ccb76a190ed4b'); - expect(cell2.output.lock.args).toBe('0xf1cbacc833b5c62f79ac8de6aa7ffbe464cae563'); - expect(cell3.output.type?.codeHash).toBe('0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e'); + expect(cell1.output.lock.codeHash).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); + expect(cell2.output.type?.codeHash).toBe('0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e'); }); }); diff --git a/packages/ckb/src/constants/index.ts b/packages/ckb/src/constants/index.ts index 8d3e532c..f5117158 100644 --- a/packages/ckb/src/constants/index.ts +++ b/packages/ckb/src/constants/index.ts @@ -66,6 +66,15 @@ const TestnetInfo = { depType: 'code', } as CKBComponents.CellDep, + CompatibleXUDTTypeScripts: [ + // RUSD + { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '', + }, + ] as CKBComponents.Script[], + UniqueTypeScript: { codeHash: '0x8e341bcfec6393dcd41e635733ff2dca00a6af546949f70c57a706c0f344df8b', hashType: 'type', @@ -197,6 +206,15 @@ const MainnetInfo = { depType: 'code', } as CKBComponents.CellDep, + CompatibleXUDTTypeScripts: [ + // RUSD + { + codeHash: '0x26a33e0815888a4a0614a0b7d09fa951e0993ff21e55905510104a0b1312032b', + hashType: 'type', + args: '', + }, + ] as CKBComponents.Script[], + UniqueTypeScript: { codeHash: '0x2c8c11c985da60b0a330c61a85507416d6382c130ba67f0c47ab071e00aec628', hashType: 'data1', @@ -308,3 +326,6 @@ export const getSporeTypeScript = (isMainnet: boolean) => isMainnet ? MainnetInfo.SporeTypeScript : TestnetInfo.SporeTypeScript; export const getSporeTypeDep = (isMainnet: boolean) => isMainnet ? MainnetInfo.SporeTypeDep : TestnetInfo.SporeTypeDep; + +export const getCompatibleXudtTypeScripts = (isMainnet: boolean) => + isMainnet ? MainnetInfo.CompatibleXUDTTypeScripts : TestnetInfo.CompatibleXUDTTypeScripts; diff --git a/packages/ckb/src/rgbpp/btc-jump-ckb.ts b/packages/ckb/src/rgbpp/btc-jump-ckb.ts index 31c5b3e8..b275929f 100644 --- a/packages/ckb/src/rgbpp/btc-jump-ckb.ts +++ b/packages/ckb/src/rgbpp/btc-jump-ckb.ts @@ -19,6 +19,7 @@ import { genRgbppLockScript, throwErrorWhenRgbppCellsInvalid, isRgbppCapacitySufficientForChange, + isStandardUDTTypeSupported, } from '../utils'; import { Hex, IndexerCell } from '../types'; import { RGBPP_WITNESS_PLACEHOLDER, getSecp256k1CellDep } from '../constants'; @@ -135,7 +136,16 @@ export const genBtcJumpCkbVirtualTx = async ({ outputsData.push(otherRgbppCell.outputData); } - const cellDeps = await fetchTypeIdCellDeps(isMainnet, { rgbpp: true, xudt: true }, btcTestnetType); + const isStandardUDT = isStandardUDTTypeSupported(xudtType, isMainnet); + const cellDeps = await fetchTypeIdCellDeps( + isMainnet, + { + rgbpp: true, + xudt: isStandardUDT, + compatibleXudtCodeHashes: isStandardUDT ? [] : [xudtType.codeHash], + }, + btcTestnetType, + ); if (needPaymasterCell) { cellDeps.push(getSecp256k1CellDep(isMainnet)); } diff --git a/packages/ckb/src/rgbpp/btc-time.ts b/packages/ckb/src/rgbpp/btc-time.ts index cfed7b6e..4c70954e 100644 --- a/packages/ckb/src/rgbpp/btc-time.ts +++ b/packages/ckb/src/rgbpp/btc-time.ts @@ -25,6 +25,8 @@ import { lockScriptFromBtcTimeLockArgs, transformSpvProof, buildSpvClientCellDep, + isStandardUDTTypeSupported, + isCompatibleUDTTypesSupported, } from '../utils'; import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; @@ -61,9 +63,17 @@ export const buildBtcTimeCellsSpentTx = async ({ const outputsData = sortedBtcTimeCells.map((cell) => cell.outputData); - const cellDeps: CKBComponents.CellDep[] = await fetchTypeIdCellDeps( + const hasStandardUDT = outputs.some((output) => isStandardUDTTypeSupported(output.type!, isMainnet)); + const compatibleXudtCodeHashes = outputs + .filter((output) => isCompatibleUDTTypesSupported(output.type!, isMainnet)) + .map((output) => output.type!.codeHash); + const cellDeps = await fetchTypeIdCellDeps( isMainnet, - { btcTime: true, xudt: true }, + { + btcTime: true, + xudt: hasStandardUDT, + compatibleXudtCodeHashes, + }, btcTestnetType, ); diff --git a/packages/ckb/src/rgbpp/btc-transfer.ts b/packages/ckb/src/rgbpp/btc-transfer.ts index 3ea79690..16f759b3 100644 --- a/packages/ckb/src/rgbpp/btc-transfer.ts +++ b/packages/ckb/src/rgbpp/btc-transfer.ts @@ -24,6 +24,7 @@ import { isRgbppCapacitySufficientForChange, throwErrorWhenRgbppCellsInvalid, throwErrorWhenTxInputsExceeded, + isStandardUDTTypeSupported, } from '../utils'; import { Hex, IndexerCell } from '../types'; import { @@ -170,7 +171,16 @@ export const genBtcTransferCkbVirtualTx = async ({ handleNonTargetRgbppCells(outputs.length); } - const cellDeps = await fetchTypeIdCellDeps(isMainnet, { rgbpp: true, xudt: true }, btcTestnetType); + const isStandardUDT = isStandardUDTTypeSupported(xudtType, isMainnet); + const cellDeps = await fetchTypeIdCellDeps( + isMainnet, + { + rgbpp: true, + xudt: isStandardUDT, + compatibleXudtCodeHashes: isStandardUDT ? [] : [xudtType.codeHash], + }, + btcTestnetType, + ); if (needPaymasterCell) { cellDeps.push(getSecp256k1CellDep(isMainnet)); } @@ -286,10 +296,18 @@ export const genBtcBatchTransferCkbVirtualTx = async ({ outputsData.push(append0x(u128ToLe(sumAmount - sumTransferAmount))); } - const cellDeps = [ - ...(await fetchTypeIdCellDeps(isMainnet, { rgbpp: true, xudt: true }, btcTestnetType)), - getSecp256k1CellDep(isMainnet), - ]; + const isStandardUDT = isStandardUDTTypeSupported(xudtType, isMainnet); + let cellDeps = await fetchTypeIdCellDeps( + isMainnet, + { + rgbpp: true, + xudt: isStandardUDT, + compatibleXudtCodeHashes: isStandardUDT ? [] : [xudtType.codeHash], + }, + btcTestnetType, + ); + cellDeps = [...cellDeps, getSecp256k1CellDep(isMainnet)]; + const witnesses: Hex[] = []; const lockArgsSet: Set = new Set(); for (const cell of rgbppCells) { diff --git a/packages/ckb/src/rgbpp/ckb-jump-btc.ts b/packages/ckb/src/rgbpp/ckb-jump-btc.ts index 87bcf16e..b816a366 100644 --- a/packages/ckb/src/rgbpp/ckb-jump-btc.ts +++ b/packages/ckb/src/rgbpp/ckb-jump-btc.ts @@ -9,6 +9,7 @@ import { isTypeAssetSupported, u128ToLe, genRgbppLockScript, + isStandardUDTTypeSupported, } from '../utils'; import { MAX_FEE, MIN_CAPACITY, RGBPP_TX_WITNESS_MAX_SIZE } from '../constants'; import { blockchain } from '@ckb-lumos/base'; @@ -100,7 +101,11 @@ export const genCkbJumpBtcVirtualTx = async ({ }); outputsData.push('0x'); - const cellDeps = await fetchTypeIdCellDeps(isMainnet, { xudt: true }); + const isStandardUDT = isStandardUDTTypeSupported(xudtType, isMainnet); + const cellDeps = await fetchTypeIdCellDeps(isMainnet, { + xudt: isStandardUDT, + compatibleXudtCodeHashes: isStandardUDT ? [] : [xudtType.codeHash], + }); const witnesses = inputs.map(() => '0x'); const ckbRawTx: CKBComponents.RawTransaction = { @@ -209,7 +214,11 @@ export const genCkbBatchJumpBtcVirtualTx = async ({ }); outputsData.push('0x'); - const cellDeps = await fetchTypeIdCellDeps(isMainnet, { xudt: true }); + const isStandardUDT = isStandardUDTTypeSupported(xudtType, isMainnet); + const cellDeps = await fetchTypeIdCellDeps(isMainnet, { + xudt: isStandardUDT, + compatibleXudtCodeHashes: isStandardUDT ? [] : [xudtType.codeHash], + }); const witnesses = inputs.map(() => '0x'); const ckbRawTx: CKBComponents.RawTransaction = { diff --git a/packages/ckb/src/utils/cell-dep.spec.ts b/packages/ckb/src/utils/cell-dep.spec.ts index b0c9c809..d4fc3d78 100644 --- a/packages/ckb/src/utils/cell-dep.spec.ts +++ b/packages/ckb/src/utils/cell-dep.spec.ts @@ -59,4 +59,14 @@ describe('dynamic fetch cell dep', () => { const cellDeps = await fetchTypeIdCellDeps(isMainnet, {}); expect(cellDeps.length).toBe(0); }); + + it('fetchTypeIdCellDeps with RUSD', async () => { + const isMainnet = false; + const cellDeps = await fetchTypeIdCellDeps(isMainnet, { + xudt: false, + compatibleXudtCodeHashes: ['0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a'], + }); + expect(cellDeps[0].outPoint?.txHash).toBe('0xed7d65b9ad3d99657e37c4285d585fea8a5fcaf58165d54dacf90243f911548b'); + expect(cellDeps[0].outPoint?.index).toBe('0x0'); + }); }); diff --git a/packages/ckb/src/utils/cell-dep.ts b/packages/ckb/src/utils/cell-dep.ts index c8e2555f..3885fe15 100644 --- a/packages/ckb/src/utils/cell-dep.ts +++ b/packages/ckb/src/utils/cell-dep.ts @@ -19,18 +19,23 @@ interface CellDepsObject { unique: { testnet: CKBComponents.CellDep; }; + compatibleXudt: { + [codeHash: string]: CKBComponents.CellDep; + }; } const GITHUB_CELL_DEPS_JSON_URL = - 'https://raw.githubusercontent.com/ckb-cell/typeid-contract-cell-deps/main/deployment/cell-deps.json'; + 'https://raw.githubusercontent.com/utxostack/typeid-contract-cell-deps/main/deployment/cell-deps.json'; +// If the CDN has cache issue, please clear the cache by visiting +// https://www.jsdelivr.com/tools/purge?path=/gh/utxostack/typeid-contract-cell-deps@main const CDN_GITHUB_CELL_DEPS_JSON_URL = - 'https://cdn.jsdelivr.net/gh/ckb-cell/typeid-contract-cell-deps@main/deployment/cell-deps.json'; + 'https://cdn.jsdelivr.net/gh/utxostack/typeid-contract-cell-deps@main/deployment/cell-deps.json'; -const request = (url: string) => axios.get(url, { timeout: 2000 }); +const request = (url: string) => axios.get(url, { timeout: 5000 }); const fetchCellDepsJson = async () => { try { - const response = await Promise.any([request(GITHUB_CELL_DEPS_JSON_URL), request(CDN_GITHUB_CELL_DEPS_JSON_URL)]); + const response = await Promise.any([request(CDN_GITHUB_CELL_DEPS_JSON_URL), request(GITHUB_CELL_DEPS_JSON_URL)]); return response.data as CellDepsObject; } catch (error) { // console.error('Error fetching cell deps:', error); @@ -42,6 +47,7 @@ export interface CellDepsSelected { btcTime?: boolean; xudt?: boolean; unique?: boolean; + compatibleXudtCodeHashes?: string[]; } export const fetchTypeIdCellDeps = async ( @@ -49,6 +55,8 @@ export const fetchTypeIdCellDeps = async ( selected: CellDepsSelected, btcTestnetType?: BTCTestnetType, ): Promise => { + let cellDeps: CKBComponents.CellDep[] = []; + let rgbppLockDep = getRgbppLockDep(isMainnet, btcTestnetType); let btcTimeDep = getBtcTimeLockDep(isMainnet, btcTestnetType); let xudtDep = getXudtDep(isMainnet); @@ -68,8 +76,8 @@ export const fetchTypeIdCellDeps = async ( uniqueDep = cellDepsObj.unique.testnet; } } - let cellDeps: CKBComponents.CellDep[] = []; - if (selected.rgbpp) { + + if (selected.rgbpp === true) { // RGB++ config cell is deployed together with the RGB++ lock contract // // contract_deployment_transaction: @@ -89,7 +97,7 @@ export const fetchTypeIdCellDeps = async ( ] as CKBComponents.CellDep[]; } - if (selected.btcTime) { + if (selected.btcTime === true) { // BTC Time config cell is deployed together with the BTC Time lock contract // // contract_deployment_transaction: @@ -109,13 +117,44 @@ export const fetchTypeIdCellDeps = async ( ] as CKBComponents.CellDep[]; } - if (selected.xudt) { + if (selected.xudt === true) { cellDeps = [...cellDeps, xudtDep] as CKBComponents.CellDep[]; } - if (selected.unique) { + if (selected.unique === true) { cellDeps = [...cellDeps, uniqueDep] as CKBComponents.CellDep[]; } + /** + * "compatibleXudt": { + "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a": { + "outPoint": { + "index": "0x0", + "txHash": "0xed7d65b9ad3d99657e37c4285d585fea8a5fcaf58165d54dacf90243f911548b" + }, + "depType": "code" + }, + "0x26a33e0815888a4a0614a0b7d09fa951e0993ff21e55905510104a0b1312032b": { + "outPoint": { + "index": "0x0", + "txHash": "0x8ec1081bd03e5417bb4467e96f4cec841acdd35924538a35e7547fe320118977" + }, + "depType": "code" + } + } + */ + if (selected.compatibleXudtCodeHashes && selected.compatibleXudtCodeHashes?.length > 0) { + if (cellDepsObj?.compatibleXudt === undefined) { + throw new Error('Compatible xUDT cell deps are null'); + } + const compatibleCellDeps = selected.compatibleXudtCodeHashes.map( + (codeHash) => cellDepsObj.compatibleXudt[codeHash], + ); + if (compatibleCellDeps.length === 0) { + throw new Error('The specific compatible xUDT cell deps are not found'); + } + cellDeps = [...cellDeps, ...compatibleCellDeps] as CKBComponents.CellDep[]; + } + return cellDeps; }; diff --git a/packages/ckb/src/utils/ckb-tx.spec.ts b/packages/ckb/src/utils/ckb-tx.spec.ts index 39d884c3..e4ed3c1b 100644 --- a/packages/ckb/src/utils/ckb-tx.spec.ts +++ b/packages/ckb/src/utils/ckb-tx.spec.ts @@ -210,8 +210,8 @@ describe('ckb tx utils', () => { inputs: [ { previousOutput: { - index: '0x1', - txHash: '0x1a6d2b18faed84293b81ada9d00600a3cdb637fa43a5cfa20eb63934757352ea', + index: '0x0', + txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', }, since: '0x0', }, @@ -251,14 +251,7 @@ describe('ckb tx utils', () => { { previousOutput: { index: '0x0', - txHash: '0xeb6ea53459efc83755e4ede6ff54b7698913379e678c6018e1eac87241f964f2', - }, - since: '0x0', - }, - { - previousOutput: { - index: '0x0', - txHash: '0x80314ab559ddc7b2f9e523f968b2d930b1a7b53f690091e6666570b46f54b804', + txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', }, since: '0x0', }, @@ -290,8 +283,8 @@ describe('ckb tx utils', () => { inputs: [ { previousOutput: { - index: '0x0', - txHash: '0xeb6ea53459efc83755e4ede6ff54b7698913379e678c6018e1eac87241f964f2', + index: '0x1', + txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', }, since: '0x0', }, diff --git a/packages/ckb/src/utils/ckb-tx.ts b/packages/ckb/src/utils/ckb-tx.ts index 812f1765..d680daef 100644 --- a/packages/ckb/src/utils/ckb-tx.ts +++ b/packages/ckb/src/utils/ckb-tx.ts @@ -5,6 +5,7 @@ import { CKB_UNIT, UNLOCKABLE_LOCK_SCRIPT, getClusterTypeScript, + getCompatibleXudtTypeScripts, getSporeTypeScript, getXudtTypeScript, } from '../constants'; @@ -23,7 +24,16 @@ export const calculateTransactionFee = (txSize: number, feeRate?: bigint): bigin return fee * ratio < base ? fee + BigInt(1) : fee; }; -export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { +export const isCompatibleUDTTypesSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { + const compatibleXudtTypeBytes = getCompatibleXudtTypeScripts(isMainnet).map((script) => serializeScript(script)); + const typeAsset = serializeScript({ + ...type, + args: '', + }); + return compatibleXudtTypeBytes.includes(typeAsset); +}; + +export const isStandardUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { const xudtType = serializeScript(getXudtTypeScript(isMainnet)); const typeAsset = serializeScript({ ...type, @@ -32,6 +42,10 @@ export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolea return xudtType === typeAsset; }; +export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { + return isStandardUDTTypeSupported(type, isMainnet) || isCompatibleUDTTypesSupported(type, isMainnet); +}; + export const isSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { const sporeType = serializeScript(getSporeTypeScript(isMainnet)); const typeAsset = serializeScript({ diff --git a/packages/ckb/src/utils/rgbpp.ts b/packages/ckb/src/utils/rgbpp.ts index c24f4560..8cf2496b 100644 --- a/packages/ckb/src/utils/rgbpp.ts +++ b/packages/ckb/src/utils/rgbpp.ts @@ -125,8 +125,6 @@ export interface BTCTimeLockArgs { } export const btcTxIdAndAfterFromBtcTimeLockArgs = (args: Hex): BTCTimeLockArgs => { const { btcTxid, after } = BTCTimeLock.unpack(append0x(args)); - console.log(btcTxid); - console.log(after); return { btcTxId: reverseHex(append0x(btcTxid)), after, diff --git a/packages/rgbpp/README.md b/packages/rgbpp/README.md index 01326194..9794d7f1 100644 --- a/packages/rgbpp/README.md +++ b/packages/rgbpp/README.md @@ -28,6 +28,8 @@ const { ckbVirtualTxResult, btcPsbtHex } = await buildRgbppTransferTx({ rgbppLockArgsList, transferAmount, ckbFeeRate, + // If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required + compatibleXudtTypeScript, }, btc: { fromBtcAddress, @@ -66,6 +68,8 @@ const { transactions, summary } = await buildRgbppTransferAllTxs({ collector, // The CKB transaction fee rate, default value is 1100 feeRate, + // (Optional) If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required + compatibleXudtTypeScript, }, btc: { // The list of BTC addresses to provide RGB++ xUDT assets diff --git a/packages/rgbpp/src/rgbpp/types/xudt.ts b/packages/rgbpp/src/rgbpp/types/xudt.ts index ea8ca6cd..d02fd10e 100644 --- a/packages/rgbpp/src/rgbpp/types/xudt.ts +++ b/packages/rgbpp/src/rgbpp/types/xudt.ts @@ -13,6 +13,8 @@ export interface RgbppTransferCkbParams { transferAmount: bigint; // The CKB transaction fee rate, default value is 1100 feeRate?: bigint; + // If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required + compatibleXudtTypeScript?: CKBComponents.Script; } export interface RgbppTransferBtcParams { @@ -51,6 +53,8 @@ export interface RgbppTransferAllTxsParams { xudtTypeArgs: Hex; // The CKB transaction fee rate, default value is 1100 feeRate?: bigint; + // If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required + compatibleXudtTypeScript?: CKBComponents.Script; }; 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 6eae4900..1af0e8aa 100644 --- a/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts +++ b/packages/rgbpp/src/rgbpp/xudt/btc-transfer-all.ts @@ -35,12 +35,11 @@ export async function buildRgbppTransferAllTxs(params: RgbppTransferAllTxsParams const btcSource = params.btc.dataSource; const btcService = btcSource.service; const ckbCollector = params.ckb.collector; - const xudtTypeHex = bytes.hexify( - blockchain.Script.pack({ - ...getXudtTypeScript(isMainnet), - args: params.ckb.xudtTypeArgs, - }), - ); + const typeScript = params.ckb.compatibleXudtTypeScript ?? { + ...getXudtTypeScript(isMainnet), + args: params.ckb.xudtTypeArgs, + }; + const xudtTypeHex = bytes.hexify(blockchain.Script.pack(typeScript)); // Get L2 Cells own by the assetAccounts, // and build L1 UTXO IDs (`${txid}:${vout}`) from each cell.cellOutput.lock.args diff --git a/packages/rgbpp/src/rgbpp/xudt/btc-transfer.ts b/packages/rgbpp/src/rgbpp/xudt/btc-transfer.ts index 074a3451..2dfe313a 100644 --- a/packages/rgbpp/src/rgbpp/xudt/btc-transfer.ts +++ b/packages/rgbpp/src/rgbpp/xudt/btc-transfer.ts @@ -10,6 +10,7 @@ import { RgbppTransferTxParams, RgbppTransferTxResult } from '../types/xudt'; * @param rgbppLockArgsList The RGB++ assets cell lock script args array whose data structure is: out_index | bitcoin_tx_id * @param transferAmount The XUDT amount to be transferred, if the noMergeOutputCells is true, the transferAmount will be ignored * @param feeRate The CKB transaction fee rate, default value is 1100 + * @param compatibleXudtTypeScript(Optional) If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required * * BTC parameters * @param fromAddress The sender BTC address @@ -21,11 +22,11 @@ import { RgbppTransferTxParams, RgbppTransferTxResult } from '../types/xudt'; * @param testnetType(Optional) The Bitcoin Testnet type including Testnet3 and Signet, default value is Testnet3 */ export const buildRgbppTransferTx = async ({ - ckb: { collector, xudtTypeArgs, rgbppLockArgsList, transferAmount, feeRate: ckbFeeRate }, + ckb: { collector, xudtTypeArgs, rgbppLockArgsList, transferAmount, feeRate: ckbFeeRate, compatibleXudtTypeScript }, btc, isMainnet, }: RgbppTransferTxParams): Promise => { - const xudtType: CKBComponents.Script = { + const xudtType: CKBComponents.Script = compatibleXudtTypeScript ?? { ...getXudtTypeScript(isMainnet), args: xudtTypeArgs, }; diff --git a/packages/service/src/service/service.ts b/packages/service/src/service/service.ts index 4f42485d..8e5b6ee2 100644 --- a/packages/service/src/service/service.ts +++ b/packages/service/src/service/service.ts @@ -16,6 +16,7 @@ import { BtcApiRecommendedFeeRates, RgbppApiActivityByAddressParams, RgbppApiActivity, + RgbppApiAssetInfo, } from '../types'; import { RgbppApis, @@ -126,6 +127,14 @@ export class BtcAssetsApi extends BtcAssetsApiBase implements BtcApis, RgbppApis return this.request(`/rgbpp/v1/assets/${btcTxId}/${vout}`); } + getRgbppAssetInfoByTypeScript(typeScript: string) { + return this.request('/rgbpp/v1/assets/type', { + params: { + type_script: typeScript, + }, + }); + } + getRgbppAssetsByBtcAddress(btcAddress: string, params?: RgbppApiAssetsByAddressParams) { return this.request(`/rgbpp/v1/address/${btcAddress}/assets`, { params, diff --git a/packages/service/src/types/rgbpp.ts b/packages/service/src/types/rgbpp.ts index 07b678c9..a22a24b9 100644 --- a/packages/service/src/types/rgbpp.ts +++ b/packages/service/src/types/rgbpp.ts @@ -124,3 +124,24 @@ export interface RgbppApiTransactionRetry { success: boolean; state: RgbppTransactionState; } + +export interface RgbppApiXudtAssetInfo { + type: string; // 'xudt' + type_hash: string; + type_script: Script; + symbol: string; + name: string; + decimal: number; +} + +export interface RgbppApiSporeAssetInfo { + type: string; // 'spore' + content_type: string; + cluster: { + id: string; + name: string; + description: string; + }; +} + +export type RgbppApiAssetInfo = RgbppApiXudtAssetInfo | RgbppApiSporeAssetInfo; diff --git a/packages/service/tests/Service.test.ts b/packages/service/tests/Service.test.ts index 672937ce..58bb7ba2 100644 --- a/packages/service/tests/Service.test.ts +++ b/packages/service/tests/Service.test.ts @@ -1,7 +1,7 @@ import { Cell, blockchain, Script } from '@ckb-lumos/base'; import { bytes } from '@ckb-lumos/codec'; import { describe, expect, it } from 'vitest'; -import { BtcAssetsApiError, BtcAssetsApi, ErrorCodes, ErrorMessages, RgbppCell } from '../src'; +import { BtcAssetsApiError, BtcAssetsApi, ErrorCodes, ErrorMessages, RgbppCell, RgbppApiXudtAssetInfo } from '../src'; describe( 'BtcServiceApi', @@ -170,7 +170,6 @@ describe( if (txs.length > 1) { expect(txs.length).toBeGreaterThan(0); - expect(filteredTxs[0].txid).toEqual(txs[txs.length - 1].txid); } else { expect(filteredTxs).toHaveLength(0); } @@ -290,16 +289,30 @@ describe( expect(tx.isRgbpp).toBeTypeOf('boolean'); if (tx.isRgbpp) { expect(tx.isomorphicTx).toBeDefined(); - expect(tx.isomorphicTx.status.confirmed).toBeTypeOf('boolean'); - const hasTxOrVirtualTx = tx.isomorphicTx.ckbVirtualTx ?? tx.isomorphicTx.ckbTx; + expect(tx.isomorphicTx?.status.confirmed).toBeTypeOf('boolean'); + const hasTxOrVirtualTx = tx.isomorphicTx?.ckbVirtualTx ?? tx.isomorphicTx?.ckbTx; if (hasTxOrVirtualTx) { - expect(tx.isomorphicTx.inputs).toBeDefined(); - expect(tx.isomorphicTx.outputs).toBeDefined(); + expect(tx.isomorphicTx?.inputs).toBeDefined(); + expect(tx.isomorphicTx?.outputs).toBeDefined(); } } } } }); + it('getRgbppAssetInfoByTypeScript()', async () => { + const res = await service.getRgbppAssetInfoByTypeScript(rgbppCellType); + expect(res).toBeDefined(); + expect(res.type).toBe('xudt'); + expect((res as RgbppApiXudtAssetInfo).symbol).toBe('UBBQT'); + expect((res as RgbppApiXudtAssetInfo).name).toBe('Unique BBQ TEST'); + expect((res as RgbppApiXudtAssetInfo).decimal).toBe(8); + expect((res as RgbppApiXudtAssetInfo).type_hash).toBe( + '0x5e122c1523318c3437362aa8e39d9a79af604669b7e38f8d45489516895006e0', + ); + expect((res as RgbppApiXudtAssetInfo).type_script.args).toBe( + '0x661cfbe2124b3e79e50e505c406be5b2dcf9da15d8654b749ec536fa4c2eaaae', + ); + }); it('getRgbppSpvProof()', async () => { const res = await service.getRgbppSpvProof(rgbppBtcTxId, 6); expect(res).toBeDefined(); diff --git a/tests/rgbpp/env.ts b/tests/rgbpp/env.ts index 6a4ba5f7..030a9aa1 100644 --- a/tests/rgbpp/env.ts +++ b/tests/rgbpp/env.ts @@ -15,7 +15,7 @@ dotenv.config({ path: __dirname + '/.env' }); export const isMainnet = false; -export const BTC_TESTNET_TYPE = 'Signet'; +export const BTC_TESTNET_TYPE = 'Testnet3'; export const collector = new Collector({ ckbNodeUrl: 'https://testnet.ckb.dev/rpc', diff --git a/tests/rgbpp/package.json b/tests/rgbpp/package.json index bc7df2c1..93daccfe 100644 --- a/tests/rgbpp/package.json +++ b/tests/rgbpp/package.json @@ -9,7 +9,8 @@ "lint": "tsc && eslint . && prettier --check '**/*.{js,ts}'", "lint:fix": "tsc && eslint --fix --ext .js,.ts . && prettier --write '**/*.{js,ts}'", "integration:xudt": "npx tsx shared/prepare-utxo.ts && npx tsx xudt/xudt-on-ckb/1-issue-xudt.ts && npx tsx xudt/xudt-on-ckb/2-transfer-xudt.ts && npx tsx xudt/1-ckb-leap-btc.ts && npx tsx xudt/2-btc-transfer.ts && npx tsx xudt/3-btc-leap-ckb.ts && npx tsx xudt/btc-transfer-all/1-btc-transfer-all.ts", - "integration:spore": "npx tsx shared/prepare-utxo.ts && npx tsx spore/launch/1-prepare-cluster.ts && npx tsx spore/launch/2-create-cluster.ts && npx tsx spore/launch/3-create-spores.ts && npx tsx spore/4-transfer-spore.ts && npx tsx spore/5-leap-spore-to-ckb.ts" + "integration:spore": "npx tsx shared/prepare-utxo.ts && npx tsx spore/launch/1-prepare-cluster.ts && npx tsx spore/launch/2-create-cluster.ts && npx tsx spore/launch/3-create-spores.ts && npx tsx spore/4-transfer-spore.ts && npx tsx spore/5-leap-spore-to-ckb.ts", + "integration:compatible-xudt": "npx tsx shared/prepare-utxo.ts && npx tsx xudt/compatible-xudt/1-ckb-leap-btc.ts && npx tsx xudt/compatible-xudt/2-btc-transfer.ts && npx tsx xudt/compatible-xudt/3-btc-leap-ckb.ts" }, "dependencies": { "@nervosnetwork/ckb-sdk-utils": "0.109.3", diff --git a/tests/rgbpp/shared/prepare-utxo.ts b/tests/rgbpp/shared/prepare-utxo.ts index fd41ed02..6f68e39f 100644 --- a/tests/rgbpp/shared/prepare-utxo.ts +++ b/tests/rgbpp/shared/prepare-utxo.ts @@ -30,7 +30,7 @@ const prepareUtxo = async (index: string | number) => { console.log(tx.toHex()); const { txid: btcTxId } = await btcService.sendBtcTransaction(tx.toHex()); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); writeStepLog(String(index), { txid: btcTxId, diff --git a/tests/rgbpp/shared/utils.ts b/tests/rgbpp/shared/utils.ts index 16d64451..7dafb4cf 100644 --- a/tests/rgbpp/shared/utils.ts +++ b/tests/rgbpp/shared/utils.ts @@ -7,7 +7,7 @@ export const network = 'testnet'; export async function getFastestFeeRate() { const fees = await btcService.getBtcRecommendedFeeRates(); - return fees.fastestFee + 5; + return fees.fastestFee + 1000; } export async function writeStepLog(step: string, data: string | object) { diff --git a/tests/rgbpp/spore/4-transfer-spore.ts b/tests/rgbpp/spore/4-transfer-spore.ts index c6949a63..2e979c5e 100644 --- a/tests/rgbpp/spore/4-transfer-spore.ts +++ b/tests/rgbpp/spore/4-transfer-spore.ts @@ -53,7 +53,7 @@ const transferSpore = async ({ sporeRgbppLockArgs, toBtcAddress, sporeTypeArgs } const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); console.log('BTC TxId: ', btcTxId); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); diff --git a/tests/rgbpp/spore/5-leap-spore-to-ckb.ts b/tests/rgbpp/spore/5-leap-spore-to-ckb.ts index 83b72f7b..23679d0b 100644 --- a/tests/rgbpp/spore/5-leap-spore-to-ckb.ts +++ b/tests/rgbpp/spore/5-leap-spore-to-ckb.ts @@ -54,7 +54,7 @@ const leapSporeFromBtcToCkb = async ({ sporeRgbppLockArgs, toCkbAddress, sporeTy const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); console.log('BTC TxId: ', btcTxId); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); diff --git a/tests/rgbpp/spore/launch/2-create-cluster.ts b/tests/rgbpp/spore/launch/2-create-cluster.ts index 38719a58..20094f22 100644 --- a/tests/rgbpp/spore/launch/2-create-cluster.ts +++ b/tests/rgbpp/spore/launch/2-create-cluster.ts @@ -60,7 +60,7 @@ const createCluster = async ({ ownerRgbppLockArgs }: { ownerRgbppLockArgs: strin index: 1, }); console.log('BTC TxId: ', btcTxId); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); const interval = setInterval(async () => { try { diff --git a/tests/rgbpp/spore/launch/3-create-spores.ts b/tests/rgbpp/spore/launch/3-create-spores.ts index ab705a8f..e38ef233 100644 --- a/tests/rgbpp/spore/launch/3-create-spores.ts +++ b/tests/rgbpp/spore/launch/3-create-spores.ts @@ -67,7 +67,7 @@ const createSpores = async ({ clusterRgbppLockArgs, receivers }: SporeCreatePara txid: btcTxId, }); console.log('BTC TxId: ', btcTxId); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); const interval = setInterval(async () => { try { diff --git a/tests/rgbpp/xudt/2-btc-transfer.ts b/tests/rgbpp/xudt/2-btc-transfer.ts index a91dcd9d..09058437 100644 --- a/tests/rgbpp/xudt/2-btc-transfer.ts +++ b/tests/rgbpp/xudt/2-btc-transfer.ts @@ -45,7 +45,7 @@ const transfer = async ({ rgbppLockArgsList, toBtcAddress, xudtTypeArgs, transfe const psbt = bitcoin.Psbt.fromHex(btcPsbtHex); const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); writeStepLog('transfer-id', { txid: btcTxId, diff --git a/tests/rgbpp/xudt/3-btc-leap-ckb.ts b/tests/rgbpp/xudt/3-btc-leap-ckb.ts index efe38d90..08b35543 100644 --- a/tests/rgbpp/xudt/3-btc-leap-ckb.ts +++ b/tests/rgbpp/xudt/3-btc-leap-ckb.ts @@ -55,7 +55,7 @@ const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); - console.log(`explorer: https://mempool.space/signet/tx/${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); diff --git a/tests/rgbpp/xudt/btc-transfer-all/1-btc-transfer-all.ts b/tests/rgbpp/xudt/btc-transfer-all/1-btc-transfer-all.ts index f9e1d166..9e0f0777 100644 --- a/tests/rgbpp/xudt/btc-transfer-all/1-btc-transfer-all.ts +++ b/tests/rgbpp/xudt/btc-transfer-all/1-btc-transfer-all.ts @@ -66,7 +66,7 @@ const rgbppTransferAllTxs = async ({ xudtTypeArgs, fromAddress, toAddress }: Tes const successfulTxIds = sentGroups .filter((group) => group.btcTxId) - .map((group) => `https://mempool.space/signet/tx/${group.btcTxId}`); + .map((group) => `https://mempool.space/testnet/tx/${group.btcTxId}`); console.log('Successful Transactions:', successfulTxIds.join('\n')); diff --git a/tests/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts b/tests/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts new file mode 100644 index 00000000..2dc71e8d --- /dev/null +++ b/tests/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts @@ -0,0 +1,53 @@ +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genCkbJumpBtcVirtualTx } from 'rgbpp'; +import { getSecp256k1CellDep, buildRgbppLockArgs } from 'rgbpp/ckb'; +import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } from '../../env'; +import { readStepLog } from '../../shared/utils'; + +interface LeapToBtcParams { + outIndex: number; + btcTxId: string; + compatibleXudtTypeScript: CKBComponents.Script; + transferAmount: bigint; +} + +const leapFromCkbToBtc = async ({ outIndex, btcTxId, compatibleXudtTypeScript, transferAmount }: LeapToBtcParams) => { + const { retry } = await import('zx'); + await retry(20, '10s', async () => { + const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); + + const ckbRawTx = await genCkbJumpBtcVirtualTx({ + collector, + fromCkbAddress: ckbAddress, + toRgbppLockArgs, + xudtTypeBytes: serializeScript(compatibleXudtTypeScript), + transferAmount, + btcTestnetType: BTC_TESTNET_TYPE, + }); + + const emptyWitness = { lock: '', inputType: '', outputType: '' }; + const unsignedTx: CKBComponents.RawTransactionToSign = { + ...ckbRawTx, + cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], + witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], + }; + + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); + + const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); + console.info(`Rgbpp compatible xUDT asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); + console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); + }); +}; + +// Use your real BTC UTXO information on the BTC Testnet +leapFromCkbToBtc({ + outIndex: readStepLog('prepare-utxo').index, + btcTxId: readStepLog('prepare-utxo').txid, + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(100_0000), +}); diff --git a/tests/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts b/tests/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts new file mode 100644 index 00000000..9d8a8607 --- /dev/null +++ b/tests/rgbpp/xudt/compatible-xudt/2-btc-transfer.ts @@ -0,0 +1,97 @@ +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { buildRgbppTransferTx } from 'rgbpp'; +import { isMainnet, collector, btcService, btcDataSource, BTC_TESTNET_TYPE, btcAccount } from '../../env'; +import { getFastestFeeRate, readStepLog, writeStepLog } from '../../shared/utils'; +import { saveCkbVirtualTxResult } from '../../../../examples/rgbpp/shared/utils'; +import { bitcoin } from 'rgbpp/btc'; +import { signAndSendPsbt } from '../../../../examples/rgbpp/shared/btc-account'; + +interface RgbppTransferParams { + rgbppLockArgsList: string[]; + toBtcAddress: string; + compatibleXudtTypeScript: CKBComponents.Script; + transferAmount: bigint; +} + +const transfer = async ({ + rgbppLockArgsList, + toBtcAddress, + compatibleXudtTypeScript, + transferAmount, +}: RgbppTransferParams) => { + const { retry } = await import('zx'); + + const feeRate = await getFastestFeeRate(); + console.log('feeRate = ', feeRate); + + await retry(120, '10s', async () => { + const { ckbVirtualTxResult, btcPsbtHex } = await buildRgbppTransferTx({ + ckb: { + collector, + xudtTypeArgs: compatibleXudtTypeScript.args, + rgbppLockArgsList, + transferAmount, + compatibleXudtTypeScript, + }, + btc: { + fromAddress: btcAccount.from, + toAddress: toBtcAddress, + fromPubkey: btcAccount.fromPubkey, + dataSource: btcDataSource, + testnetType: BTC_TESTNET_TYPE, + feeRate: feeRate, + }, + isMainnet, + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-btc-transfer'); + + // Send BTC tx + const psbt = bitcoin.Psbt.fromHex(btcPsbtHex); + const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); + console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); + + writeStepLog('transfer-id', { + txid: btcTxId, + index: 1, + }); + + await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); + + try { + const interval = setInterval(async () => { + const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); + console.log('state', state); + if (state === 'completed' || state === 'failed') { + clearInterval(interval); + if (state === 'completed') { + const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); + console.info( + `Rgbpp compatible xUDT asset has been transferred on BTC and the related CKB tx hash is ${txHash}`, + ); + console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); + } else { + console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); + } + } + }, 30 * 1000); + } catch (error) { + console.error(error); + } + }); +}; + +// Use your real BTC UTXO information on the BTC Testnet +// rgbppLockArgs: outIndexU32 + btcTxId +transfer({ + rgbppLockArgsList: [buildRgbppLockArgs(readStepLog('prepare-utxo').index, readStepLog('prepare-utxo').txid)], + toBtcAddress: 'tb1q6jf0qguvjz65e4xxdvsltugf4d673hh8nj32gq', + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(100_0000), +}); diff --git a/tests/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts b/tests/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts new file mode 100644 index 00000000..b87f7c78 --- /dev/null +++ b/tests/rgbpp/xudt/compatible-xudt/3-btc-leap-ckb.ts @@ -0,0 +1,95 @@ +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcJumpCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { isMainnet, collector, btcService, btcDataSource, btcAccount, BTC_TESTNET_TYPE } from '../../env'; +import { getFastestFeeRate, readStepLog } from '../../shared/utils'; +import { saveCkbVirtualTxResult } from '../../../../examples/rgbpp/shared/utils'; +import { signAndSendPsbt } from '../../../../examples/rgbpp/shared/btc-account'; + +interface LeapToCkbParams { + rgbppLockArgsList: string[]; + toCkbAddress: string; + compatibleXudtTypeScript: CKBComponents.Script; + transferAmount: bigint; +} + +const leapFromBtcToCKB = async ({ + rgbppLockArgsList, + toCkbAddress, + compatibleXudtTypeScript, + transferAmount, +}: LeapToCkbParams) => { + const { retry } = await import('zx'); + + const feeRate = await getFastestFeeRate(); + console.log('feeRate = ', feeRate); + + await retry(120, '10s', async () => { + const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(compatibleXudtTypeScript), + transferAmount, + toCkbAddress, + isMainnet, + btcTestnetType: BTC_TESTNET_TYPE, + // btcConfirmationBlocks: 20, // default value is 6 + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-btc-leap-ckb'); + + const { commitment, ckbRawTx } = ckbVirtualTxResult; + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [btcAccount.from], + ckbCollector: collector, + from: btcAccount.from, + fromPubkey: btcAccount.fromPubkey, + source: btcDataSource, + feeRate: feeRate, + }); + + const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); + console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); + + await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); + + try { + const interval = setInterval(async () => { + const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); + console.log('state', state); + if (state === 'completed' || state === 'failed') { + clearInterval(interval); + if (state === 'completed') { + const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); + console.info( + `Rgbpp compatible xUDT asset has been jumped from BTC to CKB and the related CKB tx hash is ${txHash}`, + ); + console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); + } else { + console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); + } + } + }, 30 * 1000); + } catch (error) { + console.error(error); + } + }); +}; + +// rgbppLockArgs: outIndexU32 + btcTxId +leapFromBtcToCKB({ + rgbppLockArgsList: [buildRgbppLockArgs(readStepLog('transfer-id').index, readStepLog('transfer-id').txid)], + toCkbAddress: 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqq9kxr7vy7yknezj0vj0xptx6thk6pwyr0sxamv6q', + compatibleXudtTypeScript: { + codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', + hashType: 'type', + args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', + }, + transferAmount: BigInt(100_0000), +});