diff --git a/.changeset/changelog-format.cjs b/.changeset/changelog-format.cjs new file mode 100644 index 00000000..3bbdb0df --- /dev/null +++ b/.changeset/changelog-format.cjs @@ -0,0 +1,130 @@ +// @ts-check +/** + * Based on the following format: + * - https://github.com/changesets/changesets/blob/main/packages/changelog-github/src/index.ts + * - https://github.com/stylelint/stylelint/blob/main/.changeset/changelog-stylelint.cjs + */ + +const { getInfo, getInfoFromPullRequest } = require('@changesets/get-github-info'); + +/** + * @type {import('@changesets/types').ChangelogFunctions} + */ +const changelogFunctions = { + async getReleaseLine(changeset, _type, options) { + if (!options || !options.repo) { + throw new Error( + 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]', + ); + } + + /** + * @type {number | undefined} + */ + let prFromSummary; + /** + * @type {string | undefined} + */ + let commitFromSummary; + /** + * @type {string[]} + */ + let usersFromSummary = []; + + const replacedChangelog = changeset.summary + .replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => { + let num = Number(pr); + if (!isNaN(num)) { + prFromSummary = num; + } + return ''; + }) + .replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => { + commitFromSummary = commit; + return ''; + }) + .replace(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => { + usersFromSummary.push(user); + return ''; + }) + .trim(); + + const [firstLine, ...futureLines] = replacedChangelog + .split('\n') + .map((l) => l.trimRight()); + + const links = await (async () => { + if (prFromSummary !== undefined) { + let { links } = await getInfoFromPullRequest({ + repo: options.repo, + pull: prFromSummary, + }); + if (commitFromSummary) { + const shortCommitId = commitFromSummary.slice(0, 7); + links = { + ...links, + commit: `[\`${shortCommitId}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`, + }; + } + return links; + } + const commitToFetchFrom = commitFromSummary || changeset.commit; + if (commitToFetchFrom) { + let { links } = await getInfo({ + repo: options.repo, + commit: commitToFetchFrom, + }); + return links; + } + return { + commit: null, + pull: null, + user: null, + }; + })(); + + const users = usersFromSummary.length + ? usersFromSummary + .map((userFromSummary) => `[@${userFromSummary}](https://github.com/${userFromSummary})`) + .join(', ') + : links.user; + + const pull = links.pull !== null ? links.pull : ''; + const commit = links.commit !== null ? links.commit : ''; + const prefix = pull || commit ? `${pull || commit}:` : ''; + const mention = users !== null ? `(${users})` : users; + const fullFirstLine = `${prefix} ${firstLine} ${mention}`; + const futureLinesText = futureLines.map((l) => ` ${l}`).join('\n'); + + return `\n\n - ${fullFirstLine}\n${futureLinesText}`; + }, + async getDependencyReleaseLine(changesets, deps, options) { + if (!options.repo) { + throw new Error( + 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]', + ); + } + if (deps.length === 0) { + return ''; + } + + const commits = await Promise.all( + changesets.map(async (cs) => { + if (cs.commit) { + let { links } = await getInfo({ + repo: options.repo, + commit: cs.commit, + }); + return links.commit; + } + }), + ); + + const changesetLink = `- Updated dependencies [${commits.join(', ')}]:`; + const updatedDeps = deps.map((dep) => ` - ${dep.name}@${dep.newVersion}`); + + return [changesetLink, ...updatedDeps].join('\n'); + }, +}; + +module.exports = changelogFunctions; diff --git a/.changeset/config.json b/.changeset/config.json index cc1698ea..ad84d74d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "ckb-cell/rgbpp-sdk" }], + "changelog": ["./changelog-format.cjs", { "repo": "ckb-cell/rgbpp-sdk" }], "commit": false, - "fixed": [["@rgbpp-sdk/*"]], + "fixed": [["@rgbpp-sdk/*", "rgbpp"]], "linked": [], "access": "public", - "baseBranch": "main", + "baseBranch": "origin/main", "updateInternalDependencies": "patch", "ignore": [] } 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/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index fb82303c..948394c8 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -6,7 +6,6 @@ on: workflow_dispatch: push: branches: - - main - develop concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -47,22 +46,28 @@ jobs: - name: Install dependencies run: pnpm i - - name: Clear cache - run: pnpm run clean:packages - - name: Build packages run: pnpm run build:packages - - name: Version packages + - name: Add snapshot changeset (ensure at least has a changeset) + run: | + cat << EOF > ".changeset/snap-release-changeset.md" + --- + "@rgbpp-sdk/btc": patch + --- + Add temp changeset for snapshot releases + EOF + + - name: Version packages to "0.0.0-snap-{timestamp}" run: npx changeset version --snapshot snap env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish to npm (ignore GitHub) + - name: Publish to npm id: changesets uses: changesets/action@v1 with: - publish: npx changeset publish --snapshot --tag snap + publish: npx changeset publish --no-git-tag --snapshot --tag snap createGithubReleases: false env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5240d084..1fb20fed 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -52,6 +52,6 @@ jobs: - name: Run tests for packages run: pnpm run test:packages env: - VITE_SERVICE_URL: ${{ secrets.SERVICE_URL }} - VITE_SERVICE_TOKEN: ${{ secrets.SERVICE_TOKEN }} - VITE_SERVICE_ORIGIN: ${{ secrets.SERVICE_ORIGIN }} + VITE_BTC_SERVICE_URL: ${{ secrets.SERVICE_URL }} + VITE_BTC_SERVICE_TOKEN: ${{ secrets.SERVICE_TOKEN }} + VITE_BTC_SERVICE_ORIGIN: ${{ secrets.SERVICE_ORIGIN }} diff --git a/.gitignore b/.gitignore index dfd9f2f7..f3c703cd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ .env.development.local .env.production.local .env.test.local +.env **/.npmrc # logs @@ -40,3 +41,4 @@ yarn-error.log* devbox.json devbox.lock .envrc + diff --git a/README.md b/README.md index b49a837b..8a342530 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository offers utilities for Bitcoin and RGB++ asset integration. - [@rgbpp-sdk/btc](./packages/btc): Bitcoin part of the SDK - [@rgbpp-sdk/ckb](./packages/ckb): Nervos CKB part of the SDK -- [@rgbpp-sdk/service](./packages/service): A wrapped class to interact with `Bitcoin/RGB++ Assets Service` +- [@rgbpp-sdk/service](./packages/service): Wrapped interfaces of `Bitcoin/RGB++ Assets Service` ## RGB++ Code Examples @@ -15,7 +15,7 @@ This repository offers utilities for Bitcoin and RGB++ asset integration. ## Related CKB Scripts (Contracts) - [CKB Bitcoin SPV Type Script](https://github.com/ckb-cell/ckb-bitcoin-spv-contracts/tree/master/contracts/ckb-bitcoin-spv-type-lock): A [type script](https://docs.nervos.org/docs/basics/glossary#type-script) for [Bitcoin SPV](https://bitcoinwiki.org/wiki/simplified-payment-verification) clients which synchronize [Bitcoin](https://bitcoin.org) state into [CKB](https://github.com/nervosnetwork/ckb) -- [RgbppLockScript and BtcTimeLockScript](https://github.com/ckb-cell/rgbpp-sdk/blob/cf25ea014d4e0fc24723df8eea8bd61f59e1060a/packages/ckb/src/constants/index.ts#L11-L121) +- [RgbppLockScript and BtcTimeLockScript](https://github.com/ckb-cell/rgbpp-sdk/blob/63df2dcd95b1b735b5d235e156e4361a3c87b0ac/packages/ckb/src/constants/index.ts#L12-L206) * design: https://github.com/ckb-cell/RGBPlusPlus-design/blob/main/docs/light-paper-en.md * testnet: https://pudge.explorer.nervos.org/scripts#RGB++ * mainnet: https://explorer.nervos.org/scripts#RGB++ @@ -36,29 +36,35 @@ This repository offers utilities for Bitcoin and RGB++ asset integration. 2. **[BTC → CKB](https://github.com/ckb-cell/rgbpp-sdk/blob/develop/packages/ckb/README.md#rgb-spore-leap-from-btc-to-ckb)** 3. **[CKB → BTC](https://github.com/ckb-cell/rgbpp-sdk/blob/develop/packages/ckb/README.md#rgb-spore-leap-from-ckb-to-btc)** *(isomorphic rgbpp_btc_tx is not required in this workflow)* - > [!IMPORTANT] + > [!IMPORTANT] > It's recommended to save the `rgbpp_ckb_tx_virtual` locally in case you need it in the future. 2. **Creation of `rgbpp_btc_tx` through [@rgbpp-sdk/btc](https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/btc)** - 1. construct isomorphic rgbpp_btc_tx based on rgbpp_ckb_tx_virtual and rgbpp commitment - 2. sign and broadcast rgbpp_btc_tx to obtain `rgbpp_btc_txid` + 1. construct isomorphic `rgbpp_btc_tx` based on `rgbpp_ckb_tx_virtual` and rgbpp commitment + 2. sign and broadcast `rgbpp_btc_tx` to obtain `rgbpp_btc_txid` 3. JoyID or dApp sends `rgbpp_btc_txid` and `rgbpp_ckb_tx_virtual` to RGB++ CKB transaction Queue (API Endpoint: `/rgbpp/v1/transaction/ckb-tx`) 4. `RGB++ CKB transaction Queue` will process the following things: 1. **verify** the received requests 2. continuously fetch request from the queue through a **cron job** - 3. check whether the **confirmations** of req.rgbpp_btc_txid is sufficient - 4. generate the **witnesses for RgbppLocks** in the rgbpp_ckb_tx_virtual - 5. add a **paymaster cell** into rgbpp_ckb_tx_virtual.inputs if the CKB capacity is insufficient + 3. check whether the **confirmations** of `req.rgbpp_btc_txid` is sufficient + 4. generate the **witnesses for RgbppLocks** in the `rgbpp_ckb_tx_virtual` + 5. add a **paymaster cell** into `rgbpp_ckb_tx_virtual`.inputs if the CKB capacity is insufficient 1. need to **verify the existence of paymaster UTXO** in the rgbpp_btc_tx 2. sign the paymaster cell and the entire transaction if needed - 6. **finalize** the rgbpp_ckb_tx_virtual to a rgbpp_ckb_tx - 7. **broadcast** rgbpp_ckb_tx and mark the job as completed upon tx-confirmation + 6. **finalize** the `rgbpp_ckb_tx_virtual` to a `rgbpp_ckb_tx` + 7. **broadcast** `rgbpp_ckb_tx` and mark the job as completed upon tx-confirmation ### Notes -- The RGB++ CKB transaction Queue is designed to streamline the transaction workflow. Developers have the option to implement its features by themselves without limitation. +- `Bitcoin/RGB++ Assets Service` is designed to streamline the transaction workflow. Developers have the option to implement its features by themselves without limitation. + + +## FAQ + +### How to get an access token of Bitcoin/RGB++ Assets Service? +See [Generate a JSON Web Token (JWT) for Bitcoin/RGB++ Assets Service](./packages/service/README.md#get-an-access-token) ## License diff --git a/apps/next/.env.example b/apps/next/.env.example index 126ccc3d..22e91499 100644 --- a/apps/next/.env.example +++ b/apps/next/.env.example @@ -1,2 +1,2 @@ -NEXT_PUBLIC_SERVICE_URL= -NEXT_PUBLIC_SERVICE_TOKEN= +NEXT_PUBLIC_BTC_SERVICE_URL= +NEXT_PUBLIC_BTC_SERVICE_TOKEN= diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx index 10cdf1b4..f68107e2 100644 --- a/apps/next/src/app/page.tsx +++ b/apps/next/src/app/page.tsx @@ -7,8 +7,8 @@ export default function Home() { const networkType = NetworkType.TESTNET; const service = BtcAssetsApi.fromToken( - process.env.NEXT_PUBLIC_SERVICE_URL!, - process.env.NEXT_PUBLIC_SERVICE_TOKEN!, + process.env.NEXT_PUBLIC_BTC_SERVICE_URL!, + process.env.NEXT_PUBLIC_BTC_SERVICE_TOKEN!, ); const source = new DataSource(service, networkType); diff --git a/apps/vite/.env.example b/apps/vite/.env.example index 2b514fe0..23e23fcf 100644 --- a/apps/vite/.env.example +++ b/apps/vite/.env.example @@ -1,2 +1,2 @@ -VITE_SERVICE_URL= -VITE_SERVICE_TOKEN= \ No newline at end of file +VITE_BTC_SERVICE_URL= +VITE_BTC_SERVICE_TOKEN= diff --git a/apps/vite/package.json b/apps/vite/package.json index 7c8824b2..c2c54150 100644 --- a/apps/vite/package.json +++ b/apps/vite/package.json @@ -27,7 +27,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "typescript": "^5.2.2", - "vite": "^5.1.4", + "vite": "^5.2.11", "vite-plugin-node-polyfills": "^0.21.0" } } diff --git a/apps/vite/src/App.tsx b/apps/vite/src/App.tsx index 9a28c179..02edecef 100644 --- a/apps/vite/src/App.tsx +++ b/apps/vite/src/App.tsx @@ -6,7 +6,10 @@ function App() { async function send() { const networkType = NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(import.meta.env.VITE_SERVICE_URL!, import.meta.env.VITE_SERVICE_TOKEN!); + const service = BtcAssetsApi.fromToken( + import.meta.env.VITE_BTC_SERVICE_URL!, + import.meta.env.VITE_BTC_SERVICE_TOKEN!, + ); const source = new DataSource(service, networkType); const psbt = await sendBtc({ diff --git a/examples/rgbpp/.env.example b/examples/rgbpp/.env.example new file mode 100644 index 00000000..281dcbcf --- /dev/null +++ b/examples/rgbpp/.env.example @@ -0,0 +1,29 @@ +# True for CKB and BTC Mainnet and false for CKB and BTC Testnet, the default value is false +IS_MAINNET=false + +# CKB Variables + +# The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix +CKB_SECP256K1_PRIVATE_KEY=0x-private-key + +# CKB node url which should be matched with IS_MAINNET +CKB_NODE_URL=https://testnet.ckb.dev/rpc + +# CKB indexer url which should be matched with IS_MAINNET +CKB_INDEXER_URL=https://testnet.ckb.dev/indexer + + +# BTC Variables + +# The BTC private key whose format is 32bytes hex string without 0x prefix +BTC_PRIVATE_KEY=private-key + +# The BTC assets api url which should be matched with IS_MAINNET +VITE_BTC_SERVICE_URL=https://btc-assets-api.testnet.mibao.pro + +# The BTC assets api token which should be matched with IS_MAINNET +# To get an access token, please refer to https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service#get-an-access-token +VITE_BTC_SERVICE_TOKEN= + +# The BTC assets api origin which should be matched with IS_MAINNET +VITE_BTC_SERVICE_ORIGIN=https://btc-test.app diff --git a/examples/rgbpp/README.md b/examples/rgbpp/README.md index ce8d018d..af072c7d 100644 --- a/examples/rgbpp/README.md +++ b/examples/rgbpp/README.md @@ -1,147 +1,171 @@ # RGB++ Examples -**All examples are just to demonstrate the use of RGB++ SDK.** +- xUDT directory: The examples for RGB++ UDT issuance, transfer, and leap +- Spore directory: The examples for RGB++ Spore creation, transfer and leap -- Local and Queue directories: The examples for RGB++ UDT issuance, transfer, and leap -- Spore directory: The examples for RGB++ spore creation, transfer and leap -- xUDT directory: The examples for xUDT issuance, mint and transfer on CKB +## How to Start -## What you must know about BTC transaction id +Copy the `.env.example` file to `.env`: -**The BTC transaction id(hash) displayed on BTC explorer is different from the BTC transaction id(hash) in RGB++ lock args. They are in reverse byte order.** +```shell +cd examples/rgbpp && cp .env.example .env +``` -We follow the following two rules: +Update the configuration values: -- Whenever you're working with transaction/block hashes **internally** (e.g. inside raw bitcoin data), you use the **natural** byte order. -- Whenever you're **displaying or searching** for transaction/block hashes, you use the **reverse** byte order. +```yaml +# True for CKB and BTC Mainnet and false for CKB and BTC Testnet, the default value is false +IS_MAINNET=false -For detailed rules, please refer to [Byte Order](https://learnmeabitcoin.com/technical/general/byte-order/) +# CKB Variables -For example, the BTC transaction id(hash) of the RGB++ lock args like this: +# The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix +CKB_SECP256K1_PRIVATE_KEY=0x-private-key -``` -4abc778213bc4da692f93745c2b07410ef2bfaee70417784d4ee8969fb258001 -``` +# CKB node url which should be matched with IS_MAINNET +CKB_NODE_URL=https://testnet.ckb.dev/rpc -But when you're searching for this transaction in [Bitcoin Core](https://bitcoin.org/en/bitcoin-core/) or on a block explorer, you'll see this byte order: +# CKB indexer url which should be matched with IS_MAINNET +CKB_INDEXER_URL=https://testnet.ckb.dev/indexer -``` -018025fb6989eed484774170eefa2bef1074b0c24537f992a64dbc138277bc4a -``` +# BTC Variables -## xUDT on CKB Examples +# The BTC private key whose format is 32bytes hex string without 0x prefix +BTC_PRIVATE_KEY=private-key -### Issue xUDT on CKB +# The BTC assets api url which should be matched with IS_MAINNET +VITE_BTC_SERVICE_URL=https://btc-assets-api.testnet.mibao.pro; -```shell -npx ts-node examples/rgbpp/xudt/1-issue-xudt.ts -``` +# The BTC assets api token which should be matched with IS_MAINNET +# To get an access token, please refer to https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service#get-an-access-token +VITE_BTC_SERVICE_TOKEN=; -### Mint/Transfer xUDT on CKB +# The BTC assets api origin which should be matched with IS_MAINNET +VITE_BTC_SERVICE_ORIGIN=https://btc-test.app; +``` -You can use this command to mint or transfer xUDT assets -```shell -npx ts-node examples/rgbpp/xudt/2-transfer-xudt.ts -``` +## RGB++ xUDT Examples -## RGB++ xUDT Examples with Queue service(Recommended) +### RGB++ xUDT Launch on BTC -### Leap xUDT from CKB to BTC +#### 1. Prepare Launch ```shell -npx ts-node examples/rgbpp/queue/1-ckb-jump-btc.ts +npx ts-node xudt/launch/1-prepare-launch.ts ``` - -### Transfer RGB++ xUDT on BTC +#### 2. Launch RGB++ xUDT on BTC ```shell -npx ts-node examples/rgbpp/queue/2-btc-transfer.ts +npx ts-node xudt/launch/2-launch-rgbpp.ts ``` - -### Leap RGB++ xUDT from BTC to CKB +#### 3. Distribute RGB++ xUDT on BTC ```shell -npx ts-node examples/rgbpp/queue/3-btc-jump-ckb.ts +npx ts-node xudt/launch/3-distribute-rgbpp.ts ``` -### Unlock xUDT BTC time cells on CKB - -A cron job in RGB++ Queue service will construct a transaction unlocking the mature BTC time cells to the their `target_ckb_address`. +### RGB++ xUDT Transfer and Leap - -## RGB++ xUDT Local Examples - -### Leap RGB++ xUDT from CKB to BTC +#### 1. Leap xUDT from CKB to BTC ```shell -npx ts-node examples/rgbpp/local/1-ckb-jump-btc.ts +npx ts-node xudt/1-ckb-leap-btc.ts ``` -### Transfer RGB++ xUDT on BTC +#### 2. Transfer RGB++ xUDT on BTC with Queue Service ```shell -npx ts-node examples/rgbpp/local/2-btc-transfer.ts +npx ts-node xudt/2-btc-transfer.ts ``` -### Leap RGB++ xUDT from BTC to CKB +#### 3. Leap RGB++ xUDT from BTC to CKB with Queue Service ```shell -npx ts-node examples/rgbpp/local/3-btc-jump-ckb.ts +npx ts-node xudt/3-btc-leap-ckb.ts ``` -### Unlock xUDT BTC time cells on CKB +#### 4. Unlock xUDT BTC time cells on CKB -**Warning: Wait at least 6 BTC confirmation blocks to unlock the BTC time cells after 4-btc-jump-ckb.ts** +A cron job in RGB++ Queue service will construct a transaction unlocking the mature BTC time cells to the their `target_ckb_address`. + +However, you can still manually unlock the spore BTC time cell through the following command + +Warning: Wait at least 6 BTC confirmation blocks to unlock the BTC time cells after 3-btc-leap-ckb.ts ```shell -npx ts-node examples/rgbpp/local/4-spend-btc-time-cell.ts +npx ts-node xudt/4-unlock-btc-time.ts ``` ## RGB++ Spore Examples -**You can use RGB++ Queue service to complete spore transfer and leap, and the examples can be found in `examples/rgbpp/spore/queue`** +### RGB++ Spores Launch on BTC -### Create RGB++ Cluster Cell +#### 1. Create RGB++ Cluster Cell ```shell -npx ts-node examples/rgbpp/spore/1-prepare-cluster.ts +npx ts-node spore/launch/1-prepare-cluster.ts -npx ts-node examples/rgbpp/spore/2-create-cluster.ts +npx ts-node spore/launch/2-create-cluster.ts ``` -### Create RGB++ Spores with Cluster on BTC +#### 2. Create RGB++ Spores with Cluster on BTC ```shell -npx ts-node examples/rgbpp/spore/3-create-spores.ts +npx ts-node spore/launch/3-create-spores.ts ``` -### Transfer RGB++ Spore on BTC +### Transfer and Leap Spore + +#### 1. Transfer RGB++ Spore on BTC with Queue Service ```shell -npx ts-node examples/rgbpp/spore/4-transfer-spore.ts +npx ts-node spore/4-transfer-spore.ts ``` -### Leap RGB++ Spore from BTC to CKB +#### 2. Leap RGB++ Spore from BTC to CKB ```shell -npx ts-node examples/rgbpp/spore/5-leap-spore-to-ckb.ts +npx ts-node spore/5-leap-spore-to-ckb.ts ``` -### Unlock Spore BTC time cells on CKB +#### 3. Unlock Spore BTC time cells on CKB A cron job in RGB++ Queue service will construct a transaction unlocking the mature BTC time cells to the their `target_ckb_address`. -However, you can still manually unlock the spore btc time cell through the following command +However, you can still manually unlock the spore BTC time cell through the following command **Warning: Wait at least 6 BTC confirmation blocks to unlock the BTC time cells after 5-leap-spore-to-ckb.ts** ```shell -npx ts-node examples/rgbpp/spore/6-unlock-btc-time-cell.ts +npx ts-node spore/6-unlock-btc-time-cell.ts ``` -### Leap Spore from CKB to BTC +#### 4. Leap Spore from CKB to BTC ```shell -npx ts-node examples/rgbpp/spore/7-leap-spore-to-btc.ts -``` \ No newline at end of file +npx ts-node spore/7-leap-spore-to-btc.ts +``` + +## What you must know about BTC transaction id + +**The BTC transaction id(hash) displayed on BTC explorer is different from the BTC transaction id(hash) in RGB++ lock args. They are in reverse byte order.** + +We follow the following two rules: + +- Whenever you're working with transaction/block hashes **internally** (e.g. inside raw bitcoin data), you use the **natural** byte order. +- Whenever you're **displaying or searching** for transaction/block hashes, you use the **reverse** byte order. + +For detailed rules, please refer to [Byte Order](https://learnmeabitcoin.com/technical/general/byte-order/) + +For example, the BTC transaction id(hash) of the RGB++ lock args like this: + +``` +4abc778213bc4da692f93745c2b07410ef2bfaee70417784d4ee8969fb258001 +``` + +But when you're searching for this transaction in [Bitcoin Core](https://bitcoin.org/en/bitcoin-core/) or on a block explorer, you'll see this byte order: + +``` +018025fb6989eed484774170eefa2bef1074b0c24537f992a64dbc138277bc4a +``` diff --git a/examples/rgbpp/env.ts b/examples/rgbpp/env.ts new file mode 100644 index 00000000..fd50a805 --- /dev/null +++ b/examples/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 = process.env.IS_MAINNET === 'true' ? true : false; + +export const collector = new Collector({ + ckbNodeUrl: process.env.CKB_NODE_URL!, + ckbIndexerUrl: process.env.CKB_INDEXER_URL!, +}); +export const CKB_PRIVATE_KEY = process.env.CKB_SECP256K1_PRIVATE_KEY!; +export const ckbAddress = privateKeyToAddress(CKB_PRIVATE_KEY, { + prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, +}); + +export const BTC_PRIVATE_KEY = process.env.BTC_PRIVATE_KEY!; +export const BTC_SERVICE_URL = process.env.VITE_BTC_SERVICE_URL!; +export const BTC_SERVICE_TOKEN = process.env.VITE_BTC_SERVICE_TOKEN!; +export const BTC_SERVICE_ORIGIN = process.env.VITE_BTC_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/examples/rgbpp/local/1-ckb-jump-btc.ts b/examples/rgbpp/local/1-ckb-jump-btc.ts deleted file mode 100644 index 9255c91d..00000000 --- a/examples/rgbpp/local/1-ckb-jump-btc.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { - genCkbJumpBtcVirtualTx, - Collector, - getSecp256k1CellDep, - buildRgbppLockArgs, - getXudtTypeScript, -} from '@rgbpp-sdk/ckb'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -const jumpFromCkbToBtc = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - - const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); - - // Warning: Please replace with your real xUDT type script here - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - const ckbRawTx = await genCkbJumpBtcVirtualTx({ - collector, - fromCkbAddress: address, - toRgbppLockArgs, - xudtTypeBytes: serializeScript(xudtType), - transferAmount: BigInt(800_0000_0000), - }); - - 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_TEST_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}`); -}; - -// Use your real BTC UTXO information on the BTC Testnet -jumpFromCkbToBtc({ - outIndex: 1, - btcTxId: '4ff1855b64b309afa19a8b9be3d4da99dcb18b083b65d2d851662995c7d99e7a', -}); diff --git a/examples/rgbpp/local/4-spend-btc-time-cell.ts b/examples/rgbpp/local/4-spend-btc-time-cell.ts deleted file mode 100644 index 45921412..00000000 --- a/examples/rgbpp/local/4-spend-btc-time-cell.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; -import { - Collector, - sendCkbTx, - buildBtcTimeCellsSpentTx, - getBtcTimeLockScript, - signBtcTimeCellSpentTx, -} from '@rgbpp-sdk/ckb'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -// Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 4-btc-jump-ckb.ts -const spendBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - - const btcTimeCells = await collector.getCells({ - lock: { - ...getBtcTimeLockScript(false), - args: btcTimeCellArgs, - }, - isDataMustBeEmpty: false, - }); - - if (!btcTimeCells || btcTimeCells.length === 0) { - throw new Error('No btc time cell found'); - } - - const btcAssetsApi = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - - const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ - btcTimeCells, - btcAssetsApi, - isMainnet, - }); - - const signedTx = await signBtcTimeCellSpentTx({ - secp256k1PrivateKey: CKB_TEST_PRIVATE_KEY, - collector, - masterCkbAddress: address, - ckbRawTx, - isMainnet, - }); - - console.log(JSON.stringify(signedTx)); - - const txHash = await sendCkbTx({ collector, signedTx }); - console.info(`BTC time cell has been spent and tx hash is ${txHash}`); -}; - -// The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 4-btc-jump-ckb.ts CKB transaction -spendBtcTimeCell({ - btcTimeCellArgs: - '0x7f000000100000005b0000005f0000004b000000100000003000000031000000d23761b364210735c19c60561d213fb3beae2fd6172743719eff6920e020baac011600000000016c61f984f12d3c8a4f649e60acda5deda0b8837c060000001c95b9d726e4ab337d6a4572680598947954d7b6ff4f1e767e605eeeec49e7ed', -}); diff --git a/examples/rgbpp/local/batch/1-ckb-jump-btc.ts b/examples/rgbpp/local/batch/1-ckb-jump-btc.ts deleted file mode 100644 index a6565d5b..00000000 --- a/examples/rgbpp/local/batch/1-ckb-jump-btc.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { - Collector, - getSecp256k1CellDep, - buildRgbppLockArgs, - genCkbBatchJumpBtcVirtualTx, - RgbppLockArgsReceiver, - getXudtTypeScript, -} from '@rgbpp-sdk/ckb'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -const batchJumpFromCkbToBtc = async (rgbppReceivers: RgbppLockArgsReceiver[]) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - const ckbRawTx = await genCkbBatchJumpBtcVirtualTx({ - collector, - fromCkbAddress: address, - xudtTypeBytes: serializeScript(xudtType), - rgbppReceivers, - }); - - 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_TEST_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}`); -}; - -// Use your real BTC UTXO information on the BTC Testnet -const rgbppReceivers: RgbppLockArgsReceiver[] = [ - { - // outIndex and btcTxId - toRgbppLockArgs: buildRgbppLockArgs(1, 'bf991a2d6d08efffa089076d59b02bc78479b73c6300e640c148ec660bba0305'), - transferAmount: BigInt(800_0000_0000), - }, - { - // outIndex and btcTxId - toRgbppLockArgs: buildRgbppLockArgs(1, 'bf991a2d6d08efffa089076d59b02bc78479b73c6300e640c148ec660bba0305'), - transferAmount: BigInt(800_0000_0000), - }, -]; -batchJumpFromCkbToBtc(rgbppReceivers); diff --git a/examples/rgbpp/local/transfer-btc.ts b/examples/rgbpp/local/transfer-btc.ts deleted file mode 100644 index be40aa1a..00000000 --- a/examples/rgbpp/local/transfer-btc.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { sendBtc, DataSource, NetworkType, bitcoin, ECPair } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// https://btc-assets-api-develop.vercel.app/docs/static/index.html -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -// This example shows how to transfer BTC on testnet -const transferBtc = async () => { - const network = bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - console.log('btc address: ', btcAddress); - - const networkType = NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, 'https://btc-test.app'); - const source = new DataSource(service, networkType); - - const psbt = await sendBtc({ - from: btcAddress!, // your P2WPKH address - tos: [ - { - address: btcAddress!, // destination btc address - value: 100000, // transfer satoshi amount - }, - ], - feeRate: 1, // optional, default to 1 sat/vbyte - source, - }); - - // Sign & finalize inputs - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - // Broadcast transaction - const tx = psbt.extractTransaction(); - const { txid: txId } = await service.sendBtcTransaction(tx.toHex()); - console.log('txId:', txId); -}; - -transferBtc(); diff --git a/examples/rgbpp/logs/.gitkeep b/examples/rgbpp/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/rgbpp/package.json b/examples/rgbpp/package.json index 7ab70fc4..9131267f 100644 --- a/examples/rgbpp/package.json +++ b/examples/rgbpp/package.json @@ -1,25 +1,22 @@ { "name": "rgbpp-examples", "version": "0.1.0", - "description": "Examples used for RGBPP assets issuance, transfer, and jumping between BTC and CKB", + "description": "Examples used for RGBPP assets issuance, transfer, and leaping between BTC and CKB", "private": true, - "type": "module", + "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}'" }, "dependencies": { - "@exact-realty/multipart-parser": "^1.0.13", "@nervosnetwork/ckb-sdk-utils": "^0.109.1", - "@rgbpp-sdk/btc": "workspace:^", - "@rgbpp-sdk/ckb": "workspace:^", - "@rgbpp-sdk/service": "workspace:^", - "@spore-sdk/core": "^0.2.0-beta.6", - "axios": "^1.6.8" + "rgbpp": "workspace:*" }, "devDependencies": { "@types/node": "^20.11.28", - "typescript": "^5.4.2" + "typescript": "^5.4.2", + "dotenv": "^16.4.5", + "@types/dotenv": "^8.2.0" } } diff --git a/examples/rgbpp/queue/1-ckb-jump-btc.ts b/examples/rgbpp/queue/1-ckb-jump-btc.ts deleted file mode 100644 index ef408c20..00000000 --- a/examples/rgbpp/queue/1-ckb-jump-btc.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { - genCkbJumpBtcVirtualTx, - Collector, - getSecp256k1CellDep, - buildRgbppLockArgs, - getXudtTypeScript, -} from '@rgbpp-sdk/ckb'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -const jumpFromCkbToBtc = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - - const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); - - // Warning: Please replace with your real xUDT type script here - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - const ckbRawTx = await genCkbJumpBtcVirtualTx({ - collector, - fromCkbAddress: address, - toRgbppLockArgs, - xudtTypeBytes: serializeScript(xudtType), - transferAmount: BigInt(800_0000_0000), - }); - - 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_TEST_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}`); -}; - -// Use your real BTC UTXO information on the BTC Testnet -jumpFromCkbToBtc({ - outIndex: 1, - btcTxId: '4ff1855b64b309afa19a8b9be3d4da99dcb18b083b65d2d851662995c7d99e7a', -}); diff --git a/examples/rgbpp/queue/2-btc-transfer.ts b/examples/rgbpp/queue/2-btc-transfer.ts deleted file mode 100644 index 86de53e1..00000000 --- a/examples/rgbpp/queue/2-btc-transfer.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Collector, buildRgbppLockArgs, genBtcTransferCkbVirtualTx, getXudtTypeScript } from '@rgbpp-sdk/ckb'; -import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { sendRgbppUtxos, DataSource, ECPair, bitcoin, NetworkType } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -interface Params { - rgbppLockArgsList: string[]; - toBtcAddress: string; - transferAmount: bigint; -} -const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmount }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // Warning: Please replace with your real xUDT type script here - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - 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, - }); - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - const btcTx = psbt.extractTransaction(); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); - - console.log('BTC TxId: ', btcTxId); - - try { - await service.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); - const interval = setInterval(async () => { - const { state, failedReason } = await service.getRgbppTransactionState(btcTxId); - console.log('state', state); - if (state === 'completed' || state === 'failed') { - clearInterval(interval); - if (state === 'completed') { - const { txhash: txHash } = await service.getRgbppTransactionHash(btcTxId); - console.info(`Rgbpp 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); - } -}; - -// Use your real BTC UTXO information on the BTC Testnet -// rgbppLockArgs: outIndexU32 + btcTxId -transferRgbppOnBtc({ - rgbppLockArgsList: [buildRgbppLockArgs(1, '64252b582aea1249ed969a20385fae48bba35bf1ab9b3df3b0fcddc754ccf592')], - toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', - // To simplify, keep the transferAmount the same as 2-ckb-jump-btc - transferAmount: BigInt(800_0000_0000), -}); diff --git a/examples/rgbpp/queue/3-btc-jump-ckb.ts b/examples/rgbpp/queue/3-btc-jump-ckb.ts deleted file mode 100644 index 3e400c0f..00000000 --- a/examples/rgbpp/queue/3-btc-jump-ckb.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { sendRgbppUtxos, DataSource, NetworkType, bitcoin, ECPair } from '@rgbpp-sdk/btc'; -import { Collector, genBtcJumpCkbVirtualTx, buildRgbppLockArgs, getXudtTypeScript } from '@rgbpp-sdk/ckb'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -interface Params { - rgbppLockArgsList: string[]; - toCkbAddress: string; - transferAmount: bigint; -} -const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmount }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // Warning: Please replace with your real xUDT type script here - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - 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, - }); - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - const btcTx = psbt.extractTransaction(); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); - - console.log('BTC TxId: ', btcTxId); - - try { - await service.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); - const interval = setInterval(async () => { - const { state, failedReason } = await service.getRgbppTransactionState(btcTxId); - console.log('state', state); - if (state === 'completed' || state === 'failed') { - clearInterval(interval); - if (state === 'completed') { - const { txhash: txHash } = await service.getRgbppTransactionHash(btcTxId); - console.info(`Rgbpp asset has been jumped 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); - } -}; - -// rgbppLockArgs: outIndexU32 + btcTxId -jumpFromBtcToCkb({ - // If the `3-btc-transfer.ts` has been executed, the BTC txId should be the new generated BTC txId by the `3-btc-transfer.ts` - // Otherwise the BTC txId should be same as the the BTC txId of the `2-ckb-jump-btc.ts` - rgbppLockArgsList: [buildRgbppLockArgs(1, '6edd4b9327506fab09fb9a0f5e5f35136a6a94bd4c9dd79af04921618fa6c800')], - toCkbAddress: 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqq9kxr7vy7yknezj0vj0xptx6thk6pwyr0sxamv6q', - // To simplify, keep the transferAmount the same as 2-ckb-jump-btc - transferAmount: BigInt(800_0000_0000), -}); diff --git a/examples/rgbpp/queue/no-merge-outputs/2-btc-transfer.ts b/examples/rgbpp/queue/no-merge-outputs/2-btc-transfer.ts deleted file mode 100644 index a07c48d6..00000000 --- a/examples/rgbpp/queue/no-merge-outputs/2-btc-transfer.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { Collector, buildRgbppLockArgs, genBtcTransferCkbVirtualTx, getXudtTypeScript } from '@rgbpp-sdk/ckb'; -import { sendRgbppUtxos, DataSource, ECPair, bitcoin, NetworkType } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -interface Params { - rgbppLockArgsList: string[]; - toBtcAddress: string; - transferAmount: bigint; -} -const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmount }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - const xudtType: CKBComponents.Script = { - ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', - }; - - const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({ - collector, - rgbppLockArgsList, - xudtTypeBytes: serializeScript(xudtType), - transferAmount, - isMainnet, - noMergeOutputCells: true, - }); - - const { commitment, ckbRawTx } = ckbVirtualTxResult; - - // Send BTC tx - const psbt = await sendRgbppUtxos({ - ckbVirtualTx: ckbRawTx, - commitment, - tos: [toBtcAddress], - ckbCollector: collector, - from: btcAddress!, - source, - }); - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - const btcTx = psbt.extractTransaction(); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); - - console.log('BTC TxId: ', btcTxId); - - try { - await service.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); - const interval = setInterval(async () => { - const { state, failedReason } = await service.getRgbppTransactionState(btcTxId); - console.log('state', state); - if (state === 'completed' || state === 'failed') { - clearInterval(interval); - if (state === 'completed') { - const { txhash: txHash } = await service.getRgbppTransactionHash(btcTxId); - console.info(`Rgbpp 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); - } -}; - -// Use your real BTC UTXO information on the BTC Testnet -// rgbppLockArgs: outIndexU32 + btcTxId -transferRgbppOnBtc({ - rgbppLockArgsList: [ - buildRgbppLockArgs(0, '4ff1855b64b309afa19a8b9be3d4da99dcb18b083b65d2d851662995c7d99e7a'), - buildRgbppLockArgs(1, '4ff1855b64b309afa19a8b9be3d4da99dcb18b083b65d2d851662995c7d99e7a'), - ], - toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', - // the transferAmount will be ignored - transferAmount: BigInt(0), -}); diff --git a/examples/rgbpp/shared/utils.ts b/examples/rgbpp/shared/utils.ts new file mode 100644 index 00000000..2c5d2e6f --- /dev/null +++ b/examples/rgbpp/shared/utils.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { + BaseCkbVirtualTxResult, + SporeVirtualTxResult, + SporeCreateVirtualTxResult, + SporeTransferVirtualTxResult, +} from 'rgbpp/ckb'; + +/** + * Save ckbVirtualTxResult to a log file + * @param ckbVirtualTxResult - The ckbVirtualTxResult to save + * @param exampleName - Example name used to distinguish different log files + */ + +export type CkbVirtualTxResultType = + | BaseCkbVirtualTxResult + | SporeVirtualTxResult + | SporeCreateVirtualTxResult + | SporeTransferVirtualTxResult; + +export const saveCkbVirtualTxResult = (ckbVirtualTxResult: CkbVirtualTxResultType, exampleName: string) => { + try { + // Define log file path + const logDir = path.resolve(__dirname, '../logs'); + const timestamp = new Date().toISOString().replace(/:/g, '-'); // Replace colons with hyphens + const logFilePath = path.join(logDir, `${exampleName}-${timestamp}-ckbVirtualTxResult.log`); + + // Ensure the log directory exists + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir); + } + + // Validate and save ckbVirtualTxResult to log file + if (typeof ckbVirtualTxResult === 'object' && ckbVirtualTxResult !== null) { + fs.writeFileSync(logFilePath, JSON.stringify(ckbVirtualTxResult, null, 2)); + console.info(`Saved ckbVirtualTxResult to ${logFilePath}`); + } else { + console.error('Invalid ckbVirtualTxResult format'); + } + + // Remind developers to save the transaction result + console.info( + `Important: It's recommended to save the rgbpp_ckb_tx_virtual locally before the isomorphic transactions are finalized.`, + ); + } catch (error) { + console.error('Failed to save ckbVirtualTxResult:', error); + } +}; diff --git a/examples/rgbpp/spore/0-cluster-info.ts b/examples/rgbpp/spore/0-cluster-info.ts deleted file mode 100644 index 4ad3c354..00000000 --- a/examples/rgbpp/spore/0-cluster-info.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const CLUSTER_DATA = { - name: 'Cluster name', - description: 'Description of the cluster', -}; diff --git a/examples/rgbpp/spore/4-transfer-spore.ts b/examples/rgbpp/spore/4-transfer-spore.ts index 9aeff02d..7646f4fe 100644 --- a/examples/rgbpp/spore/4-transfer-spore.ts +++ b/examples/rgbpp/spore/4-transfer-spore.ts @@ -1,57 +1,20 @@ -import { - Collector, - buildRgbppLockArgs, - appendCkbTxWitnesses, - updateCkbTxWithRealBtcTxId, - sendCkbTx, - getSporeTypeScript, - Hex, - generateSporeTransferCoBuild, - genTransferSporeCkbVirtualTx, -} from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { genTransferSporeCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { getSporeTypeScript, Hex } from 'rgbpp/ckb'; import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../env'; +import { saveCkbVirtualTxResult } from '../shared/utils'; -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -const transferSpore = async ({ - sporeRgbppLockArgs, - toBtcAddress, -}: { +interface SporeTransferParams { sporeRgbppLockArgs: Hex; toBtcAddress: string; -}) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); + sporeTypeArgs: Hex; +} - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cells +const transferSpore = async ({ sporeRgbppLockArgs, toBtcAddress, sporeTypeArgs }: SporeTransferParams) => { const sporeTypeBytes = serializeScript({ ...getSporeTypeScript(isMainnet), - args: '0x205fe15af04e59d3ff1ff8e0b0a1e3bc201af406a38964760c24848ed6029b6b', + args: sporeTypeArgs, }); const ckbVirtualTxResult = await genTransferSporeCkbVirtualTx({ @@ -59,12 +22,12 @@ const transferSpore = async ({ sporeRgbppLockArgs, sporeTypeBytes, isMainnet, - ckbFeeRate: BigInt(5000), }); - const { commitment, ckbRawTx, sporeCell } = ckbVirtualTxResult; + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '4-transfer-spore'); - // console.log(JSON.stringify(ckbRawTx)) + const { commitment, ckbRawTx } = ckbVirtualTxResult; // Send BTC tx const psbt = await sendRgbppUtxos({ @@ -73,51 +36,42 @@ const transferSpore = async ({ tos: [toBtcAddress], ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, feeRate: 30, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); - const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); - const interval = setInterval(async () => { - try { - console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.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, - }); - - // Replace cobuild witness with the final rgbpp lock script - ckbTx.witnesses[ckbTx.witnesses.length - 1] = generateSporeTransferCoBuild([sporeCell], ckbTx.outputs); - - // console.log('ckbTx: ', JSON.stringify(ckbTx)); - - const txHash = await sendCkbTx({ collector, signedTx: ckbTx }); - console.info(`RGB++ Spore has been transferred and tx hash is ${txHash}`); - } catch (error) { - if (!(error instanceof BtcAssetsApiError)) { - console.error(error); + 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 spore 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); + }, 30 * 1000); + } catch (error) { + console.error(error); + } }; // Use your real BTC UTXO information on the BTC Testnet // rgbppLockArgs: outIndexU32 + btcTxId transferSpore({ - // The spore rgbpp lock args is from 3-create-spore.ts - sporeRgbppLockArgs: buildRgbppLockArgs(1, 'f203c8c13eacdbd126f85d286a963c85f233f8145363b1d997c4d552afb990e1'), + sporeRgbppLockArgs: buildRgbppLockArgs(2, 'd5868dbde4be5e49876b496449df10150c356843afb6f94b08f8d81f394bb350'), toBtcAddress: 'tb1qhp9fh9qsfeyh0yhewgu27ndqhs5qlrqwau28m7', + sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', }); diff --git a/examples/rgbpp/spore/5-leap-spore-to-ckb.ts b/examples/rgbpp/spore/5-leap-spore-to-ckb.ts index dfb445d2..1f0ae59e 100644 --- a/examples/rgbpp/spore/5-leap-spore-to-ckb.ts +++ b/examples/rgbpp/spore/5-leap-spore-to-ckb.ts @@ -1,57 +1,20 @@ -import { - Collector, - buildRgbppLockArgs, - appendCkbTxWitnesses, - updateCkbTxWithRealBtcTxId, - sendCkbTx, - getSporeTypeScript, - Hex, - generateSporeTransferCoBuild, - genLeapSporeFromBtcToCkbVirtualTx, -} from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; +import { buildRgbppLockArgs } from 'rgbpp/ckb'; +import { genLeapSporeFromBtcToCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { getSporeTypeScript, Hex } from 'rgbpp/ckb'; import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../env'; +import { saveCkbVirtualTxResult } from '../shared/utils'; -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -const transferSpore = async ({ - sporeRgbppLockArgs, - toCkbAddress, -}: { +interface SporeLeapParams { sporeRgbppLockArgs: Hex; toCkbAddress: string; -}) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); + sporeTypeArgs: Hex; +} - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cells +const leapSporeFromBtcToCkb = async ({ sporeRgbppLockArgs, toCkbAddress, sporeTypeArgs }: SporeLeapParams) => { const sporeTypeBytes = serializeScript({ ...getSporeTypeScript(isMainnet), - args: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', + args: sporeTypeArgs, }); const ckbVirtualTxResult = await genLeapSporeFromBtcToCkbVirtualTx({ @@ -62,9 +25,10 @@ const transferSpore = async ({ isMainnet, }); - const { commitment, ckbRawTx, sporeCell } = ckbVirtualTxResult; + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '5-leap-spore-to-ckb'); - // console.log(JSON.stringify(ckbRawTx)) + const { commitment, ckbRawTx } = ckbVirtualTxResult; // Send BTC tx const psbt = await sendRgbppUtxos({ @@ -73,51 +37,42 @@ const transferSpore = async ({ tos: [btcAddress!], ckbCollector: collector, from: btcAddress!, - source, - feeRate: 120, + source: btcDataSource, + feeRate: 30, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); - const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); - const interval = setInterval(async () => { - try { - console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.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, - }); - - // Replace cobuild witness with the final rgbpp lock script - ckbTx.witnesses[ckbTx.witnesses.length - 1] = generateSporeTransferCoBuild([sporeCell], ckbTx.outputs); - - // console.log('ckbTx: ', JSON.stringify(ckbTx)); - - const txHash = await sendCkbTx({ collector, signedTx: ckbTx }); - console.info(`RGB++ Spore has been leaped from BTC to CKB and tx hash is ${txHash}`); - } catch (error) { - if (!(error instanceof BtcAssetsApiError)) { - console.error(error); + 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 spore 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); + }, 30 * 1000); + } catch (error) { + console.error(error); + } }; // Use your real BTC UTXO information on the BTC Testnet // rgbppLockArgs: outIndexU32 + btcTxId -transferSpore({ - // The spore rgbpp lock args is from 3-create-spore.ts +leapSporeFromBtcToCkb({ sporeRgbppLockArgs: buildRgbppLockArgs(3, 'd8a31796fbd42c546f6b22014b9b82b16586ce1df81b0e7ca9a552cdc492a0af'), toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq0e4xk4rmg5jdkn8aams492a7jlg73ue0gc0ddfj', + sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', }); diff --git a/examples/rgbpp/spore/6-unlock-btc-time-cell.ts b/examples/rgbpp/spore/6-unlock-btc-time-cell.ts index 2d333e96..65153ccb 100644 --- a/examples/rgbpp/spore/6-unlock-btc-time-cell.ts +++ b/examples/rgbpp/spore/6-unlock-btc-time-cell.ts @@ -1,35 +1,9 @@ -import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; -import { - Collector, - sendCkbTx, - getBtcTimeLockScript, - buildSporeBtcTimeCellsSpentTx, - signBtcTimeCellSpentTx, -} from '@rgbpp-sdk/ckb'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; +import { buildSporeBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; +import { CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../env'; +import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -// Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 4-btc-jump-ckb.ts +// Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 5-leap-spore-to-ckb.ts const unlockSporeBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - const btcTimeCells = await collector.getCells({ lock: { ...getBtcTimeLockScript(false), @@ -42,18 +16,16 @@ const unlockSporeBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: st throw new Error('No btc time cells found'); } - const btcAssetsApi = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const ckbRawTx: CKBComponents.RawTransaction = await buildSporeBtcTimeCellsSpentTx({ btcTimeCells, - btcAssetsApi, + btcAssetsApi: btcService, isMainnet, }); const signedTx = await signBtcTimeCellSpentTx({ - secp256k1PrivateKey: CKB_TEST_PRIVATE_KEY, + secp256k1PrivateKey: CKB_PRIVATE_KEY, collector, - masterCkbAddress: address, + masterCkbAddress: ckbAddress, ckbRawTx, isMainnet, }); diff --git a/examples/rgbpp/spore/7-leap-spore-to-btc.ts b/examples/rgbpp/spore/7-leap-spore-to-btc.ts index ae73382a..c9deb00f 100644 --- a/examples/rgbpp/spore/7-leap-spore-to-btc.ts +++ b/examples/rgbpp/spore/7-leap-spore-to-btc.ts @@ -1,36 +1,27 @@ -import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; -import { - Collector, - getSecp256k1CellDep, - buildRgbppLockArgs, - getSporeTypeScript, - genLeapSporeFromCkbToBtcRawTx, -} from '@rgbpp-sdk/ckb'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -const leapSporeFromCkbToBtc = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', address); - +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genLeapSporeFromCkbToBtcRawTx } from 'rgbpp'; +import { isMainnet, collector, ckbAddress, CKB_PRIVATE_KEY } from '../env'; +import { buildRgbppLockArgs, getSecp256k1CellDep, getSporeTypeScript } from 'rgbpp/ckb'; + +const leapSporeFromCkbToBtc = async ({ + outIndex, + btcTxId, + sporeTypeArgs, +}: { + outIndex: number; + btcTxId: string; + sporeTypeArgs: string; +}) => { const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); const sporeType: CKBComponents.Script = { ...getSporeTypeScript(isMainnet), - args: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', + args: sporeTypeArgs, }; const ckbRawTx = await genLeapSporeFromCkbToBtcRawTx({ collector, - fromCkbAddress: address, + fromCkbAddress: ckbAddress, toRgbppLockArgs, sporeTypeBytes: serializeScript(sporeType), isMainnet, @@ -43,7 +34,7 @@ const leapSporeFromCkbToBtc = async ({ outIndex, btcTxId }: { outIndex: number; witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], }; - const signedTx = collector.getCkb().signTransaction(CKB_TEST_PRIVATE_KEY)(unsignedTx); + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`RGB++ Spore has been jumped from CKB to BTC and tx hash is ${txHash}`); @@ -53,4 +44,5 @@ const leapSporeFromCkbToBtc = async ({ outIndex, btcTxId }: { outIndex: number; leapSporeFromCkbToBtc({ outIndex: 1, btcTxId: '448897515cf07b4ca0cd38af9806399ede55775b4c760b274ed2322121ed185f', + sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', }); diff --git a/examples/rgbpp/spore/launch/0-cluster-info.ts b/examples/rgbpp/spore/launch/0-cluster-info.ts new file mode 100644 index 00000000..cd89d711 --- /dev/null +++ b/examples/rgbpp/spore/launch/0-cluster-info.ts @@ -0,0 +1,6 @@ +import { RawClusterData } from 'rgbpp/ckb'; + +export const CLUSTER_DATA: RawClusterData = { + name: 'Cluster name', + description: 'Description of the cluster', +}; diff --git a/examples/rgbpp/spore/1-prepare-cluster.ts b/examples/rgbpp/spore/launch/1-prepare-cluster.ts similarity index 74% rename from examples/rgbpp/spore/1-prepare-cluster.ts rename to examples/rgbpp/spore/launch/1-prepare-cluster.ts index 2319f273..128731a5 100644 --- a/examples/rgbpp/spore/1-prepare-cluster.ts +++ b/examples/rgbpp/spore/launch/1-prepare-cluster.ts @@ -1,6 +1,5 @@ -import { AddressPrefix, addressToScript, getTransactionSize, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; import { - Collector, MAX_FEE, NoLiveCellError, SECP256K1_WITNESS_LOCK_SIZE, @@ -10,23 +9,13 @@ import { calculateTransactionFee, genRgbppLockScript, getSecp256k1CellDep, -} from '@rgbpp-sdk/ckb'; +} from 'rgbpp/ckb'; +import { ckbAddress, isMainnet, collector, CKB_PRIVATE_KEY } from '../../env'; import { CLUSTER_DATA } from './0-cluster-info'; -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - const prepareClusterCell = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - const masterLock = addressToScript(address); - console.log('ckb address: ', address); + const masterLock = addressToScript(ckbAddress); + console.log('ckb address: ', ckbAddress); // The capacity required to launch cells is determined by the token info cell capacity, and transaction fee. const clusterCellCapacity = calculateRgbppClusterCellCapacity(CLUSTER_DATA); @@ -75,7 +64,7 @@ const prepareClusterCell = async ({ outIndex, btcTxId }: { outIndex: number; btc changeCapacity -= estimatedTxFee; unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); - const signedTx = collector.getCkb().signTransaction(CKB_TEST_PRIVATE_KEY)(unsignedTx); + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`Cluster cell has been prepared and the tx hash ${txHash}`); diff --git a/examples/rgbpp/spore/2-create-cluster.ts b/examples/rgbpp/spore/launch/2-create-cluster.ts similarity index 56% rename from examples/rgbpp/spore/2-create-cluster.ts rename to examples/rgbpp/spore/launch/2-create-cluster.ts index a1269e27..97e3cdd1 100644 --- a/examples/rgbpp/spore/2-create-cluster.ts +++ b/examples/rgbpp/spore/launch/2-create-cluster.ts @@ -1,53 +1,28 @@ +import { BtcAssetsApiError, genCreateClusterCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../../env'; +import { CLUSTER_DATA } from './0-cluster-info'; +import { transactionToHex } from 'rgbpp/btc'; import { - Collector, - buildRgbppLockArgs, appendCkbTxWitnesses, - updateCkbTxWithRealBtcTxId, - sendCkbTx, - genCreateClusterCkbVirtualTx, + buildRgbppLockArgs, generateClusterCreateCoBuild, -} from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; -import { CLUSTER_DATA } from './0-cluster-info'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; + sendCkbTx, + updateCkbTxWithRealBtcTxId, +} from 'rgbpp/ckb'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; const createCluster = async ({ ownerRgbppLockArgs }: { ownerRgbppLockArgs: string }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - const ckbVirtualTxResult = await genCreateClusterCkbVirtualTx({ collector, rgbppLockArgs: ownerRgbppLockArgs, clusterData: CLUSTER_DATA, isMainnet, - ckbFeeRate: BigInt(5000), + ckbFeeRate: BigInt(2000), }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-create-cluster'); + const { commitment, ckbRawTx, clusterId } = ckbVirtualTxResult; console.log('clusterId: ', clusterId); @@ -59,25 +34,28 @@ const createCluster = async ({ ownerRgbppLockArgs }: { ownerRgbppLockArgs: strin tos: [btcAddress!], ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, feeRate: 30, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); + + console.log('The cluster rgbpp lock args: ', newCkbRawTx.outputs[0].lock.args); + const ckbTx = await appendCkbTxWitnesses({ ckbRawTx: newCkbRawTx, btcTxBytes, diff --git a/examples/rgbpp/spore/3-create-spores.ts b/examples/rgbpp/spore/launch/3-create-spores.ts similarity index 61% rename from examples/rgbpp/spore/3-create-spores.ts rename to examples/rgbpp/spore/launch/3-create-spores.ts index 38493df2..342b8e6a 100644 --- a/examples/rgbpp/spore/3-create-spores.ts +++ b/examples/rgbpp/spore/launch/3-create-spores.ts @@ -1,39 +1,28 @@ +import { BtcAssetsApiError, genCreateSporeCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { + isMainnet, + collector, + btcAddress, + btcDataSource, + btcKeyPair, + btcService, + CKB_PRIVATE_KEY, + ckbAddress, +} from '../../env'; import { - Collector, - buildRgbppLockArgs, - appendCkbTxWitnesses, - updateCkbTxWithRealBtcTxId, - sendCkbTx, - genCreateSporeCkbVirtualTx, Hex, + appendCkbTxWitnesses, appendIssuerCellToSporesCreate, + buildRgbppLockArgs, generateSporeCreateCoBuild, -} from '@rgbpp-sdk/ckb'; -import { - DataSource, - ECPair, - bitcoin, - NetworkType, - sendRgbppUtxos, - transactionToHex, - utf8ToBuffer, -} from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; -import { RawSporeData } from '@spore-sdk/core'; -import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; + sendCkbTx, + updateCkbTxWithRealBtcTxId, + RawSporeData, +} from 'rgbpp/ckb'; +import { transactionToHex, utf8ToBuffer } from 'rgbpp/btc'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; -interface Params { +interface SporeCreateParams { clusterRgbppLockArgs: Hex; receivers: { toBtcAddress: string; @@ -41,39 +30,18 @@ interface Params { }[]; } -const createSpores = async ({ clusterRgbppLockArgs, receivers }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const ckbAddress = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', ckbAddress); - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - +const createSpores = async ({ clusterRgbppLockArgs, receivers }: SporeCreateParams) => { const ckbVirtualTxResult = await genCreateSporeCkbVirtualTx({ collector, sporeDataList: receivers.map((receiver) => receiver.sporeData), clusterRgbppLockArgs, isMainnet, - ckbFeeRate: BigInt(5000), + ckbFeeRate: BigInt(2000), }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-create-spores'); + const { commitment, ckbRawTx, sumInputsCapacity, clusterCell } = ckbVirtualTxResult; // Send BTC tx @@ -85,26 +53,26 @@ const createSpores = async ({ clusterRgbppLockArgs, receivers }: Params) => { tos: btcTos, ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, feeRate: 120, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); - console.log('The new cluster cell lock script args: ', newCkbRawTx.outputs[0].lock.args); + console.log('The new cluster rgbpp lock args: ', newCkbRawTx.outputs[0].lock.args); const ckbTx = await appendCkbTxWitnesses({ ckbRawTx: newCkbRawTx, @@ -128,7 +96,7 @@ const createSpores = async ({ clusterRgbppLockArgs, receivers }: Params) => { // console.log('ckbTx: ', JSON.stringify(ckbTx)); const signedTx = await appendIssuerCellToSporesCreate({ - secp256k1PrivateKey: CKB_TEST_PRIVATE_KEY, + secp256k1PrivateKey: CKB_PRIVATE_KEY, issuerAddress: ckbAddress, ckbRawTx: ckbTx, collector, @@ -149,7 +117,9 @@ const createSpores = async ({ clusterRgbppLockArgs, receivers }: Params) => { // Use your real BTC UTXO information on the BTC Testnet // rgbppLockArgs: outIndexU32 + btcTxId createSpores({ - // The cluster rgbpp lock args is from 2-create-cluster.ts + // The cluster cell will be spent and the new cluster cell will be created in each spore creation tx, + // so the cluster rgbpp lock args should be updated after each spore creation tx is completed. + // The first cluster rgbpp lock args is from 2-create-cluster.ts and the new cluster rgbpp lock args can be found from the log in the line 71 of this file clusterRgbppLockArgs: buildRgbppLockArgs(1, '96bccaadd3c8f59b2411e3d64ae4c1743532415f953fc4f9741a5fd7a0a34483'), receivers: [ { diff --git a/examples/rgbpp/spore/local/4-transfer-spore.ts b/examples/rgbpp/spore/local/4-transfer-spore.ts new file mode 100644 index 00000000..a695c818 --- /dev/null +++ b/examples/rgbpp/spore/local/4-transfer-spore.ts @@ -0,0 +1,101 @@ +import { + buildRgbppLockArgs, + appendCkbTxWitnesses, + updateCkbTxWithRealBtcTxId, + sendCkbTx, + getSporeTypeScript, + Hex, + generateSporeTransferCoBuild, + genTransferSporeCkbVirtualTx, +} from 'rgbpp/ckb'; +import { sendRgbppUtxos, transactionToHex } from 'rgbpp/btc'; +import { BtcAssetsApiError } from 'rgbpp'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../../env'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; + +interface SporeTransferParams { + sporeRgbppLockArgs: Hex; + toBtcAddress: string; + sporeTypeArgs: Hex; +} + +// Warning: It is not recommended for developers to use local examples unless you understand the entire process of RGB++ transactions. +const transferSpore = async ({ sporeRgbppLockArgs, toBtcAddress, sporeTypeArgs }: SporeTransferParams) => { + // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cells + const sporeTypeBytes = serializeScript({ + ...getSporeTypeScript(isMainnet), + args: sporeTypeArgs, + }); + + const ckbVirtualTxResult = await genTransferSporeCkbVirtualTx({ + collector, + sporeRgbppLockArgs, + sporeTypeBytes, + isMainnet, + ckbFeeRate: BigInt(5000), + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '4-transfer-spore-local'); + + const { commitment, ckbRawTx, sporeCell } = ckbVirtualTxResult; + + // console.log(JSON.stringify(ckbRawTx)) + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [toBtcAddress], + ckbCollector: collector, + from: btcAddress!, + source: btcDataSource, + feeRate: 30, + }); + psbt.signAllInputs(btcKeyPair); + psbt.finalizeAllInputs(); + + const btcTx = psbt.extractTransaction(); + const btcTxBytes = transactionToHex(btcTx, false); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); + + console.log('BTC TxId: ', btcTxId); + + 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, + }); + + // Replace cobuild witness with the final rgbpp lock script + ckbTx.witnesses[ckbTx.witnesses.length - 1] = generateSporeTransferCoBuild([sporeCell], ckbTx.outputs); + + // console.log('ckbTx: ', JSON.stringify(ckbTx)); + + const txHash = await sendCkbTx({ collector, signedTx: ckbTx }); + console.info(`RGB++ Spore has been transferred and tx hash is ${txHash}`); + } catch (error) { + if (!(error instanceof BtcAssetsApiError)) { + console.error(error); + } + } + }, 30 * 1000); +}; + +// Use your real BTC UTXO information on the BTC Testnet +// rgbppLockArgs: outIndexU32 + btcTxId +transferSpore({ + // The spore rgbpp lock args is from 3-create-spore.ts + sporeRgbppLockArgs: buildRgbppLockArgs(1, 'f203c8c13eacdbd126f85d286a963c85f233f8145363b1d997c4d552afb990e1'), + toBtcAddress: 'tb1qhp9fh9qsfeyh0yhewgu27ndqhs5qlrqwau28m7', + sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', +}); diff --git a/examples/rgbpp/spore/local/5-leap-spore-to-ckb.ts b/examples/rgbpp/spore/local/5-leap-spore-to-ckb.ts new file mode 100644 index 00000000..06856e96 --- /dev/null +++ b/examples/rgbpp/spore/local/5-leap-spore-to-ckb.ts @@ -0,0 +1,101 @@ +import { + buildRgbppLockArgs, + appendCkbTxWitnesses, + updateCkbTxWithRealBtcTxId, + sendCkbTx, + getSporeTypeScript, + Hex, + generateSporeTransferCoBuild, + genLeapSporeFromBtcToCkbVirtualTx, +} from 'rgbpp/ckb'; +import { sendRgbppUtxos, transactionToHex } from 'rgbpp/btc'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../../env'; +import { BtcAssetsApiError } from 'rgbpp'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; + +interface SporeLeapParams { + sporeRgbppLockArgs: Hex; + toCkbAddress: string; + sporeTypeArgs: Hex; +} + +// Warning: It is not recommended for developers to use local examples unless you understand the entire process of RGB++ transactions. +const leapSpore = async ({ sporeRgbppLockArgs, toCkbAddress, sporeTypeArgs }: SporeLeapParams) => { + // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cells + const sporeTypeBytes = serializeScript({ + ...getSporeTypeScript(isMainnet), + args: sporeTypeArgs, + }); + + const ckbVirtualTxResult = await genLeapSporeFromBtcToCkbVirtualTx({ + collector, + sporeRgbppLockArgs, + sporeTypeBytes, + toCkbAddress, + isMainnet, + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '5-leap-spore-to-ckb-local'); + + const { commitment, ckbRawTx, sporeCell } = ckbVirtualTxResult; + + // console.log(JSON.stringify(ckbRawTx)) + + // Send BTC tx + const psbt = await sendRgbppUtxos({ + ckbVirtualTx: ckbRawTx, + commitment, + tos: [btcAddress!], + ckbCollector: collector, + from: btcAddress!, + source: btcDataSource, + feeRate: 120, + }); + psbt.signAllInputs(btcKeyPair); + psbt.finalizeAllInputs(); + + const btcTx = psbt.extractTransaction(); + const btcTxBytes = transactionToHex(btcTx, false); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); + + console.log('BTC TxId: ', btcTxId); + + 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, + }); + + // Replace cobuild witness with the final rgbpp lock script + ckbTx.witnesses[ckbTx.witnesses.length - 1] = generateSporeTransferCoBuild([sporeCell], ckbTx.outputs); + + // console.log('ckbTx: ', JSON.stringify(ckbTx)); + + const txHash = await sendCkbTx({ collector, signedTx: ckbTx }); + console.info(`RGB++ Spore has been leaped from BTC to CKB and tx hash is ${txHash}`); + } catch (error) { + if (!(error instanceof BtcAssetsApiError)) { + console.error(error); + } + } + }, 30 * 1000); +}; + +// Use your real BTC UTXO information on the BTC Testnet +// rgbppLockArgs: outIndexU32 + btcTxId +leapSpore({ + // The spore rgbpp lock args is from 3-create-spore.ts + sporeRgbppLockArgs: buildRgbppLockArgs(3, 'd8a31796fbd42c546f6b22014b9b82b16586ce1df81b0e7ca9a552cdc492a0af'), + toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq0e4xk4rmg5jdkn8aams492a7jlg73ue0gc0ddfj', + sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', +}); diff --git a/examples/rgbpp/spore/queue/4-transfer-spore.ts b/examples/rgbpp/spore/queue/4-transfer-spore.ts deleted file mode 100644 index 24b7b39f..00000000 --- a/examples/rgbpp/spore/queue/4-transfer-spore.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Collector, buildRgbppLockArgs, getSporeTypeScript, Hex, genTransferSporeCkbVirtualTx } from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; -import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -const transferSpore = async ({ - sporeRgbppLockArgs, - toBtcAddress, -}: { - sporeRgbppLockArgs: Hex; - toBtcAddress: string; -}) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cell - const sporeTypeBytes = serializeScript({ - ...getSporeTypeScript(isMainnet), - args: '0x205fe15af04e59d3ff1ff8e0b0a1e3bc201af406a38964760c24848ed6029b6b', - }); - - const ckbVirtualTxResult = await genTransferSporeCkbVirtualTx({ - collector, - sporeRgbppLockArgs, - sporeTypeBytes, - isMainnet, - }); - - const { commitment, ckbRawTx } = ckbVirtualTxResult; - - // console.log(JSON.stringify(ckbRawTx)) - - // Send BTC tx - const psbt = await sendRgbppUtxos({ - ckbVirtualTx: ckbRawTx, - commitment, - tos: [toBtcAddress], - ckbCollector: collector, - from: btcAddress!, - source, - feeRate: 30, - }); - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - const btcTx = psbt.extractTransaction(); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); - - console.log('BTC TxId: ', btcTxId); - - try { - await service.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); - const interval = setInterval(async () => { - const { state, failedReason } = await service.getRgbppTransactionState(btcTxId); - console.log('state', state); - if (state === 'completed' || state === 'failed') { - clearInterval(interval); - if (state === 'completed') { - const { txhash: txHash } = await service.getRgbppTransactionHash(btcTxId); - console.info(`Rgbpp spore 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); - } -}; - -// Use your real BTC UTXO information on the BTC Testnet -// rgbppLockArgs: outIndexU32 + btcTxId -transferSpore({ - // The spore rgbpp lock args is from 3-create-spore.ts - sporeRgbppLockArgs: buildRgbppLockArgs(2, 'd5868dbde4be5e49876b496449df10150c356843afb6f94b08f8d81f394bb350'), - toBtcAddress: 'tb1qhp9fh9qsfeyh0yhewgu27ndqhs5qlrqwau28m7', -}); diff --git a/examples/rgbpp/spore/queue/5-leap-spore-to-ckb.ts b/examples/rgbpp/spore/queue/5-leap-spore-to-ckb.ts deleted file mode 100644 index 9ce5497c..00000000 --- a/examples/rgbpp/spore/queue/5-leap-spore-to-ckb.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - Collector, - buildRgbppLockArgs, - getSporeTypeScript, - Hex, - genLeapSporeFromBtcToCkbVirtualTx, -} from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi } from '@rgbpp-sdk/service'; -import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -const transferSpore = async ({ - sporeRgbppLockArgs, - toCkbAddress, -}: { - sporeRgbppLockArgs: Hex; - toCkbAddress: string; -}) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // The spore type script is from 3-create-spore.ts, you can find it from the ckb tx spore output cells - const sporeTypeBytes = serializeScript({ - ...getSporeTypeScript(isMainnet), - args: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', - }); - - const ckbVirtualTxResult = await genLeapSporeFromBtcToCkbVirtualTx({ - collector, - sporeRgbppLockArgs, - sporeTypeBytes, - toCkbAddress, - isMainnet, - }); - - const { commitment, ckbRawTx } = ckbVirtualTxResult; - - // console.log(JSON.stringify(ckbRawTx)) - - // Send BTC tx - const psbt = await sendRgbppUtxos({ - ckbVirtualTx: ckbRawTx, - commitment, - tos: [btcAddress!], - ckbCollector: collector, - from: btcAddress!, - source, - feeRate: 30, - }); - psbt.signAllInputs(keyPair); - psbt.finalizeAllInputs(); - - const btcTx = psbt.extractTransaction(); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); - - console.log('BTC TxId: ', btcTxId); - - try { - await service.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); - const interval = setInterval(async () => { - const { state, failedReason } = await service.getRgbppTransactionState(btcTxId); - console.log('state', state); - if (state === 'completed' || state === 'failed') { - clearInterval(interval); - if (state === 'completed') { - const { txhash: txHash } = await service.getRgbppTransactionHash(btcTxId); - console.info(`Rgbpp spore 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); - } -}; - -// Use your real BTC UTXO information on the BTC Testnet -// rgbppLockArgs: outIndexU32 + btcTxId -transferSpore({ - // The spore rgbpp lock args is from 3-create-spore.ts - sporeRgbppLockArgs: buildRgbppLockArgs(3, 'd8a31796fbd42c546f6b22014b9b82b16586ce1df81b0e7ca9a552cdc492a0af'), - toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq0e4xk4rmg5jdkn8aams492a7jlg73ue0gc0ddfj', -}); diff --git a/examples/rgbpp/tsconfig.json b/examples/rgbpp/tsconfig.json index 3ac8b03f..b092d7f8 100644 --- a/examples/rgbpp/tsconfig.json +++ b/examples/rgbpp/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "target": "ES2015", "lib": ["dom", "dom.iterable", "esnext"], - "module": "CommonJS", + "module": "NodeNext", "composite": false, "resolveJsonModule": true, "strictNullChecks": true, @@ -14,13 +14,13 @@ "forceConsistentCasingInFileNames": true, "inlineSources": false, "isolatedModules": true, - "moduleResolution": "node", + "moduleResolution": "NodeNext", "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "skipLibCheck": true, "strict": true }, - "include": ["local/**/*.ts", "queue/**/*.ts"], + "include": ["queue/**/*.ts", "xudt/**/*.ts"], "exclude": ["node_modules"] } diff --git a/examples/rgbpp/xudt/0-token-info.ts b/examples/rgbpp/xudt/0-token-info.ts deleted file mode 100644 index a2cb7a9c..00000000 --- a/examples/rgbpp/xudt/0-token-info.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RgbppTokenInfo } from '@rgbpp-sdk/ckb'; - -export const XUDT_TOKEN_INFO: RgbppTokenInfo = { - decimal: 8, - name: 'XUDT Test Token', - symbol: 'XTT', -}; diff --git a/examples/rgbpp/xudt/1-ckb-leap-btc.ts b/examples/rgbpp/xudt/1-ckb-leap-btc.ts new file mode 100644 index 00000000..aeb221de --- /dev/null +++ b/examples/rgbpp/xudt/1-ckb-leap-btc.ts @@ -0,0 +1,49 @@ +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'; + +interface LeapToBtcParams { + outIndex: number; + btcTxId: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmount }: LeapToBtcParams) => { + 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}`); +}; + +// Use your real BTC UTXO information on the BTC Testnet +leapFromCkbToBtc({ + outIndex: 1, + btcTxId: '4ff1855b64b309afa19a8b9be3d4da99dcb18b083b65d2d851662995c7d99e7a', + xudtTypeArgs: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', + transferAmount: BigInt(800_0000_0000), +}); diff --git a/examples/rgbpp/xudt/2-btc-transfer.ts b/examples/rgbpp/xudt/2-btc-transfer.ts new file mode 100644 index 00000000..0fdd274a --- /dev/null +++ b/examples/rgbpp/xudt/2-btc-transfer.ts @@ -0,0 +1,78 @@ +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 { saveCkbVirtualTxResult } from '../shared/utils'; + +interface RgbppTransferParams { + rgbppLockArgsList: string[]; + toBtcAddress: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const transfer = async ({ rgbppLockArgsList, toBtcAddress, xudtTypeArgs, transferAmount }: RgbppTransferParams) => { + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: xudtTypeArgs, + }; + + const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(xudtType), + transferAmount, + isMainnet, + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-btc-transfer'); + + 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); + + 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}`); + } 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(1, '64252b582aea1249ed969a20385fae48bba35bf1ab9b3df3b0fcddc754ccf592')], + toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', + xudtTypeArgs: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', + transferAmount: BigInt(800_0000_0000), +}); diff --git a/examples/rgbpp/xudt/3-btc-leap-ckb.ts b/examples/rgbpp/xudt/3-btc-leap-ckb.ts new file mode 100644 index 00000000..0f54b137 --- /dev/null +++ b/examples/rgbpp/xudt/3-btc-leap-ckb.ts @@ -0,0 +1,78 @@ +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 { saveCkbVirtualTxResult } from '../shared/utils'; + +interface LeapToCkbParams { + rgbppLockArgsList: string[]; + toCkbAddress: string; + xudtTypeArgs: string; + transferAmount: bigint; +} + +const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, transferAmount }: LeapToCkbParams) => { + const xudtType: CKBComponents.Script = { + ...getXudtTypeScript(isMainnet), + args: xudtTypeArgs, + }; + + const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({ + collector, + rgbppLockArgsList, + xudtTypeBytes: serializeScript(xudtType), + transferAmount, + toCkbAddress, + isMainnet, + }); + + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-btc-leap-ckb'); + + 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); + + 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}`); + } 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(1, '6edd4b9327506fab09fb9a0f5e5f35136a6a94bd4c9dd79af04921618fa6c800')], + toCkbAddress: 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqq9kxr7vy7yknezj0vj0xptx6thk6pwyr0sxamv6q', + xudtTypeArgs: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', + transferAmount: BigInt(800_0000_0000), +}); diff --git a/examples/rgbpp/xudt/4-unlock-btc-time-cell.ts b/examples/rgbpp/xudt/4-unlock-btc-time-cell.ts new file mode 100644 index 00000000..6cece8ce --- /dev/null +++ b/examples/rgbpp/xudt/4-unlock-btc-time-cell.ts @@ -0,0 +1,41 @@ +import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; +import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; +import { 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 unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { + const btcTimeCells = await collector.getCells({ + lock: { + ...getBtcTimeLockScript(false), + 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, + }); + + 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 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 +unlockBtcTimeCell({ + btcTimeCellArgs: + '0x7f000000100000005b0000005f0000004b000000100000003000000031000000d23761b364210735c19c60561d213fb3beae2fd6172743719eff6920e020baac011600000000016c61f984f12d3c8a4f649e60acda5deda0b8837c060000001c95b9d726e4ab337d6a4572680598947954d7b6ff4f1e767e605eeeec49e7ed', +}); diff --git a/examples/rgbpp/local/launch/0-rgbpp-token-info.ts b/examples/rgbpp/xudt/launch/0-rgbpp-token-info.ts similarity index 69% rename from examples/rgbpp/local/launch/0-rgbpp-token-info.ts rename to examples/rgbpp/xudt/launch/0-rgbpp-token-info.ts index 67a7cc39..67354cbf 100644 --- a/examples/rgbpp/local/launch/0-rgbpp-token-info.ts +++ b/examples/rgbpp/xudt/launch/0-rgbpp-token-info.ts @@ -1,4 +1,4 @@ -import { RgbppTokenInfo } from '@rgbpp-sdk/ckb'; +import { RgbppTokenInfo } from 'rgbpp/ckb'; export const RGBPP_TOKEN_INFO: RgbppTokenInfo = { decimal: 8, diff --git a/examples/rgbpp/local/launch/1-prepare-launch.ts b/examples/rgbpp/xudt/launch/1-prepare-launch.ts similarity index 76% rename from examples/rgbpp/local/launch/1-prepare-launch.ts rename to examples/rgbpp/xudt/launch/1-prepare-launch.ts index 7867a194..dfd73a92 100644 --- a/examples/rgbpp/local/launch/1-prepare-launch.ts +++ b/examples/rgbpp/xudt/launch/1-prepare-launch.ts @@ -1,6 +1,5 @@ -import { AddressPrefix, addressToScript, getTransactionSize, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; import { - Collector, MAX_FEE, NoLiveCellError, RgbppTokenInfo, @@ -12,11 +11,9 @@ import { calculateTransactionFee, genRgbppLockScript, getSecp256k1CellDep, -} from '@rgbpp-sdk/ckb'; +} from 'rgbpp/ckb'; import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; +import { CKB_PRIVATE_KEY, ckbAddress, collector, isMainnet } from '../../env'; const prepareLaunchCell = async ({ outIndex, @@ -27,16 +24,8 @@ const prepareLaunchCell = async ({ btcTxId: string; rgbppTokenInfo: RgbppTokenInfo; }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const address = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - const masterLock = addressToScript(address); - console.log('ckb address: ', address); + const masterLock = addressToScript(ckbAddress); + console.log('ckb address: ', ckbAddress); // The capacity required to launch cells is determined by the token info cell capacity, and transaction fee. const launchCellCapacity = @@ -86,7 +75,7 @@ const prepareLaunchCell = async ({ changeCapacity -= estimatedTxFee; unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); - const signedTx = collector.getCkb().signTransaction(CKB_TEST_PRIVATE_KEY)(unsignedTx); + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`Launch cell has been created and the tx hash ${txHash}`); diff --git a/examples/rgbpp/local/launch/2-launch-rgbpp.ts b/examples/rgbpp/xudt/launch/2-launch-rgbpp.ts similarity index 57% rename from examples/rgbpp/local/launch/2-launch-rgbpp.ts rename to examples/rgbpp/xudt/launch/2-launch-rgbpp.ts index be5db676..d7708bcf 100644 --- a/examples/rgbpp/local/launch/2-launch-rgbpp.ts +++ b/examples/rgbpp/xudt/launch/2-launch-rgbpp.ts @@ -1,24 +1,15 @@ +import { genRgbppLaunchCkbVirtualTx, sendRgbppUtxos, BtcAssetsApiError } from 'rgbpp'; import { - Collector, buildRgbppLockArgs, - genRgbppLaunchCkbVirtualTx, RgbppTokenInfo, appendCkbTxWitnesses, updateCkbTxWithRealBtcTxId, sendCkbTx, -} from '@rgbpp-sdk/ckb'; -import { DataSource, ECPair, bitcoin, NetworkType, sendRgbppUtxos, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; +} from 'rgbpp/ckb'; import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; +import { btcAddress, btcDataSource, btcKeyPair, btcService, collector, isMainnet } from '../../env'; +import { transactionToHex } from 'rgbpp/btc'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; interface Params { ownerRgbppLockArgs: string; @@ -26,25 +17,6 @@ interface Params { rgbppTokenInfo: RgbppTokenInfo; } const launchRgppAsset = async ({ ownerRgbppLockArgs, launchAmount, rgbppTokenInfo }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - const ckbVirtualTxResult = await genRgbppLaunchCkbVirtualTx({ collector, ownerRgbppLockArgs, @@ -53,6 +25,9 @@ const launchRgppAsset = async ({ ownerRgbppLockArgs, launchAmount, rgbppTokenInf isMainnet, }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-launch-rgbpp'); + const { commitment, ckbRawTx } = ckbVirtualTxResult; console.log('RGB++ Asset type script args: ', ckbRawTx.outputs[0].type?.args); @@ -64,21 +39,21 @@ const launchRgppAsset = async ({ ownerRgbppLockArgs, launchAmount, rgbppTokenInf tos: [btcAddress!], ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); diff --git a/examples/rgbpp/local/launch/3-distribute-rgbpp.ts b/examples/rgbpp/xudt/launch/3-distribute-rgbpp.ts similarity index 57% rename from examples/rgbpp/local/launch/3-distribute-rgbpp.ts rename to examples/rgbpp/xudt/launch/3-distribute-rgbpp.ts index b1cb880e..e9c25300 100644 --- a/examples/rgbpp/local/launch/3-distribute-rgbpp.ts +++ b/examples/rgbpp/xudt/launch/3-distribute-rgbpp.ts @@ -1,63 +1,39 @@ -import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { BtcAssetsApiError, genBtcBatchTransferCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; +import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; +import { + isMainnet, + collector, + btcAddress, + btcDataSource, + btcKeyPair, + btcService, + CKB_PRIVATE_KEY, + ckbAddress, +} from '../../env'; import { - Collector, RgbppBtcAddressReceiver, appendCkbTxWitnesses, appendIssuerCellToBtcBatchTransfer, buildRgbppLockArgs, - genBtcBatchTransferCkbVirtualTx, getXudtTypeScript, sendCkbTx, updateCkbTxWithRealBtcTxId, -} from '@rgbpp-sdk/ckb'; -import { sendRgbppUtxos, DataSource, ECPair, bitcoin, NetworkType, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; -import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; +} from 'rgbpp/ckb'; +import { transactionToHex } from 'rgbpp/btc'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; interface Params { rgbppLockArgsList: string[]; receivers: RgbppBtcAddressReceiver[]; + xudtTypeArgs: string; } -const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const ckbAddress = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', ckbAddress); - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - +const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers, xudtTypeArgs }: Params) => { // Warning: Please replace with your real xUDT type script here const xudtType: CKBComponents.Script = { ...getXudtTypeScript(isMainnet), // The xUDT type script args is generated by 2-launch-rgbpp.ts, and it can be found from the log - args: '0x4c1ecf2f14edae73b76ccf115ecfa40ba68ee315c96bd4fcfd771c2fb4c69e8f', + args: xudtTypeArgs, }; const ckbVirtualTxResult = await genBtcBatchTransferCkbVirtualTx({ @@ -68,6 +44,9 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers }: Param isMainnet, }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-distribute-rgbpp'); + const { commitment, ckbRawTx, sumInputsCapacity, rgbppChangeOutIndex } = ckbVirtualTxResult; // The first output utxo is OP_RETURN @@ -81,21 +60,21 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers }: Param tos: receivers.map((receiver) => receiver.toBtcAddress), ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); @@ -106,7 +85,7 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers }: Param }); const signedTx = await appendIssuerCellToBtcBatchTransfer({ - secp256k1PrivateKey: CKB_TEST_PRIVATE_KEY, + secp256k1PrivateKey: CKB_PRIVATE_KEY, issuerAddress: ckbAddress, ckbRawTx: ckbTx, collector, @@ -129,9 +108,10 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers }: Param distributeRgbppAssetOnBtc({ // Warning: If rgbpp assets are distributed continuously, then the position of the current rgbpp asset utxo depends on the position of the previous change utxo distributed rgbppLockArgsList: [buildRgbppLockArgs(2, '012bfee9c1e8a6e9e272b63ff54d5138efe910cc7aac413221cb3634ea176866')], + xudtTypeArgs: '0x4c1ecf2f14edae73b76ccf115ecfa40ba68ee315c96bd4fcfd771c2fb4c69e8f', receivers: [ { - toBtcAddress: 'bc1p0ey32x7dwhlx569rh0l5qaxetsfnpvezanrezahelr0t02ytyegssdel0h', + toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', transferAmount: BigInt(1000) * BigInt(10 ** RGBPP_TOKEN_INFO.decimal), }, ], diff --git a/examples/rgbpp/local/2-btc-transfer.ts b/examples/rgbpp/xudt/local/2-btc-transfer.ts similarity index 52% rename from examples/rgbpp/local/2-btc-transfer.ts rename to examples/rgbpp/xudt/local/2-btc-transfer.ts index 05bddec6..92a4185d 100644 --- a/examples/rgbpp/local/2-btc-transfer.ts +++ b/examples/rgbpp/xudt/local/2-btc-transfer.ts @@ -1,54 +1,28 @@ import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcTransferCkbVirtualTx, sendRgbppUtxos, BtcAssetsApiError } from 'rgbpp'; import { - Collector, appendCkbTxWitnesses, buildRgbppLockArgs, - genBtcTransferCkbVirtualTx, sendCkbTx, getXudtTypeScript, updateCkbTxWithRealBtcTxId, -} from '@rgbpp-sdk/ckb'; -import { transactionToHex, sendRgbppUtxos, DataSource, ECPair, bitcoin, NetworkType } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; +} from 'rgbpp/ckb'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../../env'; +import { transactionToHex } from 'rgbpp/btc'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; - -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -interface Params { +interface RgbppTransferParams { rgbppLockArgsList: string[]; toBtcAddress: string; + xudtTypeArgs: string; transferAmount: bigint; } -const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmount }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - - console.log('btc address: ', btcAddress); - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // Warning: Please replace with your real xUDT type script here +// Warning: It is not recommended for developers to use local examples unless you understand the entire process of RGB++ transactions. +const transfer = async ({ rgbppLockArgsList, toBtcAddress, xudtTypeArgs, transferAmount }: RgbppTransferParams) => { const xudtType: CKBComponents.Script = { ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', + args: xudtTypeArgs, }; const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({ @@ -59,6 +33,9 @@ const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmo isMainnet, }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '2-btc-transfer-local'); + const { commitment, ckbRawTx } = ckbVirtualTxResult; // Send BTC tx @@ -68,15 +45,15 @@ const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmo tos: [toBtcAddress], ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); // Remove the witness from BTC tx for RGBPP unlock const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC TxId: ', btcTxId); @@ -84,7 +61,7 @@ const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmo const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); @@ -106,9 +83,9 @@ const transferRgbppOnBtc = async ({ rgbppLockArgsList, toBtcAddress, transferAmo // Use your real BTC UTXO information on the BTC Testnet // rgbppLockArgs: outIndexU32 + btcTxId -transferRgbppOnBtc({ +transfer({ rgbppLockArgsList: [buildRgbppLockArgs(1, '70b250e2a3cc7a33b47f7a4e94e41e1ee2501ce73b393d824db1dd4c872c5348')], toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', - // To simplify, keep the transferAmount the same as 2-ckb-jump-btc + xudtTypeArgs: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', transferAmount: BigInt(800_0000_0000), }); diff --git a/examples/rgbpp/local/3-btc-jump-ckb.ts b/examples/rgbpp/xudt/local/3-btc-leap-ckb.ts similarity index 50% rename from examples/rgbpp/local/3-btc-jump-ckb.ts rename to examples/rgbpp/xudt/local/3-btc-leap-ckb.ts index 818df2eb..d42615a8 100644 --- a/examples/rgbpp/local/3-btc-jump-ckb.ts +++ b/examples/rgbpp/xudt/local/3-btc-leap-ckb.ts @@ -1,53 +1,28 @@ import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { genBtcJumpCkbVirtualTx, sendRgbppUtxos, BtcAssetsApiError } from 'rgbpp'; import { - Collector, appendCkbTxWitnesses, - genBtcJumpCkbVirtualTx, + buildRgbppLockArgs, sendCkbTx, getXudtTypeScript, updateCkbTxWithRealBtcTxId, - buildRgbppLockArgs, -} from '@rgbpp-sdk/ckb'; -import { sendRgbppUtxos, DataSource, NetworkType, bitcoin, ECPair, transactionToHex } from '@rgbpp-sdk/btc'; -import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; - -// BTC SECP256K1 private key -const BTC_TEST_PRIVATE_KEY = '0000000000000000000000000000000000000000000000000000000000000001'; -// API docs: https://btc-assets-api.testnet.mibao.pro/docs -const BTC_ASSETS_API_URL = 'https://btc-assets-api.testnet.mibao.pro'; -// https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate -const BTC_ASSETS_TOKEN = ''; +} from 'rgbpp/ckb'; +import { isMainnet, collector, btcAddress, btcDataSource, btcKeyPair, btcService } from '../../env'; +import { transactionToHex } from 'rgbpp/btc'; +import { saveCkbVirtualTxResult } from '../../shared/utils'; -const BTC_ASSETS_ORIGIN = 'https://btc-test.app'; - -interface Params { +interface LeapToCkbParams { rgbppLockArgsList: string[]; toCkbAddress: string; + xudtTypeArgs: string; transferAmount: bigint; } -const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmount }: Params) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; - const keyPair = ECPair.fromPrivateKey(Buffer.from(BTC_TEST_PRIVATE_KEY, 'hex'), { network }); - const { address: btcAddress } = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - network, - }); - console.log('btc address: ', btcAddress); - - const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; - const service = BtcAssetsApi.fromToken(BTC_ASSETS_API_URL, BTC_ASSETS_TOKEN, BTC_ASSETS_ORIGIN); - const source = new DataSource(service, networkType); - - // Warning: Please replace with your real xUDT type script here +// Warning: It is not recommended for developers to use local examples unless you understand the entire process of RGB++ transactions. +const leapFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, transferAmount }: LeapToCkbParams) => { const xudtType: CKBComponents.Script = { ...getXudtTypeScript(isMainnet), - args: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', + args: xudtTypeArgs, }; const ckbVirtualTxResult = await genBtcJumpCkbVirtualTx({ @@ -59,6 +34,9 @@ const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmoun isMainnet, }); + // Save ckbVirtualTxResult + saveCkbVirtualTxResult(ckbVirtualTxResult, '3-btc-leap-ckb-local'); + const { commitment, ckbRawTx } = ckbVirtualTxResult; // Send BTC tx @@ -68,15 +46,15 @@ const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmoun tos: [btcAddress!], ckbCollector: collector, from: btcAddress!, - source, + source: btcDataSource, }); - psbt.signAllInputs(keyPair); + psbt.signAllInputs(btcKeyPair); psbt.finalizeAllInputs(); const btcTx = psbt.extractTransaction(); // Remove the witness from BTC tx for RGBPP unlock const btcTxBytes = transactionToHex(btcTx, false); - const { txid: btcTxId } = await service.sendBtcTransaction(btcTx.toHex()); + const { txid: btcTxId } = await btcService.sendBtcTransaction(btcTx.toHex()); console.log('BTC Tx bytes: ', btcTxBytes); console.log('BTC TxId: ', btcTxId); @@ -86,7 +64,7 @@ const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmoun const interval = setInterval(async () => { try { console.log('Waiting for BTC tx and proof to be ready'); - const rgbppApiSpvProof = await service.getRgbppSpvProof(btcTxId, 0); + const rgbppApiSpvProof = await btcService.getRgbppSpvProof(btcTxId, 0); clearInterval(interval); // Update CKB transaction with the real BTC txId const newCkbRawTx = updateCkbTxWithRealBtcTxId({ ckbRawTx, btcTxId, isMainnet }); @@ -107,11 +85,9 @@ const jumpFromBtcToCkb = async ({ rgbppLockArgsList, toCkbAddress, transferAmoun }; // rgbppLockArgs: outIndexU32 + btcTxId -jumpFromBtcToCkb({ - // If the `3-btc-transfer.ts` has been executed, the BTC txId should be the new generated BTC txId by the `3-btc-transfer.ts` - // Otherwise the BTC txId should be same as the the BTC txId of the `2-ckb-jump-btc.ts` +leapFromBtcToCkb({ rgbppLockArgsList: [buildRgbppLockArgs(1, '24e622419156dd3a277a90bcbb40c7117462a18d5329dd1ada320ca8bdfba715')], toCkbAddress: 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqq9kxr7vy7yknezj0vj0xptx6thk6pwyr0sxamv6q', - // To simplify, keep the transferAmount the same as 2-ckb-jump-btc + xudtTypeArgs: '0x1ba116c119d1cfd98a53e9d1a615cf2af2bb87d95515c9d217d367054cfc696b', transferAmount: BigInt(800_0000_0000), }); diff --git a/examples/xudt-on-ckb/.env.example b/examples/xudt-on-ckb/.env.example new file mode 100644 index 00000000..3f2ce3bf --- /dev/null +++ b/examples/xudt-on-ckb/.env.example @@ -0,0 +1,11 @@ +# True for CKB Mainnet and false for CKB Testnet, the default value is false +IS_MAINNET=false + +# The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix +CKB_SECP256K1_PRIVATE_KEY=0x-private-key + +# CKB node url which should be matched with IS_MAINNET +CKB_NODE_URL=https://testnet.ckb.dev/rpc + +# CKB indexer url which should be matched with IS_MAINNET +CKB_INDEXER_URL=https://testnet.ckb.dev/indexer diff --git a/examples/xudt-on-ckb/1-issue-xudt.ts b/examples/xudt-on-ckb/1-issue-xudt.ts new file mode 100644 index 00000000..09d9c4b7 --- /dev/null +++ b/examples/xudt-on-ckb/1-issue-xudt.ts @@ -0,0 +1,111 @@ +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'; + +/** + * 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); + + 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}`); +}; + +const XUDT_TOKEN_INFO: RgbppTokenInfo = { + decimal: 8, + name: 'XUDT Test Token', + symbol: 'XTT', +}; + +issueXudt({ xudtTotalAmount: BigInt(2100_0000), tokenInfo: XUDT_TOKEN_INFO }); diff --git a/examples/rgbpp/xudt/2-transfer-xudt.ts b/examples/xudt-on-ckb/2-transfer-xudt.ts similarity index 79% rename from examples/rgbpp/xudt/2-transfer-xudt.ts rename to examples/xudt-on-ckb/2-transfer-xudt.ts index 7a150aa5..e9f8ede4 100644 --- a/examples/rgbpp/xudt/2-transfer-xudt.ts +++ b/examples/xudt-on-ckb/2-transfer-xudt.ts @@ -1,7 +1,7 @@ -import { AddressPrefix, addressToScript, getTransactionSize, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; import { getSecp256k1CellDep, - Collector, + RgbppTokenInfo, NoLiveCellError, calculateUdtCellCapacity, MAX_FEE, @@ -13,13 +13,10 @@ import { SECP256K1_WITNESS_LOCK_SIZE, calculateTransactionFee, NoXudtLiveCellError, -} from '@rgbpp-sdk/ckb'; -import { XUDT_TOKEN_INFO } from './0-token-info'; +} from 'rgbpp/ckb'; +import { CKB_PRIVATE_KEY, ckbAddress, collector, isMainnet } from './env'; -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -interface TransferParams { +interface XudtTransferParams { xudtType: CKBComponents.Script; receivers: { toAddress: string; @@ -29,21 +26,11 @@ interface TransferParams { /** * transferXudt can be used to mint xUDT assets or transfer xUDT assets. - * @param: xudtType The xUDT type script that comes from 1-issue-xudt - * @param: receivers The receiver includes toAddress and transferAmount + * @param xudtType The xUDT type script that comes from 1-issue-xudt + * @param receivers The receiver includes toAddress and transferAmount */ -const transferXudt = async ({ xudtType, receivers }: TransferParams) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const fromAddress = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', fromAddress); - - const fromLock = addressToScript(fromAddress); +const transferXudt = async ({ xudtType, receivers }: XudtTransferParams) => { + const fromLock = addressToScript(ckbAddress); const xudtCells = await collector.getCells({ lock: fromLock, @@ -110,7 +97,6 @@ const transferXudt = async ({ xudtType, receivers }: TransferParams) => { outputs.push({ lock: fromLock, - type: xudtType, capacity: append0x(changeCapacity.toString(16)), }); outputsData.push('0x'); @@ -137,12 +123,18 @@ const transferXudt = async ({ xudtType, receivers }: TransferParams) => { unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); } - const signedTx = collector.getCkb().signTransaction(CKB_TEST_PRIVATE_KEY)(unsignedTx); + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`xUDT asset has been minted or transferred and tx hash is ${txHash}`); }; +const XUDT_TOKEN_INFO: RgbppTokenInfo = { + decimal: 8, + name: 'XUDT Test Token', + symbol: 'XTT', +}; + transferXudt({ // The xudtType comes from 1-issue-xudt xudtType: { diff --git a/examples/xudt-on-ckb/README.md b/examples/xudt-on-ckb/README.md new file mode 100644 index 00000000..5e3d4afb --- /dev/null +++ b/examples/xudt-on-ckb/README.md @@ -0,0 +1,42 @@ +# xUDT on CKB Examples + +The examples for xUDT issuance, mint and transfer on CKB + +## How to Start + +Copy the `.env.example` file to `.env`: + +```shell +cd examples/xudt && cp .env.example .env +``` + +Update the configuration values: + +```yaml +# True for CKB Mainnet and false for CKB Testnet, the default value is false +IS_MAINNET=false + +# The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix +CKB_SECP256K1_PRIVATE_KEY=0x-private-key + +# CKB node url which should be matched with IS_MAINNET +CKB_NODE_URL=https://testnet.ckb.dev/rpc + +# CKB indexer url which should be matched with IS_MAINNET +CKB_INDEXER_URL=https://testnet.ckb.dev/indexer + +``` + +### Issue xUDT on CKB + +```shell +npx ts-node 1-issue-xudt.ts +``` + +### Mint/Transfer xUDT on CKB + +You can use this command to mint or transfer xUDT assets + +```shell +npx ts-node 2-transfer-xudt.ts +``` diff --git a/examples/xudt-on-ckb/env.ts b/examples/xudt-on-ckb/env.ts new file mode 100644 index 00000000..7cf52cd7 --- /dev/null +++ b/examples/xudt-on-ckb/env.ts @@ -0,0 +1,16 @@ +import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; +import { Collector } from 'rgbpp/ckb'; +import dotenv from 'dotenv'; + +dotenv.config({ path: __dirname + '/.env' }); + +export const isMainnet = process.env.IS_MAINNET === 'true' ? true : false; + +export const collector = new Collector({ + ckbNodeUrl: process.env.CKB_NODE_URL!, + ckbIndexerUrl: process.env.CKB_INDEXER_URL!, +}); +export const CKB_PRIVATE_KEY = process.env.CKB_SECP256K1_PRIVATE_KEY!; +export const ckbAddress = privateKeyToAddress(CKB_PRIVATE_KEY, { + prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, +}); diff --git a/examples/xudt-on-ckb/package.json b/examples/xudt-on-ckb/package.json new file mode 100644 index 00000000..e44b806e --- /dev/null +++ b/examples/xudt-on-ckb/package.json @@ -0,0 +1,20 @@ +{ + "name": "xudt-on-ckb-examples", + "version": "0.1.0", + "description": "Examples used for xUDT on CKB assets issuance and transfer", + "private": true, + "type": "commonjs", + "scripts": { + "format": "prettier --write '**/*.ts'", + "lint": "tsc && eslint . && prettier --check '**/*.ts'", + "lint:fix": "tsc && eslint --fix --ext .ts . && prettier --write '**/*.ts'" + }, + "dependencies": { + "@nervosnetwork/ckb-sdk-utils": "^0.109.1", + "rgbpp": "0.1.0" + }, + "devDependencies": { + "dotenv": "^16.4.5", + "@types/dotenv": "^8.2.0" + } +} diff --git a/examples/xudt-on-ckb/tsconfig.json b/examples/xudt-on-ckb/tsconfig.json new file mode 100644 index 00000000..4cc7c200 --- /dev/null +++ b/examples/xudt-on-ckb/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"] +} \ No newline at end of file diff --git a/package.json b/package.json index e45131ff..f9543082 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,18 @@ "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=./packags/*", + "clean:packages": "turbo run clean --filter=./packages/*", "clean:dependencies": "pnpm clean:sub-dependencies && rimraf node_modules", "clean:sub-dependencies": "rimraf packages/**/node_modules apps/**/node_modules", - "release:packages": "pnpm run clean:packages && pnpm run build:packages && changeset publish" + "release:packages": "pnpm run build:packages && changeset publish" }, "devDependencies": { - "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.1", + "@changesets/types": "^6.0.0", + "@changesets/get-github-info": "^0.6.0", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "eslint": "^8.56.0", @@ -32,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/packages/btc/.env.example b/packages/btc/.env.example index df8eb3d3..15c08d44 100644 --- a/packages/btc/.env.example +++ b/packages/btc/.env.example @@ -1,3 +1,3 @@ -VITE_SERVICE_URL= # URL of the service -VITE_SERVICE_TOKEN= # JWT token to access the service -VITE_SERVICE_ORIGIN= # URL representing your token's domain +VITE_BTC_SERVICE_URL= # URL of the service +VITE_BTC_SERVICE_TOKEN= # JWT token to access the service +VITE_BTC_SERVICE_ORIGIN= # URL representing your token's domain diff --git a/packages/btc/CHANGELOG.md b/packages/btc/CHANGELOG.md index 1aa30071..aff4d077 100644 --- a/packages/btc/CHANGELOG.md +++ b/packages/btc/CHANGELOG.md @@ -1,5 +1,21 @@ # @rgbpp-sdk/btc +## v0.2.0 + +### Minor Changes + +- [#184](https://github.com/ckb-cell/rgbpp-sdk/pull/184): Support query data caching internally in TxBuilder/DataSource, preventing query from the BtcAssetsApi too often when paying fee ([@ShookLyngs](https://github.com/ShookLyngs)) + +- [#165](https://github.com/ckb-cell/rgbpp-sdk/pull/165): Replace all "void 0" to "undefined" in the btc/service lib ([@ShookLyngs](https://github.com/ShookLyngs)) + +### Patch Changes + +- [#166](https://github.com/ckb-cell/rgbpp-sdk/pull/166): Fix the message of INSUFFICIENT_UTXO error when collection failed ([@ShookLyngs](https://github.com/ShookLyngs)) + +- Updated dependencies [[`5e0e817`](https://github.com/ckb-cell/rgbpp-sdk/commit/5e0e8175a4c195e6491a37abedc755728c91cbed), [`a9b9796`](https://github.com/ckb-cell/rgbpp-sdk/commit/a9b9796f5ef8d27a9ad94d09a832bb9a7fe56c8e), [`9f9daa9`](https://github.com/ckb-cell/rgbpp-sdk/commit/9f9daa91486ca0cc1015713bd2648aa606da8717), [`299b404`](https://github.com/ckb-cell/rgbpp-sdk/commit/299b404217036feab409956d8888bfdc8fa820f4), [`e59322e`](https://github.com/ckb-cell/rgbpp-sdk/commit/e59322e7c6b9aff682bc1c8517337e3611dc122d), [`64c4312`](https://github.com/ckb-cell/rgbpp-sdk/commit/64c4312768885cb965285d41de99d023a4517ed3), [`1d58dd5`](https://github.com/ckb-cell/rgbpp-sdk/commit/1d58dd531947f4078667bb7294d2b3bb9351ead9), [`8cfc06e`](https://github.com/ckb-cell/rgbpp-sdk/commit/8cfc06e449c213868f103d9757f79f24521da280), [`4fcf4fa`](https://github.com/ckb-cell/rgbpp-sdk/commit/4fcf4fa6c0b20cf2fa957664a320f66601991817)]: + - @rgbpp-sdk/ckb@0.2.0 + - @rgbpp-sdk/service@0.2.0 + ## v0.1.0 - Release @rgbpp-sdk/btc for RGBPP BTC-side transaction construction, providing APIs to send BTC or send RGBPP UTXO. Read the docs for more information: https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/btc diff --git a/packages/btc/README.md b/packages/btc/README.md index 6866c30e..e92232f7 100644 --- a/packages/btc/README.md +++ b/packages/btc/README.md @@ -372,6 +372,8 @@ interface DataSource { allowInsufficient?: boolean; onlyNonRgbppUtxos?: boolean; onlyConfirmedUtxos?: boolean; + noAssetsApiCache?: boolean; + internalCacheKey?: string; excludeUtxos?: { txid: string; vout: number; diff --git a/packages/btc/package.json b/packages/btc/package.json index 70efc86d..5140f506 100644 --- a/packages/btc/package.json +++ b/packages/btc/package.json @@ -1,6 +1,6 @@ { "name": "@rgbpp-sdk/btc", - "version": "0.1.0", + "version": "0.2.0", "scripts": { "test": "vitest", "build": "tsc -p tsconfig.build.json", diff --git a/packages/btc/src/address.ts b/packages/btc/src/address.ts index cbde4b6a..fa562d45 100644 --- a/packages/btc/src/address.ts +++ b/packages/btc/src/address.ts @@ -28,7 +28,7 @@ export function isSupportedFromAddress(address: string) { */ export function publicKeyToPayment(publicKey: string, addressType: AddressType, networkType: NetworkType) { if (!publicKey) { - return void 0; + return undefined; } const network = networkTypeToNetwork(networkType); @@ -64,7 +64,7 @@ export function publicKeyToPayment(publicKey: string, addressType: AddressType, }); } - return void 0; + return undefined; } /** @@ -147,7 +147,7 @@ export function decodeAddress(address: string): { addressType = AddressType.P2TR; } } - if (networkType !== void 0 && addressType !== void 0) { + if (networkType !== undefined && addressType !== undefined) { return { networkType, addressType, @@ -182,7 +182,7 @@ export function decodeAddress(address: string): { addressType = AddressType.P2SH_P2WPKH; } - if (networkType !== void 0 && addressType !== void 0) { + if (networkType !== undefined && addressType !== undefined) { return { networkType, addressType, diff --git a/packages/btc/src/api/sendRgbppUtxos.ts b/packages/btc/src/api/sendRgbppUtxos.ts index 6e40efb4..0bc23cc6 100644 --- a/packages/btc/src/api/sendRgbppUtxos.ts +++ b/packages/btc/src/api/sendRgbppUtxos.ts @@ -1,4 +1,5 @@ -import { Collector, isRgbppLockCell, isBtcTimeLockCell, calculateCommitment } from '@rgbpp-sdk/ckb'; +import { Collector, checkCkbTxInputsCapacitySufficient } from '@rgbpp-sdk/ckb'; +import { isRgbppLockCell, isBtcTimeLockCell, calculateCommitment } from '@rgbpp-sdk/ckb'; import { bitcoin } from '../bitcoin'; import { Utxo } from '../transaction/utxo'; import { DataSource } from '../query/source'; @@ -165,14 +166,16 @@ async function getMergedBtcOutputs(btcOutputs: InitOutput[], props: SendRgbppUtx const isPaymasterUnmatched = defaultPaymaster?.address !== props.paymaster?.address || defaultPaymaster?.value !== props.paymaster?.value; if (defaultPaymaster && props.paymaster && isPaymasterUnmatched) { - throw new TxBuildError(ErrorCodes.PAYMASTER_MISMATCH); + throw TxBuildError.withComment( + ErrorCodes.PAYMASTER_MISMATCH, + `expected: ${defaultPaymaster}, actual: ${props.paymaster}`, + ); } - // Add paymaster output - // TODO: can be more accurate if we compare the actual capacity of inputs & outputs + // Add paymaster output, only if paymaster address exists and needed const paymaster = defaultPaymaster ?? props.paymaster; - const needPaymasterOutput = props.ckbVirtualTx.inputs.length < props.ckbVirtualTx.outputs.length; - if (paymaster && needPaymasterOutput) { + const isCkbTxCapacitySufficient = await checkCkbTxInputsCapacitySufficient(props.ckbVirtualTx, props.ckbCollector); + if (paymaster && !isCkbTxCapacitySufficient) { merged.push({ ...paymaster, fixed: true, diff --git a/packages/btc/src/bitcoin.ts b/packages/btc/src/bitcoin.ts index c18fc83c..7673eeac 100644 --- a/packages/btc/src/bitcoin.ts +++ b/packages/btc/src/bitcoin.ts @@ -1,4 +1,4 @@ -import ECPairFactory from 'ecpair'; +import ECPairFactory, { ECPairInterface } from 'ecpair'; import ecc from '@bitcoinerlab/secp256k1'; import * as bitcoin from 'bitcoinjs-lib'; import { isTaprootInput } from 'bitcoinjs-lib/src/psbt/bip371'; @@ -7,4 +7,5 @@ bitcoin.initEccLib(ecc); const ECPair = ECPairFactory(ecc); +export type { ECPairInterface }; export { ecc, ECPair, bitcoin, isTaprootInput }; diff --git a/packages/btc/src/query/cache.ts b/packages/btc/src/query/cache.ts new file mode 100644 index 00000000..046b2c8e --- /dev/null +++ b/packages/btc/src/query/cache.ts @@ -0,0 +1,62 @@ +import { Utxo } from '../transaction/utxo'; + +export class DataCache { + private utxos: Map; // Map + private hasRgbppAssets: Map; // Map<`{txid}:{vout}`, HasAssets> + + constructor() { + this.utxos = new Map(); + this.hasRgbppAssets = new Map(); + } + + setUtxos(key: string, utxos: Utxo[]) { + this.utxos.set(key, utxos); + } + getUtxos(key: string): Utxo[] | undefined { + return this.utxos.get(key); + } + cleanUtxos(key: string) { + if (this.utxos.has(key)) { + this.utxos.delete(key); + } + } + async optionalCacheUtxos(props: { key?: string; getter: () => Promise | Utxo[] }): Promise { + if (props.key && this.utxos.has(props.key)) { + return this.getUtxos(props.key) as Utxo[]; + } + + const utxos = await props.getter(); + if (props.key) { + this.setUtxos(props.key, utxos); + } + + return utxos; + } + + setHasRgbppAssets(key: string, hasAssets: boolean) { + this.hasRgbppAssets.set(key, hasAssets); + } + getHasRgbppAssets(key: string): boolean | undefined { + return this.hasRgbppAssets.get(key); + } + cleanHasRgbppAssets(key: string) { + if (this.hasRgbppAssets.has(key)) { + this.hasRgbppAssets.delete(key); + } + } + async optionalCacheHasRgbppAssets(props: { + key?: string; + getter: () => Promise | boolean; + }): Promise { + if (props.key && this.hasRgbppAssets.has(props.key)) { + return this.getHasRgbppAssets(props.key) as boolean; + } + + const hasRgbppAssets = await props.getter(); + if (props.key) { + this.setHasRgbppAssets(props.key, hasRgbppAssets); + } + + return hasRgbppAssets; + } +} diff --git a/packages/btc/src/query/source.ts b/packages/btc/src/query/source.ts index f5a0f536..d7dcff0a 100644 --- a/packages/btc/src/query/source.ts +++ b/packages/btc/src/query/source.ts @@ -6,14 +6,17 @@ import { TxAddressOutput } from '../transaction/build'; import { isOpReturnScriptPubkey } from '../transaction/embed'; import { addressToScriptPublicKeyHex, getAddressType } from '../address'; import { remove0x } from '../utils'; +import { DataCache } from './cache'; export class DataSource { + public cache: DataCache; public service: BtcAssetsApi; public networkType: NetworkType; constructor(service: BtcAssetsApi, networkType: NetworkType) { this.service = service; this.networkType = networkType; + this.cache = new DataCache(); } // Query a UTXO from the service. @@ -35,14 +38,14 @@ export class DataSource { const txId = remove0x(hash); const tx = await this.service.getBtcTransaction(txId); if (!tx) { - return void 0; + return undefined; } if (requireConfirmed && !tx.status.confirmed) { throw TxBuildError.withComment(ErrorCodes.UNCONFIRMED_UTXO, `hash: ${hash}, index: ${index}`); } const vout = tx.vout[index]; if (!vout) { - return void 0; + return undefined; } const scriptBuffer = Buffer.from(vout.scriptpubkey, 'hex'); @@ -102,6 +105,8 @@ export class DataSource { allowInsufficient?: boolean; onlyNonRgbppUtxos?: boolean; onlyConfirmedUtxos?: boolean; + noAssetsApiCache?: boolean; + internalCacheKey?: string; excludeUtxos?: { txid: string; vout: number; @@ -117,12 +122,20 @@ export class DataSource { minUtxoSatoshi, onlyConfirmedUtxos, onlyNonRgbppUtxos, + noAssetsApiCache, + internalCacheKey, allowInsufficient = false, excludeUtxos = [], } = props; - const utxos = await this.getUtxos(address, { - only_confirmed: onlyConfirmedUtxos, - min_satoshi: minUtxoSatoshi, + + const utxos = await this.cache.optionalCacheUtxos({ + key: internalCacheKey, + getter: () => + this.getUtxos(address, { + only_confirmed: onlyConfirmedUtxos, + min_satoshi: minUtxoSatoshi, + no_cache: noAssetsApiCache, + }), }); const collected = []; @@ -140,8 +153,14 @@ export class DataSource { } } if (onlyNonRgbppUtxos) { - const ckbRgbppAssets = await this.service.getRgbppAssetsByBtcUtxo(utxo.txid, utxo.vout); - if (ckbRgbppAssets && ckbRgbppAssets.length > 0) { + const hasRgbppAssets = await this.cache.optionalCacheHasRgbppAssets({ + key: `${utxo.txid}:${utxo.vout}`, + getter: async () => { + const ckbRgbppAssets = await this.service.getRgbppAssetsByBtcUtxo(utxo.txid, utxo.vout); + return Array.isArray(ckbRgbppAssets) && ckbRgbppAssets.length > 0; + }, + }); + if (hasRgbppAssets) { continue; } } diff --git a/packages/btc/src/transaction/build.ts b/packages/btc/src/transaction/build.ts index 9e8451c6..3c54b5aa 100644 --- a/packages/btc/src/transaction/build.ts +++ b/packages/btc/src/transaction/build.ts @@ -145,17 +145,19 @@ export class TxBuilder { const originalInputs = clone(this.inputs); const originalOutputs = clone(this.outputs); - // Fetch the latest average fee rate if feeRate param is not provided - // The transaction is expected be confirmed within half an hour with the fee rate - let averageFeeRate: number | undefined; + // Create a cache key to enable the internal caching, prevent querying the Utxo[] too often + // TODO: consider provide an option to disable the cache + const internalCacheKey = `${Date.now()}`; + + // Fill a default recommended fee rate if props.feeRate is not provided + let defaultFeeRate: number | undefined; if (!feeRate && !this.feeRate) { const feeRates = await this.source.service.getBtcRecommendedFeeRates(); - averageFeeRate = feeRates.fastestFee; + defaultFeeRate = feeRates.fastestFee; } - // Use the feeRate param if it is specified, - // otherwise use the average fee rate from the DataSource - const currentFeeRate = feeRate ?? this.feeRate ?? averageFeeRate!; + // Use props.feeRate if it's specified + const currentFeeRate = feeRate ?? this.feeRate ?? defaultFeeRate!; let currentFee = 0; let previousFee = 0; @@ -172,15 +174,18 @@ export class TxBuilder { const safeToProcess = inputsTotal > 0 || previousFee > 0; const returnAmount = needReturn - previousFee; if (safeToProcess && returnAmount > 0) { - // If sum(inputs) - sum(outputs) > fee, return change while deducting fee - // Note, should not deduct fee from outputs while also returning change at the same time + // If sum(inputs) - sum(outputs) > fee, return (change - fee) to a non-fixed output or to a new output. + // Note when returning change to a new output, another satoshi collection may be needed. await this.injectChange({ address: changeAddress ?? address, amount: returnAmount, fromAddress: address, fromPublicKey: publicKey, + internalCacheKey, }); } else { + // If the inputs have insufficient satoshi, a satoshi collection is required. + // For protection, at least collect 1 satoshi if the inputs are empty or the fee hasn't been calculated. const protectionAmount = safeToProcess ? 0 : 1; const targetAmount = needCollect - needReturn + previousFee + protectionAmount; await this.injectSatoshi({ @@ -189,17 +194,24 @@ export class TxBuilder { targetAmount, changeAddress, deductFromOutputs, + internalCacheKey, }); } + // Calculate network fee const addressType = getAddressType(address); currentFee = await this.calculateFee(addressType, currentFeeRate); + + // If (fee = previousFee ±1), the fee is considered acceptable/expected. isFeeExpected = [-1, 0, 1].includes(currentFee - previousFee); if (!isLoopedOnce) { isLoopedOnce = true; } } + // Clear cache for the Utxo[] list + this.source.cache.cleanUtxos(internalCacheKey); + return { fee: currentFee, feeRate: currentFeeRate, @@ -213,27 +225,34 @@ export class TxBuilder { changeAddress?: string; injectCollected?: boolean; deductFromOutputs?: boolean; + internalCacheKey?: string; }) { if (!isSupportedFromAddress(props.address)) { throw TxBuildError.withComment(ErrorCodes.UNSUPPORTED_ADDRESS_TYPE, props.address); } + const targetAmount = props.targetAmount; const injectCollected = props.injectCollected ?? false; const deductFromOutputs = props.deductFromOutputs ?? true; let collected = 0; let changeAmount = 0; - let targetAmount = props.targetAmount; - - const _collect = async (_targetAmount: number, stack?: boolean) => { - if (stack) { - targetAmount += _targetAmount; - } + /** + * Collect from the "from" address via DataSource. + * Will update the value of inputs/collected/changeAmount. + * + * The API has two layers of data caching: + * - noAssetsApiCache: BtcAssetsApi cache, can be disabled if the set to true + * - internalCacheKey: Internal cache, enabled if the key is provided + */ + const _collect = async (_targetAmount: number) => { const { utxos, satoshi } = await this.source.collectSatoshi({ address: props.address, targetAmount: _targetAmount, allowInsufficient: true, + noAssetsApiCache: true, + internalCacheKey: props.internalCacheKey, minUtxoSatoshi: this.minUtxoSatoshi, onlyNonRgbppUtxos: this.onlyNonRgbppUtxos, onlyConfirmedUtxos: this.onlyConfirmedUtxos, @@ -249,6 +268,11 @@ export class TxBuilder { collected += satoshi; _updateChangeAmount(); }; + /** + * Update changeAmount depends on injectedCollected: + * - true: If targetAmount=1000, collected=2000, changeAmount=2000+1000=3000 + * - false: If targetAmount=1000, collected=2000, changeAmount=2000-1000=1000 + */ const _updateChangeAmount = () => { if (injectCollected) { changeAmount = collected + targetAmount; @@ -257,7 +281,7 @@ export class TxBuilder { } }; - // Collect from outputs + // 1. Collect from the non-fixed outputs if (deductFromOutputs) { for (let i = 0; i < this.outputs.length; i++) { const output = this.outputs[i]; @@ -268,21 +292,20 @@ export class TxBuilder { break; } - // If output.protected is true, do not destroy the output - // Only collect the satoshi from (output.value - minUtxoSatoshi) const minUtxoSatoshi = output.minUtxoSatoshi ?? this.minUtxoSatoshi; const freeAmount = output.value - minUtxoSatoshi; const remain = targetAmount - collected; if (output.protected) { - // freeAmount=100, remain=50, collectAmount=50 - // freeAmount=100, remain=150, collectAmount=100 + // If output.protected=true: + // - Only deduct free satoshi from the output + // - Won't destroy the output, at least keep (output.value = minUtxoSatoshi) const collectAmount = Math.min(freeAmount, remain); output.value -= collectAmount; collected += collectAmount; } else { - // output.value=200, freeAmount=100, remain=50, collectAmount=50 - // output.value=200, freeAmount=100, remain=150, collectAmount=100 - // output.value=100, freeAmount=0, remain=150, collectAmount=100 + // If output.protected=false: + // - If (target collect amount > output.value), deduct all output.value + // - Destroy the output if all value is deducted const collectAmount = output.value > remain ? Math.min(freeAmount, remain) : output.value; output.value -= collectAmount; collected += collectAmount; @@ -295,22 +318,24 @@ export class TxBuilder { } } - // Collect target amount of satoshi from DataSource + // 2. Collect from the "from" address if (collected < targetAmount) { await _collect(targetAmount - collected); } - // If 0 < change amount < minUtxoSatoshi, collect one more time + // 3. Collect from "from" one more time if: + // - Need to create an output to return change (changeAmount > 0) + // - The change is insufficient for a non-dust output (changeAmount < minUtxoSatoshi) const needForChange = changeAmount > 0 && changeAmount < this.minUtxoSatoshi; const changeUtxoNeedAmount = needForChange ? this.minUtxoSatoshi - changeAmount : 0; if (needForChange) { await _collect(changeUtxoNeedAmount); } - // If not collected enough satoshi, throw error + // 4. If not collected enough satoshi, throw an error const insufficientBalance = collected < targetAmount; if (insufficientBalance) { - const recommendedDeposit = collected - targetAmount + this.minUtxoSatoshi; + const recommendedDeposit = targetAmount - collected + this.minUtxoSatoshi; throw TxBuildError.withComment( ErrorCodes.INSUFFICIENT_UTXO, `expected: ${targetAmount}, actual: ${collected}. You may wanna deposit more satoshi to prevent the error, for example: ${recommendedDeposit}`, @@ -325,7 +350,9 @@ export class TxBuilder { ); } - // Return change + // 5. Return change: + // - If changeAmount=0, no need to create a change output, and the changeIndex=-1 + // - If changeAmount>0, return change to an output or create a change output let changeIndex: number = -1; if (changeAmount > 0) { changeIndex = this.outputs.length; @@ -345,9 +372,17 @@ export class TxBuilder { }; } - async injectChange(props: { amount: number; address: string; fromAddress: string; fromPublicKey?: string }) { - const { address, fromAddress, fromPublicKey, amount } = props; + async injectChange(props: { + amount: number; + address: string; + fromAddress: string; + fromPublicKey?: string; + internalCacheKey?: string; + }) { + const { address, fromAddress, fromPublicKey, amount, internalCacheKey } = props; + // If any (output.fixed != true) is found in the outputs (search in ASC order), + // return the change value to the first matched output. for (let i = 0; i < this.outputs.length; i++) { const output = this.outputs[i]; if (output.fixed) { @@ -362,6 +397,13 @@ export class TxBuilder { } if (amount < this.minUtxoSatoshi) { + // If the change is not enough to create a non-dust output, try collect more. + // - injectCollected=true, expect to put all (collected + amount) of satoshi as change + // - deductFromOutputs=false, do not collect satoshi from the outputs + // An example: + // 1. Expected to return change of 500 satoshi, amount=500 + // 2. Collected 2000 satoshi from the "fromAddress", collected=2000 + // 3. Create a change output and return (collected + amount), output.value=2000+500=2500 const { collected } = await this.injectSatoshi({ address: fromAddress, publicKey: fromPublicKey, @@ -369,6 +411,7 @@ export class TxBuilder { changeAddress: address, injectCollected: true, deductFromOutputs: false, + internalCacheKey, }); if (collected < amount) { throw TxBuildError.withComment(ErrorCodes.INSUFFICIENT_UTXO, `expected: ${amount}, actual: ${collected}`); diff --git a/packages/btc/src/utils.ts b/packages/btc/src/utils.ts index c9b29896..bc63977f 100644 --- a/packages/btc/src/utils.ts +++ b/packages/btc/src/utils.ts @@ -72,6 +72,6 @@ export function utf8ToBuffer(text: string): Uint8Array { * Note if using for RGBPP proof, shouldn't set the "withWitness" param to "true". */ export function transactionToHex(tx: bitcoin.Transaction, withWitness?: boolean): string { - const buffer: Buffer = tx['__toBuffer'](void 0, void 0, withWitness ?? false); + const buffer: Buffer = tx['__toBuffer'](undefined, undefined, withWitness ?? false); return buffer.toString('hex'); } diff --git a/packages/btc/tests/shared/env.ts b/packages/btc/tests/shared/env.ts index 79c6e203..195a200b 100644 --- a/packages/btc/tests/shared/env.ts +++ b/packages/btc/tests/shared/env.ts @@ -6,9 +6,9 @@ export const config = networkTypeToConfig(networkType); export const network = config.network; export const service = BtcAssetsApi.fromToken( - process.env.VITE_SERVICE_URL!, - process.env.VITE_SERVICE_TOKEN!, - process.env.VITE_SERVICE_ORIGIN!, + process.env.VITE_BTC_SERVICE_URL!, + process.env.VITE_BTC_SERVICE_TOKEN!, + process.env.VITE_BTC_SERVICE_ORIGIN!, ); export const source = new DataSource(service, networkType); diff --git a/packages/ckb/CHANGELOG.md b/packages/ckb/CHANGELOG.md index 9636deec..93b550d7 100644 --- a/packages/ckb/CHANGELOG.md +++ b/packages/ckb/CHANGELOG.md @@ -1,5 +1,28 @@ # @rgbpp-sdk/ckb +## v0.2.0 + +### Minor Changes + +- [#179](https://github.com/ckb-cell/rgbpp-sdk/pull/179): Increase the max length of RGB++ inputs to 40 ([@duanyytop](https://github.com/duanyytop)) + +- [#160](https://github.com/ckb-cell/rgbpp-sdk/pull/160): Collect all RGB++ inputs without isMax parameter ([@duanyytop](https://github.com/duanyytop)) + +- [#190](https://github.com/ckb-cell/rgbpp-sdk/pull/190): Filter xudt cell whose amount is valid for collector ([@duanyytop](https://github.com/duanyytop)) + +- [#172](https://github.com/ckb-cell/rgbpp-sdk/pull/172): Check spore type script for spore transfer and leap ([@duanyytop](https://github.com/duanyytop)) + +- [#171](https://github.com/ckb-cell/rgbpp-sdk/pull/171): Build ckb raw tx to be signed for spores creation ([@duanyytop](https://github.com/duanyytop)) + +- [#174](https://github.com/ckb-cell/rgbpp-sdk/pull/174): Update ckb cell fields size to make the code more readable ([@duanyytop](https://github.com/duanyytop)) + +- [#187](https://github.com/ckb-cell/rgbpp-sdk/pull/187): Update RRB++ witnesses for BTC batch transfer TX ([@duanyytop](https://github.com/duanyytop)) + +### Patch Changes + +- Updated dependencies [[`9f9daa9`](https://github.com/ckb-cell/rgbpp-sdk/commit/9f9daa91486ca0cc1015713bd2648aa606da8717), [`e59322e`](https://github.com/ckb-cell/rgbpp-sdk/commit/e59322e7c6b9aff682bc1c8517337e3611dc122d)]: + - @rgbpp-sdk/service@0.2.0 + ## v0.1.0 - Release @rgbpp-sdk/ckb for RGBPP CKB-side transaction construction, providing APIs to bind/transfer/leap assets on CKB/BTC. Read the docs for more information: https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/ckb diff --git a/packages/ckb/README.md b/packages/ckb/README.md index 632d4e8c..a94cf28e 100644 --- a/packages/ckb/README.md +++ b/packages/ckb/README.md @@ -147,8 +147,6 @@ export interface SporeCreateVirtualTxResult { * @param collector The collector that collects CKB live cells and transactions * @param clusterRgbppLockArgs The cluster rgbpp cell lock script args whose data structure is: out_index | bitcoin_tx_id * @param sporeDataList The spore's data list, including name and description. - * @param witnessLockPlaceholderSize The WitnessArgs.lock placeholder bytes array size and the default value is 5000 - * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 */ export const genCreateSporeCkbVirtualTx = async ({ collector, diff --git a/packages/ckb/package.json b/packages/ckb/package.json index ade8494d..0971879f 100644 --- a/packages/ckb/package.json +++ b/packages/ckb/package.json @@ -1,6 +1,6 @@ { "name": "@rgbpp-sdk/ckb", - "version": "0.1.0", + "version": "0.2.0", "scripts": { "test": "vitest", "build": "tsc -p tsconfig.build.json", diff --git a/packages/ckb/src/collector/index.ts b/packages/ckb/src/collector/index.ts index 88b5739a..032e2290 100644 --- a/packages/ckb/src/collector/index.ts +++ b/packages/ckb/src/collector/index.ts @@ -110,6 +110,7 @@ export class Collector { const changeCapacity = config?.minCapacity ?? MIN_CAPACITY; const inputs: CKBComponents.CellInput[] = []; let sumInputsCapacity = BigInt(0); + const isRgbppLock = liveCells.length > 0 && isRgbppLockCellIgnoreChain(liveCells[0].output); for (const cell of liveCells) { inputs.push({ previousOutput: { @@ -119,7 +120,7 @@ export class Collector { since: '0x0', }); sumInputsCapacity += BigInt(cell.output.capacity); - if (sumInputsCapacity >= needCapacity + changeCapacity + fee && !config?.isMax) { + if (sumInputsCapacity >= needCapacity + changeCapacity + fee && !isRgbppLock) { break; } } @@ -136,6 +137,9 @@ export class Collector { let sumAmount = BigInt(0); const isRgbppLock = liveCells.length > 0 && isRgbppLockCellIgnoreChain(liveCells[0].output); for (const cell of liveCells) { + if (cell.outputData === '0x') { + continue; + } inputs.push({ previousOutput: { txHash: cell.outPoint.txHash, diff --git a/packages/ckb/src/constants/index.ts b/packages/ckb/src/constants/index.ts index 5430a88b..cf92757a 100644 --- a/packages/ckb/src/constants/index.ts +++ b/packages/ckb/src/constants/index.ts @@ -4,7 +4,7 @@ export const MIN_CAPACITY = BigInt(61) * BigInt(10000_0000); export const SECP256K1_WITNESS_LOCK_SIZE = 65; export const BTC_JUMP_CONFIRMATION_BLOCKS = 6; export const RGBPP_TX_WITNESS_MAX_SIZE = 5000; -export const RGBPP_TX_INPUTS_MAX_LENGTH = 10; +export const RGBPP_TX_INPUTS_MAX_LENGTH = 40; export const RGBPP_WITNESS_PLACEHOLDER = '0xFF'; export const RGBPP_TX_ID_PLACEHOLDER = '0000000000000000000000000000000000000000000000000000000000000000'; diff --git a/packages/ckb/src/rgbpp/btc-jump-ckb.ts b/packages/ckb/src/rgbpp/btc-jump-ckb.ts index 7fd2538b..6e3de3e4 100644 --- a/packages/ckb/src/rgbpp/btc-jump-ckb.ts +++ b/packages/ckb/src/rgbpp/btc-jump-ckb.ts @@ -20,6 +20,7 @@ import { genBtcTimeLockScript, genRgbppLockScript, throwErrorWhenRgbppCellsInvalid, + isRgbppCapacitySufficientForChange, } from '../utils/rgbpp'; import { Hex, IndexerCell } from '../types'; import { @@ -95,9 +96,10 @@ export const genBtcJumpCkbVirtualTx = async ({ throw new Error('The lock script size of the to ckb address is too large'); } - const needChange = sumAmount > transferAmount; + let needPaymasterCell = false; + const needRgbppChange = sumAmount > transferAmount; // To simplify, when the xUDT does not need change, all the capacity of the inputs will be given to the receiver - const receiverOutputCapacity = needChange ? BigInt(rgbppTargetCells[0].output.capacity) : sumInputsCapacity; + const receiverOutputCapacity = needRgbppChange ? BigInt(rgbppTargetCells[0].output.capacity) : sumInputsCapacity; // The BTC time cell does not need to be bound to the BTC UTXO const outputs: CKBComponents.CellOutput[] = [ { @@ -108,9 +110,11 @@ export const genBtcJumpCkbVirtualTx = async ({ ]; const outputsData = [append0x(u128ToLe(transferAmount))]; - if (needChange) { - // When the number of sender's inputs is greater than 1, the sender needs to recover the excess capacity. - const udtChangeCapacity = inputs.length > 1 ? sumInputsCapacity - receiverOutputCapacity : rgbppCellCapacity; + if (needRgbppChange) { + const isCapacitySufficient = isRgbppCapacitySufficientForChange(sumInputsCapacity, receiverOutputCapacity); + needPaymasterCell = !isCapacitySufficient; + // When the capacity of inputs is enough for the outputs, the sender needs to recover the excess capacity. + const udtChangeCapacity = isCapacitySufficient ? sumInputsCapacity - receiverOutputCapacity : rgbppCellCapacity; outputs.push({ // The Vouts[0] for OP_RETURN and Vouts[1] for RGBPP assets, BTC time cells don't need btc tx out_index lock: genRgbppLockScript(buildPreLockArgs(1), isMainnet), @@ -136,7 +140,6 @@ export const genBtcJumpCkbVirtualTx = async ({ } const cellDeps = [getRgbppLockDep(isMainnet), getXudtDep(isMainnet), getRgbppLockConfigDep(isMainnet)]; - const needPaymasterCell = inputs.length < outputs.length; if (needPaymasterCell) { cellDeps.push(getSecp256k1CellDep(isMainnet)); } diff --git a/packages/ckb/src/rgbpp/btc-transfer.ts b/packages/ckb/src/rgbpp/btc-transfer.ts index ab5a59a1..4bc38ada 100644 --- a/packages/ckb/src/rgbpp/btc-transfer.ts +++ b/packages/ckb/src/rgbpp/btc-transfer.ts @@ -23,6 +23,7 @@ import { compareInputs, estimateWitnessSize, genRgbppLockScript, + isRgbppCapacitySufficientForChange, throwErrorWhenRgbppCellsInvalid, throwErrorWhenTxInputsExceeded, } from '../utils/rgbpp'; @@ -96,6 +97,7 @@ export const genBtcTransferCkbVirtualTx = async ({ let sumInputsCapacity = BigInt(0); const outputs: CKBComponents.CellOutput[] = []; const outputsData: Hex[] = []; + let needPaymasterCell = false; // The non-target RGBPP outputs correspond to the RGBPP inputs one-to-one, and the outputs are still bound to the sender’s BTC UTXOs const handleNonTargetRgbppCells = (targetRgbppOutputLen: number) => { @@ -142,9 +144,9 @@ export const genBtcTransferCkbVirtualTx = async ({ const rgbppCellCapacity = calculateRgbppCellCapacity(xudtType); - const needChange = collectResult.sumAmount > transferAmount; + const needRgbppChange = collectResult.sumAmount > transferAmount; // To simplify, when the xUDT does not need change, all the capacity of the inputs will be given to the receiver - const receiverOutputCapacity = needChange ? BigInt(rgbppTargetCells[0].output.capacity) : sumInputsCapacity; + const receiverOutputCapacity = needRgbppChange ? BigInt(rgbppTargetCells[0].output.capacity) : sumInputsCapacity; // The Vouts[0] for OP_RETURN and Vouts[1] for target transfer RGBPP assets outputs.push({ lock: genRgbppLockScript(buildPreLockArgs(1), isMainnet), @@ -153,9 +155,11 @@ export const genBtcTransferCkbVirtualTx = async ({ }); outputsData.push(append0x(u128ToLe(transferAmount))); - if (needChange) { - // When the number of sender's inputs is greater than 1, the sender needs to recover the excess capacity. - const udtChangeCapacity = inputs.length > 1 ? sumInputsCapacity - receiverOutputCapacity : rgbppCellCapacity; + if (needRgbppChange) { + const isCapacitySufficient = isRgbppCapacitySufficientForChange(sumInputsCapacity, receiverOutputCapacity); + needPaymasterCell = !isCapacitySufficient; + // When the capacity of inputs is enough for the outputs, the sender needs to recover the excess capacity. + const udtChangeCapacity = isCapacitySufficient ? sumInputsCapacity - receiverOutputCapacity : rgbppCellCapacity; // The Vouts[2] for target change RGBPP assets outputs.push({ lock: genRgbppLockScript(buildPreLockArgs(2), isMainnet), @@ -169,7 +173,6 @@ export const genBtcTransferCkbVirtualTx = async ({ } const cellDeps = [getRgbppLockDep(isMainnet), getXudtDep(isMainnet), getRgbppLockConfigDep(isMainnet)]; - const needPaymasterCell = inputs.length < outputs.length; if (needPaymasterCell) { cellDeps.push(getSecp256k1CellDep(isMainnet)); } @@ -289,7 +292,17 @@ export const genBtcBatchTransferCkbVirtualTx = async ({ getRgbppLockConfigDep(isMainnet), getSecp256k1CellDep(isMainnet), ]; - const witnesses: Hex[] = inputs.map((_, index) => (index === 0 ? RGBPP_WITNESS_PLACEHOLDER : '0x')); + + const witnesses: Hex[] = []; + const lockArgsSet: Set = new Set(); + for (const cell of rgbppCells) { + if (lockArgsSet.has(cell.output.lock.args)) { + witnesses.push('0x'); + } else { + lockArgsSet.add(cell.output.lock.args); + witnesses.push(RGBPP_WITNESS_PLACEHOLDER); + } + } const ckbRawTx: CKBComponents.RawTransaction = { version: '0x0', diff --git a/packages/ckb/src/rgbpp/launch.ts b/packages/ckb/src/rgbpp/launch.ts index 905750b0..a851bfcd 100644 --- a/packages/ckb/src/rgbpp/launch.ts +++ b/packages/ckb/src/rgbpp/launch.ts @@ -57,7 +57,7 @@ export const genRgbppLaunchCkbVirtualTx = async ({ const infoCellCapacity = calculateRgbppTokenInfoCellCapacity(rgbppTokenInfo, isMainnet); const txFee = MAX_FEE; - const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, infoCellCapacity, txFee, { isMax: true }); + const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, infoCellCapacity, txFee); let rgbppCellCapacity = sumInputsCapacity - infoCellCapacity; const outputs: CKBComponents.CellOutput[] = [ diff --git a/packages/ckb/src/spore/index.ts b/packages/ckb/src/spore/index.ts index 0dde8337..c257a082 100644 --- a/packages/ckb/src/spore/index.ts +++ b/packages/ckb/src/spore/index.ts @@ -1,3 +1,5 @@ +export type { RawSporeData, RawClusterData } from '@spore-sdk/core'; + export * from './cluster'; export * from './spore'; export * from './leap'; diff --git a/packages/ckb/src/spore/leap.ts b/packages/ckb/src/spore/leap.ts index b2e2f64b..612af062 100644 --- a/packages/ckb/src/spore/leap.ts +++ b/packages/ckb/src/spore/leap.ts @@ -61,7 +61,7 @@ export const genLeapSporeFromBtcToCkbVirtualTx = async ({ }; const sporeCells = await collector.getCells({ lock: sporeRgbppLock, isDataMustBeEmpty: false }); - throwErrorWhenSporeCellsInvalid(sporeCells, sporeTypeBytes); + throwErrorWhenSporeCellsInvalid(sporeCells, sporeTypeBytes, isMainnet); const sporeCell = sporeCells![0]; diff --git a/packages/ckb/src/spore/spore.ts b/packages/ckb/src/spore/spore.ts index a3dd1d79..0ff01f31 100644 --- a/packages/ckb/src/spore/spore.ts +++ b/packages/ckb/src/spore/spore.ts @@ -9,6 +9,7 @@ import { import { buildPreLockArgs, calculateCommitment, genRgbppLockScript } from '../utils/rgbpp'; import { AppendIssuerCellToSporeCreate, + BuildAppendingIssuerCellTxParams, CreateSporeCkbVirtualTxParams, Hex, SporeCreateVirtualTxResult, @@ -56,8 +57,6 @@ import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; * @param collector The collector that collects CKB live cells and transactions * @param clusterRgbppLockArgs The cluster rgbpp cell lock script args whose data structure is: out_index | bitcoin_tx_id * @param sporeDataList The spore's data list, including name and description. - * @param witnessLockPlaceholderSize The WitnessArgs.lock placeholder bytes array size and the default value is 5000 - * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 */ export const genCreateSporeCkbVirtualTx = async ({ collector, @@ -155,28 +154,27 @@ export const genCreateSporeCkbVirtualTx = async ({ }; }; +const CELL_DEP_SIZE = 32 + 4 + 1; + /** - * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key - * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer + * Append paymaster cell to the ckb transaction inputs and build the raw tx to be signed for spores creation * @param issuerAddress The issuer ckb address * @param collector The collector that collects CKB live cells and transactions * @param ckbRawTx CKB raw transaction * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee + * @param witnessLockPlaceholderSize The WitnessArgs.lock placeholder bytes array size and the default value is 65 * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 */ -export const appendIssuerCellToSporesCreate = async ({ - secp256k1PrivateKey, +export const buildAppendingIssuerCellToSporesCreateTx = async ({ issuerAddress, collector, ckbRawTx, sumInputsCapacity, - isMainnet, + witnessLockPlaceholderSize = SECP256K1_WITNESS_LOCK_SIZE, ckbFeeRate, -}: AppendIssuerCellToSporeCreate): Promise => { +}: BuildAppendingIssuerCellTxParams): Promise => { const rawTx = ckbRawTx as CKBComponents.RawTransactionToSign; - const rgbppInputsLength = rawTx.inputs.length; - const sumOutputsCapacity: bigint = rawTx.outputs .map((output) => BigInt(output.capacity)) .reduce((prev, current) => prev + current, BigInt(0)); @@ -205,13 +203,46 @@ export const appendIssuerCellToSporesCreate = async ({ rawTx.outputs = [...rawTx.outputs, changeOutput]; rawTx.outputsData = [...rawTx.outputsData, '0x']; - rawTx.cellDeps = [...rawTx.cellDeps, getSecp256k1CellDep(isMainnet)]; - - const txSize = getTransactionSize(rawTx) + SECP256K1_WITNESS_LOCK_SIZE; + const txSize = getTransactionSize(rawTx) + witnessLockPlaceholderSize + CELL_DEP_SIZE; const estimatedTxFee = calculateTransactionFee(txSize, ckbFeeRate); changeCapacity -= estimatedTxFee; rawTx.outputs[rawTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); + return rawTx; +}; + +/** + * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key + * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer + * @param issuerAddress The issuer ckb address + * @param collector The collector that collects CKB live cells and transactions + * @param ckbRawTx CKB raw transaction + * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee + * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 + */ +export const appendIssuerCellToSporesCreate = async ({ + secp256k1PrivateKey, + issuerAddress, + collector, + ckbRawTx, + sumInputsCapacity, + isMainnet, + ckbFeeRate, +}: AppendIssuerCellToSporeCreate): Promise => { + const rgbppInputsLength = ckbRawTx.inputs.length; + + const rawTx = await buildAppendingIssuerCellToSporesCreateTx({ + issuerAddress, + collector, + ckbRawTx, + sumInputsCapacity, + ckbFeeRate, + }); + + rawTx.cellDeps = [...rawTx.cellDeps, getSecp256k1CellDep(isMainnet)]; + + const issuerLock = addressToScript(issuerAddress); + const keyMap = new Map(); keyMap.set(scriptToHash(issuerLock), secp256k1PrivateKey); keyMap.set(scriptToHash(getRgbppLockScript(isMainnet)), ''); @@ -272,7 +303,7 @@ export const genTransferSporeCkbVirtualTx = async ({ }; const sporeCells = await collector.getCells({ lock: sporeRgbppLock, isDataMustBeEmpty: false }); - throwErrorWhenSporeCellsInvalid(sporeCells, sporeTypeBytes); + throwErrorWhenSporeCellsInvalid(sporeCells, sporeTypeBytes, isMainnet); const sporeCell = sporeCells![0]; diff --git a/packages/ckb/src/types/collector.ts b/packages/ckb/src/types/collector.ts index 9bbb7716..f1db7bc8 100644 --- a/packages/ckb/src/types/collector.ts +++ b/packages/ckb/src/types/collector.ts @@ -24,7 +24,6 @@ export interface CollectUdtResult extends CollectResult { } export interface CollectConfig { - isMax?: boolean; minCapacity?: bigint; errMsg?: string; } diff --git a/packages/ckb/src/types/spore.ts b/packages/ckb/src/types/spore.ts index 871c6e75..56a5afcd 100644 --- a/packages/ckb/src/types/spore.ts +++ b/packages/ckb/src/types/spore.ts @@ -51,6 +51,21 @@ export interface SporeCreateVirtualTxResult { clusterCell: IndexerCell; } +export interface BuildAppendingIssuerCellTxParams { + // The issuer ckb address + issuerAddress: Address; + // The collector that collects CKB live cells and transactions + collector: Collector; + // CKB raw transaction + ckbRawTx: CKBComponents.RawTransaction; + // The sum capacity of the ckb inputs + sumInputsCapacity: Hex; + // The WitnessArgs.lock placeholder bytes array size and the default value is 5000 + witnessLockPlaceholderSize?: number; + // The CKB transaction fee rate, default value is 1100 + ckbFeeRate?: bigint; +} + export interface AppendIssuerCellToSporeCreate { // The Secp256k1 private key of the issuer cells maintainer secp256k1PrivateKey: Hex; diff --git a/packages/ckb/src/utils/ckb-tx.spec.ts b/packages/ckb/src/utils/ckb-tx.spec.ts index efe37556..3ff2d3fc 100644 --- a/packages/ckb/src/utils/ckb-tx.spec.ts +++ b/packages/ckb/src/utils/ckb-tx.spec.ts @@ -10,9 +10,12 @@ import { isLockArgsSizeExceeded, isScriptEqual, isTypeAssetSupported, + checkCkbTxInputsCapacitySufficient, } from './ckb-tx'; -import { utf8ToHex } from '.'; import { hexToBytes } from '@nervosnetwork/ckb-sdk-utils'; +import { Collector } from '../collector'; +import { NoLiveCellError } from '../error'; +import { utf8ToHex } from './hex'; describe('ckb tx utils', () => { it('calculateTransactionFee', () => { @@ -168,4 +171,123 @@ describe('ckb tx utils', () => { ), ); }); + + it('checkCkbTxInputsCapacitySufficient', { timeout: 20000 }, async () => { + const collector = new Collector({ + ckbNodeUrl: 'https://testnet.ckb.dev/rpc', + ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', + }); + const ckbTxWithDeadCell: CKBComponents.RawTransaction = { + cellDeps: [], + headerDeps: [], + inputs: [ + { + previousOutput: { + index: '0x1', + txHash: '0x1a6d2b18faed84293b81ada9d00600a3cdb637fa43a5cfa20eb63934757352ea', + }, + since: '0x0', + }, + ], + outputs: [ + { + capacity: '0x5e9f53e00', + lock: { + args: '0x01000000850cf65f93ed86e53044e94049ae76115ab25a4897de9247f947d390dcf4a4fc', + codeHash: '0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248', + hashType: 'type', + }, + type: { + args: '0x6b6a9580fc2aceb920c63adea27a667acfc180f67cf875b36f31b42546ac4920', + codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', + hashType: 'type', + }, + }, + ], + outputsData: ['0x00743ba40b0000000000000000000000'], + version: '0x0', + witnesses: [], + }; + try { + await checkCkbTxInputsCapacitySufficient(ckbTxWithDeadCell, collector); + } catch (error) { + if (error instanceof NoLiveCellError) { + expect(102).toBe(error.code); + expect('The cell with the specific out point is dead').toBe(error.message); + } + } + + const invalidCkbTx: CKBComponents.RawTransaction = { + cellDeps: [], + headerDeps: [], + inputs: [ + { + previousOutput: { + index: '0x0', + txHash: '0xeb6ea53459efc83755e4ede6ff54b7698913379e678c6018e1eac87241f964f2', + }, + since: '0x0', + }, + { + previousOutput: { + index: '0x0', + txHash: '0x80314ab559ddc7b2f9e523f968b2d930b1a7b53f690091e6666570b46f54b804', + }, + since: '0x0', + }, + ], + outputs: [ + { + capacity: '0x65e9f53e00', + lock: { + args: '0x01000000850cf65f93ed86e53044e94049ae76115ab25a4897de9247f947d390dcf4a4fc', + codeHash: '0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248', + hashType: 'type', + }, + type: { + args: '0x6b6a9580fc2aceb920c63adea27a667acfc180f67cf875b36f31b42546ac4920', + codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', + hashType: 'type', + }, + }, + ], + outputsData: ['0x00743ba40b0000000000000000000000'], + version: '0x0', + witnesses: [], + }; + expect(false).toBe(await checkCkbTxInputsCapacitySufficient(invalidCkbTx, collector)); + + const ckbTx: CKBComponents.RawTransaction = { + cellDeps: [], + headerDeps: [], + inputs: [ + { + previousOutput: { + index: '0x0', + txHash: '0xeb6ea53459efc83755e4ede6ff54b7698913379e678c6018e1eac87241f964f2', + }, + since: '0x0', + }, + ], + outputs: [ + { + capacity: '0x5e9f53e00', + lock: { + args: '0x01000000850cf65f93ed86e53044e94049ae76115ab25a4897de9247f947d390dcf4a4fc', + codeHash: '0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248', + hashType: 'type', + }, + type: { + args: '0x6b6a9580fc2aceb920c63adea27a667acfc180f67cf875b36f31b42546ac4920', + codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', + hashType: 'type', + }, + }, + ], + outputsData: ['0x00743ba40b0000000000000000000000'], + version: '0x0', + witnesses: [], + }; + expect(true).toBe(await checkCkbTxInputsCapacitySufficient(ckbTx, collector)); + }); }); diff --git a/packages/ckb/src/utils/ckb-tx.ts b/packages/ckb/src/utils/ckb-tx.ts index cc19dfb3..943e5cd2 100644 --- a/packages/ckb/src/utils/ckb-tx.ts +++ b/packages/ckb/src/utils/ckb-tx.ts @@ -11,6 +11,8 @@ import { import { Hex, RgbppTokenInfo } from '../types'; import { PERSONAL, blake2b, hexToBytes, serializeInput, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; import { encodeRgbppTokenInfo, genBtcTimeLockScript } from './rgbpp'; +import { Collector } from '../collector'; +import { NoLiveCellError } from '../error'; export const calculateTransactionFee = (txSize: number, feeRate?: bigint): bigint => { const rate = feeRate ?? BigInt(1100); @@ -27,20 +29,31 @@ export const isUDTTypeSupported = (type: CKBComponents.Script, isMainnet: boolea return xudtType === typeAsset; }; -export const isClusterSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { +export const isSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { const sporeType = serializeScript(getSporeTypeScript(isMainnet)); + const typeAsset = serializeScript({ + ...type, + args: '', + }); + return sporeType === typeAsset; +}; + +export const isClusterSporeTypeSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { const typeAsset = serializeScript({ ...type, args: '', }); const clusterType = serializeScript(getClusterTypeScript(isMainnet)); - return sporeType === typeAsset || clusterType === typeAsset; + return isSporeTypeSupported(type, isMainnet) || clusterType === typeAsset; }; export const isTypeAssetSupported = (type: CKBComponents.Script, isMainnet: boolean): boolean => { return isUDTTypeSupported(type, isMainnet) || isClusterSporeTypeSupported(type, isMainnet); }; +const CELL_CAPACITY_SIZE = 8; +const UDT_CELL_DATA_SIZE = 16; + // The BTC_TIME_CELL_INCREASED_SIZE is related to the specific lock script. // We assume that the maximum length of lock script args is 26 bytes. If it exceeds, an error will be thrown. const LOCK_ARGS_HEX_MAX_SIZE = 26 * 2; @@ -61,7 +74,8 @@ const RGBPP_LOCK_SIZE = 32 + 1 + 36; export const calculateRgbppCellCapacity = (xudtType?: CKBComponents.Script): bigint => { const typeArgsSize = xudtType ? remove0x(xudtType.args).length / 2 : 32; const udtTypeSize = 33 + typeArgsSize; - const cellSize = RGBPP_LOCK_SIZE + udtTypeSize + 8 + 16 + BTC_TIME_CELL_INCREASED_SIZE; + const cellSize = + RGBPP_LOCK_SIZE + udtTypeSize + CELL_CAPACITY_SIZE + UDT_CELL_DATA_SIZE + BTC_TIME_CELL_INCREASED_SIZE; return BigInt(cellSize + 1) * CKB_UNIT; }; @@ -72,7 +86,9 @@ const DEFAULT_UDT_ARGS_SIZE = 32; export const calculateUdtCellCapacity = (lock: CKBComponents.Script, udtType?: CKBComponents.Script): bigint => { const lockArgsSize = remove0x(lock.args).length / 2; const typeArgsSize = udtType ? remove0x(udtType.args).length / 2 : DEFAULT_UDT_ARGS_SIZE; - const cellSize = 33 + lockArgsSize + 33 + typeArgsSize + 8 + 16; + const lockSize = 33 + lockArgsSize; + const typeSize = 33 + typeArgsSize; + const cellSize = lockSize + typeSize + CELL_CAPACITY_SIZE + UDT_CELL_DATA_SIZE; return BigInt(cellSize + 1) * CKB_UNIT; }; @@ -81,7 +97,7 @@ export const calculateXudtTokenInfoCellCapacity = (tokenInfo: RgbppTokenInfo, lo const lockSize = remove0x(lock.args).length / 2 + 33; const cellDataSize = remove0x(encodeRgbppTokenInfo(tokenInfo)).length / 2; const uniqueTypeSize = 32 + 1 + 20; - const cellSize = lockSize + uniqueTypeSize + 8 + cellDataSize; + const cellSize = lockSize + uniqueTypeSize + CELL_CAPACITY_SIZE + cellDataSize; return BigInt(cellSize) * CKB_UNIT; }; @@ -91,7 +107,7 @@ export const calculateRgbppTokenInfoCellCapacity = (tokenInfo: RgbppTokenInfo, i const lockSize = remove0x(btcTimeLock.args).length / 2 + 33; const cellDataSize = remove0x(encodeRgbppTokenInfo(tokenInfo)).length / 2; const typeSize = 32 + 1 + 20; - const cellSize = lockSize + typeSize + 8 + cellDataSize; + const cellSize = lockSize + typeSize + CELL_CAPACITY_SIZE + cellDataSize; return BigInt(cellSize) * CKB_UNIT; }; @@ -109,7 +125,7 @@ export const generateUniqueTypeArgs = (firstInput: CKBComponents.CellInput, firs export const calculateRgbppClusterCellCapacity = (clusterData: RawClusterData): bigint => { const clusterDataSize = packRawClusterData(clusterData).length; const clusterTypeSize = 32 + 1 + 32; - const cellSize = RGBPP_LOCK_SIZE + clusterTypeSize + 8 + clusterDataSize; + const cellSize = RGBPP_LOCK_SIZE + clusterTypeSize + CELL_CAPACITY_SIZE + clusterDataSize; return BigInt(cellSize + 1) * CKB_UNIT; }; @@ -125,7 +141,7 @@ export const calculateRgbppClusterCellCapacity = (clusterData: RawClusterData): export const calculateRgbppSporeCellCapacity = (sporeData: SporeDataProps): bigint => { const sporeDataSize = packRawSporeData(sporeData).length; const sporeTypeSize = 32 + 1 + 32; - const cellSize = RGBPP_LOCK_SIZE + sporeTypeSize + 8 + sporeDataSize + BTC_TIME_CELL_INCREASED_SIZE; + const cellSize = RGBPP_LOCK_SIZE + sporeTypeSize + CELL_CAPACITY_SIZE + sporeDataSize + BTC_TIME_CELL_INCREASED_SIZE; return BigInt(cellSize + 1) * CKB_UNIT; }; @@ -139,3 +155,27 @@ export const isScriptEqual = (s1: Hex | CKBComponents.Script, s2: Hex | CKBCompo const temp2 = typeof s2 === 'string' ? remove0x(s2) : remove0x(serializeScript(s2)); return temp1 === temp2; }; + +/** + * Check whether the capacity of inputs is sufficient for outputs + * @param ckbTx CKB raw transaction + * @param collector The collector that collects CKB live cells and transactions + * @returns When the capacity of inputs is sufficient for outputs, return true, otherwise return false + */ +export const checkCkbTxInputsCapacitySufficient = async ( + ckbTx: CKBComponents.RawTransaction, + collector: Collector, +): Promise => { + let sumInputsCapacity = BigInt(0); + for await (const input of ckbTx.inputs) { + const liveCell = await collector.getLiveCell(input.previousOutput!); + if (!liveCell) { + throw new NoLiveCellError('The cell with the specific out point is dead'); + } + sumInputsCapacity += BigInt(liveCell.output.capacity); + } + const sumOutputsCapacity = ckbTx.outputs + .map((output) => BigInt(output.capacity)) + .reduce((prev, current) => prev + current, BigInt(0)); + return sumInputsCapacity > sumOutputsCapacity; +}; diff --git a/packages/ckb/src/utils/rgbpp.spec.ts b/packages/ckb/src/utils/rgbpp.spec.ts index e4c720a0..560ce034 100644 --- a/packages/ckb/src/utils/rgbpp.spec.ts +++ b/packages/ckb/src/utils/rgbpp.spec.ts @@ -16,6 +16,7 @@ import { transformSpvProof, throwErrorWhenTxInputsExceeded, throwErrorWhenRgbppCellsInvalid, + isRgbppCapacitySufficientForChange, } from './rgbpp'; import { getXudtTypeScript } from '../constants'; import { IndexerCell, RgbppCkbVirtualTx } from '../types'; @@ -376,4 +377,16 @@ describe('rgbpp tests', () => { } } }); + + it('isRgbppCapacityEnoughForChange', () => { + expect(false).toBe( + isRgbppCapacitySufficientForChange(BigInt(500) * BigInt(10000_0000), BigInt(254) * BigInt(10000_0000)), + ); + expect(false).toBe( + isRgbppCapacitySufficientForChange(BigInt(507) * BigInt(10000_0000), BigInt(254) * BigInt(10000_0000)), + ); + expect(true).toBe( + isRgbppCapacitySufficientForChange(BigInt(508) * BigInt(10000_0000), BigInt(254) * BigInt(10000_0000)), + ); + }); }); diff --git a/packages/ckb/src/utils/rgbpp.ts b/packages/ckb/src/utils/rgbpp.ts index 684cab7c..19515685 100644 --- a/packages/ckb/src/utils/rgbpp.ts +++ b/packages/ckb/src/utils/rgbpp.ts @@ -3,6 +3,7 @@ import { Hex, IndexerCell, RgbppCkbVirtualTx, RgbppTokenInfo, SpvClientCellTxPro import { append0x, remove0x, reverseHex, u32ToLe, u8ToHex, utf8ToHex } from './hex'; import { BTC_JUMP_CONFIRMATION_BLOCKS, + CKB_UNIT, RGBPP_TX_ID_PLACEHOLDER, RGBPP_TX_INPUTS_MAX_LENGTH, RGBPP_TX_WITNESS_MAX_SIZE, @@ -28,7 +29,7 @@ import { RgbppCkbTxInputsExceededError, RgbppUtxoBindMultiTypeAssetsError, } from '../error'; -import { isScriptEqual, isUDTTypeSupported } from './ckb-tx'; +import { calculateRgbppCellCapacity, isScriptEqual, isUDTTypeSupported } from './ckb-tx'; export const genRgbppLockScript = (rgbppLockArgs: Hex, isMainnet: boolean) => { return { @@ -239,6 +240,7 @@ export const throwErrorWhenRgbppCellsInvalid = ( if (typeCells.length === 0) { throw new NoRgbppLiveCellError('No rgbpp cells found with the rgbpp lock args'); } + const isUDTTypeNotSupported = typeCells.some( (cell) => cell.output.type && !isUDTTypeSupported(cell.output.type, isMainnet), ); @@ -253,3 +255,14 @@ export const throwErrorWhenRgbppCellsInvalid = ( throw new NoRgbppLiveCellError('No rgbpp cells found with the xudt type script and the rgbpp lock args'); } }; + +/** + * Check if the tx's unoccupied capacity is enough to create a new rgbpp-cell as a UDT change cell + */ +export const isRgbppCapacitySufficientForChange = ( + sumUdtInputsCapacity: bigint, + receiverOutputCapacity: bigint, +): boolean => { + const rgbppOccupiedCapacity = calculateRgbppCellCapacity() - CKB_UNIT; + return sumUdtInputsCapacity > receiverOutputCapacity + rgbppOccupiedCapacity; +}; diff --git a/packages/ckb/src/utils/spore.spec.ts b/packages/ckb/src/utils/spore.spec.ts index 9c362fde..9f1dce0e 100644 --- a/packages/ckb/src/utils/spore.spec.ts +++ b/packages/ckb/src/utils/spore.spec.ts @@ -117,7 +117,7 @@ describe('spore utils', () => { }); try { - throwErrorWhenSporeCellsInvalid([], sporeTypeBytes); + throwErrorWhenSporeCellsInvalid([], sporeTypeBytes, false); } catch (error) { if (error instanceof NoRgbppLiveCellError) { expect(104).toBe(error.code); @@ -172,7 +172,7 @@ describe('spore utils', () => { }, ]; try { - throwErrorWhenSporeCellsInvalid(multiSporeCells, sporeTypeBytes); + throwErrorWhenSporeCellsInvalid(multiSporeCells, sporeTypeBytes, false); } catch (error) { if (error instanceof RgbppUtxoBindMultiTypeAssetsError) { expect(110).toBe(error.code); @@ -180,6 +180,39 @@ describe('spore utils', () => { } } + const noSupportedSporeCell: IndexerCell[] = [ + { + blockNumber: '0x0', + outPoint: { + txHash: '0xf2bfcd0ec5f7b2a33577168b7a647e71cc81a731560a7ad23b1c31fc08bbe1bb', + index: '0x1', + }, + output: { + capacity: '0x460913c00', + lock: { + args: '0x0200000050b34b391fd8f8084bf9b6af4368350c1510df4964496b87495ebee4bd8d86d5', + codeHash: '0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248', + hashType: 'type', + }, + type: { + args: '0xf2bfcd0ec5f7b2a33577168b7a647e71cc81a731560a7ad23b1c31fc08bbe1bb', + codeHash: '0xf2bfcd0ec5f7b2a33577168b7a647e71cc81a731560a7ad23b1c31fc08bbe1bb', + hashType: 'data1', + }, + }, + outputData: '0x2d000000100000001e0000002d0000000a000000746578742f706c61696e0b00000046697273742053706f7265', + txIndex: '0x0', + }, + ]; + try { + throwErrorWhenSporeCellsInvalid(noSupportedSporeCell, sporeTypeBytes, false); + } catch (error) { + if (error instanceof RgbppSporeTypeMismatchError) { + expect(111).toBe(error.code); + expect('The cell type is not the supported spore type script').toBe(error.message); + } + } + const noTargetCells: IndexerCell[] = [ { blockNumber: '0x0', @@ -205,7 +238,7 @@ describe('spore utils', () => { }, ]; try { - throwErrorWhenSporeCellsInvalid(noTargetCells, sporeTypeBytes); + throwErrorWhenSporeCellsInvalid(noTargetCells, sporeTypeBytes, false); } catch (error) { if (error instanceof RgbppSporeTypeMismatchError) { expect(111).toBe(error.code); diff --git a/packages/ckb/src/utils/spore.ts b/packages/ckb/src/utils/spore.ts index b43145db..471f73e7 100644 --- a/packages/ckb/src/utils/spore.ts +++ b/packages/ckb/src/utils/spore.ts @@ -12,7 +12,7 @@ import { import { u64ToLe } from './hex'; import { Hex, IndexerCell, SporesCreateCobuildParams } from '../types'; import { NoRgbppLiveCellError, RgbppSporeTypeMismatchError, RgbppUtxoBindMultiTypeAssetsError } from '../error'; -import { isScriptEqual } from './ckb-tx'; +import { isScriptEqual, isSporeTypeSupported } from './ckb-tx'; // Generate type id for cluster id export const generateClusterId = (firstInput: CKBComponents.CellInput, firstOutputIndex: number) => { @@ -101,7 +101,11 @@ export const generateSporeTransferCoBuild = ( }; // Check the validity of RGB++ spore cells and throw an exception if the conditions are not met to avoid building invalid CKB TX -export const throwErrorWhenSporeCellsInvalid = (sporeCells: IndexerCell[] | undefined, sporeTypeBytes: Hex) => { +export const throwErrorWhenSporeCellsInvalid = ( + sporeCells: IndexerCell[] | undefined, + sporeTypeBytes: Hex, + isMainnet: boolean, +) => { if (!sporeCells || sporeCells.length === 0) { throw new NoRgbppLiveCellError('No spore rgbpp cells found with the spore rgbpp lock args'); } @@ -114,6 +118,10 @@ export const throwErrorWhenSporeCellsInvalid = (sporeCells: IndexerCell[] | unde throw new RgbppSporeTypeMismatchError('The cell with the rgbpp lock args has no spore asset'); } + if (!isSporeTypeSupported(sporeCell.output.type, isMainnet)) { + throw new RgbppSporeTypeMismatchError('The cell type is not the supported spore type script'); + } + if (!isScriptEqual(sporeCell.output.type, sporeTypeBytes)) { throw new RgbppSporeTypeMismatchError('The spore cell type with the rgbpp lock args does not match'); } diff --git a/packages/rgbpp/CHANGELOG.md b/packages/rgbpp/CHANGELOG.md new file mode 100644 index 00000000..a8a4cc28 --- /dev/null +++ b/packages/rgbpp/CHANGELOG.md @@ -0,0 +1,16 @@ +# rgbpp + +## v0.2.0 + +### Minor Changes + +- [#157](https://github.com/ckb-cell/rgbpp-sdk/pull/157): Add rgbpp sub package ([@duanyytop](https://github.com/duanyytop)) + +### Patch Changes + +- [#177](https://github.com/ckb-cell/rgbpp-sdk/pull/177): Fix the export of NetworkType/AddressType in the rgbpp lib ([@ShookLyngs](https://github.com/ShookLyngs)) + +- Updated dependencies [[`8a8e11b`](https://github.com/ckb-cell/rgbpp-sdk/commit/8a8e11bdca4d3fb8b8d20c771e116bb1684bb1c6), [`5e0e817`](https://github.com/ckb-cell/rgbpp-sdk/commit/5e0e8175a4c195e6491a37abedc755728c91cbed), [`a9b9796`](https://github.com/ckb-cell/rgbpp-sdk/commit/a9b9796f5ef8d27a9ad94d09a832bb9a7fe56c8e), [`9f9daa9`](https://github.com/ckb-cell/rgbpp-sdk/commit/9f9daa91486ca0cc1015713bd2648aa606da8717), [`299b404`](https://github.com/ckb-cell/rgbpp-sdk/commit/299b404217036feab409956d8888bfdc8fa820f4), [`e59322e`](https://github.com/ckb-cell/rgbpp-sdk/commit/e59322e7c6b9aff682bc1c8517337e3611dc122d), [`64c4312`](https://github.com/ckb-cell/rgbpp-sdk/commit/64c4312768885cb965285d41de99d023a4517ed3), [`d37cf5b`](https://github.com/ckb-cell/rgbpp-sdk/commit/d37cf5b1aaf50f42a2f900f9b6aa073a916840b2), [`1d58dd5`](https://github.com/ckb-cell/rgbpp-sdk/commit/1d58dd531947f4078667bb7294d2b3bb9351ead9), [`8cfc06e`](https://github.com/ckb-cell/rgbpp-sdk/commit/8cfc06e449c213868f103d9757f79f24521da280), [`4fcf4fa`](https://github.com/ckb-cell/rgbpp-sdk/commit/4fcf4fa6c0b20cf2fa957664a320f66601991817)]: + - @rgbpp-sdk/btc@0.2.0 + - @rgbpp-sdk/ckb@0.2.0 + - @rgbpp-sdk/service@0.2.0 diff --git a/packages/rgbpp/README.md b/packages/rgbpp/README.md new file mode 100644 index 00000000..8680c297 --- /dev/null +++ b/packages/rgbpp/README.md @@ -0,0 +1,15 @@ +# rgbpp + +RGB++ SDK + +A root package to integrate of common functions from the RGB++ SDK sub-packages(btc/ckb/service). + +## Installation + +``` +$ npm i rgbpp +# or +$ yarn add rgbpp +# or +$ pnpm add rgbpp +``` \ No newline at end of file diff --git a/packages/rgbpp/package.json b/packages/rgbpp/package.json new file mode 100644 index 00000000..42a4a3dd --- /dev/null +++ b/packages/rgbpp/package.json @@ -0,0 +1,44 @@ +{ + "name": "rgbpp", + "version": "0.2.0", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "lint": "tsc && eslint --ext .ts src/* && prettier --check 'src/*.ts'", + "lint:fix": "tsc && eslint --fix --ext .ts src/* && prettier --write 'src/*.ts'", + "clean": "pnpm run clean:cache & pnpm run clean:build", + "clean:build": "rimraf lib && pnpm run clean:buildinfo", + "clean:buildinfo": "rimraf tsconfig.*tsbuildinfo", + "clean:cache": "rimraf .turbo" + }, + "main": "lib", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./btc": { + "types": "./lib/btc.d.ts", + "default": "./lib/btc.js" + }, + "./ckb": { + "types": "./lib/ckb.d.ts", + "default": "./lib/ckb.js" + }, + "./service": { + "types": "./lib/service.d.ts", + "default": "./lib/service.js" + } + }, + "files": [ + "lib" + ], + "types": "./lib/index.d.ts", + "dependencies": { + "@rgbpp-sdk/btc": "workspace:*", + "@rgbpp-sdk/ckb": "workspace:*", + "@rgbpp-sdk/service": "workspace:*" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/rgbpp/src/btc.ts b/packages/rgbpp/src/btc.ts new file mode 100644 index 00000000..749dca24 --- /dev/null +++ b/packages/rgbpp/src/btc.ts @@ -0,0 +1 @@ +export * from '@rgbpp-sdk/btc'; diff --git a/packages/rgbpp/src/ckb.ts b/packages/rgbpp/src/ckb.ts new file mode 100644 index 00000000..8232ce2c --- /dev/null +++ b/packages/rgbpp/src/ckb.ts @@ -0,0 +1 @@ +export * from '@rgbpp-sdk/ckb'; diff --git a/packages/rgbpp/src/index.ts b/packages/rgbpp/src/index.ts new file mode 100644 index 00000000..b7836861 --- /dev/null +++ b/packages/rgbpp/src/index.ts @@ -0,0 +1,56 @@ +/** + * ckb + */ +export { + genCreateClusterCkbVirtualTx, + genCreateSporeCkbVirtualTx, + genLeapSporeFromBtcToCkbVirtualTx, + genTransferSporeCkbVirtualTx, + genBtcTransferCkbVirtualTx, + genCkbJumpBtcVirtualTx, + genBtcBatchTransferCkbVirtualTx, + genLeapSporeFromCkbToBtcRawTx, + genRgbppLaunchCkbVirtualTx, + genBtcJumpCkbVirtualTx, + buildBtcTimeCellsSpentTx, + buildSporeBtcTimeCellsSpentTx, + signBtcTimeCellSpentTx, +} from '@rgbpp-sdk/ckb'; +export type { + CreateClusterCkbVirtualTxParams, + CreateSporeCkbVirtualTxParams, + LeapSporeFromBtcToCkbVirtualTxParams, + TransferSporeCkbVirtualTxParams, + BtcTransferVirtualTxParams, + BtcJumpCkbVirtualTxParams, + BtcBatchTransferVirtualTxParams, + CkbJumpBtcVirtualTxParams, + SporeCreateVirtualTxResult, + BtcTransferVirtualTxResult, + BtcJumpCkbVirtualTxResult, + BtcBatchTransferVirtualTxResult, + SporeTransferVirtualTxResult, + SporeLeapVirtualTxResult, + SporeVirtualTxResult, +} from '@rgbpp-sdk/ckb'; + +/** + * service + */ +export { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; + +/** + * btc + */ +export { + DataSource, + NetworkType, + AddressType, + sendBtc, + sendUtxos, + sendRgbppUtxos, + createSendBtcBuilder, + createSendUtxosBuilder, + createSendRgbppUtxosBuilder, +} from '@rgbpp-sdk/btc'; +export type { SendBtcProps, SendUtxosProps, SendRgbppUtxosProps } from '@rgbpp-sdk/btc'; diff --git a/packages/rgbpp/src/service.ts b/packages/rgbpp/src/service.ts new file mode 100644 index 00000000..550abcff --- /dev/null +++ b/packages/rgbpp/src/service.ts @@ -0,0 +1 @@ +export * from '@rgbpp-sdk/service'; diff --git a/packages/rgbpp/tsconfig.build.json b/packages/rgbpp/tsconfig.build.json new file mode 100644 index 00000000..f800f1bf --- /dev/null +++ b/packages/rgbpp/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "rootDir": "src", + "outDir": "lib", + "noEmit": false + } +} diff --git a/packages/rgbpp/tsconfig.json b/packages/rgbpp/tsconfig.json new file mode 100644 index 00000000..cf4cb2bf --- /dev/null +++ b/packages/rgbpp/tsconfig.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2015", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "ES2020", + "outDir": "./lib", + "composite": false, + "resolveJsonModule": true, + "strictNullChecks": true, + "noEmit": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/service/.env.example b/packages/service/.env.example index df8eb3d3..15c08d44 100644 --- a/packages/service/.env.example +++ b/packages/service/.env.example @@ -1,3 +1,3 @@ -VITE_SERVICE_URL= # URL of the service -VITE_SERVICE_TOKEN= # JWT token to access the service -VITE_SERVICE_ORIGIN= # URL representing your token's domain +VITE_BTC_SERVICE_URL= # URL of the service +VITE_BTC_SERVICE_TOKEN= # JWT token to access the service +VITE_BTC_SERVICE_ORIGIN= # URL representing your token's domain diff --git a/packages/service/CHANGELOG.md b/packages/service/CHANGELOG.md index 6d3d62d3..c8f94e79 100644 --- a/packages/service/CHANGELOG.md +++ b/packages/service/CHANGELOG.md @@ -1,5 +1,15 @@ # @rgbpp-sdk/service +## v0.2.0 + +### Minor Changes + +- [#165](https://github.com/ckb-cell/rgbpp-sdk/pull/165): Replace all "void 0" to "undefined" in the btc/service lib ([@ShookLyngs](https://github.com/ShookLyngs)) + +### Patch Changes + +- [#181](https://github.com/ckb-cell/rgbpp-sdk/pull/181): add no_cache params to btc/rgbpp service api ([@ahonn](https://github.com/ahonn)) + ## v0.1.0 - Release @rgbpp-sdk/service for communicating with the [btc-assets-api](https://github.com/ckb-cell/btc-assets-api), providing APIs to query data from or post transactions to the service. Read the docs for more information: https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service diff --git a/packages/service/README.md b/packages/service/README.md index 478cce02..a6bb4898 100644 --- a/packages/service/README.md +++ b/packages/service/README.md @@ -10,6 +10,9 @@ The `@rgbpp-sdk/service` package provides a wrapped class to interact with `Bitc - Simplify RGB++ assets workflows with **RGB++ CKB transaction Queue** and cron jobs - More detailed API documentation can be found on [Testnet](https://btc-assets-api.testnet.mibao.pro/docs) and [Mainnet](https://api.rgbpp.io/docs) +> [!NOTE] +> `Bitcoin/RGB++ Assets Service` is designed to streamline the transaction workflow. Developers have the option to implement its features by themselves without limitation. + ## Installation ```bash @@ -25,14 +28,22 @@ $ pnpm add @rgbpp-sdk/service ### Get an access token -The BtcAssetsApi is currently limited to verified apps only. -If you're a developer and want to access the BtcAssetsApi service, -please email us to request a JWT token for your app: f@cell.studio. +#### Testnet + +You can get a testnet access token through the [/token/generate](https://btc-assets-api.testnet.mibao.pro/docs/static/index.html#/Token/post_token_generate) API directly. + +#### Mainnet -In the email, you should provide us some information about your app: +The mainnet BtcAssetsApi is currently limited to verified apps only. + +When your app development is ready on testnet, and requires a mainnet access token, +please email us at f@cell.studio to request a mainnet JWT token. + +In the email, please provide the following information about your app: - `name`: Your app name, e.g. "rgbpp-app" -- `domain`: Your app domain, e.g. "rgbpp.app" (without protocol) +- `domain`: Your app domain, e.g. "rgbpp.app" (without protocol prefix and port suffix) + ### Initialize the service @@ -83,7 +94,7 @@ console.log(res); // } ``` -All available APIs in the [BtcAssetsApi](#btcassetsapi-1) section. +All available APIs in the [BtcAssetsApi](#types) section. ### Handling service errors @@ -212,6 +223,7 @@ interface BtcApiBlockTransactionIds { interface BtcApiBalanceParams { min_satoshi?: number; + no_cache?: boolean; } interface BtcApiBalance { @@ -225,6 +237,7 @@ interface BtcApiBalance { interface BtcApiUtxoParams { only_confirmed?: boolean; min_satoshi?: number; + no_cache?: boolean; } interface BtcApiUtxo { @@ -318,6 +331,7 @@ interface RgbppApiTransactionState { interface RgbppApiAssetsByAddressParams { type_script?: string; + no_cache?: boolean; } interface RgbppApiSpvProof { diff --git a/packages/service/package.json b/packages/service/package.json index d2253082..f55ae9da 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -1,6 +1,6 @@ { "name": "@rgbpp-sdk/service", - "version": "0.1.0", + "version": "0.2.0", "scripts": { "test": "vitest", "build": "tsc -p tsconfig.build.json", diff --git a/packages/service/src/error.ts b/packages/service/src/error.ts index fcb6c34f..e1da2e1a 100644 --- a/packages/service/src/error.ts +++ b/packages/service/src/error.ts @@ -37,7 +37,7 @@ export class BtcAssetsApiError extends Error { static withComment(code: ErrorCodes, comment?: string, context?: BtcAssetsApiContext): BtcAssetsApiError { const prefixMessage = ErrorMessages[code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; - const message = comment ? `${prefixMessage}: ${comment}` : void 0; + const message = comment ? `${prefixMessage}: ${comment}` : undefined; return new BtcAssetsApiError({ code, message, context }); } } diff --git a/packages/service/src/service/base.ts b/packages/service/src/service/base.ts index 5a1ece03..5abf1453 100644 --- a/packages/service/src/service/base.ts +++ b/packages/service/src/service/base.ts @@ -32,15 +32,14 @@ export class BtcAssetsApiBase implements BaseApis { await this.init(); } - const packedParams = params ? '?' + new URLSearchParams(pickBy(params, (val) => val !== undefined)).toString() : ''; - const withOriginHeaders = this.origin ? { origin: this.origin } : void 0; - const withAuthHeaders = requireToken && this.token ? { Authorization: `Bearer ${this.token}` } : void 0; + const pickedParams = pickBy(params, (val) => val !== undefined); + const packedParams = params ? '?' + new URLSearchParams(pickedParams).toString() : ''; const url = `${this.url}${route}${packedParams}`; const res = await fetch(url, { method, headers: { - ...withOriginHeaders, - ...withAuthHeaders, + authorization: this.token ? `Bearer ${this.token}` : undefined, + origin: this.origin, ...headers, }, ...otherOptions, @@ -96,8 +95,8 @@ export class BtcAssetsApiBase implements BaseApis { if (status !== 200 && status !== 404 && !allow404) { throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESPONSE_ERROR, comment, context); } - if (status !== 200) { - return void 0 as T; + if (status === 404 && allow404) { + return undefined as T; } return json! as T; @@ -141,8 +140,8 @@ export class BtcAssetsApiBase implements BaseApis { function tryParseBody(body: unknown): Record | undefined { try { - return typeof body === 'string' ? JSON.parse(body) : void 0; + return typeof body === 'string' ? JSON.parse(body) : undefined; } catch { - return void 0; + return undefined; } } diff --git a/packages/service/src/types/btc.ts b/packages/service/src/types/btc.ts index 1b320bcd..d5bbd031 100644 --- a/packages/service/src/types/btc.ts +++ b/packages/service/src/types/btc.ts @@ -58,6 +58,7 @@ export interface BtcApiRecommendedFeeRates { export interface BtcApiBalanceParams { min_satoshi?: number; + no_cache?: boolean; } export interface BtcApiBalance { address: string; @@ -70,6 +71,7 @@ export interface BtcApiBalance { export interface BtcApiUtxoParams { only_confirmed?: boolean; min_satoshi?: number; + no_cache?: boolean; } export interface BtcApiUtxo { txid: string; diff --git a/packages/service/src/types/rgbpp.ts b/packages/service/src/types/rgbpp.ts index 2a6b39b1..3f42bb5a 100644 --- a/packages/service/src/types/rgbpp.ts +++ b/packages/service/src/types/rgbpp.ts @@ -44,6 +44,7 @@ export interface RgbppApiTransactionState { export interface RgbppApiAssetsByAddressParams { type_script?: string; + no_cache?: boolean; } export interface RgbppApiSpvProof { diff --git a/packages/service/tests/Service.test.ts b/packages/service/tests/Service.test.ts index edfd0910..e2205335 100644 --- a/packages/service/tests/Service.test.ts +++ b/packages/service/tests/Service.test.ts @@ -11,15 +11,15 @@ describe( () => { const btcAddress = 'tb1qm06rvrq8jyyckzc5v709u7qpthel9j4d9f7nh3'; const service = BtcAssetsApi.fromToken( - process.env.VITE_SERVICE_URL!, - process.env.VITE_SERVICE_TOKEN!, - process.env.VITE_SERVICE_ORIGIN!, + process.env.VITE_BTC_SERVICE_URL!, + process.env.VITE_BTC_SERVICE_TOKEN!, + process.env.VITE_BTC_SERVICE_ORIGIN!, ); describe('Initiation and token generation', () => { it('Generate a valid token', async () => { const serviceWithApp = new BtcAssetsApi({ - url: process.env.VITE_SERVICE_URL!, + url: process.env.VITE_BTC_SERVICE_URL!, app: 'btc-test-app', domain: 'btc-test.app', origin: 'https://btc-test.app', @@ -36,14 +36,14 @@ describe( expect( () => new BtcAssetsApi({ - url: process.env.VITE_SERVICE_URL!, + url: process.env.VITE_BTC_SERVICE_URL!, domain: 'https://btc-test.app', }), ).toThrow(`${ErrorMessages[ErrorCodes.ASSETS_API_INVALID_PARAM]}: domain`); }); it('Try generate token without the "app" param', async () => { const serviceWithoutApp = new BtcAssetsApi({ - url: process.env.VITE_SERVICE_URL!, + url: process.env.VITE_BTC_SERVICE_URL!, domain: 'btc-test.app', }); @@ -92,6 +92,12 @@ describe( expect(filteredBalance.satoshi).toEqual(0); expect(filteredBalance.dust_satoshi).toEqual(originalBalance.satoshi + originalBalance.dust_satoshi); }); + it('getBtcBalance() with no_cache', async () => { + const res = await service.getBtcBalance(btcAddress, { + no_cache: true, + }); + expect(res.address).toEqual(btcAddress); + }); it('getBtcUtxos()', async () => { const res = await service.getBtcUtxos(btcAddress); expect(Array.isArray(res)).toBe(true); @@ -131,6 +137,12 @@ describe( expect(utxo.status.confirmed).toBe(true); } }); + it('getBtcUtxos() with no_cache', async () => { + const utxos = await service.getBtcUtxos(btcAddress, { + no_cache: true, + }); + expect(Array.isArray(utxos)).toBe(true); + }); it('getBtcTransactions()', async () => { const res = await service.getBtcTransactions(btcAddress); console.log(res.map((tx) => tx.txid)); @@ -152,13 +164,13 @@ describe( expect(txs.length).toBeGreaterThan(0); const filteredTxs = await service.getBtcTransactions(btcAddress, { - after_txid: txs[0].txid, + after_txid: txs[txs.length - 2].txid, }); expect(Array.isArray(filteredTxs)).toBe(true); if (txs.length > 1) { expect(txs.length).toBeGreaterThan(0); - expect(filteredTxs[0].txid).toEqual(txs[1].txid); + expect(filteredTxs[0].txid).toEqual(txs[txs.length - 1].txid); } else { expect(filteredTxs).toHaveLength(0); } @@ -248,6 +260,13 @@ describe( expectCell(cell); } }); + it('getRgbppAssetsByBtcAddress() with no_cache', async () => { + const res = await service.getRgbppAssetsByBtcAddress(rgbppBtcAddress, { + no_cache: true, + }); + expect(res).toBeDefined(); + expect(res.length).toBeGreaterThan(0); + }); it('getRgbppSpvProof()', async () => { const res = await service.getRgbppSpvProof(rgbppBtcTxId, 6); expect(res).toBeDefined(); diff --git a/packages/service/vitest.config.mts b/packages/service/vitest.config.mts index 2d9b2ac0..69b2c0b4 100644 --- a/packages/service/vitest.config.mts +++ b/packages/service/vitest.config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { watch: false, - testTimeout: 15000, + testTimeout: 20000, reporters: ['verbose'], exclude: ['lib', 'node_modules'], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bce19ada..d58421ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,15 @@ importers: .: devDependencies: - '@changesets/changelog-github': - specifier: ^0.5.0 - version: 0.5.0 '@changesets/cli': specifier: ^2.27.1 version: 2.27.1 + '@changesets/get-github-info': + specifier: ^0.6.0 + version: 0.6.0 + '@changesets/types': + specifier: ^6.0.0 + version: 6.0.0 '@typescript-eslint/eslint-plugin': specifier: ^7.8.0 version: 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.56.0)(typescript@5.4.3) @@ -37,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 @@ -114,7 +117,7 @@ importers: version: 7.0.2(eslint@8.56.0)(typescript@5.4.2) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.2.1(vite@5.1.4) + version: 4.2.1(vite@5.2.11) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -128,43 +131,50 @@ importers: specifier: ^5.2.2 version: 5.4.2 vite: - specifier: ^5.1.4 - version: 5.1.4(@types/node@20.12.2) + specifier: ^5.2.11 + version: 5.2.11(@types/node@20.12.12) vite-plugin-node-polyfills: specifier: ^0.21.0 - version: 0.21.0(vite@5.1.4) + version: 0.21.0(vite@5.2.11) examples/rgbpp: dependencies: - '@exact-realty/multipart-parser': - specifier: ^1.0.13 - version: 1.0.13 '@nervosnetwork/ckb-sdk-utils': specifier: ^0.109.1 version: 0.109.1 - '@rgbpp-sdk/btc': - specifier: workspace:^ - version: link:../../packages/btc - '@rgbpp-sdk/ckb': - specifier: workspace:^ - version: link:../../packages/ckb - '@rgbpp-sdk/service': - specifier: workspace:^ - version: link:../../packages/service - '@spore-sdk/core': - specifier: ^0.2.0-beta.6 - version: 0.2.0-beta.6(@ckb-lumos/lumos@0.22.0-next.5)(lodash@4.17.21) - axios: - specifier: ^1.6.8 - version: 1.6.8 + rgbpp: + specifier: workspace:* + version: link:../../packages/rgbpp devDependencies: + '@types/dotenv': + specifier: ^8.2.0 + version: 8.2.0 '@types/node': specifier: ^20.11.28 version: 20.11.28 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 typescript: specifier: ^5.4.2 version: 5.4.2 + examples/xudt-on-ckb: + dependencies: + '@nervosnetwork/ckb-sdk-utils': + specifier: ^0.109.1 + version: 0.109.1 + rgbpp: + specifier: 0.1.0 + version: link:../../packages/rgbpp + devDependencies: + '@types/dotenv': + specifier: ^8.2.0 + version: 8.2.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + packages/btc: dependencies: '@bitcoinerlab/secp256k1': @@ -200,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: @@ -246,7 +256,19 @@ 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: + '@rgbpp-sdk/btc': + specifier: workspace:* + version: link:../btc + '@rgbpp-sdk/ckb': + specifier: workspace:* + version: link:../ckb + '@rgbpp-sdk/service': + specifier: workspace:* + version: link:../service packages/service: dependencies: @@ -268,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: @@ -568,16 +615,6 @@ packages: '@changesets/types': 6.0.0 dev: true - /@changesets/changelog-github@0.5.0: - resolution: {integrity: sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==} - dependencies: - '@changesets/get-github-info': 0.6.0 - '@changesets/types': 6.0.0 - dotenv: 8.6.0 - transitivePeerDependencies: - - encoding - dev: true - /@changesets/cli@2.27.1: resolution: {integrity: sha512-iJ91xlvRnnrJnELTp4eJJEOPjgpF3NOh4qeQehM6Ugiz9gJPRZ2t+TsXun6E3AMN4hScZKjqVXl0TX+C7AB3ZQ==} hasBin: true @@ -960,15 +997,6 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@esbuild/aix-ppc64@0.19.12: - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - /@esbuild/aix-ppc64@0.20.2: resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -978,15 +1006,6 @@ packages: dev: true optional: true - /@esbuild/android-arm64@0.19.12: - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.20.2: resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} @@ -996,15 +1015,6 @@ packages: dev: true optional: true - /@esbuild/android-arm@0.19.12: - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.20.2: resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} @@ -1014,15 +1024,6 @@ packages: dev: true optional: true - /@esbuild/android-x64@0.19.12: - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.20.2: resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} @@ -1032,15 +1033,6 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64@0.19.12: - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.20.2: resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} @@ -1050,15 +1042,6 @@ packages: dev: true optional: true - /@esbuild/darwin-x64@0.19.12: - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.20.2: resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} @@ -1068,15 +1051,6 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64@0.19.12: - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.20.2: resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} @@ -1086,15 +1060,6 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64@0.19.12: - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.20.2: resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} @@ -1104,15 +1069,6 @@ packages: dev: true optional: true - /@esbuild/linux-arm64@0.19.12: - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.20.2: resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} @@ -1122,15 +1078,6 @@ packages: dev: true optional: true - /@esbuild/linux-arm@0.19.12: - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.20.2: resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} @@ -1140,15 +1087,6 @@ packages: dev: true optional: true - /@esbuild/linux-ia32@0.19.12: - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.20.2: resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} @@ -1158,15 +1096,6 @@ packages: dev: true optional: true - /@esbuild/linux-loong64@0.19.12: - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.20.2: resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} @@ -1176,15 +1105,6 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el@0.19.12: - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.20.2: resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} @@ -1194,15 +1114,6 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64@0.19.12: - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.20.2: resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} @@ -1212,15 +1123,6 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64@0.19.12: - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.20.2: resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} @@ -1230,15 +1132,6 @@ packages: dev: true optional: true - /@esbuild/linux-s390x@0.19.12: - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.20.2: resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} @@ -1248,15 +1141,6 @@ packages: dev: true optional: true - /@esbuild/linux-x64@0.19.12: - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.20.2: resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} @@ -1266,15 +1150,6 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64@0.19.12: - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.20.2: resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} @@ -1284,15 +1159,6 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64@0.19.12: - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.20.2: resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} @@ -1302,15 +1168,6 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.19.12: - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.20.2: resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} @@ -1320,15 +1177,6 @@ packages: dev: true optional: true - /@esbuild/win32-arm64@0.19.12: - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.20.2: resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} @@ -1338,15 +1186,6 @@ packages: dev: true optional: true - /@esbuild/win32-ia32@0.19.12: - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.20.2: resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} @@ -1356,15 +1195,6 @@ packages: dev: true optional: true - /@esbuild/win32-x64@0.19.12: - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.20.2: resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} @@ -1697,14 +1527,6 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/rollup-android-arm-eabi@4.13.0: - resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-android-arm-eabi@4.13.2: resolution: {integrity: sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==} cpu: [arm] @@ -1713,14 +1535,6 @@ packages: dev: true optional: true - /@rollup/rollup-android-arm64@4.13.0: - resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-android-arm64@4.13.2: resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==} cpu: [arm64] @@ -1729,14 +1543,6 @@ packages: dev: true optional: true - /@rollup/rollup-darwin-arm64@4.13.0: - resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-darwin-arm64@4.13.2: resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==} cpu: [arm64] @@ -1745,14 +1551,6 @@ packages: dev: true optional: true - /@rollup/rollup-darwin-x64@4.13.0: - resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-darwin-x64@4.13.2: resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==} cpu: [x64] @@ -1761,14 +1559,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.0: - resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.2: resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==} cpu: [arm] @@ -1777,14 +1567,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.0: - resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.2: resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==} cpu: [arm64] @@ -1793,14 +1575,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.13.0: - resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm64-musl@4.13.2: resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==} cpu: [arm64] @@ -1817,14 +1591,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.0: - resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.2: resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==} cpu: [riscv64] @@ -1841,14 +1607,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.13.0: - resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-x64-gnu@4.13.2: resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==} cpu: [x64] @@ -1857,14 +1615,6 @@ packages: dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.13.0: - resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-x64-musl@4.13.2: resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==} cpu: [x64] @@ -1873,14 +1623,6 @@ packages: dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.0: - resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.2: resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==} cpu: [arm64] @@ -1889,14 +1631,6 @@ packages: dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.0: - resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.2: resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==} cpu: [ia32] @@ -1905,14 +1639,6 @@ packages: dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.13.0: - resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-x64-msvc@4.13.2: resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==} cpu: [x64] @@ -1998,14 +1724,38 @@ packages: resolution: {integrity: sha512-VvMETBojHvhX4f+ocYTySQlXMZfxKV3Jyb7iCWlWaC+exbedkv6Iv2bZZqI736qXjVguH6IH7bzwMBMfTT+zuQ==} dev: false + /@types/dotenv@8.2.0: + resolution: {integrity: sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==} + deprecated: This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed. + dependencies: + dotenv: 16.4.5 + dev: true + /@types/estree@1.0.5: 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: @@ -2037,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==} @@ -2341,7 +2091,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react@4.2.1(vite@5.1.4): + /@vitejs/plugin-react@4.2.1(vite@5.2.11): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -2352,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.1.4(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - supports-color dev: true @@ -3284,9 +3034,9 @@ packages: engines: {node: '>=10'} dev: true - /dotenv@8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} dev: true /eastasianwidth@0.2.0: @@ -3450,37 +3200,6 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - dev: true - /esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -5166,15 +4885,6 @@ packages: source-map-js: 1.1.0 dev: false - /postcss@8.4.36: - resolution: {integrity: sha512-/n7eumA6ZjFHAsbX30yhHup/IMkOmlmvtEi7P+6RMYf+bGJSUHc3geH4a0NSZxAz/RJfiS9tooCTs9LAVYUZKw==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.1.0 - dev: true - /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} @@ -5482,29 +5192,6 @@ packages: hash-base: 3.1.0 inherits: 2.0.4 - /rollup@4.13.0: - resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.13.0 - '@rollup/rollup-android-arm64': 4.13.0 - '@rollup/rollup-darwin-arm64': 4.13.0 - '@rollup/rollup-darwin-x64': 4.13.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 - '@rollup/rollup-linux-arm64-gnu': 4.13.0 - '@rollup/rollup-linux-arm64-musl': 4.13.0 - '@rollup/rollup-linux-riscv64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-musl': 4.13.0 - '@rollup/rollup-win32-arm64-msvc': 4.13.0 - '@rollup/rollup-win32-ia32-msvc': 4.13.0 - '@rollup/rollup-win32-x64-msvc': 4.13.0 - fsevents: 2.3.3 - dev: true - /rollup@4.13.2: resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5721,6 +5408,7 @@ packages: /source-map-js@1.1.0: resolution: {integrity: sha512-9vC2SfsJzlej6MAaMPLu8HiBSHGdRAJ9hVFYN1ibZoNkeanmDmLUcIrj6G9DGL7XMJ54AKg/G75akXl1/izTOw==} engines: {node: '>=0.10.0'} + dev: false /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} @@ -6018,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: @@ -6037,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 @@ -6247,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==} @@ -6313,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 @@ -6322,7 +6009,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.7(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - '@types/node' - less @@ -6334,20 +6021,20 @@ packages: - terser dev: true - /vite-plugin-node-polyfills@0.21.0(vite@5.1.4): + /vite-plugin-node-polyfills@0.21.0(vite@5.2.11): resolution: {integrity: sha512-Sk4DiKnmxN8E0vhgEhzLudfJQfaT8k4/gJ25xvUPG54KjLJ6HAmDKbr4rzDD/QWEY+Lwg80KE85fGYBQihEPQA==} peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: '@rollup/plugin-inject': 5.0.5 node-stdlib-browser: 1.2.0 - vite: 5.1.4(@types/node@20.12.2) + vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - rollup dev: true - /vite@5.1.4(@types/node@20.12.2): - resolution: {integrity: sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==} + /vite@5.2.11(@types/node@20.12.12): + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6374,43 +6061,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.12.2 - esbuild: 0.19.12 - postcss: 8.4.36 - rollup: 4.13.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vite@5.2.7(@types/node@20.12.2): - resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - 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 @@ -6418,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 @@ -6443,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 @@ -6461,8 +6112,8 @@ packages: strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.3 - vite: 5.2.7(@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 @@ -6684,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/examples/rgbpp/xudt/1-issue-xudt.ts b/tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts similarity index 63% rename from examples/rgbpp/xudt/1-issue-xudt.ts rename to tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts index a8acc356..5430e4b7 100644 --- a/examples/rgbpp/xudt/1-issue-xudt.ts +++ b/tests/rgbpp/xudt/xudt-on-ckb/1-issue-xudt.ts @@ -1,13 +1,7 @@ -import { - AddressPrefix, - addressToScript, - getTransactionSize, - privateKeyToAddress, - scriptToHash, -} from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, getTransactionSize, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'; import { getSecp256k1CellDep, - Collector, + RgbppTokenInfo, NoLiveCellError, calculateUdtCellCapacity, MAX_FEE, @@ -22,29 +16,18 @@ import { SECP256K1_WITNESS_LOCK_SIZE, calculateTransactionFee, generateUniqueTypeArgs, -} from '@rgbpp-sdk/ckb'; -import { calculateXudtTokenInfoCellCapacity } from '@rgbpp-sdk/ckb/src/utils'; -import { XUDT_TOKEN_INFO } from './0-token-info'; - -// CKB SECP256K1 private key -const CKB_TEST_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; + 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 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 }: { xudtTotalAmount: bigint }) => { - const collector = new Collector({ - ckbNodeUrl: 'https://testnet.ckb.dev/rpc', - ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', - }); - const isMainnet = false; - const issueAddress = privateKeyToAddress(CKB_TEST_PRIVATE_KEY, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - }); - console.log('ckb address: ', issueAddress); - - const issueLock = addressToScript(issueAddress); +const issueXudt = async ({ xudtTotalAmount, tokenInfo }: { xudtTotalAmount: bigint; tokenInfo: RgbppTokenInfo }) => { + const issueLock = addressToScript(ckbAddress); let emptyCells = await collector.getCells({ lock: issueLock, @@ -55,7 +38,7 @@ const issueXudt = async ({ xudtTotalAmount }: { xudtTotalAmount: bigint }) => { emptyCells = emptyCells.filter((cell) => !cell.output.type); const xudtCapacity = calculateUdtCellCapacity(issueLock); - const xudtInfoCapacity = calculateXudtTokenInfoCellCapacity(XUDT_TOKEN_INFO, issueLock); + const xudtInfoCapacity = calculateXudtTokenInfoCellCapacity(tokenInfo, issueLock); const txFee = MAX_FEE; const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, xudtCapacity + xudtInfoCapacity, txFee, { @@ -69,6 +52,12 @@ const issueXudt = async ({ xudtTotalAmount }: { xudtTotalAmount: bigint }) => { 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[] = [ { @@ -89,8 +78,8 @@ const issueXudt = async ({ xudtTotalAmount }: { xudtTotalAmount: bigint }) => { capacity: append0x(changeCapacity.toString(16)), }, ]; - const totalAmount = xudtTotalAmount * BigInt(10 ** XUDT_TOKEN_INFO.decimal); - const outputsData = [append0x(u128ToLe(totalAmount)), encodeRgbppTokenInfo(XUDT_TOKEN_INFO), '0x']; + 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')); @@ -114,10 +103,17 @@ const issueXudt = async ({ xudtTotalAmount }: { xudtTotalAmount: bigint }) => { unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); } - const signedTx = collector.getCkb().signTransaction(CKB_TEST_PRIVATE_KEY)(unsignedTx); + const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); - console.info(`xUDT asset has been issued and tx hash is ${txHash}`); + 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) }); +issueXudt({ xudtTotalAmount: BigInt(2100_0000), tokenInfo: XUDT_TOKEN_INFO });