diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml new file mode 100644 index 00000000..e70dfaad --- /dev/null +++ b/.github/workflows/integration-test.yaml @@ -0,0 +1,44 @@ +# Test the entire process of RGBPP to ensure the proper functioning of the rgbpp-sdk package. + +name: Integration Tests + +on: + workflow_dispatch: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout rgbpp-sdk + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - uses: pnpm/action-setup@v3 + name: Install -g pnpm + with: + version: 8 + run_install: false + + - name: Install dependencies + run: pnpm i + + - name: Build packages + run: pnpm run build:packages + + - name: Run integration:xudt script + working-directory: ./tests/rgbpp + run: pnpm run integration:xudt + env: + VITE_SERVICE_URL: ${{ secrets.SERVICE_URL }} + VITE_SERVICE_TOKEN: ${{ secrets.SERVICE_TOKEN }} + VITE_SERVICE_ORIGIN: ${{ secrets.SERVICE_ORIGIN }} + INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_PRIVATE_KEY }} + INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_PRIVATE_KEY }} diff --git a/package.json b/package.json index e9eb6282..f9543082 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "test:packages": "turbo run test --filter=./packages/*", "build:packages": "turbo run build --filter=./packages/*", "lint:fix": "turbo run lint:fix", - "lint:packages": "turbo run lint --filter=./{packages,examples}/*", - "format": "prettier --write '{packages,apps,examples}/**/*.{js,jsx,ts,tsx}'", + "lint:packages": "turbo run lint --filter=./{packages,examples,tests}/*", + "format": "prettier --write '{packages,apps,examples,tests}/**/*.{js,jsx,ts,tsx}'", "clean": "turbo run clean", "clean:packages": "turbo run clean --filter=./packages/*", "clean:dependencies": "pnpm clean:sub-dependencies && rimraf node_modules", @@ -33,7 +33,7 @@ "typescript": "^5.4.3" }, "lint-staged": { - "{packages,apps,examples}/**/*.{js,jsx,ts,tsx}": [ + "{packages,apps,examples,tests}/**/*.{js,jsx,ts,tsx}": [ "eslint --fix", "prettier --ignore-unknown --write" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dd4ecbd..d58421ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 5.0.5 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) + version: 10.9.2(@types/node@20.12.12)(typescript@5.4.3) turbo: specifier: ^1.13.0 version: 1.13.0 @@ -132,7 +132,7 @@ importers: version: 5.4.2 vite: specifier: ^5.2.11 - version: 5.2.11(@types/node@20.12.2) + version: 5.2.11(@types/node@20.12.12) vite-plugin-node-polyfills: specifier: ^0.21.0 version: 0.21.0(vite@5.2.11) @@ -210,7 +210,7 @@ importers: version: 4.17.0 vitest: specifier: ^1.4.0 - version: 1.4.0(@types/node@20.12.2) + version: 1.4.0(@types/node@20.12.12) packages/ckb: dependencies: @@ -256,7 +256,7 @@ importers: version: 4.17.0 vitest: specifier: ^1.4.0 - version: 1.4.0(@types/node@20.12.2) + version: 1.4.0(@types/node@20.12.12) packages/rgbpp: dependencies: @@ -290,7 +290,32 @@ importers: version: 4.17.0 vitest: specifier: ^1.4.0 - version: 1.4.0(@types/node@20.12.2) + version: 1.4.0(@types/node@20.12.12) + + tests/rgbpp: + dependencies: + '@nervosnetwork/ckb-sdk-utils': + specifier: ^0.109.1 + version: 0.109.1 + rgbpp: + specifier: workspace:* + version: link:../../packages/rgbpp + zx: + specifier: ^8.0.2 + version: 8.1.0 + devDependencies: + '@types/dotenv': + specifier: ^8.2.0 + version: 8.2.0 + '@types/node': + specifier: ^20.11.28 + version: 20.12.12 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + typescript: + specifier: ^5.4.2 + version: 5.4.3 packages: @@ -1710,10 +1735,27 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true + /@types/fs-extra@11.0.4: + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + requiresBuild: true + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.12.12 + dev: false + optional: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonfile@6.1.4: + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + requiresBuild: true + dependencies: + '@types/node': 20.12.12 + dev: false + optional: true + /@types/lodash.isequal@4.5.8: resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==} dependencies: @@ -1745,11 +1787,11 @@ packages: undici-types: 5.26.5 dev: true - /@types/node@20.12.2: - resolution: {integrity: sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==} + /@types/node@20.12.12: + resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} + requiresBuild: true dependencies: undici-types: 5.26.5 - dev: true /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2060,7 +2102,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.24.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.2.11(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - supports-color dev: true @@ -5664,7 +5706,7 @@ packages: typescript: 5.4.3 dev: true - /ts-node@10.9.2(@types/node@20.12.2)(typescript@5.4.3): + /ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -5683,7 +5725,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.2 + '@types/node': 20.12.12 acorn: 8.11.3 acorn-walk: 8.3.2 arg: 4.1.3 @@ -5893,7 +5935,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -5959,7 +6000,7 @@ packages: safe-buffer: 5.2.1 dev: false - /vite-node@1.4.0(@types/node@20.12.2): + /vite-node@1.4.0(@types/node@20.12.12): resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5968,7 +6009,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.11(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - '@types/node' - less @@ -5987,12 +6028,12 @@ packages: dependencies: '@rollup/plugin-inject': 5.0.5 node-stdlib-browser: 1.2.0 - vite: 5.2.11(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - rollup dev: true - /vite@5.2.11(@types/node@20.12.2): + /vite@5.2.11(@types/node@20.12.12): resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -6020,7 +6061,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.12.2 + '@types/node': 20.12.12 esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.13.2 @@ -6028,7 +6069,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.4.0(@types/node@20.12.2): + /vitest@1.4.0(@types/node@20.12.12): resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -6053,7 +6094,7 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.12.2 + '@types/node': 20.12.12 '@vitest/expect': 1.4.0 '@vitest/runner': 1.4.0 '@vitest/snapshot': 1.4.0 @@ -6071,8 +6112,8 @@ packages: strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.3 - vite: 5.2.11(@types/node@20.12.2) - vite-node: 1.4.0(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) + vite-node: 1.4.0(@types/node@20.12.12) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -6294,3 +6335,12 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zx@8.1.0: + resolution: {integrity: sha512-2BCoOK6JTWikAkwPCV2dFr+1ou29WoY+6XltLu+Ou9dvxrqm/p+HuHCgBtMRMIVFexQzUSGfB5VbYeY8XmGBPQ==} + engines: {node: '>= 12.17.0'} + hasBin: true + optionalDependencies: + '@types/fs-extra': 11.0.4 + '@types/node': 20.12.12 + dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a1167940..1dfad62c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - "packages/*" - "apps/*" - "examples/*" + - "tests/*" diff --git a/tests/rgbpp/env.ts b/tests/rgbpp/env.ts new file mode 100644 index 00000000..e65e8a27 --- /dev/null +++ b/tests/rgbpp/env.ts @@ -0,0 +1,34 @@ +import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; +import { DataSource, BtcAssetsApi } from 'rgbpp'; +import { ECPair, ECPairInterface, bitcoin, NetworkType } from 'rgbpp/btc'; +import dotenv from 'dotenv'; +import { Collector } from 'rgbpp/ckb'; + +dotenv.config({ path: __dirname + '/.env' }); + +export const isMainnet = false; + +export const collector = new Collector({ + ckbNodeUrl: 'https://testnet.ckb.dev/rpc', + ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', +}); +export const CKB_PRIVATE_KEY = process.env.INTEGRATION_CKB_PRIVATE_KEY!; +export const ckbAddress = privateKeyToAddress(CKB_PRIVATE_KEY, { + prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, +}); + +export const BTC_PRIVATE_KEY = process.env.INTEGRATION_BTC_PRIVATE_KEY!; +export const BTC_SERVICE_URL = process.env.VITE_SERVICE_URL!; +export const BTC_SERVICE_TOKEN = process.env.VITE_SERVICE_TOKEN!; +export const BTC_SERVICE_ORIGIN = process.env.VITE_SERVICE_ORIGIN!; + +const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; +export const btcKeyPair: ECPairInterface = ECPair.fromPrivateKey(Buffer.from(BTC_PRIVATE_KEY, 'hex'), { network }); +export const { address: btcAddress } = bitcoin.payments.p2wpkh({ + pubkey: btcKeyPair.publicKey, + network, +}); + +const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; +export const btcService = BtcAssetsApi.fromToken(BTC_SERVICE_URL, BTC_SERVICE_TOKEN, BTC_SERVICE_ORIGIN); +export const btcDataSource = new DataSource(btcService, networkType); diff --git a/tests/rgbpp/package.json b/tests/rgbpp/package.json new file mode 100644 index 00000000..33f834c2 --- /dev/null +++ b/tests/rgbpp/package.json @@ -0,0 +1,24 @@ +{ + "name": "rgbpp-integration-tests", + "version": "0.1.0", + "description": "Test the entire process of RGBPP to ensure the proper functioning of the rgbpp-sdk package.", + "private": true, + "type": "commonjs", + "scripts": { + "format": "prettier --write '**/*.{js,ts}'", + "lint": "tsc && eslint . && prettier --check '**/*.{js,ts}'", + "lint:fix": "tsc && eslint --fix --ext .js,.ts . && prettier --write '**/*.{js,ts}'", + "integration:xudt": "npx ts-node shared/prepare-utxo.ts && npx ts-node xudt/xudt-on-ckb/1-issue-xudt.ts && npx ts-node xudt/1-ckb-leap-btc.ts && npx ts-node xudt/2-btc-transfer.ts && npx ts-node xudt/3-btc-leap-ckb.ts" + }, + "dependencies": { + "@nervosnetwork/ckb-sdk-utils": "^0.109.1", + "rgbpp": "workspace:*", + "zx": "^8.0.2" + }, + "devDependencies": { + "@types/node": "^20.11.28", + "typescript": "^5.4.2", + "dotenv": "^16.4.5", + "@types/dotenv": "^8.2.0" + } +} diff --git a/tests/rgbpp/shared/prepare-utxo.ts b/tests/rgbpp/shared/prepare-utxo.ts new file mode 100644 index 00000000..582917d5 --- /dev/null +++ b/tests/rgbpp/shared/prepare-utxo.ts @@ -0,0 +1,57 @@ +import { sendBtc } from 'rgbpp/btc'; +import { getFastestFeeRate, writeStepLog } from './utils'; +import { BtcAssetsApiError } from 'rgbpp/service'; +import { btcAddress, btcDataSource, btcKeyPair, btcService } from '../env'; + +const prepareUtxo = async () => { + const feeRate = await getFastestFeeRate(); + console.log('feeRate = ', feeRate); + console.log(btcAddress); + + // Send BTC tx + const psbt = await sendBtc({ + from: btcAddress!, + tos: [ + { + address: btcAddress!, + value: 546, + minUtxoSatoshi: 546, + }, + ], + feeRate: feeRate, + source: btcDataSource, + }); + + // Sign & finalize inputs + psbt.signAllInputs(btcKeyPair); + psbt.finalizeAllInputs(); + + // Broadcast transaction + const tx = psbt.extractTransaction(); + console.log(tx.toHex()); + + const { txid: btcTxId } = await btcService.sendBtcTransaction(tx.toHex()); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); + + writeStepLog('0', { + txid: btcTxId, + index: 0, + }); + + const interval = setInterval(async () => { + try { + console.log('Waiting for BTC tx to be confirmed'); + const tx = await btcService.getBtcTransaction(btcTxId); + if (tx.status.confirmed) { + clearInterval(interval); + console.info(`Utxo is confirmed ${btcTxId}:0`); + } + } catch (error) { + if (!(error instanceof BtcAssetsApiError)) { + console.error(error); + } + } + }, 20 * 1000); +}; + +prepareUtxo(); diff --git a/tests/rgbpp/shared/utils.ts b/tests/rgbpp/shared/utils.ts new file mode 100644 index 00000000..79394338 --- /dev/null +++ b/tests/rgbpp/shared/utils.ts @@ -0,0 +1,25 @@ +import 'dotenv/config'; +import * as fs from 'fs'; +import * as path from 'path'; +import { btcService } from '../env'; + +export const network = 'testnet'; + +export async function getFastestFeeRate() { + const fees = await btcService.getBtcRecommendedFeeRates(); + return Math.ceil(fees.fastestFee * 3); +} + +export async function writeStepLog(step: string, data: string | object) { + const file = path.join(__dirname, `../${network}/step-${step}.log`); + if (typeof data !== 'string') { + data = JSON.stringify(data); + } + + fs.writeFileSync(file, data); +} + +export function readStepLog(step: string) { + const file = path.join(__dirname, `../${network}/step-${step}.log`); + return JSON.parse(fs.readFileSync(file).toString()); +} diff --git a/tests/rgbpp/testnet/.gitkeep b/tests/rgbpp/testnet/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/rgbpp/tsconfig.json b/tests/rgbpp/tsconfig.json new file mode 100644 index 00000000..b5f09128 --- /dev/null +++ b/tests/rgbpp/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2015", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "NodeNext", + "composite": false, + "resolveJsonModule": true, + "strictNullChecks": true, + "noEmit": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "NodeNext", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true + }, + "exclude": ["node_modules"] +} diff --git a/tests/rgbpp/xudt/1-ckb-leap-btc.ts b/tests/rgbpp/xudt/1-ckb-leap-btc.ts new file mode 100644 index 00000000..93401b91 --- /dev/null +++ b/tests/rgbpp/xudt/1-ckb-leap-btc.ts @@ -0,0 +1,54 @@ +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genCkbJumpBtcVirtualTx } from 'rgbpp'; +import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; +import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress } from '../env'; +import { readStepLog } from '../shared/utils'; + +interface LeapToBtcParams { + outIndex: number; + btcTxId: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmount }: LeapToBtcParams) => { + const { retry } = await import('zx'); + await retry(20, '10s', async () => { + const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); + + // Warning: Please replace with your real xUDT type script here + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: xudtTypeArgs, + }; + + const ckbRawTx = await genCkbJumpBtcVirtualTx({ + collector, + fromCkbAddress: ckbAddress, + toRgbppLockArgs, + xudtTypeBytes: serializeScript(xudtType), + transferAmount, + }); + + const emptyWitness = { lock: '', inputType: '', outputType: '' }; + const unsignedTx: CKBComponents.RawTransactionToSign = { + ...ckbRawTx, + cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(false)], + 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 asset has been jumped from CKB to BTC and 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('0').index, + btcTxId: readStepLog('0').txid, + xudtTypeArgs: readStepLog('1').args, + transferAmount: BigInt(800_0000_0000), +}); diff --git a/tests/rgbpp/xudt/2-btc-transfer.ts b/tests/rgbpp/xudt/2-btc-transfer.ts new file mode 100644 index 00000000..884697eb --- /dev/null +++ b/tests/rgbpp/xudt/2-btc-transfer.ts @@ -0,0 +1,85 @@ +import { buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcTransferCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { isMainnet, collector, btcAddress, btcKeyPair, btcService, btcDataSource } from '../env'; +import { readStepLog, writeStepLog } from '../shared/utils'; + +interface RgbppTransferParams { + rgbppLockArgsList: string[]; + toBtcAddress: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const transfer = async ({ rgbppLockArgsList, toBtcAddress, xudtTypeArgs, transferAmount }: RgbppTransferParams) => { + const { retry } = await import('zx'); + await retry(120, '10s', async () => { + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: xudtTypeArgs, + }; + + const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(xudtType), + transferAmount, + isMainnet, + }); + + const { commitment, ckbRawTx } = ckbVirtualTxResult; + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [toBtcAddress], + ckbCollector: collector, + from: btcAddress!, + source: btcDataSource, + }); + psbt.signAllInputs(btcKeyPair); + psbt.finalizeAllInputs(); + + const btcTx = psbt.extractTransaction(); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); + + console.log('BTC TxId: ', btcTxId); + console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); + + writeStepLog('2', { + 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 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('0').index, readStepLog('0').txid)], + toBtcAddress: 'tb1qtt2vh9q8xam35xxsy35ec6majad8lz8fep8w04', + xudtTypeArgs: readStepLog('1').args, + transferAmount: BigInt(500_0000_0000), +}); diff --git a/tests/rgbpp/xudt/3-btc-leap-ckb.ts b/tests/rgbpp/xudt/3-btc-leap-ckb.ts new file mode 100644 index 00000000..578dafcc --- /dev/null +++ b/tests/rgbpp/xudt/3-btc-leap-ckb.ts @@ -0,0 +1,80 @@ +import { buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcJumpCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { isMainnet, collector, btcAddress, btcKeyPair, btcService, btcDataSource } from '../env'; +import { readStepLog } from '../shared/utils'; + +interface LeapToCkbParams { + rgbppLockArgsList: string[]; + toCkbAddress: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, transferAmount }: LeapToCkbParams) => { + const { retry } = await import('zx'); + await retry(120, '10s', async () => { + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: xudtTypeArgs, + }; + + const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(xudtType), + transferAmount, + toCkbAddress, + isMainnet, + }); + + const { commitment, ckbRawTx } = ckbVirtualTxResult; + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [btcAddress!], + ckbCollector: collector, + from: btcAddress!, + source: btcDataSource, + }); + psbt.signAllInputs(btcKeyPair); + psbt.finalizeAllInputs(); + + const btcTx = psbt.extractTransaction(); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); + + console.log('BTC 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 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('2').index, readStepLog('2').txid)], + toCkbAddress: 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqq9kxr7vy7yknezj0vj0xptx6thk6pwyr0sxamv6q', + xudtTypeArgs: readStepLog('1').args, + transferAmount: BigInt(300_0000_0000), +}); diff --git a/tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts b/tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts new file mode 100644 index 00000000..5430e4b7 --- /dev/null +++ b/tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts @@ -0,0 +1,119 @@ +import { addressToScript, getTransactionSize, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'; +import { + getSecp256k1CellDep, + RgbppTokenInfo, + NoLiveCellError, + calculateUdtCellCapacity, + MAX_FEE, + MIN_CAPACITY, + getXudtTypeScript, + append0x, + getUniqueTypeScript, + u128ToLe, + encodeRgbppTokenInfo, + getXudtDep, + getUniqueTypeDep, + SECP256K1_WITNESS_LOCK_SIZE, + calculateTransactionFee, + generateUniqueTypeArgs, + calculateXudtTokenInfoCellCapacity, +} from 'rgbpp/ckb'; +import { CKB_PRIVATE_KEY, ckbAddress, collector, isMainnet } from '../../env'; +import { writeStepLog } from '../../shared/utils'; + +/** + * issueXudt can be used to issue xUDT assets with unique cell as token info cell. + * @param xudtTotalAmount The xudtTotalAmount specifies the total amount of asset issuance + * @param tokenInfo The xUDT token info which includes decimal, name and symbol + */ +const issueXudt = async ({ xudtTotalAmount, tokenInfo }: { xudtTotalAmount: bigint; tokenInfo: RgbppTokenInfo }) => { + const issueLock = addressToScript(ckbAddress); + + let emptyCells = await collector.getCells({ + lock: issueLock, + }); + if (!emptyCells || emptyCells.length === 0) { + throw new NoLiveCellError('The address has no empty cells'); + } + emptyCells = emptyCells.filter((cell) => !cell.output.type); + + const xudtCapacity = calculateUdtCellCapacity(issueLock); + const xudtInfoCapacity = calculateXudtTokenInfoCellCapacity(tokenInfo, issueLock); + + const txFee = MAX_FEE; + const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, xudtCapacity + xudtInfoCapacity, txFee, { + minCapacity: MIN_CAPACITY, + }); + + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: append0x(scriptToHash(issueLock)), + }; + + console.log('xUDT type script', xudtType); + + writeStepLog('1', { + codeHash: xudtType.codeHash, + hashType: xudtType.hashType, + args: xudtType.args, + }); + + let changeCapacity = sumInputsCapacity - xudtCapacity - xudtInfoCapacity; + const outputs: CKBComponents.CellOutput[] = [ + { + lock: issueLock, + type: xudtType, + capacity: append0x(xudtCapacity.toString(16)), + }, + { + lock: issueLock, + type: { + ...getUniqueTypeScript(isMainnet), + args: generateUniqueTypeArgs(inputs[0], 1), + }, + capacity: append0x(xudtInfoCapacity.toString(16)), + }, + { + lock: issueLock, + capacity: append0x(changeCapacity.toString(16)), + }, + ]; + const totalAmount = xudtTotalAmount * BigInt(10 ** tokenInfo.decimal); + const outputsData = [append0x(u128ToLe(totalAmount)), encodeRgbppTokenInfo(tokenInfo), '0x']; + + const emptyWitness = { lock: '', inputType: '', outputType: '' }; + const witnesses = inputs.map((_, index) => (index === 0 ? emptyWitness : '0x')); + + const cellDeps = [getSecp256k1CellDep(isMainnet), getUniqueTypeDep(isMainnet), getXudtDep(isMainnet)]; + + const unsignedTx = { + version: '0x0', + cellDeps, + headerDeps: [], + inputs, + outputs, + outputsData, + witnesses, + }; + + if (txFee === MAX_FEE) { + const txSize = getTransactionSize(unsignedTx) + SECP256K1_WITNESS_LOCK_SIZE; + const estimatedTxFee = calculateTransactionFee(txSize); + changeCapacity -= estimatedTxFee; + unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); + } + + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); + const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); + + console.info(`xUDT asset on CKB has been issued and tx hash is ${txHash}`); + console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); +}; + +const XUDT_TOKEN_INFO: RgbppTokenInfo = { + decimal: 8, + name: 'XUDT Test Token', + symbol: 'PDD', +}; + +issueXudt({ xudtTotalAmount: BigInt(2100_0000), tokenInfo: XUDT_TOKEN_INFO });