├── .changeset ├── README.md ├── changelog-format.cjs └── config.json ├── .dockerignore ├── .eslintrc.json ├── .github └── workflows │ ├── docker.yml │ ├── integration-test.yaml │ ├── release.yml │ ├── snapshot.yml │ └── test.yaml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── apps └── service │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── docker-compose.yml │ ├── nest-cli.json │ ├── package.json │ ├── src │ ├── app.module.ts │ ├── app.service.ts │ ├── env.ts │ ├── json-rpc │ │ ├── json-rpc.decorators.ts │ │ ├── json-rpc.module.ts │ │ └── json-rpc.server.ts │ ├── main.ts │ ├── rgbpp │ │ ├── rgbpp.module.ts │ │ ├── rgbpp.service.ts │ │ └── types.ts │ └── utils │ │ ├── case.ts │ │ └── json.ts │ ├── tests │ └── Utils.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.project.mts ├── examples ├── rgbpp │ ├── .env.example │ ├── README.md │ ├── env.ts │ ├── logs │ │ └── .gitkeep │ ├── package.json │ ├── shared │ │ ├── btc-account.ts │ │ └── utils.ts │ ├── spore │ │ ├── 4-transfer-spore.ts │ │ ├── 5-leap-spore-to-ckb.ts │ │ ├── 6-unlock-btc-time-cell.ts │ │ ├── 7-leap-spore-to-btc.ts │ │ ├── launch │ │ │ ├── 0-cluster-info.ts │ │ │ ├── 1-prepare-cluster.ts │ │ │ ├── 2-create-cluster.ts │ │ │ └── 3-create-spores.ts │ │ └── local │ │ │ ├── 4-transfer-spore.ts │ │ │ └── 5-leap-spore-to-ckb.ts │ ├── tsconfig.json │ └── xudt │ │ ├── 1-ckb-leap-btc.ts │ │ ├── 2-btc-transfer.ts │ │ ├── 3-btc-leap-ckb.ts │ │ ├── 4-unlock-btc-time-cell.ts │ │ ├── btc-transfer-all │ │ └── 1-btc-transfer-all.ts │ │ ├── compatible-xudt │ │ ├── 1-ckb-leap-btc.ts │ │ ├── 2-btc-transfer.ts │ │ ├── 3-btc-leap-ckb.ts │ │ ├── 4-unlock-btc-time-cell.ts │ │ └── assets-api.ts │ │ ├── launch │ │ ├── 0-rgbpp-token-info.ts │ │ ├── 1-prepare-launch.ts │ │ ├── 2-launch-rgbpp.ts │ │ └── 3-distribute-rgbpp.ts │ │ ├── local │ │ ├── 2-btc-transfer.ts │ │ └── 3-btc-leap-ckb.ts │ │ └── offline │ │ ├── 0-rgbpp-token-info.ts │ │ ├── 1-prepare-launch.ts │ │ ├── 2-launch-rgbpp.ts │ │ ├── 3-distribute-rgbpp.ts │ │ ├── 4-btc-leap-ckb.ts │ │ ├── 5-unlock-btc-time-cell.ts │ │ ├── 6-ckb-leap-btc.ts │ │ └── compatible-xudt │ │ ├── 1-ckb-leap-btc.ts │ │ ├── 2-btc-transfer.ts │ │ ├── 3-btc-leap-ckb.ts │ │ └── 4-unlock-btc-time-cell.ts └── xudt-on-ckb │ ├── .env.example │ ├── 1-issue-xudt.ts │ ├── 2-transfer-xudt.ts │ ├── 3-generate-btc-time-cell.ts │ ├── README.md │ ├── env.ts │ ├── package.json │ └── tsconfig.json ├── package.json ├── packages ├── btc │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── address.ts │ │ ├── api │ │ │ ├── sendBtc.ts │ │ │ ├── sendRbf.ts │ │ │ ├── sendRgbppUtxos.ts │ │ │ └── sendUtxos.ts │ │ ├── bitcoin.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── preset │ │ │ ├── config.ts │ │ │ ├── network.ts │ │ │ └── types.ts │ │ ├── query │ │ │ ├── cache.ts │ │ │ └── source.ts │ │ ├── script.ts │ │ ├── transaction │ │ │ ├── build.ts │ │ │ ├── embed.ts │ │ │ ├── fee.ts │ │ │ └── utxo.ts │ │ └── utils.ts │ ├── tests │ │ ├── Address.test.ts │ │ ├── DataSource.test.ts │ │ ├── Embed.test.ts │ │ ├── Network.test.ts │ │ ├── Script.test.ts │ │ ├── Transaction.test.ts │ │ ├── Utils.test.ts │ │ └── shared │ │ │ ├── env.ts │ │ │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── ckb │ ├── CHANGELOG.md │ ├── README.md │ ├── example │ │ ├── launch.ts │ │ └── paymaster.ts │ ├── package.json │ ├── src │ │ ├── collector │ │ │ ├── collector.spec.ts │ │ │ ├── index.ts │ │ │ └── offline.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── error │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── paymaster │ │ │ └── index.ts │ │ ├── rgbpp │ │ │ ├── btc-jump-ckb.ts │ │ │ ├── btc-time.ts │ │ │ ├── btc-transfer.ts │ │ │ ├── ckb-builder.ts │ │ │ ├── ckb-jump-btc.ts │ │ │ ├── index.ts │ │ │ ├── launch.ts │ │ │ └── schemas.spec.ts │ │ ├── schemas │ │ │ ├── customized.ts │ │ │ ├── generated │ │ │ │ ├── blockchain.ts │ │ │ │ └── rgbpp.ts │ │ │ ├── lumos-molecule-codegen.json │ │ │ └── schemas │ │ │ │ ├── blockchain.mol │ │ │ │ └── rgbpp.mol │ │ ├── spore │ │ │ ├── cluster.ts │ │ │ ├── index.ts │ │ │ ├── leap.ts │ │ │ └── spore.ts │ │ ├── types │ │ │ ├── collector.ts │ │ │ ├── common.ts │ │ │ ├── index.ts │ │ │ ├── rgbpp.ts │ │ │ ├── spore.ts │ │ │ └── spv.ts │ │ └── utils │ │ │ ├── case-parser.spec.ts │ │ │ ├── case-parser.ts │ │ │ ├── cell-dep.spec.ts │ │ │ ├── cell-dep.ts │ │ │ ├── ckb-tx.spec.ts │ │ │ ├── ckb-tx.ts │ │ │ ├── hex.spec.ts │ │ │ ├── hex.ts │ │ │ ├── id.spec.ts │ │ │ ├── id.ts │ │ │ ├── index.ts │ │ │ ├── rgbpp.spec.ts │ │ │ ├── rgbpp.ts │ │ │ ├── spore.spec.ts │ │ │ └── spore.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── rgbpp │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── btc.ts │ │ ├── ckb.ts │ │ ├── index.ts │ │ ├── rgbpp │ │ │ ├── error.ts │ │ │ ├── summary │ │ │ │ └── asset-summarizer.ts │ │ │ ├── types │ │ │ │ └── xudt.ts │ │ │ ├── utils │ │ │ │ ├── group.ts │ │ │ │ └── transaction.ts │ │ │ └── xudt │ │ │ │ ├── btc-transfer-all.ts │ │ │ │ └── btc-transfer.ts │ │ └── service.ts │ ├── tests │ │ ├── Group.test.ts │ │ ├── RgbppXudt.test.ts │ │ ├── __snapshots__ │ │ │ └── RgbppXudt.test.ts.snap │ │ ├── mocked │ │ │ └── 50-included-41-excluded.ts │ │ └── shared │ │ │ ├── account.ts │ │ │ └── env.ts │ ├── tsconfig.json │ └── tsup.config.ts └── service │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── ckb-types.d.ts │ ├── error.ts │ ├── index.ts │ ├── service │ │ ├── base.ts │ │ ├── index.ts │ │ ├── offline-service.ts │ │ └── service.ts │ ├── types │ │ ├── base.ts │ │ ├── btc.ts │ │ ├── index.ts │ │ └── rgbpp.ts │ └── utils.ts │ ├── tests │ ├── Service.test.ts │ └── Utils.test.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests └── rgbpp │ ├── env.ts │ ├── package.json │ ├── shared │ ├── prepare-utxo.ts │ └── utils.ts │ ├── spore │ ├── 4-transfer-spore.ts │ ├── 5-leap-spore-to-ckb.ts │ └── launch │ │ ├── 0-cluster-info.ts │ │ ├── 1-prepare-cluster.ts │ │ ├── 2-create-cluster.ts │ │ └── 3-create-spores.ts │ ├── testnet │ └── .gitkeep │ ├── tsconfig.json │ └── xudt │ ├── 1-ckb-leap-btc.ts │ ├── 2-btc-transfer.ts │ ├── 3-btc-leap-ckb.ts │ ├── btc-transfer-all │ └── 1-btc-transfer-all.ts │ ├── compatible-xudt │ ├── 1-ckb-leap-btc.ts │ ├── 2-btc-transfer.ts │ └── 3-btc-leap-ckb.ts │ └── xudt-on-ckb │ ├── 1-issue-xudt.ts │ └── 2-transfer-xudt.ts ├── vitest.config.mts └── vitest.workspace.mts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": ["./changelog-format.cjs", { "repo": "ckb-cell/rgbpp-sdk" }], 4 | "commit": false, 5 | "fixed": [["@rgbpp-sdk/*", "rgbpp"]], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "origin/main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .gitignore 4 | *.md 5 | tests 6 | examples 7 | dist 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "ignorePatterns": ["dist/"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - develop 9 | tags: 10 | - v*.*.* 11 | 12 | jobs: 13 | docker-build-push: 14 | runs-on: ubuntu-22.04 15 | permissions: 16 | packages: write 17 | contents: read 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Docker metadata 23 | id: meta 24 | uses: docker/metadata-action@v5 25 | with: 26 | context: git 27 | images: ghcr.io/${{ github.repository }}-service 28 | flavor: | 29 | latest=auto 30 | tags: | 31 | type=ref,event=tag 32 | type=semver,pattern={{version}} 33 | type=ref,event=branch 34 | type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmm'}} 35 | type=sha,enable=true,prefix=sha-,format=short 36 | env: 37 | DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v3 41 | 42 | - name: Login to GitHub Container Registry 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.actor }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Build Docker image 50 | uses: docker/build-push-action@v5 51 | with: 52 | context: . 53 | target: service 54 | push: true 55 | provenance: false 56 | platforms: linux/amd64, linux/arm64 57 | tags: ${{ steps.meta.outputs.tags }} 58 | labels: ${{ steps.meta.outputs.labels }} 59 | annotations: ${{ steps.meta.outputs.annotations }} 60 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yaml: -------------------------------------------------------------------------------- 1 | # Test the entire process of RGBPP to ensure the proper functioning of the rgbpp-sdk package. 2 | 3 | name: Integration Tests 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | # Run integration-tests every day 9 | schedule: 10 | - cron: '59 0 * * *' 11 | 12 | # https://docs.github.com/en/actions/using-jobs/using-concurrency 13 | concurrency: 14 | group: ${{ github.workflow }} 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | env_set: [ xudt, spore, compatible-xudt ] 24 | 25 | steps: 26 | - name: Checkout rgbpp-sdk 27 | uses: actions/checkout@v4 28 | 29 | - name: Install Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 21 33 | 34 | - uses: pnpm/action-setup@v3 35 | name: Install -g pnpm 36 | with: 37 | version: 9 38 | run_install: false 39 | 40 | - name: Install dependencies 41 | run: pnpm i 42 | 43 | - name: Build packages 44 | run: pnpm run build:packages 45 | 46 | 47 | - name: Run integration:xudt script 48 | working-directory: ./tests/rgbpp 49 | if: ${{ matrix.env_set == 'xudt' }} 50 | run: pnpm run integration:xudt 51 | env: 52 | VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro 53 | VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} 54 | VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro 55 | INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_PRIVATE_KEY }} 56 | INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_PRIVATE_KEY }} 57 | 58 | - name: Run integration:spore script 59 | working-directory: ./tests/rgbpp 60 | if: ${{ matrix.env_set == 'spore' }} 61 | run: pnpm run integration:spore 62 | env: 63 | VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro 64 | VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} 65 | VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro 66 | INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_SPORE_PRIVATE_KEY }} 67 | INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_SPORE_PRIVATE_KEY }} 68 | 69 | - name: Run integration:compatible-xudt script 70 | working-directory: ./tests/rgbpp 71 | if: ${{ matrix.env_set == 'compatible-xudt' }} 72 | run: pnpm run integration:compatible-xudt 73 | env: 74 | VITE_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro 75 | VITE_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} 76 | VITE_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro 77 | INTEGRATION_CKB_PRIVATE_KEY: ${{ secrets.INTEGRATION_CKB_compatible_xudt_PRIVATE_KEY }} 78 | INTEGRATION_BTC_PRIVATE_KEY: ${{ secrets.INTEGRATION_BTC_compatible_xudt_PRIVATE_KEY }} 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Release packages to NPM and GitHub. 2 | 3 | name: Release 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: ${{ github.workflow }}-${{ github.ref }} 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 21 27 | 28 | - uses: pnpm/action-setup@v3 29 | name: Install pnpm 30 | with: 31 | version: 9 32 | run_install: false 33 | 34 | - name: Get pnpm store directory 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 38 | 39 | - uses: actions/cache@v4 40 | name: Setup pnpm cache 41 | with: 42 | path: ${{ env.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | - name: Install dependencies 48 | run: pnpm i 49 | 50 | - name: Create bump PR or release version 51 | uses: changesets/action@v1 52 | id: changesets 53 | with: 54 | publish: pnpm run release:packages 55 | title: "bump: assumable rgbpp-sdk version" 56 | commit: "bump: assumable rgbpp-sdk version" 57 | env: 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - name: Create comment on commit 62 | uses: actions/github-script@v7 63 | if: steps.changesets.outputs.published 64 | env: 65 | PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} 66 | with: 67 | script: | 68 | const packages = JSON.parse(process.env.PACKAGES) 69 | const packagesTable = packages.map((p) => `| ${p.name} | \`${p.version}\` |`).join('\n') 70 | const body = ['New official version of the rgbpp-sdk packages have been released:', '| Name | Version |', '| --- | --- |', packagesTable].join('\n') 71 | github.rest.repos.createCommitComment({ 72 | commit_sha: context.sha, 73 | owner: context.repo.owner, 74 | repo: context.repo.repo, 75 | body: body, 76 | }); 77 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | # Release snapshot packages to NPM. 2 | 3 | name: Release Snapshots 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - develop 10 | 11 | concurrency: ${{ github.workflow }}-${{ github.ref }} 12 | 13 | jobs: 14 | release-snapshots: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 21 26 | 27 | - uses: pnpm/action-setup@v3 28 | name: Install pnpm 29 | with: 30 | version: 9 31 | run_install: false 32 | 33 | - name: Get pnpm store directory 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 37 | 38 | - uses: actions/cache@v4 39 | name: Setup pnpm cache 40 | with: 41 | path: ${{ env.STORE_PATH }} 42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pnpm-store- 45 | 46 | - name: Install dependencies 47 | run: pnpm i 48 | 49 | - name: Build packages 50 | run: pnpm run build:packages 51 | 52 | - name: Add snapshot changeset (ensure at least has a changeset) 53 | run: | 54 | cat << EOF > ".changeset/snap-release-changeset.md" 55 | --- 56 | "@rgbpp-sdk/btc": patch 57 | --- 58 | Add temp changeset for snapshot releases 59 | EOF 60 | 61 | - name: Version packages to "0.0.0-snap-{timestamp}" 62 | run: npx changeset version --snapshot snap 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - name: Publish snapshot versions to npm 67 | uses: changesets/action@v1 68 | id: changesets 69 | with: 70 | publish: npx changeset publish --snapshot --tag snap 71 | createGithubReleases: false 72 | env: 73 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | 76 | - name: Create comment on commit 77 | uses: actions/github-script@v7 78 | if: steps.changesets.outputs.published 79 | env: 80 | PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} 81 | with: 82 | script: | 83 | const packages = JSON.parse(process.env.PACKAGES) 84 | const packagesTable = packages.map((p) => `| ${p.name} | \`${p.version}\` |`).join('\n') 85 | const body = ['New snapshot version of the rgbpp-sdk packages have been released:', '| Name | Version |', '| --- | --- |', packagesTable].join('\n') 86 | github.rest.repos.createCommitComment({ 87 | commit_sha: context.sha, 88 | owner: context.repo.owner, 89 | repo: context.repo.repo, 90 | body: body, 91 | }); 92 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # Test the functionality of the rgbpp-sdk packages. 2 | 3 | name: Unit Tests 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout rgbpp-sdk 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 21 23 | 24 | - uses: pnpm/action-setup@v3 25 | name: Install -g pnpm 26 | with: 27 | version: 9 28 | run_install: false 29 | 30 | - name: Get pnpm store directory 31 | shell: bash 32 | run: | 33 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 34 | 35 | - uses: actions/cache@v4 36 | name: Setup pnpm cache 37 | with: 38 | path: ${{ env.STORE_PATH }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install dependencies 44 | run: pnpm i 45 | 46 | - name: Build packages 47 | run: pnpm run build:packages 48 | 49 | - name: Lint packages 50 | run: pnpm run lint 51 | 52 | - name: Run unit tests for the packages 53 | run: pnpm run test:packages 54 | env: 55 | VITE_CKB_NODE_URL: https://testnet.ckb.dev/rpc 56 | VITE_CKB_INDEXER_URL: https://testnet.ckb.dev/indexer 57 | VITE_BTC_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro 58 | VITE_BTC_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }} 59 | VITE_BTC_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro 60 | 61 | - name: Run unit tests for the rgbpp-sdk-service 62 | run: pnpm run test:service 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # editors 4 | **/.idea 5 | **/.vscode 6 | 7 | # dependencies 8 | **/node_modules 9 | **/.pnp 10 | **/.pnp.js 11 | 12 | # testing 13 | **/coverage 14 | **/tmp 15 | 16 | # production 17 | **/lib 18 | **/cjs 19 | **/dist 20 | **/build 21 | **/*.tsbuildinfo 22 | 23 | # generated 24 | **/.next 25 | **/.turbo 26 | 27 | # misc 28 | .DS_Store 29 | .env.local 30 | .env.development.local 31 | .env.production.local 32 | .env.test.local 33 | .env 34 | **/.npmrc 35 | 36 | # logs 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | 41 | devbox.json 42 | devbox.lock 43 | .envrc 44 | 45 | # examples logs 46 | examples/rgbpp/logs -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # editors 2 | **/.git 3 | **/.idea 4 | **/.vscode 5 | 6 | # dependencies 7 | **/node_modules 8 | **/.pnp.js 9 | **/.pnp 10 | 11 | # testing 12 | **/coverage 13 | 14 | # production 15 | **/dist 16 | **/build 17 | **/tsconfig.tsbuildinfo 18 | 19 | # misc 20 | **/.DS_Store 21 | **/.env.local 22 | **/.env.development.local 23 | **/.env.test.local 24 | **/.env.production.local 25 | 26 | **/npm-debug.log* 27 | **/yarn-debug.log* 28 | **/yarn-error.log* 29 | 30 | # moleculec 31 | **/*.mol 32 | 33 | # generated typing 34 | **/.next 35 | **/next-env.d.ts 36 | **/auto-imports.d.ts 37 | **/vite-imports.d.ts 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "jsxSingleQuote": false, 7 | "trailingComma": "all", 8 | "endOfLine": "lf", 9 | "printWidth": 120 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS base 2 | 3 | ENV PNPM_HOME="/pnpm" 4 | ENV PATH="$PNPM_HOME:$PATH" 5 | RUN corepack enable 6 | RUN corepack use pnpm@latest 7 | 8 | COPY . /app 9 | WORKDIR /app 10 | 11 | FROM base AS prod-deps 12 | RUN npm pkg delete scripts.prepare 13 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile 14 | 15 | FROM base AS build 16 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 17 | RUN pnpm run build 18 | 19 | FROM base AS service 20 | COPY --from=prod-deps /app/node_modules /node_modules 21 | COPY --from=build /app/packages /app/packages 22 | COPY --from=build /app/apps/service /app/apps/service 23 | 24 | WORKDIR /app/apps/service 25 | RUN pnpm add @nestjs/cli -D 26 | 27 | ENV NODE_ENV=production 28 | EXPOSE 3000 29 | CMD [ "pnpm", "start" ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2024 CELL Studio 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /apps/service/.env.example: -------------------------------------------------------------------------------- 1 | # The network includes testnet and mainnet, the default value is testnet 2 | NETWORK=testnet 3 | 4 | # The Bitcoin Testnet type including Testnet3 and Signet, default value is Signet 5 | # Testnet3: https://mempool.space/testnet 6 | # Signet: https://mempool.space/signet 7 | BTC_TESTNET_TYPE=Signet 8 | 9 | # CKB node url which should match NETWORK 10 | CKB_RPC_URL=https://testnet.ckb.dev 11 | 12 | # The BTC assets api url which should match NETWORK 13 | # The BTC Testnet Service url is: https://api.testnet.rgbpp.io 14 | # The BTC Signet Service url is: https://api.signet.rgbpp.io 15 | BTC_SERVICE_URL=https://api.signet.rgbpp.io 16 | 17 | # The BTC assets api token which should match NETWORK and BTC_TESTNET_TYPE 18 | # To get an access token, please refer to https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service#get-an-access-token 19 | BTC_SERVICE_TOKEN= 20 | 21 | # The BTC assets api origin which should match `BTC_SERVICE_TOKEN` 22 | # JWT Debugger: https://jwt.io 23 | BTC_SERVICE_ORIGIN=https://btc-test.app 24 | -------------------------------------------------------------------------------- /apps/service/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /apps/service/README.md: -------------------------------------------------------------------------------- 1 | # RGB++ SDK Service 2 | 3 | A RPC service for Wrapping essential RGB++ Assets Operations(**only support RGB++ Transfer on BTC now**) with [rgbpp-sdk](https://github.com/ckb-cell/rgbpp-sdk) for other programming languages to quickly manage RGB++ Assets. 4 | 5 | ## Quick Start 6 | 7 | ### Clone rgbpp-sdk repository 8 | 9 | ```shell 10 | $ git clone https://github.com/ckb-cell/rgbpp-sdk.git 11 | $ pnpm install 12 | $ cd apps/service 13 | ``` 14 | 15 | ### Update Environment Variables 16 | 17 | Copy the `.env.example` file to `.env`: 18 | 19 | ```shell 20 | $ cp .env.example .env 21 | ``` 22 | 23 | Update the configuration values: 24 | 25 | ```yml 26 | # The network includes testnet and mainnet, the default value is testnet 27 | NETWORK=testnet # or mainnet 28 | 29 | # The Bitcoin Testnet type includs Testnet3 and Signet, the default value is Signet 30 | # Testnet3: https://mempool.space/testnet 31 | # Signet: https://mempool.space/signet 32 | BTC_TESTNET_TYPE=Signet 33 | 34 | # CKB node url which should match NETWORK 35 | CKB_RPC_URL=https://testnet.ckb.dev 36 | 37 | # The BTC assets api url which should match NETWORK and BTC_TESTNET_TYPE 38 | # The BTC Testnet Service url is: https://api.testnet.rgbpp.io 39 | # The BTC Signet Service url is: https://api.signet.rgbpp.io 40 | BTC_SERVICE_URL=hhttps://api.signet.rgbpp.io 41 | 42 | # The BTC assets api token which should match NETWORK and BTC_TESTNET_TYPE 43 | # To get an access token, please refer to https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service#get-an-access-token 44 | BTC_SERVICE_TOKEN= 45 | 46 | # The BTC assets api origin which should match `BTC_SERVICE_TOKEN` 47 | # JWT Debugger: https://jwt.io 48 | BTC_SERVICE_ORIGIN=https://btc-test.app 49 | ``` 50 | 51 | ### Run RGB++ SDK Service 52 | 53 | ```shell 54 | # Debug environment 55 | $ pnpm start:dev 56 | 57 | # Production environment 58 | $ pnpm start:prod 59 | ``` 60 | 61 | ## Deploy and Manage RGB++ SDK Service 62 | 63 | ### 1. Use a process manager like PM2 64 | 65 | - Install PM2 66 | 67 | ```shell 68 | $ npm install -g pm2 69 | ``` 70 | 71 | - Start RGB++ SDK Service with PM2 72 | 73 | ``` 74 | $ pm2 start dist/src/main.js --name rgbpp-sdk-service 75 | ``` 76 | 77 | ### 2. Use Docker 78 | 79 | - Copy the `.env.example` file to `.env` and Update the configuration values 80 | 81 | - Use the provided `docker-compose.yml` file to run the service: 82 | 83 | ```bash 84 | $ docker-compose up 85 | ``` 86 | 87 | ### 3. Deploy to Vercel 88 | 89 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fckb-cell%2Frgbpp-sdk%2Ftree%2Fmain%2Fapps%2Fservice&env=NETWORK,CKB_RPC_URL,BTC_SERVICE_URL,BTC_SERVICE_TOKEN,BTC_SERVICE_ORIGIN&project-name=rgbpp-sdk-service&repository-name=rgbpp-sdk) 90 | -------------------------------------------------------------------------------- /apps/service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | rgbpp-sdk-service: 5 | build: 6 | context: ../.. 7 | target: service 8 | # image: ghcr.io/ckb-cell/rgbpp-sdk-service:develop 9 | ports: 10 | - '3000:3000' 11 | env_file: 12 | - .env 13 | 14 | -------------------------------------------------------------------------------- /apps/service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgbpp-sdk-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "nest build", 7 | "format": "prettier --write \"src/**/*.ts\"", 8 | "dev": "pnpm run start:dev", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch", 11 | "start:debug": "nest start --debug --watch", 12 | "start:prod": "node dist/src/main", 13 | "lint": "eslint \"src/**/*.ts\" --fix", 14 | "test": "vitest", 15 | "test:watch": "vitest --watch", 16 | "test:cov": "vitest run --coverage", 17 | "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false" 18 | }, 19 | "dependencies": { 20 | "@nestjs/common": "^10.0.0", 21 | "@nestjs/config": "^3.2.2", 22 | "@nestjs/core": "^10.3.9", 23 | "@nestjs/platform-fastify": "^10.3.9", 24 | "camelcase-keys": "^7.0.2", 25 | "json-rpc-2.0": "^1.7.0", 26 | "lodash": "^4.17.21", 27 | "reflect-metadata": "^0.2.0", 28 | "rgbpp": "workspace:*", 29 | "rxjs": "^7.8.1", 30 | "snakecase-keys": "^8.0.1", 31 | "zod": "^3.23.8" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^10.0.0", 35 | "@nestjs/schematics": "^10.0.0", 36 | "@nestjs/testing": "^10.0.0", 37 | "@swc/core": "^1.7.11", 38 | "@types/node": "^20.3.1", 39 | "@types/supertest": "^6.0.0", 40 | "@vitest/coverage-v8": "^2.0.5", 41 | "source-map-support": "^0.5.21", 42 | "supertest": "^6.3.3", 43 | "ts-loader": "^9.4.3", 44 | "ts-node": "^10.9.1", 45 | "tsconfig-paths": "^4.2.0", 46 | "type-fest": "^4.24.0", 47 | "typescript": "^5.1.3", 48 | "unplugin-swc": "^1.5.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { BtcAssetsApi } from 'rgbpp/service'; 4 | import { BTCTestnetType, Collector } from 'rgbpp/ckb'; 5 | import JsonRpcModule from './json-rpc/json-rpc.module'; 6 | import { RgbppModule } from './rgbpp/rgbpp.module'; 7 | import { AppService } from './app.service'; 8 | import { envSchema } from './env'; 9 | 10 | @Global() 11 | @Module({ 12 | imports: [ 13 | ConfigModule.forRoot({ 14 | validate: envSchema.parse, 15 | envFilePath: ['.env', '.env.local'], 16 | }), 17 | JsonRpcModule.forRoot({ 18 | path: '/json-rpc', 19 | }), 20 | RgbppModule, 21 | ], 22 | providers: [ 23 | AppService, 24 | { 25 | provide: 'IS_MAINNET', 26 | useFactory: (configService: ConfigService): boolean => configService.get('NETWORK') === 'mainnet', 27 | inject: [ConfigService], 28 | }, 29 | { 30 | provide: 'BTC_TESTNET_TYPE', 31 | useFactory: (configService: ConfigService): BTCTestnetType => configService.get('BTC_TESTNET_TYPE'), 32 | inject: [ConfigService], 33 | }, 34 | { 35 | provide: 'CKB_COLLECTOR', 36 | useFactory: (configService: ConfigService) => { 37 | const ckbRpcUrl = configService.get('CKB_RPC_URL'); 38 | return new Collector({ 39 | ckbIndexerUrl: ckbRpcUrl, 40 | ckbNodeUrl: ckbRpcUrl, 41 | }); 42 | }, 43 | inject: [ConfigService], 44 | }, 45 | { 46 | provide: 'BTC_ASSETS_API', 47 | useFactory: (configService: ConfigService) => { 48 | const url = configService.get('BTC_SERVICE_URL'); 49 | const token = configService.get('BTC_SERVICE_TOKEN'); 50 | const origin = configService.get('BTC_SERVICE_ORIGIN'); 51 | return BtcAssetsApi.fromToken(url, token, origin); 52 | }, 53 | inject: [ConfigService], 54 | }, 55 | ], 56 | exports: ['IS_MAINNET', 'CKB_COLLECTOR', 'BTC_ASSETS_API', 'BTC_TESTNET_TYPE'], 57 | }) 58 | export class AppModule {} 59 | -------------------------------------------------------------------------------- /apps/service/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { RpcHandler, RpcMethodHandler } from './json-rpc/json-rpc.decorators.js'; 2 | import pkg from '../package.json'; 3 | 4 | @RpcHandler() 5 | export class AppService { 6 | @RpcMethodHandler({ name: 'get_version' }) 7 | public getAppVersion(): string { 8 | return pkg.version; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/service/src/env.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const envSchema = z.object({ 4 | NETWORK: z.enum(['mainnet', 'testnet']).default('testnet'), 5 | CKB_RPC_URL: z.string().default('https://testnet.ckb.dev'), 6 | 7 | BTC_SERVICE_URL: z.string(), 8 | BTC_SERVICE_TOKEN: z.string(), 9 | BTC_SERVICE_ORIGIN: z.string(), 10 | BTC_TESTNET_TYPE: z.string().default('Testnet3'), 11 | }); 12 | -------------------------------------------------------------------------------- /apps/service/src/json-rpc/json-rpc.decorators.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Injectable, InjectableOptions, SetMetadata } from '@nestjs/common'; 2 | 3 | export const JsonRpcMetadataKey = '__json-rpc@metadata__'; 4 | export interface JsonRpcMetadata { 5 | name?: string; 6 | } 7 | 8 | export const JsonRpcMethodMetadataKey = '__json-rpc-method@metadata__'; 9 | export interface JsonRpcMethodMetadata { 10 | name?: string; 11 | } 12 | 13 | export const RpcHandler = (data?: JsonRpcMetadata & InjectableOptions) => { 14 | const { name, scope } = data ?? {}; 15 | return applyDecorators(SetMetadata(JsonRpcMetadataKey, name ? { name } : {}), Injectable({ scope })); 16 | }; 17 | 18 | export const RpcMethodHandler = (data?: JsonRpcMethodMetadata) => { 19 | return applyDecorators(SetMetadata(JsonRpcMethodMetadataKey, data ?? {})); 20 | }; 21 | -------------------------------------------------------------------------------- /apps/service/src/json-rpc/json-rpc.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Logger, Module, OnModuleInit } from '@nestjs/common'; 2 | import { JsonRpcServer } from './json-rpc.server'; 3 | 4 | export const JSON_RPC_OPTIONS = '__JSON_RPC_OPTIONS__'; 5 | 6 | export interface JsonRpcConfig { 7 | path: string; 8 | } 9 | 10 | @Module({}) 11 | export class JsonRpcModule implements OnModuleInit { 12 | private logger = new Logger(JsonRpcModule.name); 13 | 14 | constructor( 15 | @Inject(JSON_RPC_OPTIONS) private config: JsonRpcConfig, 16 | private jsonRpcServer: JsonRpcServer, 17 | ) {} 18 | 19 | public static forRoot(config: JsonRpcConfig) { 20 | return { 21 | module: JsonRpcModule, 22 | providers: [ 23 | { 24 | provide: JSON_RPC_OPTIONS, 25 | useValue: config, 26 | }, 27 | JsonRpcServer, 28 | ], 29 | }; 30 | } 31 | 32 | public async onModuleInit() { 33 | await this.jsonRpcServer.resolve(); 34 | this.jsonRpcServer.run(this.config); 35 | this.logger.log(`JSON-RPC server is running on ${this.config.path}`); 36 | } 37 | } 38 | 39 | export default JsonRpcModule; 40 | -------------------------------------------------------------------------------- /apps/service/src/json-rpc/json-rpc.server.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { HttpAdapterHost, ModuleRef, ModulesContainer } from '@nestjs/core'; 3 | import { JSONRPCServer, SimpleJSONRPCMethod } from 'json-rpc-2.0'; 4 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 5 | import { JsonRpcMetadataKey, JsonRpcMethodMetadataKey } from './json-rpc.decorators'; 6 | import { JsonRpcConfig } from './json-rpc.module'; 7 | 8 | class JsonRpcServerError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | } 12 | } 13 | 14 | @Injectable() 15 | export class JsonRpcServer { 16 | private server: JSONRPCServer; 17 | private logger = new Logger(JsonRpcServer.name); 18 | 19 | constructor( 20 | private httpAdapterHost: HttpAdapterHost, 21 | private modulesContainer: ModulesContainer, 22 | private moduleRef: ModuleRef, 23 | ) { 24 | this.server = new JSONRPCServer(); 25 | } 26 | 27 | private getRegisteredHandlers() { 28 | const modules = [...this.modulesContainer.values()]; 29 | const wrappers = modules.reduce( 30 | (providers, module) => providers.concat([...module.providers.values()]), 31 | [] as InstanceWrapper[], 32 | ); 33 | 34 | const rpcHandlers = new Map>(); 35 | wrappers.forEach((wrapper) => { 36 | const { instance } = wrapper; 37 | if (!instance) { 38 | return; 39 | } 40 | const metadata = Reflect.getMetadata(JsonRpcMetadataKey, instance.constructor); 41 | if (!metadata) { 42 | return; 43 | } 44 | 45 | const properties = Object.getOwnPropertyNames(Object.getPrototypeOf(instance)); 46 | properties.forEach((methodName) => { 47 | const methodMetadata = Reflect.getMetadata(JsonRpcMethodMetadataKey, instance[methodName]); 48 | if (!methodMetadata) { 49 | return; 50 | } 51 | const name = metadata.name 52 | ? `${metadata.name}.${methodMetadata.name ?? methodName}` 53 | : (methodMetadata.name ?? methodName); 54 | const handler = (params: unknown) => { 55 | const instanceRef = this.moduleRef.get(instance.constructor, { strict: false }); 56 | return instanceRef[methodName](params); 57 | }; 58 | if (rpcHandlers.has(name)) { 59 | throw new JsonRpcServerError(`Duplicate JSON-RPC method: ${name}`); 60 | } 61 | rpcHandlers.set(name, handler); 62 | }); 63 | }); 64 | return rpcHandlers; 65 | } 66 | 67 | public async resolve() { 68 | const handlers = this.getRegisteredHandlers(); 69 | handlers.forEach((handler, name) => { 70 | this.logger.log(`Registering JSON-RPC method: ${name}`); 71 | this.server.addMethod(name, handler); 72 | }); 73 | } 74 | 75 | public async run(config: JsonRpcConfig) { 76 | this.httpAdapterHost.httpAdapter.post(config.path, async (req, res) => { 77 | this.logger.debug(`Received JSON-RPC request: ${JSON.stringify(req.body)}`); 78 | const jsonRpcResponse = await this.server.receive(req.body); 79 | this.httpAdapterHost.httpAdapter.setHeader(res, 'Content-Type', 'application/json'); 80 | this.httpAdapterHost.httpAdapter.reply(res, jsonRpcResponse); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /apps/service/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const logger = new Logger('AppBootstrap'); 8 | const app = await NestFactory.create(AppModule, new FastifyAdapter(), { 9 | cors: true, 10 | }); 11 | await app.listen(3000, '0.0.0.0'); 12 | logger.log('Application is running on: http://0.0.0.0:3000'); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /apps/service/src/rgbpp/rgbpp.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RgbppService } from './rgbpp.service'; 3 | 4 | @Module({ 5 | imports: [], 6 | providers: [RgbppService], 7 | }) 8 | export class RgbppModule {} 9 | -------------------------------------------------------------------------------- /apps/service/src/rgbpp/types.ts: -------------------------------------------------------------------------------- 1 | import { AddressToPubkeyMap } from 'rgbpp/btc'; 2 | import { Hex } from 'rgbpp/ckb'; 3 | 4 | export interface RgbppTransferReq { 5 | // The transferred RGB++ xUDT type script args 6 | xudtTypeArgs: Hex; 7 | // The rgbpp assets cell lock script args array whose data structure is: out_index | btc_tx_id 8 | rgbppLockArgsList: string[]; 9 | // The xUDT amount to be transferred 10 | transferAmount: Hex; 11 | // The sender BTC address 12 | fromBtcAddress: string; 13 | // The receiver BTC address 14 | toBtcAddress: string; 15 | } 16 | 17 | export interface RgbppCkbBtcTransaction { 18 | // The JSON string for the `BtcTransferVirtualTxResult` 19 | ckbVirtualTxResult: string; 20 | // The BTC PSBT hex string which can be used to construct Bitcoin PSBT 21 | btcPsbtHex: Hex; 22 | } 23 | 24 | export interface RgbppCkbTxBtcTxId { 25 | // The JSON string for the `BtcTransferVirtualTxResult` 26 | ckbVirtualTxResult: string; 27 | // The BTC transaction id of the RGB++ operations 28 | btcTxId: Hex; 29 | } 30 | 31 | export interface RgbppStateReq { 32 | btcTxId: Hex; 33 | params?: { 34 | withData?: boolean; 35 | }; 36 | } 37 | 38 | export interface RgbppCkbTxHashReq { 39 | btcTxId: Hex; 40 | } 41 | 42 | export interface BtcTxSendReq { 43 | txHex: Hex; 44 | } 45 | 46 | export interface RgbppTransferAllReq { 47 | ckb: { 48 | // The transferred RGB++ xUDT type script args 49 | xudtTypeArgs: Hex; 50 | // The CKB transaction fee rate in hex format, default value is 0x44c (1100) 51 | feeRate?: Hex; 52 | }; 53 | btc: { 54 | // The list of BTC addresses to provide RGB++ xUDT assets 55 | // All available amounts of the target asset (specified by ckb.xudtTypeArgs) will be included in the transfers 56 | // However, if more than 40 cells are bound to the same UTXO, the amounts within those 40 cells are excluded 57 | assetAddresses: string[]; 58 | // The BTC address for paying all the transaction costs, but not provide any RGB++ assets 59 | fromAddress: string; 60 | // The BTC address for receiving all the RGB++ assets 61 | toAddress: string; 62 | // The public key of sender BTC address, must fill if the fromAddress is a P2TR address 63 | fromPubkey?: string; 64 | // The map helps find the corresponding public key of a BTC address, 65 | // note that you must specify a pubkey for each P2TR address in assetAddresses/fromAddress 66 | pubkeyMap?: AddressToPubkeyMap; 67 | // The BTC address to return change satoshi, default value is fromAddress 68 | changeAddress?: string; 69 | // The fee rate of the BTC transactions, will use the fastest fee rate if not specified 70 | feeRate?: number; 71 | }; 72 | } 73 | 74 | export interface RgbppTransferAllResp { 75 | ckbVirtualTxResult: string; 76 | btcPsbtHex: string; 77 | btcFeeRate: number; 78 | btcFee: number; 79 | } 80 | -------------------------------------------------------------------------------- /apps/service/src/utils/case.ts: -------------------------------------------------------------------------------- 1 | import type { SnakeCasedPropertiesDeep, CamelCasedPropertiesDeep } from 'type-fest'; 2 | import snakeCaseKeys from 'snakecase-keys'; 3 | import camelCaseKeys from 'camelcase-keys'; 4 | 5 | export type SnakeCased = SnakeCasedPropertiesDeep; 6 | export type CamelCased = CamelCasedPropertiesDeep; 7 | 8 | // This regex is used to exclude hex strings from being converted to snake_case or camelCase 9 | // Because hex strings in object keys should be kept as is 10 | const excludeHexRegex = /^0x.+/g; 11 | 12 | export function toSnakeCase(obj: T, options?: snakeCaseKeys.Options): SnakeCased { 13 | return snakeCaseKeys(obj as Record, { 14 | exclude: [excludeHexRegex], 15 | deep: true, 16 | ...options, 17 | }) as SnakeCased; 18 | } 19 | 20 | export function toCamelCase(obj: T, options?: camelCaseKeys.Options): CamelCased { 21 | return camelCaseKeys(obj as Record, { 22 | exclude: [excludeHexRegex], 23 | deep: true, 24 | ...options, 25 | }) as CamelCased; 26 | } 27 | -------------------------------------------------------------------------------- /apps/service/src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject'; 2 | 3 | export function ensureSafeJson(json: Input): Output { 4 | if (!isPlainObject(json) && !Array.isArray(json)) { 5 | return json as unknown as Output; 6 | } 7 | 8 | const obj = Array.isArray(json) ? [] : {}; 9 | for (const key of Object.keys(json)) { 10 | const value = json[key]; 11 | if (isPlainObject(value) || Array.isArray(value)) { 12 | obj[key] = ensureSafeJson(value); 13 | } else { 14 | // XXX: Convert BigInt to hex string for JSON.stringify() compatibility 15 | if (typeof value === 'bigint') { 16 | obj[key] = `0x${value.toString(16)}`; 17 | } else { 18 | obj[key] = value; 19 | } 20 | } 21 | } 22 | 23 | return obj as unknown as Output; 24 | } 25 | -------------------------------------------------------------------------------- /apps/service/tests/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ensureSafeJson } from '../src/utils/json'; 3 | import { toSnakeCase, toCamelCase } from '../src/utils/case'; 4 | 5 | describe('Utils', () => { 6 | it('toSnakeCase()', () => { 7 | expect( 8 | toSnakeCase({ 9 | misterA: 1, 10 | MisterB: 2, 11 | mister_C: 3, 12 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 13 | }), 14 | ).toStrictEqual({ 15 | mister_a: 1, 16 | mister_b: 2, 17 | mister_c: 3, 18 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 19 | }); 20 | expect( 21 | toSnakeCase([ 22 | { 23 | misterA: 1, 24 | MisterB: 2, 25 | mister_C: 3, 26 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 27 | }, 28 | ]), 29 | ).toStrictEqual([ 30 | { 31 | mister_a: 1, 32 | mister_b: 2, 33 | mister_c: 3, 34 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 35 | }, 36 | ]); 37 | }); 38 | it('toCamelCase()', () => { 39 | expect( 40 | toCamelCase({ 41 | misterA: 1, 42 | mister_b: 2, 43 | MisterC: 3, 44 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 45 | }), 46 | ).toStrictEqual({ 47 | misterA: 1, 48 | misterB: 2, 49 | misterC: 3, 50 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 51 | }); 52 | expect( 53 | toCamelCase([ 54 | { 55 | misterA: 1, 56 | mister_b: 2, 57 | MisterC: 3, 58 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 59 | }, 60 | ]), 61 | ).toStrictEqual([ 62 | { 63 | misterA: 1, 64 | misterB: 2, 65 | misterC: 3, 66 | '0x06c1c265d475e69bac3b42f8deca5ac982efabfa640eff96a0f5d15345583e6e': 4, 67 | }, 68 | ]); 69 | }); 70 | it('ensureSafeJson()', () => { 71 | expect( 72 | ensureSafeJson({ 73 | number: 1, 74 | boolean: true, 75 | string: 'string', 76 | object: { 77 | number: 1, 78 | bigint1: BigInt('0x64'), 79 | }, 80 | array: [ 81 | { 82 | number: 1, 83 | bigint1: BigInt('0x64'), 84 | }, 85 | ], 86 | bigint1: BigInt(100), 87 | bigint2: BigInt('100'), 88 | bigint3: BigInt('0x64'), 89 | }), 90 | ).toStrictEqual({ 91 | number: 1, 92 | boolean: true, 93 | string: 'string', 94 | object: { 95 | number: 1, 96 | bigint1: '0x64', 97 | }, 98 | array: [ 99 | { 100 | number: 1, 101 | bigint1: '0x64', 102 | }, 103 | ], 104 | bigint1: '0x64', 105 | bigint2: '0x64', 106 | bigint3: '0x64', 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /apps/service/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "declaration": true, 5 | "resolveJsonModule": true, 6 | "removeComments": true, 7 | "moduleResolution": "NodeNext", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "ES2021", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./", 15 | "incremental": true, 16 | "skipLibCheck": true, 17 | "strictNullChecks": false, 18 | "noImplicitAny": false, 19 | "strictBindCallApply": false, 20 | "forceConsistentCasingInFileNames": false, 21 | "noFallthroughCasesInSwitch": false 22 | }, 23 | "exclude": ["dist"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/service/vitest.project.mts: -------------------------------------------------------------------------------- 1 | import swc from 'unplugin-swc'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | root: './', 7 | }, 8 | plugins: [ 9 | // This is required to build the test files with SWC 10 | swc.vite({ 11 | // Explicitly set the module type to avoid inheriting this value from a `.swcrc` config file 12 | module: { type: 'es6' }, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /examples/rgbpp/.env.example: -------------------------------------------------------------------------------- 1 | # True for CKB and BTC Mainnet and false for CKB and BTC Testnet, the default value is false 2 | IS_MAINNET=false 3 | 4 | # CKB Variables 5 | 6 | # The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix 7 | CKB_SECP256K1_PRIVATE_KEY=0x-private-key 8 | 9 | # CKB node url which should match IS_MAINNET 10 | CKB_NODE_URL=https://testnet.ckb.dev/rpc 11 | 12 | # CKB indexer url which should match IS_MAINNET 13 | CKB_INDEXER_URL=https://testnet.ckb.dev/indexer 14 | 15 | 16 | # BTC Variables 17 | 18 | # The Bitcoin Testnet type including Testnet3 and Signet, default value is Signet 19 | # Testnet3: https://mempool.space/testnet 20 | # Signet: https://mempool.space/signet 21 | BTC_TESTNET_TYPE=Signet 22 | 23 | # The BTC private key whose format is 32bytes hex string without 0x prefix 24 | BTC_PRIVATE_KEY=private-key 25 | 26 | # The BTC address type to use, available options: P2WPKH or P2TR 27 | # The Native Segwit P2WPKH address will be generated with the BTC private key as default 28 | # Read more about P2WPKH in BIP141: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh 29 | BTC_ADDRESS_TYPE=P2WPKH 30 | 31 | # The BTC assets api url which should match IS_MAINNET and BTC_TESTNET_TYPE 32 | # The BTC Testnet Service url is: https://api.testnet.rgbpp.io 33 | # The BTC Signet Service url is: https://api.signet.rgbpp.io 34 | VITE_BTC_SERVICE_URL=https://api.signet.rgbpp.io 35 | 36 | # The BTC assets api token which should match IS_MAINNET and BTC_TESTNET_TYPE 37 | # To get an access token, please refer to https://github.com/ckb-cell/rgbpp-sdk/tree/develop/packages/service#get-an-access-token 38 | VITE_BTC_SERVICE_TOKEN= 39 | 40 | # The BTC assets api origin which should match IS_MAINNET and BTC_TESTNET_TYPE 41 | VITE_BTC_SERVICE_ORIGIN=https://btc-test.app 42 | 43 | -------------------------------------------------------------------------------- /examples/rgbpp/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utxostack/rgbpp-sdk/2d547132ede28616647e87d603aea63daada4841/examples/rgbpp/logs/.gitkeep -------------------------------------------------------------------------------- /examples/rgbpp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgbpp-examples", 3 | "version": "0.1.0", 4 | "description": "Examples used for RGBPP assets issuance, transfer, and leaping between BTC and CKB", 5 | "private": true, 6 | "type": "commonjs", 7 | "scripts": { 8 | "format": "prettier --write '**/*.{js,ts}'", 9 | "lint": "tsc && eslint . && prettier --check '**/*.{js,ts}'", 10 | "lint:fix": "tsc && eslint --fix --ext .js,.ts . && prettier --write '**/*.{js,ts}'" 11 | }, 12 | "dependencies": { 13 | "@nervosnetwork/ckb-sdk-utils": "0.109.5", 14 | "rgbpp": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "dotenv": "^16.4.5", 18 | "@types/dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/rgbpp/shared/btc-account.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addressToScriptPublicKeyHex, 3 | networkTypeToNetwork, 4 | remove0x, 5 | toXOnly, 6 | transactionToHex, 7 | tweakSigner, 8 | } from 'rgbpp/btc'; 9 | import { AddressType, NetworkType, bitcoin, ECPair } from 'rgbpp/btc'; 10 | import { BtcAssetsApi } from 'rgbpp/service'; 11 | 12 | export interface BtcAccount { 13 | from: string; 14 | fromPubkey?: string; 15 | keyPair: bitcoin.Signer; 16 | payment: bitcoin.Payment; 17 | addressType: AddressType; 18 | networkType: NetworkType; 19 | } 20 | 21 | export function createBtcAccount(privateKey: string, addressType: AddressType, networkType: NetworkType): BtcAccount { 22 | const network = networkTypeToNetwork(networkType); 23 | 24 | const key = Buffer.from(remove0x(privateKey), 'hex'); 25 | const keyPair = ECPair.fromPrivateKey(key, { network }); 26 | 27 | if (addressType === AddressType.P2WPKH) { 28 | const p2wpkh = bitcoin.payments.p2wpkh({ 29 | pubkey: keyPair.publicKey, 30 | network, 31 | }); 32 | return { 33 | from: p2wpkh.address!, 34 | payment: p2wpkh, 35 | keyPair, 36 | addressType, 37 | networkType, 38 | }; 39 | } else if (addressType === AddressType.P2TR) { 40 | const p2tr = bitcoin.payments.p2tr({ 41 | internalPubkey: toXOnly(keyPair.publicKey), 42 | network, 43 | }); 44 | return { 45 | from: p2tr.address!, 46 | fromPubkey: keyPair.publicKey.toString('hex'), 47 | payment: p2tr, 48 | keyPair, 49 | addressType, 50 | networkType, 51 | }; 52 | } else { 53 | throw new Error('Unsupported address type, only support P2WPKH and P2TR'); 54 | } 55 | } 56 | 57 | export function signPsbt(psbt: bitcoin.Psbt, account: BtcAccount): bitcoin.Psbt { 58 | const accountScript = addressToScriptPublicKeyHex(account.from, account.networkType); 59 | const tweakedSigner = tweakSigner(account.keyPair, { 60 | network: account.payment.network, 61 | }); 62 | 63 | psbt.data.inputs.forEach((input, index) => { 64 | if (input.witnessUtxo) { 65 | const script = input.witnessUtxo.script.toString('hex'); 66 | if (script === accountScript && account.addressType === AddressType.P2WPKH) { 67 | psbt.signInput(index, account.keyPair); 68 | } 69 | if (script === accountScript && account.addressType === AddressType.P2TR) { 70 | psbt.signInput(index, tweakedSigner); 71 | } 72 | } 73 | }); 74 | 75 | return psbt; 76 | } 77 | 78 | export async function signAndSendPsbt( 79 | psbt: bitcoin.Psbt, 80 | account: BtcAccount, 81 | service: BtcAssetsApi, 82 | ): Promise<{ 83 | txId: string; 84 | txHex: string; 85 | rawTxHex: string; 86 | }> { 87 | signPsbt(psbt, account); 88 | psbt.finalizeAllInputs(); 89 | 90 | const tx = psbt.extractTransaction(true); 91 | const txHex = tx.toHex(); 92 | 93 | const { txid } = await service.sendBtcTransaction(txHex); 94 | 95 | return { 96 | txHex, 97 | txId: txid, 98 | // Exclude witness from the BTC_TX for unlocking RGBPP assets 99 | rawTxHex: transactionToHex(tx, false), 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /examples/rgbpp/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { 4 | BaseCkbVirtualTxResult, 5 | SporeVirtualTxResult, 6 | SporeCreateVirtualTxResult, 7 | SporeTransferVirtualTxResult, 8 | RgbppLaunchVirtualTxResult, 9 | } from 'rgbpp/ckb'; 10 | 11 | /** 12 | * Save ckbVirtualTxResult to a log file 13 | * @param ckbVirtualTxResult - The ckbVirtualTxResult to save 14 | * @param exampleName - Example name used to distinguish different log files 15 | */ 16 | 17 | export type CkbVirtualTxResultType = 18 | | BaseCkbVirtualTxResult 19 | | RgbppLaunchVirtualTxResult 20 | | SporeVirtualTxResult 21 | | SporeCreateVirtualTxResult 22 | | SporeTransferVirtualTxResult; 23 | 24 | export const saveCkbVirtualTxResult = (ckbVirtualTxResult: CkbVirtualTxResultType, exampleName: string) => { 25 | try { 26 | // Define log file path 27 | const logDir = path.resolve(__dirname, '../logs'); 28 | const timestamp = new Date().toISOString().replace(/:/g, '-'); // Replace colons with hyphens 29 | const logFilePath = path.join(logDir, `${exampleName}-${timestamp}-ckbVirtualTxResult.log`); 30 | 31 | // Ensure the log directory exists 32 | if (!fs.existsSync(logDir)) { 33 | fs.mkdirSync(logDir); 34 | } 35 | 36 | // Validate and save ckbVirtualTxResult to log file 37 | if (typeof ckbVirtualTxResult === 'object' && ckbVirtualTxResult !== null) { 38 | fs.writeFileSync(logFilePath, JSON.stringify(ckbVirtualTxResult, null, 2)); 39 | console.info(`Saved ckbVirtualTxResult to ${logFilePath}`); 40 | } else { 41 | console.error('Invalid ckbVirtualTxResult format'); 42 | } 43 | 44 | // Remind developers to save the transaction result 45 | console.info( 46 | `Important: It's recommended to save the rgbpp_ckb_tx_virtual locally before the isomorphic transactions are finalized.`, 47 | ); 48 | } catch (error) { 49 | console.error('Failed to save ckbVirtualTxResult:', error); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/4-transfer-spore.ts: -------------------------------------------------------------------------------- 1 | import { buildRgbppLockArgs } from 'rgbpp/ckb'; 2 | import { genTransferSporeCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; 3 | import { getSporeTypeScript, Hex } from 'rgbpp/ckb'; 4 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 5 | import { isMainnet, collector, btcDataSource, btcService, btcAccount, BTC_TESTNET_TYPE } from '../env'; 6 | import { saveCkbVirtualTxResult } from '../shared/utils'; 7 | import { signAndSendPsbt } from '../shared/btc-account'; 8 | 9 | interface SporeTransferParams { 10 | sporeRgbppLockArgs: Hex; 11 | toBtcAddress: string; 12 | sporeTypeArgs: Hex; 13 | } 14 | 15 | const transferSpore = async ({ sporeRgbppLockArgs, toBtcAddress, sporeTypeArgs }: SporeTransferParams) => { 16 | const sporeTypeBytes = serializeScript({ 17 | ...getSporeTypeScript(isMainnet), 18 | args: sporeTypeArgs, 19 | }); 20 | 21 | const ckbVirtualTxResult = await genTransferSporeCkbVirtualTx({ 22 | collector, 23 | sporeRgbppLockArgs, 24 | sporeTypeBytes, 25 | isMainnet, 26 | btcTestnetType: BTC_TESTNET_TYPE, 27 | }); 28 | 29 | // Save ckbVirtualTxResult 30 | saveCkbVirtualTxResult(ckbVirtualTxResult, '4-transfer-spore'); 31 | 32 | const { commitment, ckbRawTx, needPaymasterCell } = ckbVirtualTxResult; 33 | 34 | // Send BTC tx 35 | const psbt = await sendRgbppUtxos({ 36 | ckbVirtualTx: ckbRawTx, 37 | commitment, 38 | tos: [toBtcAddress], 39 | needPaymaster: needPaymasterCell, 40 | ckbCollector: collector, 41 | from: btcAccount.from, 42 | fromPubkey: btcAccount.fromPubkey, 43 | source: btcDataSource, 44 | feeRate: 30, 45 | }); 46 | 47 | const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); 48 | console.log('BTC TxId: ', btcTxId); 49 | 50 | await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); 51 | 52 | try { 53 | const interval = setInterval(async () => { 54 | const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); 55 | console.log('state', state); 56 | if (state === 'completed' || state === 'failed') { 57 | clearInterval(interval); 58 | if (state === 'completed') { 59 | const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); 60 | console.info(`Rgbpp spore has been transferred on BTC and the related CKB tx hash is ${txHash}`); 61 | } else { 62 | console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); 63 | } 64 | } 65 | }, 30 * 1000); 66 | } catch (error) { 67 | console.error(error); 68 | } 69 | }; 70 | 71 | // Please use your real BTC UTXO information on the BTC Testnet 72 | // BTC Testnet3: https://mempool.space/testnet 73 | // BTC Signet: https://mempool.space/signet 74 | 75 | // rgbppLockArgs: outIndexU32 + btcTxId 76 | transferSpore({ 77 | sporeRgbppLockArgs: buildRgbppLockArgs(2, 'd5868dbde4be5e49876b496449df10150c356843afb6f94b08f8d81f394bb350'), 78 | toBtcAddress: 'tb1qhp9fh9qsfeyh0yhewgu27ndqhs5qlrqwau28m7', 79 | // Please use your own RGB++ spore asset's sporeTypeArgs 80 | sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', 81 | }); 82 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/5-leap-spore-to-ckb.ts: -------------------------------------------------------------------------------- 1 | import { buildRgbppLockArgs } from 'rgbpp/ckb'; 2 | import { genLeapSporeFromBtcToCkbVirtualTx, sendRgbppUtxos } from 'rgbpp'; 3 | import { getSporeTypeScript, Hex } from 'rgbpp/ckb'; 4 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 5 | import { isMainnet, collector, btcDataSource, btcService, btcAccount, BTC_TESTNET_TYPE } from '../env'; 6 | import { saveCkbVirtualTxResult } from '../shared/utils'; 7 | import { signAndSendPsbt } from '../shared/btc-account'; 8 | 9 | interface SporeLeapParams { 10 | sporeRgbppLockArgs: Hex; 11 | toCkbAddress: string; 12 | sporeTypeArgs: Hex; 13 | } 14 | 15 | const leapSporeFromBtcToCkb = async ({ sporeRgbppLockArgs, toCkbAddress, sporeTypeArgs }: SporeLeapParams) => { 16 | const sporeTypeBytes = serializeScript({ 17 | ...getSporeTypeScript(isMainnet), 18 | args: sporeTypeArgs, 19 | }); 20 | 21 | const ckbVirtualTxResult = await genLeapSporeFromBtcToCkbVirtualTx({ 22 | collector, 23 | sporeRgbppLockArgs, 24 | sporeTypeBytes, 25 | toCkbAddress, 26 | isMainnet, 27 | btcTestnetType: BTC_TESTNET_TYPE, 28 | }); 29 | 30 | // Save ckbVirtualTxResult 31 | saveCkbVirtualTxResult(ckbVirtualTxResult, '5-leap-spore-to-ckb'); 32 | 33 | const { commitment, ckbRawTx, needPaymasterCell } = ckbVirtualTxResult; 34 | 35 | // Send BTC tx 36 | const psbt = await sendRgbppUtxos({ 37 | ckbVirtualTx: ckbRawTx, 38 | commitment, 39 | tos: [btcAccount.from], 40 | needPaymaster: needPaymasterCell, 41 | ckbCollector: collector, 42 | from: btcAccount.from, 43 | fromPubkey: btcAccount.fromPubkey, 44 | source: btcDataSource, 45 | feeRate: 30, 46 | }); 47 | 48 | const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); 49 | console.log('BTC TxId: ', btcTxId); 50 | 51 | await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); 52 | 53 | try { 54 | const interval = setInterval(async () => { 55 | const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); 56 | console.log('state', state); 57 | if (state === 'completed' || state === 'failed') { 58 | clearInterval(interval); 59 | if (state === 'completed') { 60 | const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); 61 | console.info(`Rgbpp spore has been leaped from BTC to CKB and the related CKB tx hash is ${txHash}`); 62 | } else { 63 | console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); 64 | } 65 | } 66 | }, 30 * 1000); 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | }; 71 | 72 | // Please use your real BTC UTXO information on the BTC Testnet 73 | // BTC Testnet3: https://mempool.space/testnet 74 | // BTC Signet: https://mempool.space/signet 75 | 76 | // rgbppLockArgs: outIndexU32 + btcTxId 77 | leapSporeFromBtcToCkb({ 78 | sporeRgbppLockArgs: buildRgbppLockArgs(3, 'd8a31796fbd42c546f6b22014b9b82b16586ce1df81b0e7ca9a552cdc492a0af'), 79 | toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsq0e4xk4rmg5jdkn8aams492a7jlg73ue0gc0ddfj', 80 | // Please use your own RGB++ spore asset's sporeTypeArgs 81 | sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', 82 | }); 83 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/6-unlock-btc-time-cell.ts: -------------------------------------------------------------------------------- 1 | import { buildSporeBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; 2 | import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../env'; 3 | import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; 4 | 5 | // Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 5-leap-spore-to-ckb.ts 6 | const unlockSporeBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { 7 | const btcTimeCells = await collector.getCells({ 8 | lock: { 9 | ...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE), 10 | args: btcTimeCellArgs, 11 | }, 12 | isDataMustBeEmpty: false, 13 | }); 14 | 15 | if (!btcTimeCells || btcTimeCells.length === 0) { 16 | throw new Error('No btc time cells found'); 17 | } 18 | 19 | const ckbRawTx: CKBComponents.RawTransaction = await buildSporeBtcTimeCellsSpentTx({ 20 | btcTimeCells, 21 | btcAssetsApi: btcService, 22 | isMainnet, 23 | btcTestnetType: BTC_TESTNET_TYPE, 24 | }); 25 | 26 | const signedTx = await signBtcTimeCellSpentTx({ 27 | secp256k1PrivateKey: CKB_PRIVATE_KEY, 28 | collector, 29 | masterCkbAddress: ckbAddress, 30 | ckbRawTx, 31 | isMainnet, 32 | }); 33 | 34 | const txHash = await sendCkbTx({ collector, signedTx }); 35 | console.info(`Spore BTC time cell has been unlocked and tx hash is ${txHash}`); 36 | }; 37 | 38 | // The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 5-leap-spore-to-ckb.ts CKB transaction 39 | unlockSporeBtcTimeCell({ 40 | btcTimeCellArgs: 41 | '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000f9a9ad51ed14936d33f7bb854aaefa5f47a3ccbd060000002997fa043e977cb0a9bcc75ec308ad1323331c5295caf8fc721b0a2761bef305', 42 | }); 43 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/7-leap-spore-to-btc.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genLeapSporeFromCkbToBtcRawTx } from 'rgbpp'; 3 | import { isMainnet, collector, ckbAddress, CKB_PRIVATE_KEY, BTC_TESTNET_TYPE } from '../env'; 4 | import { buildRgbppLockArgs, getSecp256k1CellDep, getSporeTypeScript } from 'rgbpp/ckb'; 5 | 6 | const leapSporeFromCkbToBtc = async ({ 7 | outIndex, 8 | btcTxId, 9 | sporeTypeArgs, 10 | }: { 11 | outIndex: number; 12 | btcTxId: string; 13 | sporeTypeArgs: string; 14 | }) => { 15 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 16 | 17 | const sporeType: CKBComponents.Script = { 18 | ...getSporeTypeScript(isMainnet), 19 | args: sporeTypeArgs, 20 | }; 21 | 22 | const ckbRawTx = await genLeapSporeFromCkbToBtcRawTx({ 23 | collector, 24 | fromCkbAddress: ckbAddress, 25 | toRgbppLockArgs, 26 | sporeTypeBytes: serializeScript(sporeType), 27 | isMainnet, 28 | btcTestnetType: BTC_TESTNET_TYPE, 29 | }); 30 | 31 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 32 | const unsignedTx: CKBComponents.RawTransactionToSign = { 33 | ...ckbRawTx, 34 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 35 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 36 | }; 37 | 38 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 39 | 40 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 41 | console.info(`RGB++ Spore has been jumped from CKB to BTC and tx hash is ${txHash}`); 42 | }; 43 | 44 | // Please use your real BTC UTXO information on the BTC Testnet 45 | // BTC Testnet3: https://mempool.space/testnet 46 | // BTC Signet: https://mempool.space/signet 47 | leapSporeFromCkbToBtc({ 48 | outIndex: 1, 49 | btcTxId: '448897515cf07b4ca0cd38af9806399ede55775b4c760b274ed2322121ed185f', 50 | // Please use your own RGB++ spore asset's sporeTypeArgs 51 | sporeTypeArgs: '0x42898ea77062256f46e8f1b861d526ae47810ecc51ab50477945d5fa90452706', 52 | }); 53 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/launch/0-cluster-info.ts: -------------------------------------------------------------------------------- 1 | import { RawClusterData } from 'rgbpp/ckb'; 2 | 3 | export const CLUSTER_DATA: RawClusterData = { 4 | name: 'Cluster name', 5 | description: 'Description of the cluster', 6 | }; 7 | -------------------------------------------------------------------------------- /examples/rgbpp/spore/launch/1-prepare-cluster.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { 3 | MAX_FEE, 4 | NoLiveCellError, 5 | SECP256K1_WITNESS_LOCK_SIZE, 6 | append0x, 7 | buildRgbppLockArgs, 8 | calculateRgbppClusterCellCapacity, 9 | calculateTransactionFee, 10 | genRgbppLockScript, 11 | getSecp256k1CellDep, 12 | } from 'rgbpp/ckb'; 13 | import { ckbAddress, isMainnet, collector, CKB_PRIVATE_KEY, BTC_TESTNET_TYPE } from '../../env'; 14 | import { CLUSTER_DATA } from './0-cluster-info'; 15 | 16 | const prepareClusterCell = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { 17 | const masterLock = addressToScript(ckbAddress); 18 | console.log('ckb address: ', ckbAddress); 19 | 20 | // The capacity required to launch cells is determined by the token info cell capacity, and transaction fee. 21 | const clusterCellCapacity = calculateRgbppClusterCellCapacity(CLUSTER_DATA); 22 | 23 | let emptyCells = await collector.getCells({ 24 | lock: masterLock, 25 | }); 26 | if (!emptyCells || emptyCells.length === 0) { 27 | throw new NoLiveCellError('The address has no empty cells'); 28 | } 29 | emptyCells = emptyCells.filter((cell) => !cell.output.type); 30 | 31 | const txFee = MAX_FEE; 32 | const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, clusterCellCapacity, txFee); 33 | 34 | const outputs: CKBComponents.CellOutput[] = [ 35 | { 36 | lock: genRgbppLockScript(buildRgbppLockArgs(outIndex, btcTxId), isMainnet, BTC_TESTNET_TYPE), 37 | capacity: append0x(clusterCellCapacity.toString(16)), 38 | }, 39 | ]; 40 | let changeCapacity = sumInputsCapacity - clusterCellCapacity; 41 | outputs.push({ 42 | lock: masterLock, 43 | capacity: append0x(changeCapacity.toString(16)), 44 | }); 45 | const outputsData = ['0x', '0x']; 46 | 47 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 48 | const witnesses = inputs.map((_, index) => (index === 0 ? emptyWitness : '0x')); 49 | 50 | const cellDeps = [getSecp256k1CellDep(isMainnet)]; 51 | 52 | const unsignedTx = { 53 | version: '0x0', 54 | cellDeps, 55 | headerDeps: [], 56 | inputs, 57 | outputs, 58 | outputsData, 59 | witnesses, 60 | }; 61 | 62 | const txSize = getTransactionSize(unsignedTx) + SECP256K1_WITNESS_LOCK_SIZE; 63 | const estimatedTxFee = calculateTransactionFee(txSize); 64 | changeCapacity -= estimatedTxFee; 65 | unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); 66 | 67 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 68 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 69 | 70 | console.info(`Cluster cell has been prepared and the tx hash ${txHash}`); 71 | }; 72 | 73 | // Please use your real BTC UTXO information on the BTC Testnet 74 | prepareClusterCell({ 75 | outIndex: 3, 76 | btcTxId: 'aee4e8e3aa95e9e9ab1f0520714031d92d3263262099dcc7f7d64e62fa2fcb44', 77 | }); 78 | -------------------------------------------------------------------------------- /examples/rgbpp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2015", 5 | "lib": ["esnext"], 6 | "module": "ES2015", 7 | "composite": false, 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noEmit": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "inlineSources": false, 16 | "isolatedModules": true, 17 | "moduleResolution": "Bundler", 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "preserveWatchOutput": true, 21 | "skipLibCheck": true, 22 | "strict": true 23 | }, 24 | "include": ["spore", "xudt", "shared"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/1-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; 4 | import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } from '../env'; 5 | 6 | interface LeapToBtcParams { 7 | outIndex: number; 8 | btcTxId: string; 9 | xudtTypeArgs: string; 10 | transferAmount: bigint; 11 | } 12 | 13 | const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmount }: LeapToBtcParams) => { 14 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 15 | 16 | // Warning: Please replace with your real xUDT type script here 17 | const xudtType: CKBComponents.Script = { 18 | ...getXudtTypeScript(isMainnet), 19 | args: xudtTypeArgs, 20 | }; 21 | 22 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 23 | collector, 24 | fromCkbAddress: ckbAddress, 25 | toRgbppLockArgs, 26 | xudtTypeBytes: serializeScript(xudtType), 27 | transferAmount, 28 | btcTestnetType: BTC_TESTNET_TYPE, 29 | }); 30 | 31 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 32 | const unsignedTx: CKBComponents.RawTransactionToSign = { 33 | ...ckbRawTx, 34 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 35 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 36 | }; 37 | 38 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 39 | 40 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 41 | console.info(`Rgbpp asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); 42 | }; 43 | 44 | // Please use your real BTC UTXO information on the BTC Testnet 45 | // BTC Testnet3: https://mempool.space/testnet 46 | // BTC Signet: https://mempool.space/signet 47 | leapFromCkbToBtc({ 48 | outIndex: 1, 49 | btcTxId: '3f6db9a387587006cb2fa8c6352bc728984bf39cb010789dffe574f27775a6ac', 50 | // Please use your own RGB++ xudt asset's xudtTypeArgs 51 | xudtTypeArgs: '0x562e4e8a2f64a3e9c24beb4b7dd002d0ad3b842d0cc77924328e36ad114e3ebe', 52 | transferAmount: BigInt(800_0000_0000), 53 | }); 54 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/2-btc-transfer.ts: -------------------------------------------------------------------------------- 1 | import { buildRgbppLockArgs } from 'rgbpp/ckb'; 2 | import { buildRgbppTransferTx } from 'rgbpp'; 3 | import { isMainnet, collector, btcService, btcAccount, btcDataSource, BTC_TESTNET_TYPE } from '../env'; 4 | import { saveCkbVirtualTxResult } from '../shared/utils'; 5 | import { signAndSendPsbt } from '../shared/btc-account'; 6 | import { bitcoin } from 'rgbpp/btc'; 7 | 8 | interface RgbppTransferParams { 9 | rgbppLockArgsList: string[]; 10 | toBtcAddress: string; 11 | xudtTypeArgs: string; 12 | transferAmount: bigint; 13 | } 14 | 15 | const transfer = async ({ rgbppLockArgsList, toBtcAddress, xudtTypeArgs, transferAmount }: RgbppTransferParams) => { 16 | const { ckbVirtualTxResult, btcPsbtHex } = await buildRgbppTransferTx({ 17 | ckb: { 18 | collector, 19 | xudtTypeArgs, 20 | rgbppLockArgsList, 21 | transferAmount, 22 | }, 23 | btc: { 24 | fromAddress: btcAccount.from, 25 | toAddress: toBtcAddress, 26 | fromPubkey: btcAccount.fromPubkey, 27 | dataSource: btcDataSource, 28 | testnetType: BTC_TESTNET_TYPE, 29 | }, 30 | isMainnet, 31 | }); 32 | 33 | // Save ckbVirtualTxResult 34 | saveCkbVirtualTxResult(ckbVirtualTxResult, '2-btc-transfer'); 35 | 36 | // Send BTC tx 37 | const psbt = bitcoin.Psbt.fromHex(btcPsbtHex); 38 | const { txId: btcTxId } = await signAndSendPsbt(psbt, btcAccount, btcService); 39 | console.log(`BTC ${BTC_TESTNET_TYPE} TxId: ${btcTxId}`); 40 | 41 | await btcService.sendRgbppCkbTransaction({ btc_txid: btcTxId, ckb_virtual_result: ckbVirtualTxResult }); 42 | 43 | try { 44 | const interval = setInterval(async () => { 45 | const { state, failedReason } = await btcService.getRgbppTransactionState(btcTxId); 46 | console.log('state', state); 47 | if (state === 'completed' || state === 'failed') { 48 | clearInterval(interval); 49 | if (state === 'completed') { 50 | const { txhash: txHash } = await btcService.getRgbppTransactionHash(btcTxId); 51 | console.info(`Rgbpp asset has been transferred on BTC and the related CKB tx hash is ${txHash}`); 52 | } else { 53 | console.warn(`Rgbpp CKB transaction failed and the reason is ${failedReason} `); 54 | } 55 | } 56 | }, 30 * 1000); 57 | } catch (error) { 58 | console.error(error); 59 | } 60 | }; 61 | 62 | // Please use your real BTC UTXO information on the BTC Testnet 63 | // BTC Testnet3: https://mempool.space/testnet 64 | // BTC Signet: https://mempool.space/signet 65 | 66 | // rgbppLockArgs: outIndexU32 + btcTxId 67 | transfer({ 68 | rgbppLockArgsList: [buildRgbppLockArgs(1, '5ddd7b60ba93e01d9781be50eaa5c1aa634f799fc9c47bf59d1566eacf47b1e8')], 69 | toBtcAddress: 'tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', 70 | // Please use your own RGB++ xudt asset's xudtTypeArgs 71 | xudtTypeArgs: '0x562e4e8a2f64a3e9c24beb4b7dd002d0ad3b842d0cc77924328e36ad114e3ebe', 72 | transferAmount: BigInt(800_0000_0000), 73 | }); 74 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/4-unlock-btc-time-cell.ts: -------------------------------------------------------------------------------- 1 | import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; 2 | import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; 3 | import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../env'; 4 | 5 | // Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 3-btc-leap-ckb.ts 6 | const unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { 7 | const btcTimeCells = await collector.getCells({ 8 | lock: { 9 | ...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE), 10 | args: btcTimeCellArgs, 11 | }, 12 | isDataMustBeEmpty: false, 13 | }); 14 | 15 | if (!btcTimeCells || btcTimeCells.length === 0) { 16 | throw new Error('No btc time cell found'); 17 | } 18 | 19 | const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ 20 | btcTimeCells, 21 | btcAssetsApi: btcService, 22 | isMainnet, 23 | btcTestnetType: BTC_TESTNET_TYPE, 24 | }); 25 | 26 | const signedTx = await signBtcTimeCellSpentTx({ 27 | secp256k1PrivateKey: CKB_PRIVATE_KEY, 28 | collector, 29 | masterCkbAddress: ckbAddress, 30 | ckbRawTx, 31 | isMainnet, 32 | }); 33 | 34 | const txHash = await sendCkbTx({ collector, signedTx }); 35 | console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`); 36 | }; 37 | 38 | // The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction 39 | unlockBtcTimeCell({ 40 | btcTimeCellArgs: 41 | '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000f9a9ad51ed14936d33f7bb854aaefa5f47a3ccbd880d0100ffc34d3d23f86df84a23a3b2cf72b45c8a309fec417ab196bee8e7a74483e05f', 42 | }); 43 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/btc-transfer-all/1-btc-transfer-all.ts: -------------------------------------------------------------------------------- 1 | import { bitcoin } from 'rgbpp/btc'; 2 | import { buildRgbppTransferAllTxs, sendRgbppTxGroups } from 'rgbpp'; 3 | import { btcDataSource, isMainnet, collector, btcAccount } from '../../env'; 4 | import { signPsbt } from '../../shared/btc-account'; 5 | import { saveCkbVirtualTxResult } from '../../shared/utils'; 6 | 7 | interface TestParams { 8 | xudtTypeArgs: string; 9 | fromAddress: string; 10 | toAddress: string; 11 | } 12 | 13 | const rgbppTransferAllTxs = async ({ xudtTypeArgs, fromAddress, toAddress }: TestParams) => { 14 | const result = await buildRgbppTransferAllTxs({ 15 | ckb: { 16 | xudtTypeArgs, 17 | collector, 18 | }, 19 | btc: { 20 | assetAddresses: [fromAddress], 21 | fromAddress: fromAddress, 22 | toAddress: toAddress, 23 | dataSource: btcDataSource, 24 | feeRate: 5, 25 | }, 26 | isMainnet, 27 | }); 28 | 29 | console.log('result.transactions.length', result.transactions.length); 30 | console.log('result.summary.included.assets', result.summary.included.xudtAssets); 31 | console.log('result.summary.excluded.assets', result.summary.excluded.xudtAssets); 32 | 33 | const signedGroups = await Promise.all( 34 | result.transactions.map(async (group) => { 35 | const psbt = bitcoin.Psbt.fromHex(group.btc.psbtHex); 36 | 37 | // Sign transactions 38 | signPsbt(psbt, btcAccount); 39 | 40 | psbt.finalizeAllInputs(); 41 | 42 | return { 43 | ckbVirtualTxResult: JSON.stringify(group.ckb.virtualTxResult), 44 | btcTxHex: psbt.extractTransaction().toHex(), 45 | }; 46 | }), 47 | ); 48 | 49 | const signedGroupsData = JSON.parse(JSON.stringify(signedGroups, null, 2)); 50 | 51 | // Save signedGroupsData 52 | saveCkbVirtualTxResult(signedGroupsData, '1-btc-transfer-all'); 53 | 54 | console.log('signedGroups', signedGroupsData); 55 | 56 | // Send transactions 57 | const sentGroups = await sendRgbppTxGroups({ 58 | txGroups: signedGroups, 59 | btcService: btcDataSource.service, 60 | }); 61 | console.log('sentGroups', JSON.stringify(sentGroups, null, 2)); 62 | }; 63 | 64 | rgbppTransferAllTxs({ 65 | // Please use your own RGB++ xudt asset's xudtTypeArgs 66 | xudtTypeArgs: '0xdec25e81ad1d5b909926265b0cdf404e270250b9885d436852b942d56d06be38', 67 | fromAddress: btcAccount.from, 68 | toAddress: 'tb1qdnvvnyhc5wegxgh0udwaej04n8w08ahrr0w4q9', 69 | }).catch(console.error); 70 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, CompatibleXUDTRegistry } from 'rgbpp/ckb'; 4 | import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } from '../../env'; 5 | 6 | interface LeapToBtcParams { 7 | outIndex: number; 8 | btcTxId: string; 9 | transferAmount: bigint; 10 | compatibleXudtTypeScript: CKBComponents.Script; 11 | } 12 | 13 | const leapRusdFromCkbToBtc = async ({ 14 | outIndex, 15 | btcTxId, 16 | transferAmount, 17 | compatibleXudtTypeScript, 18 | }: LeapToBtcParams) => { 19 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 20 | 21 | // Refresh the cache by fetching the latest compatible xUDT list from the specified URL. 22 | // The default URL is: 23 | // https://raw.githubusercontent.com/utxostack/typeid-contract-cell-deps/main/compatible-udt.json 24 | // You can set your own trusted URL to fetch the compatible xUDT list. 25 | // await CompatibleXUDTRegistry.refreshCache("https://your-own-trusted-compatible-xudt-url"); 26 | await CompatibleXUDTRegistry.refreshCache(); 27 | 28 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 29 | collector, 30 | fromCkbAddress: ckbAddress, 31 | toRgbppLockArgs, 32 | xudtTypeBytes: serializeScript(compatibleXudtTypeScript), 33 | transferAmount, 34 | btcTestnetType: BTC_TESTNET_TYPE, 35 | }); 36 | 37 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 38 | const unsignedTx: CKBComponents.RawTransactionToSign = { 39 | ...ckbRawTx, 40 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 41 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 42 | }; 43 | 44 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 45 | 46 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 47 | console.info(`Rgbpp compatible xUDT asset has been leaped from CKB to BTC and CKB tx hash is ${txHash}`); 48 | }; 49 | 50 | // Please use your real BTC UTXO information on the BTC Testnet 51 | // BTC Testnet3: https://mempool.space/testnet 52 | // BTC Signet: https://mempool.space/signet 53 | leapRusdFromCkbToBtc({ 54 | outIndex: 4, 55 | btcTxId: '44de1b4e3ddaa95cc85cc8b1c60f3e439d343002f0c60980fb4c70841ee0c75e', 56 | // Please use your own RGB++ compatible xUDT asset's type script 57 | compatibleXudtTypeScript: { 58 | codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 59 | hashType: 'type', 60 | args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 61 | }, 62 | transferAmount: BigInt(1000_0000), 63 | }); 64 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/compatible-xudt/4-unlock-btc-time-cell.ts: -------------------------------------------------------------------------------- 1 | import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; 2 | import { sendCkbTx, getBtcTimeLockScript } from 'rgbpp/ckb'; 3 | import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../env'; 4 | 5 | // Warning: Wait at least 6 BTC confirmation blocks to spend the BTC time cells after 3-btc-leap-ckb.ts 6 | const unlockRusdBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { 7 | const btcTimeCells = await collector.getCells({ 8 | lock: { 9 | ...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE), 10 | args: btcTimeCellArgs, 11 | }, 12 | isDataMustBeEmpty: false, 13 | }); 14 | 15 | if (!btcTimeCells || btcTimeCells.length === 0) { 16 | throw new Error('No btc time cell found'); 17 | } 18 | 19 | const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ 20 | btcTimeCells, 21 | btcAssetsApi: btcService, 22 | isMainnet, 23 | btcTestnetType: BTC_TESTNET_TYPE, 24 | }); 25 | 26 | const signedTx = await signBtcTimeCellSpentTx({ 27 | secp256k1PrivateKey: CKB_PRIVATE_KEY, 28 | collector, 29 | masterCkbAddress: ckbAddress, 30 | ckbRawTx, 31 | isMainnet, 32 | }); 33 | 34 | const txHash = await sendCkbTx({ collector, signedTx }); 35 | console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`); 36 | }; 37 | 38 | // The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction 39 | unlockRusdBtcTimeCell({ 40 | btcTimeCellArgs: 41 | '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000f9a9ad51ed14936d33f7bb854aaefa5f47a3ccbd0600000038036f35121682517b5f79732fc6a182e0050cfe1ad4cce0a1314c229a1ba364', 42 | }); 43 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/compatible-xudt/assets-api.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { btcService } from '../../env'; 3 | 4 | (async () => { 5 | // const assets = await btcService.getRgbppAssetsByBtcAddress('tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', { 6 | // type_script: serializeScript({ 7 | // codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 8 | // hashType: 'type', 9 | // args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 10 | // }), 11 | // }); 12 | // console.log('RUSD Assets: ', JSON.stringify(assets)); 13 | 14 | // const activities = await btcService.getRgbppActivityByBtcAddress('tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', { 15 | // type_script: serializeScript({ 16 | // codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 17 | // hashType: 'type', 18 | // args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 19 | // }), 20 | // }); 21 | // console.log('RUSD Activities: ', JSON.stringify(activities)); 22 | 23 | // const balance = await btcService.getRgbppBalanceByBtcAddress('tb1qvt7p9g6mw70sealdewtfp0sekquxuru6j3gwmt', { 24 | // type_script: serializeScript({ 25 | // codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 26 | // hashType: 'type', 27 | // args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 28 | // }), 29 | // no_cache: true, 30 | // }); 31 | // console.log('RUSD balance from btc-assets-api: ', JSON.stringify(balance)); 32 | 33 | // const info = await btcService.getRgbppAssetInfoByTypeScript( 34 | // serializeScript({ 35 | // codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', 36 | // hashType: 'type', 37 | // args: '0x661cfbe2124b3e79e50e505c406be5b2dcf9da15d8654b749ec536fa4c2eaaae', 38 | // }), 39 | // ); 40 | // console.log('Standard xUDT info: ', JSON.stringify(info)); 41 | 42 | const rusdInfo = await btcService.getRgbppAssetInfoByTypeScript( 43 | serializeScript({ 44 | codeHash: '0xcc9dc33ef234e14bc788c43a4848556a5fb16401a04662fc55db9bb201987037', 45 | hashType: 'type', 46 | args: '0x71fd1985b2971a9903e4d8ed0d59e6710166985217ca0681437883837b86162f', 47 | }), 48 | ); 49 | console.log('RUSD xUDT info: ', JSON.stringify(rusdInfo)); 50 | 51 | // const utxoAirdropInfo = await btcService.getRgbppAssetInfoByTypeScript( 52 | // serializeScript({ 53 | // codeHash: '0xf5da9003e31fa9301a3915fe304de9bdb80524b5f0d8fc325fb699317998ee7a', 54 | // hashType: 'type', 55 | // args: '0xa63d308c04b4c075eb1d7d5cac891cf20276e3ddb2ec855fc981c88d8134dbe2', 56 | // }), 57 | // ); 58 | // console.log('UTXO Airdrop xUDT info: ', JSON.stringify(utxoAirdropInfo)); 59 | })(); 60 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/launch/0-rgbpp-token-info.ts: -------------------------------------------------------------------------------- 1 | import { RgbppTokenInfo } from 'rgbpp/ckb'; 2 | 3 | export const RGBPP_TOKEN_INFO: RgbppTokenInfo = { 4 | decimal: 8, 5 | name: 'RGBPP Test Token', 6 | symbol: 'RTT', 7 | }; 8 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/offline/0-rgbpp-token-info.ts: -------------------------------------------------------------------------------- 1 | import { RgbppTokenInfo } from 'rgbpp/ckb'; 2 | 3 | export const RGBPP_TOKEN_INFO: RgbppTokenInfo = { 4 | decimal: 8, 5 | name: 'OK RGBPP Test Token', 6 | symbol: 'OKRTT', 7 | }; 8 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/offline/1-prepare-launch.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { 3 | MAX_FEE, 4 | RgbppTokenInfo, 5 | SECP256K1_WITNESS_LOCK_SIZE, 6 | append0x, 7 | buildRgbppLockArgs, 8 | calculateRgbppCellCapacity, 9 | calculateRgbppTokenInfoCellCapacity, 10 | calculateTransactionFee, 11 | genRgbppLockScript, 12 | getSecp256k1CellDep, 13 | signCkbTransaction, 14 | } from 'rgbpp/ckb'; 15 | import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; 16 | import { 17 | BTC_TESTNET_TYPE, 18 | CKB_PRIVATE_KEY, 19 | ckbAddress, 20 | collector, 21 | initOfflineCkbCollector, 22 | isMainnet, 23 | } from '../../env'; 24 | 25 | const prepareLaunchCell = async ({ 26 | outIndex, 27 | btcTxId, 28 | rgbppTokenInfo, 29 | }: { 30 | outIndex: number; 31 | btcTxId: string; 32 | rgbppTokenInfo: RgbppTokenInfo; 33 | }) => { 34 | const masterLock = addressToScript(ckbAddress); 35 | console.log('ckb address: ', ckbAddress); 36 | 37 | // The capacity required to launch cells is determined by the token info cell capacity, and transaction fee. 38 | const launchCellCapacity = 39 | calculateRgbppCellCapacity() + calculateRgbppTokenInfoCellCapacity(rgbppTokenInfo, isMainnet); 40 | 41 | const { collector: offlineCollector, cells: emptyCells } = await initOfflineCkbCollector([{ lock: masterLock }]); 42 | 43 | const txFee = MAX_FEE; 44 | const { inputs, sumInputsCapacity } = offlineCollector.collectInputs(emptyCells, launchCellCapacity, txFee); 45 | 46 | const outputs: CKBComponents.CellOutput[] = [ 47 | { 48 | lock: genRgbppLockScript(buildRgbppLockArgs(outIndex, btcTxId), isMainnet, BTC_TESTNET_TYPE), 49 | capacity: append0x(launchCellCapacity.toString(16)), 50 | }, 51 | ]; 52 | let changeCapacity = sumInputsCapacity - launchCellCapacity; 53 | outputs.push({ 54 | lock: masterLock, 55 | capacity: append0x(changeCapacity.toString(16)), 56 | }); 57 | const outputsData = ['0x', '0x']; 58 | 59 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 60 | const witnesses = inputs.map((_, index) => (index === 0 ? emptyWitness : '0x')); 61 | 62 | const cellDeps = [getSecp256k1CellDep(isMainnet)]; 63 | 64 | const unsignedTx = { 65 | version: '0x0', 66 | cellDeps, 67 | headerDeps: [], 68 | inputs, 69 | outputs, 70 | outputsData, 71 | witnesses, 72 | }; 73 | 74 | const txSize = getTransactionSize(unsignedTx) + SECP256K1_WITNESS_LOCK_SIZE; 75 | const estimatedTxFee = calculateTransactionFee(txSize); 76 | changeCapacity -= estimatedTxFee; 77 | unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); 78 | 79 | const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx); 80 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 81 | 82 | console.info(`Launch cell has been created and the CKB tx hash ${txHash}`); 83 | }; 84 | 85 | prepareLaunchCell({ 86 | outIndex: 3, 87 | btcTxId: 'd6cbc8c4418cb1c4cab200c60e653ee886fd67d1c839197b1ac73a88a6360473', 88 | rgbppTokenInfo: RGBPP_TOKEN_INFO, 89 | }); 90 | 91 | /* 92 | npx tsx examples/rgbpp/xudt/offline/1-prepare-launch.ts 93 | */ 94 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts: -------------------------------------------------------------------------------- 1 | import { buildBtcTimeCellsSpentTx } from 'rgbpp'; 2 | import { 3 | sendCkbTx, 4 | getBtcTimeLockScript, 5 | btcTxIdAndAfterFromBtcTimeLockArgs, 6 | prepareBtcTimeCellSpentUnsignedTx, 7 | addressToScriptHash, 8 | signCkbTransaction, 9 | } from 'rgbpp/ckb'; 10 | import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../env'; 11 | import { OfflineBtcAssetsDataSource, SpvProofEntry } from 'rgbpp/service'; 12 | 13 | const unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string }) => { 14 | const btcTimeCells = await collector.getCells({ 15 | lock: { 16 | ...getBtcTimeLockScript(isMainnet, BTC_TESTNET_TYPE), 17 | args: btcTimeCellArgs, 18 | }, 19 | isDataMustBeEmpty: false, 20 | }); 21 | if (!btcTimeCells || btcTimeCells.length === 0) { 22 | throw new Error('No btc time cell found'); 23 | } 24 | 25 | const spvProofs: SpvProofEntry[] = []; 26 | for (const btcTimeCell of btcTimeCells) { 27 | const { btcTxId, after } = btcTxIdAndAfterFromBtcTimeLockArgs(btcTimeCell.output.lock.args); 28 | spvProofs.push({ 29 | txid: btcTxId, 30 | confirmations: after, 31 | proof: await btcService.getRgbppSpvProof(btcTxId, after), 32 | }); 33 | } 34 | 35 | const offlineBtcAssetsDataSource = new OfflineBtcAssetsDataSource({ 36 | txs: [], 37 | utxos: [], 38 | rgbppSpvProofs: spvProofs, 39 | }); 40 | 41 | const ckbRawTx: CKBComponents.RawTransaction = await buildBtcTimeCellsSpentTx({ 42 | btcTimeCells, 43 | btcAssetsApi: offlineBtcAssetsDataSource, 44 | isMainnet, 45 | btcTestnetType: BTC_TESTNET_TYPE, 46 | }); 47 | 48 | const { ckbRawTx: unsignedTx, inputCells } = await prepareBtcTimeCellSpentUnsignedTx({ 49 | collector, 50 | masterCkbAddress: ckbAddress, 51 | ckbRawTx, 52 | isMainnet, 53 | }); 54 | 55 | const keyMap = new Map(); 56 | keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY); 57 | const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true); 58 | 59 | const txHash = await sendCkbTx({ collector, signedTx }); 60 | console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`); 61 | }; 62 | 63 | // The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction 64 | unlockBtcTimeCell({ 65 | btcTimeCellArgs: 66 | '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c06000000f2aa85670171e5727da232e041e194508b0beced672e731f638ff84abf8d5ff8', 67 | }); 68 | 69 | /* 70 | npx tsx examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts 71 | */ 72 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript, signCkbTransaction } from 'rgbpp/ckb'; 4 | import { 5 | CKB_PRIVATE_KEY, 6 | isMainnet, 7 | collector, 8 | ckbAddress, 9 | BTC_TESTNET_TYPE, 10 | initOfflineCkbCollector, 11 | vendorCellDeps, 12 | } from '../../env'; 13 | 14 | interface LeapToBtcParams { 15 | outIndex: number; 16 | btcTxId: string; 17 | xudtTypeArgs: string; 18 | transferAmount: bigint; 19 | } 20 | 21 | const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmount }: LeapToBtcParams) => { 22 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 23 | 24 | // Warning: Please replace with your real xUDT type script here 25 | const xudtType: CKBComponents.Script = { 26 | ...getXudtTypeScript(isMainnet), 27 | args: xudtTypeArgs, 28 | }; 29 | 30 | const { collector: offlineCollector } = await initOfflineCkbCollector([ 31 | { lock: addressToScript(ckbAddress), type: xudtType }, 32 | { lock: addressToScript(ckbAddress) }, 33 | ]); 34 | 35 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 36 | collector: offlineCollector, 37 | fromCkbAddress: ckbAddress, 38 | toRgbppLockArgs, 39 | xudtTypeBytes: serializeScript(xudtType), 40 | transferAmount, 41 | btcTestnetType: BTC_TESTNET_TYPE, 42 | vendorCellDeps, 43 | }); 44 | 45 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 46 | const unsignedTx: CKBComponents.RawTransactionToSign = { 47 | ...ckbRawTx, 48 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 49 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 50 | }; 51 | 52 | const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx); 53 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 54 | console.info(`Rgbpp asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); 55 | }; 56 | 57 | // Please use your real BTC UTXO information on the BTC Testnet 58 | // BTC Testnet3: https://mempool.space/testnet 59 | // BTC Signet: https://mempool.space/signet 60 | leapFromCkbToBtc({ 61 | outIndex: 0, 62 | btcTxId: 'c1db31abe6bab345b5d5ab4a19c8f34c8cfe23efa4ec6bfa7b05c8e7b4f965b8', 63 | // Please use your own RGB++ xudt asset's xudtTypeArgs 64 | xudtTypeArgs: '0xe402314a4b31223afe00a9c69c0b872863b990219525e1547ec05d9d88434b24', 65 | transferAmount: BigInt(10_0000_0000), 66 | }); 67 | 68 | /* 69 | npx tsx examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts 70 | */ 71 | -------------------------------------------------------------------------------- /examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, signCkbTransaction } from 'rgbpp/ckb'; 4 | import { 5 | CKB_PRIVATE_KEY, 6 | isMainnet, 7 | collector, 8 | ckbAddress, 9 | BTC_TESTNET_TYPE, 10 | initOfflineCkbCollector, 11 | vendorCellDeps, 12 | } from '../../../env'; 13 | 14 | interface LeapToBtcParams { 15 | outIndex: number; 16 | btcTxId: string; 17 | transferAmount: bigint; 18 | compatibleXudtTypeScript: CKBComponents.Script; 19 | } 20 | 21 | const leapRusdFromCkbToBtc = async ({ 22 | outIndex, 23 | btcTxId, 24 | transferAmount, 25 | compatibleXudtTypeScript, 26 | }: LeapToBtcParams) => { 27 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 28 | 29 | const { collector: offlineCollector } = await initOfflineCkbCollector([ 30 | { lock: addressToScript(ckbAddress), type: compatibleXudtTypeScript }, 31 | { lock: addressToScript(ckbAddress) }, 32 | ]); 33 | 34 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 35 | collector: offlineCollector, 36 | fromCkbAddress: ckbAddress, 37 | toRgbppLockArgs, 38 | xudtTypeBytes: serializeScript(compatibleXudtTypeScript), 39 | transferAmount, 40 | btcTestnetType: BTC_TESTNET_TYPE, 41 | vendorCellDeps, 42 | }); 43 | 44 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 45 | const unsignedTx: CKBComponents.RawTransactionToSign = { 46 | ...ckbRawTx, 47 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 48 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 49 | }; 50 | 51 | const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx); 52 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 53 | console.info(`Rgbpp compatible xUDT asset has been leaped from CKB to BTC and CKB tx hash is ${txHash}`); 54 | }; 55 | 56 | // Please use your real BTC UTXO information on the BTC Testnet 57 | // BTC Testnet3: https://mempool.space/testnet 58 | // BTC Signet: https://mempool.space/signet 59 | leapRusdFromCkbToBtc({ 60 | outIndex: 2, 61 | btcTxId: '4239d2f9fe566513b0604e4dfe10f3b85b6bebe25096cf426559a89c87c68d1a', 62 | compatibleXudtTypeScript: { 63 | codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 64 | hashType: 'type', 65 | args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 66 | }, 67 | transferAmount: BigInt(200_0000), 68 | }); 69 | 70 | /* 71 | npx tsx examples/rgbpp/xudt/offline/compatible-xudt/1-ckb-leap-btc.ts 72 | */ 73 | -------------------------------------------------------------------------------- /examples/xudt-on-ckb/.env.example: -------------------------------------------------------------------------------- 1 | # True for CKB Mainnet and false for CKB Testnet, the default value is false 2 | IS_MAINNET=false 3 | 4 | # The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix 5 | CKB_SECP256K1_PRIVATE_KEY=0x-private-key 6 | 7 | # CKB node url which should match IS_MAINNET 8 | CKB_NODE_URL=https://testnet.ckb.dev/rpc 9 | 10 | # CKB indexer url which should match IS_MAINNET 11 | CKB_INDEXER_URL=https://testnet.ckb.dev/indexer 12 | -------------------------------------------------------------------------------- /examples/xudt-on-ckb/README.md: -------------------------------------------------------------------------------- 1 | # xUDT on CKB Examples 2 | 3 | The examples for xUDT issuance, mint and transfer on CKB 4 | 5 | ## How to Start 6 | 7 | Copy the `.env.example` file to `.env`: 8 | 9 | ```shell 10 | cd examples/xudt && cp .env.example .env 11 | ``` 12 | 13 | Update the configuration values: 14 | 15 | ```yaml 16 | # True for CKB Mainnet and false for CKB Testnet, the default value is false 17 | IS_MAINNET=false 18 | 19 | # The CKB secp256k1 private key whose format is 32bytes hex string with 0x prefix 20 | CKB_SECP256K1_PRIVATE_KEY=0x-private-key 21 | 22 | # CKB node url which should match IS_MAINNET 23 | CKB_NODE_URL=https://testnet.ckb.dev/rpc 24 | 25 | # CKB indexer url which should match IS_MAINNET 26 | CKB_INDEXER_URL=https://testnet.ckb.dev/indexer 27 | 28 | ``` 29 | 30 | ### Issue xUDT on CKB 31 | 32 | ```shell 33 | npx tsx 1-issue-xudt.ts 34 | ``` 35 | 36 | ### Mint/Transfer xUDT on CKB 37 | 38 | You can use this command to mint or transfer xUDT assets 39 | 40 | ```shell 41 | npx tsx 2-transfer-xudt.ts 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/xudt-on-ckb/env.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blake160, 3 | bytesToHex, 4 | privateKeyToPublicKey, 5 | scriptToAddress, 6 | systemScripts, 7 | } from '@nervosnetwork/ckb-sdk-utils'; 8 | import { Collector } from 'rgbpp/ckb'; 9 | import dotenv from 'dotenv'; 10 | 11 | dotenv.config({ path: __dirname + '/.env' }); 12 | 13 | export const isMainnet = process.env.IS_MAINNET === 'true' ? true : false; 14 | 15 | export const collector = new Collector({ 16 | ckbNodeUrl: process.env.CKB_NODE_URL!, 17 | ckbIndexerUrl: process.env.CKB_INDEXER_URL!, 18 | }); 19 | export const CKB_PRIVATE_KEY = process.env.CKB_SECP256K1_PRIVATE_KEY!; 20 | const secp256k1Lock: CKBComponents.Script = { 21 | ...systemScripts.SECP256K1_BLAKE160, 22 | args: bytesToHex(blake160(privateKeyToPublicKey(CKB_PRIVATE_KEY))), 23 | }; 24 | export const ckbAddress = scriptToAddress(secp256k1Lock, isMainnet); 25 | -------------------------------------------------------------------------------- /examples/xudt-on-ckb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xudt-on-ckb-examples", 3 | "version": "0.1.0", 4 | "description": "Examples used for xUDT on CKB assets issuance and transfer", 5 | "private": true, 6 | "type": "commonjs", 7 | "scripts": { 8 | "format": "prettier --write '**/*.ts'", 9 | "lint": "tsc && eslint . && prettier --check '**/*.ts'", 10 | "lint:fix": "tsc && eslint --fix --ext .ts . && prettier --write '**/*.ts'" 11 | }, 12 | "dependencies": { 13 | "@nervosnetwork/ckb-sdk-utils": "0.109.5", 14 | "rgbpp": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "dotenv": "^16.4.5", 18 | "@types/dotenv": "^8.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/xudt-on-ckb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "esnext"], 5 | "module": "ES2015", 6 | "composite": false, 7 | "resolveJsonModule": true, 8 | "strictNullChecks": true, 9 | "noEmit": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "inlineSources": false, 15 | "isolatedModules": true, 16 | "moduleResolution": "Bundler", 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "preserveWatchOutput": true, 20 | "skipLibCheck": true, 21 | "strict": true 22 | }, 23 | "exclude": ["node_modules"] 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgbpp-sdk", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "prepare": "husky", 9 | "build": "pnpm run --r --filter \"./{packages,apps,examples,tests}/**\" build", 10 | "build:packages": "pnpm run --r --filter \"./packages/**\" build", 11 | "test:packages": "pnpm run --r --filter \"./packages/**\" test", 12 | "test:service": "pnpm run --r --filter=./apps/service test", 13 | "dev:service": "pnpm run --r --filter=./apps/service dev", 14 | "lint": "eslint {packages,apps,examples,tests}/**/*.ts && prettier --check '{packages,apps,examples,tests}/**/*.ts'", 15 | "lint:fix": "eslint --fix {packages,apps,examples,tests}/**/*.ts", 16 | "format": "prettier --write '{packages,apps,examples,tests}/**/*.{js,jsx,ts,tsx}'", 17 | "clean:dependencies": "pnpm clean:sub-dependencies && rimraf node_modules", 18 | "clean:sub-dependencies": "rimraf packages/**/node_modules apps/**/node_modules", 19 | "release:packages": "pnpm run build:packages && changeset publish" 20 | }, 21 | "devDependencies": { 22 | "@changesets/cli": "^2.27.1", 23 | "@changesets/get-github-info": "^0.6.0", 24 | "@changesets/types": "^6.0.0", 25 | "@types/lodash": "^4.17.0", 26 | "@typescript-eslint/eslint-plugin": "^7.8.0", 27 | "@typescript-eslint/parser": "^7.8.0", 28 | "eslint": "^8.56.0", 29 | "husky": "^9.1.7", 30 | "lint-staged": "^15.3.0", 31 | "prettier": "^3.4.2", 32 | "tsx": "4.16.3", 33 | "tsup": "^8.3.5", 34 | "typescript": "5.4.3", 35 | "vitest": "2.1.9" 36 | }, 37 | "lint-staged": { 38 | "{packages,apps,examples,tests}/**/*.{js,jsx,ts,tsx}": [ 39 | "eslint --fix", 40 | "prettier --ignore-unknown --write" 41 | ] 42 | }, 43 | "packageManager": "pnpm@9.0.0", 44 | "engines": { 45 | "node": ">=21.0.0", 46 | "pnpm": ">=9.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/btc/.env.example: -------------------------------------------------------------------------------- 1 | VITE_BTC_SERVICE_URL= # URL of the service 2 | VITE_BTC_SERVICE_TOKEN= # JWT token to access the service 3 | VITE_BTC_SERVICE_ORIGIN= # URL representing your token's domain 4 | -------------------------------------------------------------------------------- /packages/btc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rgbpp-sdk/btc", 3 | "version": "0.7.0", 4 | "scripts": { 5 | "test": "vitest", 6 | "build": "tsup", 7 | "lint": "tsc && eslint '{src,tests}/**/*.{js,ts}' && prettier --check '{src,tests}/**/*.{js,ts}'", 8 | "lint:fix": "tsc && eslint --fix '{src,tests}/**/*.{js,ts}' && prettier --write '{src,tests}/**/*.{js,ts}'" 9 | }, 10 | "sideEffects": false, 11 | "main": "./dist/index.js", 12 | "types": "./dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": { 16 | "types": "./dist/index.d.mts", 17 | "default": "./dist/index.mjs" 18 | }, 19 | "require": { 20 | "types": "./dist/index.d.js", 21 | "default": "./dist/index.js" 22 | } 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "files": [ 27 | "src", 28 | "dist" 29 | ], 30 | "dependencies": { 31 | "@bitcoinerlab/secp256k1": "^1.1.1", 32 | "@ckb-lumos/codec": "0.22.2", 33 | "@nervosnetwork/ckb-types": "0.109.5", 34 | "@rgbpp-sdk/ckb": "workspace:^", 35 | "@rgbpp-sdk/service": "workspace:^", 36 | "bip32": "^4.0.0", 37 | "bitcoinjs-lib": "^6.1.5", 38 | "ecpair": "^2.1.0", 39 | "lodash": "^4.17.21", 40 | "p-limit": "^3.1.0" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/btc/src/api/sendBtc.ts: -------------------------------------------------------------------------------- 1 | import { bitcoin } from '../bitcoin'; 2 | import { DataSource } from '../query/source'; 3 | import { InitOutput, TxBuilder } from '../transaction/build'; 4 | import { createSendUtxosBuilder } from './sendUtxos'; 5 | 6 | export interface SendBtcProps { 7 | from: string; 8 | tos: InitOutput[]; 9 | source: DataSource; 10 | feeRate?: number; 11 | fromPubkey?: string; 12 | changeAddress?: string; 13 | minUtxoSatoshi?: number; 14 | onlyConfirmedUtxos?: boolean; 15 | } 16 | 17 | export async function createSendBtcBuilder(props: SendBtcProps): Promise<{ 18 | builder: TxBuilder; 19 | fee: number; 20 | feeRate: number; 21 | changeIndex: number; 22 | }> { 23 | // By default, all outputs in the sendBtc() API are fixed 24 | const outputs = props.tos.map((to) => ({ 25 | fixed: true, 26 | ...to, 27 | })); 28 | 29 | return await createSendUtxosBuilder({ 30 | inputs: [], 31 | outputs: outputs, 32 | from: props.from, 33 | source: props.source, 34 | feeRate: props.feeRate, 35 | fromPubkey: props.fromPubkey, 36 | changeAddress: props.changeAddress, 37 | minUtxoSatoshi: props.minUtxoSatoshi, 38 | onlyConfirmedUtxos: props.onlyConfirmedUtxos, 39 | }); 40 | } 41 | 42 | export async function sendBtc(props: SendBtcProps): Promise { 43 | const { builder } = await createSendBtcBuilder(props); 44 | return builder.toPsbt(); 45 | } 46 | -------------------------------------------------------------------------------- /packages/btc/src/api/sendUtxos.ts: -------------------------------------------------------------------------------- 1 | import { bitcoin } from '../bitcoin'; 2 | import { DataSource } from '../query/source'; 3 | import { TxBuilder, InitOutput } from '../transaction/build'; 4 | import { BaseOutput, Utxo, prepareUtxoInputs } from '../transaction/utxo'; 5 | import { AddressToPubkeyMap, addAddressToPubkeyMap } from '../address'; 6 | import { TxBuildError } from '../error'; 7 | 8 | export interface SendUtxosProps { 9 | inputs: Utxo[]; 10 | outputs: InitOutput[]; 11 | source: DataSource; 12 | from: string; 13 | feeRate?: number; 14 | fromPubkey?: string; 15 | changeAddress?: string; 16 | minUtxoSatoshi?: number; 17 | onlyConfirmedUtxos?: boolean; 18 | excludeUtxos?: BaseOutput[]; 19 | 20 | // EXPERIMENTAL: the below props are unstable and can be altered at any time 21 | skipInputsValidation?: boolean; 22 | pubkeyMap?: AddressToPubkeyMap; 23 | } 24 | 25 | export async function createSendUtxosBuilder(props: SendUtxosProps): Promise<{ 26 | builder: TxBuilder; 27 | fee: number; 28 | feeRate: number; 29 | changeIndex: number; 30 | }> { 31 | const tx = new TxBuilder({ 32 | source: props.source, 33 | feeRate: props.feeRate, 34 | minUtxoSatoshi: props.minUtxoSatoshi, 35 | onlyConfirmedUtxos: props.onlyConfirmedUtxos, 36 | }); 37 | 38 | try { 39 | // Prepare the UTXO inputs: 40 | // 1. Fill pubkey for each P2TR UTXO, and throw if the corresponding pubkey is not found 41 | // 2. Throw if unconfirmed UTXOs are found (if onlyConfirmedUtxos == true && skipInputsValidation == false) 42 | const pubkeyMap = addAddressToPubkeyMap(props.pubkeyMap ?? {}, props.from, props.fromPubkey); 43 | const inputs = await prepareUtxoInputs({ 44 | utxos: props.inputs, 45 | source: props.source, 46 | requireConfirmed: props.onlyConfirmedUtxos && !props.skipInputsValidation, 47 | requirePubkey: true, 48 | pubkeyMap, 49 | }); 50 | 51 | tx.addInputs(inputs); 52 | tx.addOutputs(props.outputs); 53 | 54 | const paid = await tx.payFee({ 55 | address: props.from, 56 | publicKey: pubkeyMap[props.from], 57 | changeAddress: props.changeAddress, 58 | excludeUtxos: props.excludeUtxos, 59 | }); 60 | 61 | return { 62 | builder: tx, 63 | fee: paid.fee, 64 | feeRate: paid.feeRate, 65 | changeIndex: paid.changeIndex, 66 | }; 67 | } catch (e) { 68 | // When caught TxBuildError, add TxBuilder as the context 69 | if (e instanceof TxBuildError) { 70 | e.setContext({ tx }); 71 | } 72 | 73 | throw e; 74 | } 75 | } 76 | 77 | export async function sendUtxos(props: SendUtxosProps): Promise { 78 | const { builder } = await createSendUtxosBuilder(props); 79 | return builder.toPsbt(); 80 | } 81 | -------------------------------------------------------------------------------- /packages/btc/src/bitcoin.ts: -------------------------------------------------------------------------------- 1 | import ecc from '@bitcoinerlab/secp256k1'; 2 | import * as bitcoin from 'bitcoinjs-lib'; 3 | import { ECPairFactory, ECPairInterface } from 'ecpair'; 4 | import { isTaprootInput } from 'bitcoinjs-lib/src/psbt/bip371.js'; 5 | import { isP2TR, isP2WPKH, isP2PKH } from 'bitcoinjs-lib/src/psbt/psbtutils.js'; 6 | 7 | bitcoin.initEccLib(ecc); 8 | 9 | const ECPair = ECPairFactory(ecc); 10 | 11 | export type { ECPairInterface }; 12 | export { ecc, ECPair, bitcoin, isP2TR, isP2PKH, isP2WPKH, isTaprootInput }; 13 | -------------------------------------------------------------------------------- /packages/btc/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preset/types'; 2 | export * from './preset/config'; 3 | export * from './preset/network'; 4 | 5 | export * from './utils'; 6 | export * from './error'; 7 | export * from './bitcoin'; 8 | export * from './address'; 9 | export * from './script'; 10 | 11 | export * from './query/source'; 12 | 13 | export * from './transaction/build'; 14 | export * from './transaction/embed'; 15 | export * from './transaction/utxo'; 16 | export * from './transaction/fee'; 17 | 18 | export * from './api/sendBtc'; 19 | export * from './api/sendRbf'; 20 | export * from './api/sendUtxos'; 21 | export * from './api/sendRgbppUtxos'; 22 | -------------------------------------------------------------------------------- /packages/btc/src/preset/config.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep.js'; 2 | import { bitcoin } from '../bitcoin'; 3 | import { ErrorCodes, TxBuildError } from '../error'; 4 | import { NetworkType, RgbppBtcConfig } from './types'; 5 | 6 | const defaultConfigs: Record<'testnet' | 'mainnet', RgbppBtcConfig> = { 7 | testnet: { 8 | feeRate: 1, 9 | btcUtxoDustLimit: 1000, 10 | rgbppUtxoDustLimit: 546, 11 | network: bitcoin.networks.testnet, 12 | networkType: NetworkType.TESTNET, 13 | }, 14 | mainnet: { 15 | feeRate: 20, 16 | btcUtxoDustLimit: 10000, 17 | rgbppUtxoDustLimit: 546, 18 | network: bitcoin.networks.bitcoin, 19 | networkType: NetworkType.MAINNET, 20 | }, 21 | }; 22 | 23 | /** 24 | * Get RgbppBtcConfig by a network type. 25 | * If the network type is "REGTEST", it throws an unsupported network error. 26 | */ 27 | export function networkTypeToConfig(networkType: NetworkType): RgbppBtcConfig { 28 | if (networkType === NetworkType.TESTNET) { 29 | return cloneDeep(defaultConfigs.testnet); 30 | } 31 | if (networkType === NetworkType.MAINNET) { 32 | return cloneDeep(defaultConfigs.mainnet); 33 | } 34 | 35 | throw new TxBuildError(ErrorCodes.UNSUPPORTED_NETWORK_TYPE); 36 | } 37 | 38 | /** 39 | * Get RgbppBtcConfig by a bitcoinjs-lib network object. 40 | * If the network is not recognized, it throws an unsupported network error. 41 | */ 42 | export function networkToConfig(network: bitcoin.Network): RgbppBtcConfig { 43 | if (network.bech32 == bitcoin.networks.bitcoin.bech32) { 44 | return cloneDeep(defaultConfigs.mainnet); 45 | } 46 | if (network.bech32 == bitcoin.networks.testnet.bech32) { 47 | return cloneDeep(defaultConfigs.testnet); 48 | } 49 | 50 | throw new TxBuildError(ErrorCodes.UNSUPPORTED_NETWORK_TYPE); 51 | } 52 | -------------------------------------------------------------------------------- /packages/btc/src/preset/network.ts: -------------------------------------------------------------------------------- 1 | import { bitcoin } from '../bitcoin'; 2 | import { NetworkType } from './types'; 3 | import { networkToConfig, networkTypeToConfig } from './config'; 4 | 5 | /** 6 | * Convert network type to bitcoinjs-lib network. 7 | */ 8 | export function networkTypeToNetwork(networkType: NetworkType): bitcoin.Network { 9 | const config = networkTypeToConfig(networkType); 10 | return config.network; 11 | } 12 | 13 | /** 14 | * Convert bitcoinjs-lib network to network type. 15 | */ 16 | export function networkToNetworkType(network: bitcoin.Network): NetworkType { 17 | const config = networkToConfig(network); 18 | return config.networkType; 19 | } 20 | -------------------------------------------------------------------------------- /packages/btc/src/preset/types.ts: -------------------------------------------------------------------------------- 1 | import { bitcoin } from '../bitcoin'; 2 | 3 | export enum NetworkType { 4 | MAINNET, 5 | TESTNET, 6 | REGTEST, // deprecated 7 | } 8 | 9 | export interface RgbppBtcConfig { 10 | /** 11 | * The minimum fee rate that can be declared in a BTC transaction, in satoshi per byte. 12 | * Note this value can be different in different networks. 13 | */ 14 | feeRate: number; 15 | /** 16 | * The minimum satoshi amount that can be declared in a BTC_UTXO. 17 | * BTC_UTXOs with satoshi below this constant are considered dust and will not be collected/created. 18 | * Officially, this constant should be 1,0000, but currently we are using 1,000 for testing purposes. 19 | */ 20 | btcUtxoDustLimit: number; 21 | /** 22 | * The minimum satoshi amount that can be declared in a RGBPP_UTXO. 23 | * RGBPP_UTXOs with satoshi below this constant are considered dust and will not be created. 24 | */ 25 | rgbppUtxoDustLimit: number; 26 | /** 27 | * The bitcoin-js lib predefined network object. 28 | * It contains crucial data to define what network we're working on. 29 | */ 30 | network: bitcoin.Network; 31 | /** 32 | * The network type on RgbppBtc. 33 | * Note the "REGTEST" network is a deprecated network type, so you shouldn't use it. 34 | */ 35 | networkType: NetworkType; 36 | } 37 | -------------------------------------------------------------------------------- /packages/btc/src/query/cache.ts: -------------------------------------------------------------------------------- 1 | import { Utxo } from '../transaction/utxo'; 2 | 3 | export class DataCache { 4 | private utxos: Map; // Map 5 | 6 | constructor() { 7 | this.utxos = new Map(); 8 | } 9 | 10 | setUtxos(key: string, utxos: Utxo[]) { 11 | this.utxos.set(key, utxos); 12 | } 13 | getUtxos(key: string): Utxo[] | undefined { 14 | return this.utxos.get(key); 15 | } 16 | cleanUtxos(key: string) { 17 | if (this.utxos.has(key)) { 18 | this.utxos.delete(key); 19 | } 20 | } 21 | async optionalCacheUtxos(props: { key?: string; getter: () => Promise | Utxo[] }): Promise { 22 | if (props.key && this.utxos.has(props.key)) { 23 | return this.getUtxos(props.key) as Utxo[]; 24 | } 25 | 26 | const utxos = await props.getter(); 27 | if (props.key) { 28 | this.setUtxos(props.key, utxos); 29 | } 30 | 31 | return utxos; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/btc/src/script.ts: -------------------------------------------------------------------------------- 1 | import { isP2TR, isP2WPKH } from './bitcoin'; 2 | 3 | export function isP2wpkhScript(script: Buffer | string): boolean { 4 | const buffer = typeof script === 'string' ? Buffer.from(script, 'hex') : script; 5 | return isP2WPKH(buffer); 6 | } 7 | 8 | export function isP2trScript(script: Buffer | string): boolean { 9 | const buffer = typeof script === 'string' ? Buffer.from(script, 'hex') : script; 10 | return isP2TR(buffer); 11 | } 12 | -------------------------------------------------------------------------------- /packages/btc/src/transaction/embed.ts: -------------------------------------------------------------------------------- 1 | import { remove0x } from '../utils'; 2 | import { bitcoin } from '../bitcoin'; 3 | import { ErrorCodes, TxBuildError } from '../error'; 4 | 5 | /** 6 | * Convert data to OP_RETURN script pubkey. 7 | * The data size should be ranged in 1 to 80 bytes. 8 | * 9 | * @example 10 | * const data = Buffer.from('01020304', 'hex'); 11 | * const scriptPk = dataToOpReturnScriptPubkey(data); // 12 | * const scriptPkHex = scriptPk.toString('hex'); // 6a0401020304 13 | */ 14 | export function dataToOpReturnScriptPubkey(data: Buffer | string): Buffer { 15 | if (typeof data === 'string') { 16 | data = Buffer.from(remove0x(data), 'hex'); 17 | } 18 | 19 | const payment = bitcoin.payments.embed({ data: [data] }); 20 | return payment.output!; 21 | } 22 | 23 | /** 24 | * Get data from a OP_RETURN script pubkey. 25 | * 26 | * @example 27 | * const scriptPk = Buffer.from('6a0401020304', 'hex'); 28 | * const data = opReturnScriptPubKeyToData(scriptPk); // 29 | * const hex = data.toString('hex'); // 01020304 30 | */ 31 | export function opReturnScriptPubKeyToData(script: Buffer): Buffer { 32 | if (!isOpReturnScriptPubkey(script)) { 33 | throw TxBuildError.withComment(ErrorCodes.UNSUPPORTED_OP_RETURN_SCRIPT, script.toString('hex')); 34 | } 35 | 36 | const res = bitcoin.script.decompile(script)!; 37 | return res[1] as Buffer; 38 | } 39 | 40 | /** 41 | * Check if a script pubkey is an OP_RETURN script. 42 | * 43 | * A valid OP_RETURN script should have the following structure: 44 | * - 45 | * - 46 | * 47 | * @example 48 | * // 49 | * isOpReturnScriptPubkey(Buffer.from('6a0401020304', 'hex')); // true 50 | * // 51 | * isOpReturnScriptPubkey(Buffer.from('6a4c0f746573742d636f6d6d69746d656e74', 'hex')); // true 52 | * // 53 | * isOpReturnScriptPubkey(Buffer.from('6a4c', 'hex')); // false 54 | * // 55 | * isOpReturnScriptPubkey(Buffer.from('6a01', 'hex')); // false 56 | * // ... (not an OP_RETURN script) 57 | * isOpReturnScriptPubkey(Buffer.from('76a914a802fc56c704ce87c42d7c92eb75e7896bdc41e788ac', 'hex')); // false 58 | */ 59 | export function isOpReturnScriptPubkey(script: Buffer): boolean { 60 | const scripts = bitcoin.script.decompile(script); 61 | if (!scripts || scripts.length !== 2) { 62 | return false; 63 | } 64 | 65 | const [op, data] = scripts!; 66 | // OP_RETURN opcode is 0x6a in hex or 106 in integer 67 | if (op !== bitcoin.opcodes.OP_RETURN) { 68 | return false; 69 | } 70 | // Standard OP_RETURN data size is up to 80 bytes 71 | if (!(data instanceof Buffer) || data.byteLength < 1 || data.byteLength > 80) { 72 | return false; 73 | } 74 | 75 | // No false condition matched, it's an OP_RETURN script 76 | return true; 77 | } 78 | -------------------------------------------------------------------------------- /packages/btc/tests/Address.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { bitcoin, ECPair, toXOnly } from '../src'; 3 | import { network } from './shared/env'; 4 | 5 | describe('Address', () => { 6 | it('Create SegWit (P2WPKH) address', () => { 7 | const keyPair = ECPair.fromPrivateKey( 8 | Buffer.from('8d3c23d340ac0841e6c3b58a9bbccb9a28e94ab444f972cff35736fa2fcf9f3f', 'hex'), 9 | { network }, 10 | ); 11 | 12 | expect(keyPair.publicKey.toString('hex')).toEqual( 13 | '037dff8ff2e0bd222690d785f9277e0c4800fc88b0fad522f1442f21a8226253ce', 14 | ); 15 | 16 | const { address } = bitcoin.payments.p2wpkh({ 17 | pubkey: keyPair.publicKey, 18 | network, 19 | }); 20 | 21 | expect(address).toEqual('tb1qm06rvrq8jyyckzc5v709u7qpthel9j4d9f7nh3'); 22 | }); 23 | it('Create Taproot (P2TR) address', () => { 24 | const keyPair = ECPair.fromPrivateKey( 25 | Buffer.from('8d3c23d340ac0841e6c3b58a9bbccb9a28e94ab444f972cff35736fa2fcf9f3f', 'hex'), 26 | { network }, 27 | ); 28 | 29 | const tapInternalPubkey = toXOnly(keyPair.publicKey); 30 | expect(tapInternalPubkey.toString('hex')).toEqual( 31 | '7dff8ff2e0bd222690d785f9277e0c4800fc88b0fad522f1442f21a8226253ce', 32 | ); 33 | 34 | const p2tr = bitcoin.payments.p2tr({ 35 | internalPubkey: tapInternalPubkey, 36 | network, 37 | }); 38 | 39 | expect(p2tr.pubkey).toBeDefined(); 40 | expect(p2tr.pubkey!.toString('hex')).toEqual('fff71aebedf8ac5a3041f32a7a05bde104b8f523371be6aa63c6f9c00cc05809'); 41 | 42 | expect(p2tr.output).toBeDefined(); 43 | expect(p2tr.output!.toString('hex')).toEqual( 44 | '5120fff71aebedf8ac5a3041f32a7a05bde104b8f523371be6aa63c6f9c00cc05809', 45 | ); 46 | 47 | expect(p2tr.address).toBeDefined(); 48 | expect(p2tr.address).toEqual('tb1pllm346ldlzk95vzp7v485pdauyzt3afrxud7d2nrcmuuqrxqtqysepxvl0'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/btc/tests/DataSource.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { source } from './shared/env'; 3 | import { ErrorCodes } from '../src'; 4 | 5 | describe('DataSource', { retry: 3 }, () => { 6 | it('Get OP_RETURN output via getOutput()', async () => { 7 | const output = await source.getOutput('70b250e2a3cc7a33b47f7a4e94e41e1ee2501ce73b393d824db1dd4c872c5348', 0); 8 | 9 | expect(output).toBeDefined(); 10 | expect(output.txid).toBeTypeOf('string'); 11 | expect(output.vout).toBeTypeOf('number'); 12 | expect(output.value).toBeTypeOf('number'); 13 | expect(output.scriptPk).toBeTypeOf('string'); 14 | expect(output).not.toHaveProperty('address'); 15 | expect(output).not.toHaveProperty('addressType'); 16 | }); 17 | it('Get OP_RETURN output via getUtxo()', async () => { 18 | await expect(() => 19 | source.getUtxo('70b250e2a3cc7a33b47f7a4e94e41e1ee2501ce73b393d824db1dd4c872c5348', 0), 20 | ).rejects.toHaveProperty('code', ErrorCodes.UNSPENDABLE_OUTPUT); 21 | }); 22 | describe('collectSatoshi()', () => { 23 | const address = 'tb1qn5kgn70tpwsw4nuxrch8l7qa9nqn4fahxgzjg6'; 24 | const totalSatoshi = 546 + 2000 + 1500; 25 | const nonRgbppSatoshi = 1500; 26 | const nonRgbppUtxo = 1; 27 | const totalUtxo = 3; 28 | 29 | it('onlyNonRgbppUtxos = false', async () => { 30 | const c = await source.collectSatoshi({ 31 | address, 32 | targetAmount: totalSatoshi, 33 | onlyNonRgbppUtxos: false, 34 | }); 35 | expect(c.utxos).toHaveLength(totalUtxo); 36 | expect(c.satoshi).toEqual(totalSatoshi); 37 | }); 38 | it('onlyNonRgbppUtxos = true', async () => { 39 | const c = await source.collectSatoshi({ 40 | address, 41 | targetAmount: nonRgbppSatoshi, 42 | onlyNonRgbppUtxos: true, 43 | }); 44 | expect(c.utxos).toHaveLength(nonRgbppUtxo); 45 | expect(c.satoshi).toEqual(nonRgbppSatoshi); 46 | }); 47 | it('Try onlyNonRgbppUtxos = true and targetAmount = totalSatoshi', async () => { 48 | await expect(() => 49 | source.collectSatoshi({ 50 | address, 51 | targetAmount: totalSatoshi, 52 | onlyNonRgbppUtxos: true, 53 | }), 54 | ).rejects.toThrowError(); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/btc/tests/Embed.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { dataToOpReturnScriptPubkey, opReturnScriptPubKeyToData } from '../src'; 3 | 4 | describe('Embed', () => { 5 | it('Encode UTF-8 data to OP_RETURN script pubkey', () => { 6 | const data = Buffer.from('test-commitment', 'utf-8'); 7 | const script = dataToOpReturnScriptPubkey(data); 8 | 9 | expect(script.toString('hex')).toEqual('6a0f746573742d636f6d6d69746d656e74'); 10 | }); 11 | it('Decode UTF-8 data from OP_RETURN script pubkey', () => { 12 | const script = Buffer.from('6a0f746573742d636f6d6d69746d656e74', 'hex'); 13 | const data = opReturnScriptPubKeyToData(script); 14 | 15 | expect(data.toString('utf-8')).toEqual('test-commitment'); 16 | }); 17 | 18 | it('Decode 32-byte hex from OP_RETURN script pubkey', () => { 19 | const hex = '00'.repeat(32); 20 | const script = Buffer.from('6a20' + hex, 'hex'); 21 | const data = opReturnScriptPubKeyToData(script); 22 | 23 | expect(data.toString('hex')).toEqual(hex); 24 | }); 25 | 26 | it('Encode 80-byte data to OP_RETURN script pubkey', () => { 27 | const hex = '00'.repeat(80); 28 | const data = Buffer.from(hex, 'hex'); 29 | const script = dataToOpReturnScriptPubkey(data); 30 | 31 | expect(script.toString('hex')).toEqual('6a4c50' + hex); 32 | }); 33 | it('Decode 80-byte hex from OP_RETURN script pubkey', () => { 34 | const hex = '00'.repeat(80); 35 | const script = Buffer.from('6a4c50' + hex, 'hex'); 36 | const data = opReturnScriptPubKeyToData(script); 37 | 38 | expect(data.toString('hex')).toEqual(hex); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/btc/tests/Network.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { bitcoin, NetworkType, networkTypeToConfig, networkToConfig } from '../src'; 3 | import { networkToNetworkType, networkTypeToNetwork } from '../src'; 4 | 5 | describe('Network', () => { 6 | it('networkTypeToConfig()', () => { 7 | const testnet = networkTypeToConfig(NetworkType.TESTNET); 8 | expect(testnet).toBeDefined(); 9 | expect(testnet.networkType).toBe(NetworkType.TESTNET); 10 | expect(testnet.network).toEqual(bitcoin.networks.testnet); 11 | 12 | const mainnet = networkTypeToConfig(NetworkType.MAINNET); 13 | expect(mainnet).toBeDefined(); 14 | expect(mainnet.networkType).toBe(NetworkType.MAINNET); 15 | expect(mainnet.network).toEqual(bitcoin.networks.bitcoin); 16 | 17 | expect(() => networkTypeToConfig(NetworkType.REGTEST)).toThrow(); 18 | }); 19 | it('networkToConfig()', () => { 20 | const testnet = networkToConfig(bitcoin.networks.testnet); 21 | expect(testnet).toBeDefined(); 22 | expect(testnet.networkType).toBe(NetworkType.TESTNET); 23 | expect(testnet.network).toEqual(bitcoin.networks.testnet); 24 | 25 | const mainnet = networkToConfig(bitcoin.networks.bitcoin); 26 | expect(mainnet).toBeDefined(); 27 | expect(mainnet.networkType).toBe(NetworkType.MAINNET); 28 | expect(mainnet.network).toEqual(bitcoin.networks.bitcoin); 29 | 30 | expect(() => networkToConfig(bitcoin.networks.regtest)).toThrow(); 31 | }); 32 | it('networkTypeToNetwork()', () => { 33 | const testnet = networkTypeToNetwork(NetworkType.TESTNET); 34 | expect(testnet).toEqual(bitcoin.networks.testnet); 35 | 36 | const mainnet = networkTypeToNetwork(NetworkType.MAINNET); 37 | expect(mainnet).toEqual(bitcoin.networks.bitcoin); 38 | 39 | expect(() => networkTypeToNetwork(NetworkType.REGTEST)).toThrow(); 40 | }); 41 | it('networkToNetworkType()', () => { 42 | const testnet = networkToNetworkType(bitcoin.networks.testnet); 43 | expect(testnet).toEqual(NetworkType.TESTNET); 44 | 45 | const mainnet = networkToNetworkType(bitcoin.networks.bitcoin); 46 | expect(mainnet).toEqual(NetworkType.MAINNET); 47 | 48 | expect(() => networkToNetworkType(bitcoin.networks.regtest)).toThrow(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/btc/tests/Script.test.ts: -------------------------------------------------------------------------------- 1 | import { isP2trScript, isP2wpkhScript } from '../src'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { accounts } from './shared/env'; 4 | 5 | describe('Script', () => { 6 | const p2wpkh = accounts.charlie.p2wpkh.scriptPubkey; 7 | const p2tr = accounts.charlie.p2tr.scriptPubkey; 8 | 9 | it('isP2trScript()', () => { 10 | expect(isP2trScript(p2tr)).toBe(true); 11 | expect(isP2trScript(p2wpkh)).toBe(false); 12 | }); 13 | it('isP2wpkhScript()', () => { 14 | expect(isP2wpkhScript(p2wpkh)).toBe(true); 15 | expect(isP2wpkhScript(p2tr)).toBe(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/btc/tests/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { bitcoin, decodeUtxoId, encodeUtxoId, transactionToHex } from '../src'; 3 | 4 | describe('Utils', () => { 5 | it('transactionToHex()', () => { 6 | const originalHex = 7 | '02000000000101177e673414fb4a393f0e1faf27a317d92e9f1a7b9a3ff36713d46ef5b7a1a6190100000000ffffffff020000000000000000226a20849f5b17209de17af5a94f0111e2ba03d1409da87a0f06894abb85b3b5024726df3c0f000000000016001462fc12a35b779f0cf7edcb9690be19b0386e0f9a024830450221009d869f20ef22864e02603571ce40da0586c03f20f5b8fb6295a4d636141d39dc02207082fdef40b34f6189491cba98c861ddfc8889d91c48f11f4660f11e93b1153b012103e1c38cf06691d449961d2b8f261a9a238c53da91d3a1e948497f7b1fe717968000000000'; 8 | const tx = bitcoin.Transaction.fromHex(originalHex); 9 | 10 | const defaultHex = tx.toHex(); 11 | const hexWithWitnesses = transactionToHex(tx, true); 12 | const hexWithoutWitnesses = transactionToHex(tx, false); 13 | 14 | expect(defaultHex).toEqual(originalHex); 15 | expect(defaultHex).toEqual(hexWithWitnesses); 16 | 17 | expect(hexWithoutWitnesses.length).toBeLessThan(hexWithWitnesses.length); 18 | expect(hexWithoutWitnesses).toEqual( 19 | '0200000001177e673414fb4a393f0e1faf27a317d92e9f1a7b9a3ff36713d46ef5b7a1a6190100000000ffffffff020000000000000000226a20849f5b17209de17af5a94f0111e2ba03d1409da87a0f06894abb85b3b5024726df3c0f000000000016001462fc12a35b779f0cf7edcb9690be19b0386e0f9a00000000', 20 | ); 21 | }); 22 | it('encodeUtxoId()', () => { 23 | expect(encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0)).toEqual( 24 | '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0', 25 | ); 26 | expect(encodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0)).toEqual( 27 | '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0', 28 | ); 29 | expect(encodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0xffffffff)).toEqual( 30 | '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:4294967295', 31 | ); 32 | expect(() => encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b22', 0)).toThrowError(); 33 | expect(() => 34 | encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0xffffffff01), 35 | ).toThrowError(); 36 | }); 37 | it('decodeUtxoId()', () => { 38 | expect(decodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0')).toStrictEqual({ 39 | txid: '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 40 | vout: 0, 41 | }); 42 | expect(decodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:4294967295')).toStrictEqual({ 43 | txid: '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 44 | vout: 4294967295, 45 | }); 46 | 47 | expect(() => decodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0')).toThrowError(); 48 | expect(() => 49 | decodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:42949672951'), 50 | ).toThrowError(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/btc/tests/shared/env.ts: -------------------------------------------------------------------------------- 1 | import { BtcAssetsApi } from '@rgbpp-sdk/service'; 2 | import { DataSource, NetworkType, networkTypeToConfig } from '../../src'; 3 | import { createAccount } from './utils'; 4 | 5 | export const networkType = NetworkType.TESTNET; 6 | export const config = networkTypeToConfig(networkType); 7 | export const network = config.network; 8 | 9 | export const service = BtcAssetsApi.fromToken( 10 | process.env.VITE_BTC_SERVICE_URL!, 11 | process.env.VITE_BTC_SERVICE_TOKEN!, 12 | process.env.VITE_BTC_SERVICE_ORIGIN!, 13 | ); 14 | 15 | export const source = new DataSource(service, networkType); 16 | 17 | export const accounts = { 18 | charlie: createAccount({ 19 | privateKey: '8d3c23d340ac0841e6c3b58a9bbccb9a28e94ab444f972cff35736fa2fcf9f3f', 20 | network, 21 | }), 22 | }; 23 | -------------------------------------------------------------------------------- /packages/btc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Bundler", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "strict": true, 8 | "noEmit": true, 9 | "allowJs": true, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "strictNullChecks": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/btc/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | name: '@rgbpp-sdk/btc', 5 | dts: true, 6 | clean: true, 7 | sourcemap: true, 8 | format: ['esm', 'cjs'], 9 | entry: ['src/index.ts'], 10 | }); 11 | -------------------------------------------------------------------------------- /packages/ckb/example/launch.ts: -------------------------------------------------------------------------------- 1 | import { AddressPrefix, privateKeyToAddress, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from '../src/rgbpp'; 3 | import { Collector } from '../src/collector'; 4 | import { u32ToLe } from '../src/utils'; 5 | import { getSecp256k1CellDep } from '../src/constants'; 6 | 7 | // SECP256K1 private key 8 | const LAUNCH_SECP256K1_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; 9 | 10 | const launchRgbppAsset = async () => { 11 | const collector = new Collector({ 12 | ckbNodeUrl: 'https://testnet.ckb.dev/rpc', 13 | ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', 14 | }); 15 | const address = privateKeyToAddress(LAUNCH_SECP256K1_PRIVATE_KEY, { prefix: AddressPrefix.Testnet }); 16 | console.log('master address: ', address); 17 | 18 | // TODO: Use real btc utxo information 19 | const outIndex = 1; 20 | const btcTxId = '47448104a611ecb16ab8d8e500b2166689612c93fc7ef18783d8189f3079f447'; 21 | const toRgbppLockArgs = `0x${u32ToLe(outIndex)}${btcTxId}`; 22 | 23 | // TODO: Use real XUDT type script 24 | const xudtType: CKBComponents.Script = { 25 | codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb', 26 | hashType: 'type', 27 | args: '0xaafd7e7eab79726c669d7565888b194dc06bd1dbec16749a721462151e4f1762', 28 | }; 29 | 30 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 31 | collector, 32 | fromCkbAddress: address, 33 | toRgbppLockArgs, 34 | xudtTypeBytes: serializeScript(xudtType), 35 | transferAmount: BigInt(200_0000_0000), 36 | }); 37 | 38 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 39 | const unsignedTx: CKBComponents.RawTransactionToSign = { 40 | ...ckbRawTx, 41 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(false)], 42 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 43 | }; 44 | 45 | const signedTx = collector.getCkb().signTransaction(LAUNCH_SECP256K1_PRIVATE_KEY)(unsignedTx); 46 | 47 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 48 | console.info(`Rgbpp asset has been jumping from CKB to BTC and tx hash is ${txHash}`); 49 | }; 50 | 51 | launchRgbppAsset(); 52 | -------------------------------------------------------------------------------- /packages/ckb/example/paymaster.ts: -------------------------------------------------------------------------------- 1 | import { AddressPrefix, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { splitMultiCellsWithSecp256k1 } from '../src/paymaster'; 3 | import { Collector } from '../src/collector'; 4 | 5 | // SECP256K1 private key 6 | const MASTER_SECP256K1_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'; 7 | const RECEIVER_ADDRESS = 8 | 'ckt1qrfrwcdnvssswdwpn3s9v8fp87emat306ctjwsm3nmlkjg8qyza2cqgqqxqyukftmpfang0z2ks6w6syjutass94fujlf09a'; 9 | 10 | const splitPaymasterCells = async () => { 11 | const collector = new Collector({ 12 | ckbNodeUrl: 'https://testnet.ckb.dev/rpc', 13 | ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', 14 | }); 15 | const address = privateKeyToAddress(MASTER_SECP256K1_PRIVATE_KEY, { prefix: AddressPrefix.Testnet }); 16 | console.log('master address: ', address); 17 | 18 | // Split 200 cells whose capacity are 316CKB for the receiver address 19 | await splitMultiCellsWithSecp256k1({ 20 | masterPrivateKey: MASTER_SECP256K1_PRIVATE_KEY, 21 | collector, 22 | receiverAddress: RECEIVER_ADDRESS, 23 | capacityWithCKB: 316, 24 | cellAmount: 200, 25 | }); 26 | }; 27 | 28 | splitPaymasterCells(); 29 | -------------------------------------------------------------------------------- /packages/ckb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rgbpp-sdk/ckb", 3 | "version": "0.7.0", 4 | "scripts": { 5 | "test": "vitest", 6 | "build": "tsup", 7 | "lint": "tsc && eslint --ext .ts {src,example}/* && prettier --check '{src,example}/**/*.{js,ts}'", 8 | "lint:fix": "tsc && eslint --fix --ext .ts {src,example}/* && prettier --write '{src,example}/**/*.{js,ts}'", 9 | "splitCells": "npx tsx example/paymaster.ts" 10 | }, 11 | "sideEffects": false, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "exports": { 15 | ".": { 16 | "import": { 17 | "types": "./dist/index.d.mts", 18 | "default": "./dist/index.mjs" 19 | }, 20 | "require": { 21 | "types": "./dist/index.d.ts", 22 | "default": "./dist/index.js" 23 | } 24 | }, 25 | "./package.json": "./package.json" 26 | }, 27 | "files": [ 28 | "src", 29 | "dist" 30 | ], 31 | "dependencies": { 32 | "@ckb-lumos/base": "^0.22.2", 33 | "@ckb-lumos/codec": "^0.22.2", 34 | "@spore-sdk/core": "^0.2.0-beta.6", 35 | "@nervosnetwork/ckb-sdk-core": "0.109.5", 36 | "@nervosnetwork/ckb-sdk-utils": "0.109.5", 37 | "@nervosnetwork/ckb-types": "0.109.5", 38 | "@rgbpp-sdk/service": "workspace:^", 39 | "@exact-realty/multipart-parser": "^1.0.13", 40 | "axios": "^1.7.4", 41 | "camelcase-keys": "^7.0.2", 42 | "js-sha256": "^0.11.0" 43 | }, 44 | "devDependencies": { 45 | "@ckb-lumos/molecule": "0.0.0-canary-66bbbfd-20240805132534" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/ckb/src/collector/collector.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Collector } from '.'; 3 | 4 | describe('collector', () => { 5 | const collector = new Collector({ 6 | ckbNodeUrl: 'https://testnet.ckb.dev/rpc', 7 | ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', 8 | }); 9 | 10 | it('getLiveCell', async () => { 11 | const cell = await collector.getLiveCell({ 12 | txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', 13 | index: '0x0', 14 | }); 15 | expect(cell.output.lock.codeHash).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); 16 | }); 17 | 18 | it('getLiveCells', async () => { 19 | const [cell1, cell2] = await collector.getLiveCells([ 20 | // Genesis block 21 | { txHash: '0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f', index: '0x0' }, 22 | // Nervos DAO 23 | { txHash: '0x8277d74d33850581f8d843613ded0c2a1722dec0e87e748f45c115dfb14210f1', index: '0x0' }, 24 | ]); 25 | expect(cell1.output.lock.codeHash).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); 26 | expect(cell2.output.type?.codeHash).toBe('0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/ckb/src/collector/offline.ts: -------------------------------------------------------------------------------- 1 | import { IndexerCell } from '../types/collector'; 2 | import { Collector } from './index'; 3 | import { isScriptEqual } from '../utils/ckb-tx'; 4 | import { Hex } from '../types'; 5 | 6 | export class OfflineCollector extends Collector { 7 | private cells: IndexerCell[]; 8 | 9 | constructor(cells: IndexerCell[]) { 10 | super({ ckbNodeUrl: '', ckbIndexerUrl: '' }); 11 | this.cells = cells; 12 | } 13 | 14 | getCkb(): never { 15 | throw new Error('OfflineCollector does not have a CKB instance'); 16 | } 17 | 18 | async getCells({ 19 | lock, 20 | type, 21 | isDataMustBeEmpty = true, 22 | outputCapacityRange, 23 | }: { 24 | lock?: CKBComponents.Script; 25 | type?: CKBComponents.Script; 26 | isDataMustBeEmpty?: boolean; 27 | outputCapacityRange?: Hex[]; 28 | }): Promise { 29 | let cells: IndexerCell[] = []; 30 | 31 | if (lock) { 32 | cells = this.cells.filter((cell) => { 33 | if (type) { 34 | return isScriptEqual(cell.output.lock, lock) && cell.output.type && isScriptEqual(cell.output.type, type); 35 | } 36 | return isScriptEqual(cell.output.lock, lock); 37 | }); 38 | } else if (type) { 39 | cells = this.cells.filter((cell) => { 40 | if (!cell.output.type) { 41 | return false; 42 | } 43 | return isScriptEqual(cell.output.type, type); 44 | }); 45 | } 46 | 47 | if (isDataMustBeEmpty && !type) { 48 | cells = cells.filter((cell) => cell.outputData === '0x' || cell.outputData === ''); 49 | } 50 | 51 | if (outputCapacityRange) { 52 | if (outputCapacityRange.length === 2) { 53 | cells = cells.filter((cell) => { 54 | const capacity = BigInt(cell.output.capacity); 55 | return capacity >= BigInt(outputCapacityRange[0]) && capacity < BigInt(outputCapacityRange[1]); 56 | }); 57 | } else { 58 | throw new Error('Invalid output capacity range'); 59 | } 60 | } 61 | 62 | return cells.map((cell) => ({ 63 | blockNumber: cell.blockNumber, 64 | outPoint: cell.outPoint, 65 | output: cell.output, 66 | outputData: cell.outputData, 67 | txIndex: cell.txIndex, 68 | })); 69 | } 70 | 71 | // https://github.com/nervosnetwork/ckb/blob/master/rpc/README.md#method-get_live_cell 72 | async getLiveCell(outPoint: CKBComponents.OutPoint, withData = true): Promise { 73 | const cell = this.cells.find((cell) => { 74 | return outPoint.txHash === cell.outPoint.txHash && outPoint.index === cell.outPoint.index; 75 | }); 76 | if (!cell) { 77 | throw new Error( 78 | `Cell corresponding to the outPoint: {txHash: ${outPoint.txHash}, index: ${outPoint.index}} not found`, 79 | ); 80 | } 81 | 82 | return { 83 | output: cell.output, 84 | data: withData 85 | ? { 86 | content: cell.outputData, 87 | hash: '', // not used, leave it empty for now 88 | } 89 | : undefined, 90 | }; 91 | } 92 | 93 | async getLiveCells(outPoints: CKBComponents.OutPoint[], withData = false): Promise { 94 | return Promise.all(outPoints.map((outPoint) => this.getLiveCell(outPoint, withData))); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/ckb/src/error/index.ts: -------------------------------------------------------------------------------- 1 | enum ErrorCode { 2 | CapacityNotEnough = 100, 3 | IndexerRpcError = 101, 4 | NoLiveCell = 102, 5 | NoXudtLiveCell = 103, 6 | NoRgbppLiveCell = 104, 7 | UdtAmountNotEnough = 105, 8 | InputsCapacityNotEnough = 106, 9 | TypeAssetNotSupported = 107, 10 | InputsOrOutputsLenInvalid = 108, 11 | RgbppCkbTxInputsExceeded = 109, 12 | RgbppUtxoBindMultiTypeAssets = 110, 13 | RgbppSporeTypeMismatch = 111, 14 | InvalidCellId = 112, 15 | } 16 | 17 | export class CapacityNotEnoughError extends Error { 18 | code = ErrorCode.CapacityNotEnough; 19 | constructor(message: string) { 20 | super(message); 21 | } 22 | } 23 | 24 | export class IndexerError extends Error { 25 | code = ErrorCode.IndexerRpcError; 26 | constructor(message: string) { 27 | super(message); 28 | } 29 | } 30 | 31 | export class NoLiveCellError extends Error { 32 | code = ErrorCode.NoLiveCell; 33 | constructor(message: string) { 34 | super(message); 35 | } 36 | } 37 | 38 | export class NoXudtLiveCellError extends Error { 39 | code = ErrorCode.NoXudtLiveCell; 40 | constructor(message: string) { 41 | super(message); 42 | } 43 | } 44 | 45 | export class NoRgbppLiveCellError extends Error { 46 | code = ErrorCode.NoRgbppLiveCell; 47 | constructor(message: string) { 48 | super(message); 49 | } 50 | } 51 | 52 | export class UdtAmountNotEnoughError extends Error { 53 | code = ErrorCode.UdtAmountNotEnough; 54 | constructor(message: string) { 55 | super(message); 56 | } 57 | } 58 | 59 | export class InputsCapacityNotEnoughError extends Error { 60 | code = ErrorCode.InputsCapacityNotEnough; 61 | constructor(message: string) { 62 | super(message); 63 | } 64 | } 65 | 66 | export class TypeAssetNotSupportedError extends Error { 67 | code = ErrorCode.TypeAssetNotSupported; 68 | constructor(message: string) { 69 | super(message); 70 | } 71 | } 72 | 73 | export class InputsOrOutputsLenError extends Error { 74 | code = ErrorCode.InputsOrOutputsLenInvalid; 75 | constructor(message: string) { 76 | super(message); 77 | } 78 | } 79 | 80 | export class RgbppCkbTxInputsExceededError extends Error { 81 | code = ErrorCode.RgbppCkbTxInputsExceeded; 82 | constructor(message: string) { 83 | super(message); 84 | } 85 | } 86 | 87 | export class RgbppUtxoBindMultiTypeAssetsError extends Error { 88 | code = ErrorCode.RgbppUtxoBindMultiTypeAssets; 89 | constructor(message: string) { 90 | super(message); 91 | } 92 | } 93 | 94 | export class RgbppSporeTypeMismatchError extends Error { 95 | code = ErrorCode.RgbppSporeTypeMismatch; 96 | constructor(message: string) { 97 | super(message); 98 | } 99 | } 100 | 101 | export class InvalidCellIdError extends Error { 102 | code = ErrorCode.InvalidCellId; 103 | constructor(message: string) { 104 | super(message); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/ckb/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schemas/generated/blockchain'; 2 | export * from './schemas/generated/rgbpp'; 3 | export * from './collector'; 4 | export * from './collector/offline'; 5 | export * from './error'; 6 | export * from './paymaster'; 7 | export * from './types'; 8 | export * from './rgbpp'; 9 | export * from './utils'; 10 | export * from './constants'; 11 | export * from './spore'; 12 | -------------------------------------------------------------------------------- /packages/ckb/src/paymaster/index.ts: -------------------------------------------------------------------------------- 1 | import { ConstructPaymasterParams } from '../types/rgbpp'; 2 | import { NoLiveCellError } from '../error'; 3 | import { CKB_UNIT, MAX_FEE, SECP256K1_WITNESS_LOCK_SIZE, getSecp256k1CellDep } from '../constants'; 4 | import { append0x, calculateTransactionFee } from '../utils'; 5 | import { AddressPrefix, addressToScript, getTransactionSize, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils'; 6 | 7 | const SECP256K1_MIN_CAPACITY = BigInt(61) * CKB_UNIT; 8 | 9 | export const splitMultiCellsWithSecp256k1 = async ({ 10 | masterPrivateKey, 11 | collector, 12 | receiverAddress, 13 | capacityWithCKB, 14 | cellAmount, 15 | }: ConstructPaymasterParams) => { 16 | const isMainnet = receiverAddress.startsWith('ckb'); 17 | const masterAddress = privateKeyToAddress(masterPrivateKey, { 18 | prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, 19 | }); 20 | const masterLock = addressToScript(masterAddress); 21 | 22 | let emptyCells = await collector.getCells({ 23 | lock: masterLock, 24 | }); 25 | if (!emptyCells || emptyCells.length === 0) { 26 | throw new NoLiveCellError('The address has no empty cells'); 27 | } 28 | emptyCells = emptyCells.filter((cell) => !cell.output.type); 29 | 30 | const cellCapacity = BigInt(capacityWithCKB) * CKB_UNIT; 31 | const needCapacity = cellCapacity * BigInt(cellAmount); 32 | const txFee = MAX_FEE; 33 | const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, needCapacity, txFee, { 34 | minCapacity: SECP256K1_MIN_CAPACITY, 35 | }); 36 | 37 | const outputs: CKBComponents.CellOutput[] = new Array(cellAmount).fill({ 38 | lock: addressToScript(receiverAddress), 39 | capacity: append0x(cellCapacity.toString(16)), 40 | }); 41 | 42 | const changeCapacity = sumInputsCapacity - needCapacity - txFee; 43 | outputs.push({ 44 | lock: masterLock, 45 | capacity: append0x(changeCapacity.toString(16)), 46 | }); 47 | const outputsData = new Array(cellAmount + 1).fill('0x'); 48 | 49 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 50 | const witnesses = inputs.map((_, index) => (index === 0 ? emptyWitness : '0x')); 51 | 52 | const cellDeps = [getSecp256k1CellDep(isMainnet)]; 53 | 54 | const unsignedTx = { 55 | version: '0x0', 56 | cellDeps, 57 | headerDeps: [], 58 | inputs, 59 | outputs, 60 | outputsData, 61 | witnesses, 62 | }; 63 | 64 | if (txFee === MAX_FEE) { 65 | const txSize = getTransactionSize(unsignedTx) + SECP256K1_WITNESS_LOCK_SIZE; 66 | const estimatedTxFee = calculateTransactionFee(txSize); 67 | const estimatedChangeCapacity = changeCapacity + (MAX_FEE - estimatedTxFee); 68 | unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(estimatedChangeCapacity.toString(16)); 69 | } 70 | 71 | const signedTx = collector.getCkb().signTransaction(masterPrivateKey)(unsignedTx); 72 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 73 | 74 | console.info(`Paymaster cells has been split and tx hash is ${txHash}`); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/ckb/src/rgbpp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './btc-transfer'; 2 | export * from './ckb-builder'; 3 | export * from './btc-jump-ckb'; 4 | export * from './btc-time'; 5 | export * from './ckb-jump-btc'; 6 | export * from './launch'; 7 | -------------------------------------------------------------------------------- /packages/ckb/src/rgbpp/schemas.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { buildRgbppUnlockWitness } from './ckb-builder'; 3 | import { buildBtcTimeUnlockWitness } from './btc-time'; 4 | 5 | describe('RGBPP schemas', () => { 6 | // The test data is from RGBPP lock contract test 7 | it('buildRgbppUnlockWitness', () => { 8 | const btcTx = 9 | '0x000000000181aad32adc6b05a424281403c9a835279247befaf84fe4c411bbcfaffe0a7cea0000000020a7c2244ec166e1fa756ba0e5562fd13ade25df22e9ced69dea8364c4671261b0000000000200000000000001f4226a2067b900ca0203de66033cff3d62bfbff11f41d43071f56b11e2818ee2aa6e2b0000000000000001f42097f437158a724699056fc27259a9a73b3b5f39792760bad78582ec334b81c16600000000'; 10 | const txProof = 11 | '0xb900000014000000180000001c00000025000000020000002a0000000500000070726f6f660200000064000000e803000001000000000000000000000000000000000000000000000000000000000000000303030303030303030303030303030303030303030303030303030303030303e8030000d007000001000000000000000000000000000000000000000000000000000000000000000303030303030303030303030303030303030303030303030303030303030303'; 12 | const inputsLen = 1; 13 | const outputsLen = 2; 14 | const rgbppUnlock = buildRgbppUnlockWitness(btcTx, txProof, inputsLen, outputsLen); 15 | expect( 16 | '0x80010000140000001600000018000000c300000000000102a7000000000000000181aad32adc6b05a424281403c9a835279247befaf84fe4c411bbcfaffe0a7cea0000000020a7c2244ec166e1fa756ba0e5562fd13ade25df22e9ced69dea8364c4671261b0000000000200000000000001f4226a2067b900ca0203de66033cff3d62bfbff11f41d43071f56b11e2818ee2aa6e2b0000000000000001f42097f437158a724699056fc27259a9a73b3b5f39792760bad78582ec334b81c16600000000b9000000b900000014000000180000001c00000025000000020000002a0000000500000070726f6f660200000064000000e803000001000000000000000000000000000000000000000000000000000000000000000303030303030303030303030303030303030303030303030303030303030303e8030000d007000001000000000000000000000000000000000000000000000000000000000000000303030303030303030303030303030303030303030303030303030303030303', 17 | ).toBe(rgbppUnlock); 18 | }); 19 | 20 | it('buildBtcTimeUnlockWitness', () => { 21 | const txProof = '0x0102030405060708'; 22 | const btcTimeUnlock = buildBtcTimeUnlockWitness(txProof); 23 | expect('0x1400000008000000080000000102030405060708').toBe(btcTimeUnlock); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/ckb/src/schemas/customized.ts: -------------------------------------------------------------------------------- 1 | import { createFixedBytesCodec, number } from '@ckb-lumos/codec'; 2 | 3 | const { Uint32, Uint64, Uint128 } = number; 4 | 5 | /** 6 | *
 7 |  *  0b0000000 0
 8 |  *    ───┬─── │
 9 |  *       │    ▼
10 |  *       │   type - the last bit indicates locating contract(script) via type hash and runs in the latest version of the CKB-VM
11 |  *       │
12 |  *       ▼
13 |  * data* - the first 7 bits indicate locating contract(script) via code hash and runs in the specified version of the CKB-VM
14 |  * 
15 | * 16 | */ 17 | const HashType = createFixedBytesCodec<'data' | 'type' | 'data1' | 'data2'>({ 18 | byteLength: 1, 19 | // prettier-ignore 20 | pack: (hashType) => { 21 | if (hashType === "type") return new Uint8Array([0b0000000_1]); 22 | if (hashType === "data") return new Uint8Array([0b0000000_0]); 23 | if (hashType === "data1") return new Uint8Array([0b0000001_0]); 24 | if (hashType === "data2") return new Uint8Array([0b0000010_0]); 25 | 26 | throw new Error('Unknown hash type') 27 | }, 28 | unpack: (byte) => { 29 | if (byte[0] === 0b0000000_1) return 'type'; 30 | if (byte[0] === 0b0000000_0) return 'data'; 31 | if (byte[0] === 0b0000001_0) return 'data1'; 32 | if (byte[0] === 0b0000010_0) return 'data2'; 33 | 34 | throw new Error('Unknown hash type'); 35 | }, 36 | }); 37 | 38 | const DepType = createFixedBytesCodec<'code' | 'depGroup'>({ 39 | byteLength: 1, 40 | // prettier-ignore 41 | pack: (depType) => { 42 | if (depType === "code") return new Uint8Array([0]); 43 | if (depType === "depGroup") return new Uint8Array([1]); 44 | 45 | throw new Error("Unknown dep type"); 46 | }, 47 | unpack: (byte) => { 48 | if (byte[0] === 0) return 'code'; 49 | if (byte[0] === 1) return 'depGroup'; 50 | 51 | throw new Error('Unknown dep type'); 52 | }, 53 | }); 54 | 55 | export { Uint32, Uint64, Uint128, DepType, HashType }; 56 | -------------------------------------------------------------------------------- /packages/ckb/src/schemas/generated/rgbpp.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by @ckb-lumos/molecule, please do not modify it manually. 2 | /* eslint-disable */ 3 | import { bytes, createBytesCodec, createFixedBytesCodec, molecule } from '@ckb-lumos/codec'; 4 | import { Uint32, Uint64, Uint128, DepType, HashType } from '../customized'; 5 | import { 6 | Byte32, 7 | Uint256, 8 | Bytes, 9 | BytesOpt, 10 | BytesOptVec, 11 | BytesVec, 12 | Byte32Vec, 13 | ProposalShortId, 14 | ProposalShortIdVec, 15 | Script, 16 | OutPoint, 17 | CellInput, 18 | CellDep, 19 | RawHeader, 20 | Header, 21 | UncleBlock, 22 | CellbaseWitness, 23 | WitnessArgs, 24 | ScriptOpt, 25 | UncleBlockVec, 26 | CellDepVec, 27 | CellInputVec, 28 | CellOutput, 29 | CellOutputVec, 30 | RawTransaction, 31 | Transaction, 32 | TransactionVec, 33 | Block, 34 | BlockV1, 35 | } from './blockchain'; 36 | 37 | const { array, vector, union, option, struct, table, byteVecOf } = molecule; 38 | 39 | const fallbackBytesCodec = byteVecOf({ pack: bytes.bytify, unpack: bytes.hexify }); 40 | 41 | function createFallbackFixedBytesCodec(byteLength: number) { 42 | return createFixedBytesCodec({ 43 | pack: bytes.bytify, 44 | unpack: bytes.hexify, 45 | byteLength, 46 | }); 47 | } 48 | 49 | const byte = createFallbackFixedBytesCodec(1); 50 | 51 | export const RGBPPConfig = struct( 52 | { 53 | btcLcTypeHash: Byte32, 54 | btcTimeLockTypeHash: Byte32, 55 | }, 56 | ['btcLcTypeHash', 'btcTimeLockTypeHash'], 57 | ); 58 | 59 | export const RGBPPLock = struct( 60 | { 61 | outIndex: Uint32, 62 | btcTxid: Byte32, 63 | }, 64 | ['outIndex', 'btcTxid'], 65 | ); 66 | 67 | export const ExtraCommitmentData = struct( 68 | { 69 | inputLen: byte, 70 | outputLen: byte, 71 | }, 72 | ['inputLen', 'outputLen'], 73 | ); 74 | 75 | export const Uint16 = createFallbackFixedBytesCodec(2); 76 | 77 | export const RGBPPUnlock = table( 78 | { 79 | version: Uint16, 80 | extraData: ExtraCommitmentData, 81 | btcTx: Bytes, 82 | btcTxProof: Bytes, 83 | }, 84 | ['version', 'extraData', 'btcTx', 'btcTxProof'], 85 | ); 86 | 87 | export const BTCTimeLock = table( 88 | { 89 | lockScript: Script, 90 | after: Uint32, 91 | btcTxid: Byte32, 92 | }, 93 | ['lockScript', 'after', 'btcTxid'], 94 | ); 95 | 96 | export const BTCTimeLockConfig = struct( 97 | { 98 | btcLcTypeHash: Byte32, 99 | }, 100 | ['btcLcTypeHash'], 101 | ); 102 | 103 | export const BTCTimeUnlock = table( 104 | { 105 | btcTxProof: Bytes, 106 | }, 107 | ['btcTxProof'], 108 | ); 109 | -------------------------------------------------------------------------------- /packages/ckb/src/schemas/lumos-molecule-codegen.json: -------------------------------------------------------------------------------- 1 | { 2 | "objectKeyFormat": "camelcase", 3 | "prepend": "import { Uint32, Uint64, Uint128, DepType, HashType } from './customized'", 4 | "schemaDir": "schemas", 5 | "outDir": "generated" 6 | } 7 | -------------------------------------------------------------------------------- /packages/ckb/src/schemas/schemas/blockchain.mol: -------------------------------------------------------------------------------- 1 | 2 | /* Basic Types */ 3 | 4 | // The `UintN` is used to store a `N` bits unsigned integer 5 | // as a byte array in little endian. 6 | array Uint32 [byte; 4]; 7 | array Uint64 [byte; 8]; 8 | array Uint128 [byte; 16]; 9 | array Byte32 [byte; 32]; 10 | array Uint256 [byte; 32]; 11 | 12 | vector Bytes ; 13 | option BytesOpt (Bytes); 14 | vector BytesOptVec ; 15 | vector BytesVec ; 16 | vector Byte32Vec ; 17 | 18 | /* Types for Chain */ 19 | 20 | option ScriptOpt (Script); 21 | 22 | array ProposalShortId [byte; 10]; 23 | 24 | vector UncleBlockVec ; 25 | vector TransactionVec ; 26 | vector ProposalShortIdVec ; 27 | vector CellDepVec ; 28 | vector CellInputVec ; 29 | vector CellOutputVec ; 30 | 31 | table Script { 32 | code_hash: Byte32, 33 | hash_type: byte, 34 | args: Bytes, 35 | } 36 | 37 | struct OutPoint { 38 | tx_hash: Byte32, 39 | index: Uint32, 40 | } 41 | 42 | struct CellInput { 43 | since: Uint64, 44 | previous_output: OutPoint, 45 | } 46 | 47 | table CellOutput { 48 | capacity: Uint64, 49 | lock: Script, 50 | type_: ScriptOpt, 51 | } 52 | 53 | struct CellDep { 54 | out_point: OutPoint, 55 | dep_type: byte, 56 | } 57 | 58 | table RawTransaction { 59 | version: Uint32, 60 | cell_deps: CellDepVec, 61 | header_deps: Byte32Vec, 62 | inputs: CellInputVec, 63 | outputs: CellOutputVec, 64 | outputs_data: BytesVec, 65 | } 66 | 67 | table Transaction { 68 | raw: RawTransaction, 69 | witnesses: BytesVec, 70 | } 71 | 72 | struct RawHeader { 73 | version: Uint32, 74 | compact_target: Uint32, 75 | timestamp: Uint64, 76 | number: Uint64, 77 | epoch: Uint64, 78 | parent_hash: Byte32, 79 | transactions_root: Byte32, 80 | proposals_hash: Byte32, 81 | extra_hash: Byte32, 82 | dao: Byte32, 83 | } 84 | 85 | struct Header { 86 | raw: RawHeader, 87 | nonce: Uint128, 88 | } 89 | 90 | table UncleBlock { 91 | header: Header, 92 | proposals: ProposalShortIdVec, 93 | } 94 | 95 | table Block { 96 | header: Header, 97 | uncles: UncleBlockVec, 98 | transactions: TransactionVec, 99 | proposals: ProposalShortIdVec, 100 | } 101 | 102 | table BlockV1 { 103 | header: Header, 104 | uncles: UncleBlockVec, 105 | transactions: TransactionVec, 106 | proposals: ProposalShortIdVec, 107 | extension: Bytes, 108 | } 109 | 110 | table CellbaseWitness { 111 | lock: Script, 112 | message: Bytes, 113 | } 114 | 115 | table WitnessArgs { 116 | lock: BytesOpt, // Lock args 117 | input_type: BytesOpt, // Type args for input 118 | output_type: BytesOpt, // Type args for output 119 | } 120 | -------------------------------------------------------------------------------- /packages/ckb/src/schemas/schemas/rgbpp.mol: -------------------------------------------------------------------------------- 1 | import blockchain; 2 | /* RGBPP Types */ 3 | 4 | // Type hash of bitcoin light client and type hash of bitcoin time lock contract 5 | struct RGBPPConfig { 6 | btc_lc_type_hash: Byte32, 7 | btc_time_lock_type_hash: Byte32, 8 | } 9 | 10 | struct RGBPPLock { 11 | out_index: Uint32, 12 | btc_txid: Byte32, 13 | } 14 | 15 | struct ExtraCommitmentData { 16 | input_len: byte, 17 | output_len: byte, 18 | } 19 | 20 | array Uint16 [byte; 2]; 21 | 22 | table RGBPPUnlock { 23 | version: Uint16, 24 | extra_data: ExtraCommitmentData, 25 | btc_tx: Bytes, 26 | btc_tx_proof: Bytes, 27 | } 28 | 29 | table BTCTimeLock { 30 | lock_script: Script, 31 | after: Uint32, 32 | btc_txid: Byte32, 33 | } 34 | 35 | struct BTCTimeLockConfig { 36 | btc_lc_type_hash: Byte32, 37 | } 38 | 39 | table BTCTimeUnlock { 40 | btc_tx_proof: Bytes, 41 | } -------------------------------------------------------------------------------- /packages/ckb/src/spore/index.ts: -------------------------------------------------------------------------------- 1 | export type { RawSporeData, RawClusterData } from '@spore-sdk/core'; 2 | 3 | export * from './cluster'; 4 | export * from './spore'; 5 | export * from './leap'; 6 | -------------------------------------------------------------------------------- /packages/ckb/src/types/collector.ts: -------------------------------------------------------------------------------- 1 | import { Capacity, Hex } from './common'; 2 | 3 | export interface IndexerCell { 4 | blockNumber: CKBComponents.BlockNumber; 5 | outPoint: CKBComponents.OutPoint; 6 | output: CKBComponents.CellOutput; 7 | outputData: Hex; 8 | txIndex: Hex; 9 | } 10 | 11 | export interface IndexerCapacity { 12 | blockNumber: CKBComponents.BlockNumber; 13 | blockHash: CKBComponents.Hash; 14 | capacity: Hex; 15 | } 16 | 17 | export interface CollectResult { 18 | inputs: CKBComponents.CellInput[]; 19 | sumInputsCapacity: Capacity; 20 | } 21 | 22 | export interface CollectUdtResult extends CollectResult { 23 | sumAmount: bigint; 24 | } 25 | 26 | export interface CollectConfig { 27 | minCapacity?: bigint; 28 | errMsg?: string; 29 | } 30 | -------------------------------------------------------------------------------- /packages/ckb/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type Hex = string; 2 | export type U32 = bigint; 3 | export type Address = string; 4 | export type Capacity = bigint; 5 | 6 | export type BTCTestnetType = 'Testnet3' | 'Signet'; 7 | -------------------------------------------------------------------------------- /packages/ckb/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './collector'; 3 | export * from './rgbpp'; 4 | export * from './spv'; 5 | export * from './spore'; 6 | -------------------------------------------------------------------------------- /packages/ckb/src/types/spv.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from './common'; 2 | 3 | export interface SpvClientCellTxProof { 4 | // The OutPoint of spv client cell 5 | spvClient: CKBComponents.OutPoint; 6 | // The BTC transaction proof 7 | proof: Hex; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/case-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { toCamelcase } from './case-parser'; 3 | 4 | interface TestType { 5 | firstField: number; 6 | secondField: string[]; 7 | thirdField: { subFirstField: boolean; subSecondField: 'abc' }; 8 | } 9 | 10 | describe('case parser', () => { 11 | it('toCamelcase', () => { 12 | const result = toCamelcase({ 13 | first_field: 1, 14 | second_field: ['0', '1'], 15 | third_field: { sub_first_field: false, sub_second_field: 'abc' }, 16 | }); 17 | expect(1).toBe(result?.firstField); 18 | expect('0').toBe(result?.secondField[0]); 19 | expect('abc').toBe(result?.thirdField.subSecondField); 20 | 21 | const list = toCamelcase([ 22 | { 23 | first_field: 1, 24 | second_field: ['0', '1'], 25 | third_field: { sub_first_field: false, sub_second_field: 'abc' }, 26 | }, 27 | ]); 28 | expect(1).toBe(list![0].firstField); 29 | expect('0').toBe(list![0].secondField[0]); 30 | expect('abc').toBe(list![0].thirdField.subSecondField); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/case-parser.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | 3 | export const toCamelcase = (obj: object): T | null => { 4 | try { 5 | return camelcaseKeys(obj, { 6 | deep: true, 7 | }) as T; 8 | } catch (error) { 9 | console.error(error); 10 | } 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/hex.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { leToU128, reverseHex, u128ToLe, u32ToLe, u32ToLeHex, u64ToLe, u8ToHex, utf8ToHex } from './hex'; 3 | import { bytesToHex } from '@nervosnetwork/ckb-sdk-utils'; 4 | 5 | describe('number to little endian', () => { 6 | it('u32toLe', () => { 7 | const expected = u32ToLe(21000000); 8 | expect('406f4001').toBe(expected); 9 | }); 10 | 11 | it('u32toLeHex', () => { 12 | expect('0x2c01').toBe(u32ToLeHex(300)); 13 | expect('0x1').toBe(u32ToLeHex(1)); 14 | expect('0xe803').toBe(u32ToLeHex(1000)); 15 | }); 16 | 17 | it('u64ToLe', () => { 18 | const expected = u64ToLe(BigInt(21000000)); 19 | expect('406f400100000000').toBe(expected); 20 | }); 21 | 22 | it('u128ToLe', () => { 23 | const expected = u128ToLe(BigInt(2100_0000) * BigInt(10 ** 8)); 24 | expect('0040075af07507000000000000000000').toBe(expected); 25 | }); 26 | 27 | it('leToU128', () => { 28 | const expected = leToU128('0x00b864d9450000000000000000000000'); 29 | expect(BigInt(3000_0000_0000)).toBe(expected); 30 | }); 31 | 32 | it('bytesToHex', () => { 33 | const expected = bytesToHex(new Uint8Array([0x12, 0x34, 0x56])); 34 | expect('0x123456').toBe(expected); 35 | }); 36 | 37 | it('u8ToHex', () => { 38 | const actual = u8ToHex(8); 39 | expect(actual).toBe('08'); 40 | }); 41 | 42 | it('utf8ToHex', () => { 43 | const actual = utf8ToHex('RGBPP Test Token'); 44 | expect(actual).toBe('0x5247425050205465737420546f6b656e'); 45 | }); 46 | 47 | it('reverseHex', () => { 48 | const expected1 = reverseHex('0x2f061a27abcab1d1d146514ffada6a83c0d974fe0813835ad8be2a39a6b1a6ee'); 49 | expect(expected1).toBe('0xeea6b1a6392abed85a831308fe74d9c0836adafa4f5146d1d1b1caab271a062f'); 50 | 51 | const expected2 = reverseHex('2f061a27abcab1d1d146514ffada6a83c0d974fe0813835ad8be2a39a6b1a6ee'); 52 | expect(expected2).toBe('0xeea6b1a6392abed85a831308fe74d9c0836adafa4f5146d1d1b1caab271a062f'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/id.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { encodeCellId, decodeCellId } from './id'; 3 | 4 | describe('cell id', () => { 5 | it('encodeCellId', () => { 6 | expect(encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0x0')).toBe( 7 | '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0', 8 | ); 9 | expect(encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0xffffffff')).toBe( 10 | '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff', 11 | ); 12 | 13 | expect(() => 14 | encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e6', '0x0'), 15 | ).toThrowError(); 16 | expect(() => 17 | encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0xffffffff01'), 18 | ).toThrowError(); 19 | expect(() => 20 | encodeCellId('7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0x0'), 21 | ).toThrowError(); 22 | expect(() => 23 | encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0'), 24 | ).toThrowError(); 25 | }); 26 | it('decodeCellId', () => { 27 | expect(decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0')).toStrictEqual({ 28 | txHash: '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', 29 | index: '0x0', 30 | }); 31 | expect(decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff')).toStrictEqual( 32 | { 33 | txHash: '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', 34 | index: '0xffffffff', 35 | }, 36 | ); 37 | 38 | expect(() => decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e6:0x0')).toThrowError(); 39 | expect(() => 40 | decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff01'), 41 | ).toThrowError(); 42 | expect(() => decodeCellId('7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0')).toThrowError(); 43 | expect(() => decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0')).toThrowError(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/id.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexNumber, OutPoint, blockchain } from '@ckb-lumos/base'; 2 | import { InvalidCellIdError } from '../error'; 3 | import { append0x } from './hex'; 4 | 5 | export const encodeCellId = (txHash: Hash, index: HexNumber): string => { 6 | if (!txHash.startsWith('0x') || !index.startsWith('0x')) { 7 | throw new InvalidCellIdError(`Cannot encode CellId due to valid format: txHash=${txHash}, index=${index}`); 8 | } 9 | try { 10 | blockchain.OutPoint.pack({ 11 | txHash, 12 | index, 13 | }); 14 | return `${txHash}:${index}`; 15 | } catch { 16 | throw new InvalidCellIdError(`Cannot encode CellId due to valid format: txHash=${txHash}, index=${index}`); 17 | } 18 | }; 19 | 20 | export const decodeCellId = (cellId: string): OutPoint => { 21 | const [txHash, index] = cellId.split(':'); 22 | if (!txHash.startsWith('0x') || !index.startsWith('0x')) { 23 | throw new InvalidCellIdError(`Cannot decode CellId: ${cellId}`); 24 | } 25 | try { 26 | blockchain.OutPoint.pack({ 27 | txHash, 28 | index, 29 | }); 30 | return { 31 | txHash: append0x(txHash), 32 | index: append0x(index), 33 | }; 34 | } catch { 35 | throw new InvalidCellIdError(`Cannot decode CellId due to valid format: ${cellId}`); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /packages/ckb/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './case-parser'; 2 | export * from './hex'; 3 | export * from './ckb-tx'; 4 | export * from './rgbpp'; 5 | export * from './spore'; 6 | export * from './cell-dep'; 7 | export * from './id'; 8 | -------------------------------------------------------------------------------- /packages/ckb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "module": "ESNext", 6 | "target": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "strict": true, 9 | "noEmit": true, 10 | "allowJs": true, 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "strictNullChecks": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src"], 19 | "exclude": ["node_modules", "dist", "**/*.spec.ts", "example"], 20 | } 21 | -------------------------------------------------------------------------------- /packages/ckb/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | name: '@rgbpp-sdk/ckb', 5 | dts: true, 6 | clean: true, 7 | sourcemap: true, 8 | format: ['esm', 'cjs'], 9 | entry: ['src/index.ts'], 10 | }); 11 | -------------------------------------------------------------------------------- /packages/rgbpp/.env.example: -------------------------------------------------------------------------------- 1 | # Network 2 | VITE_IS_MAINNET=false 3 | 4 | # CKB 5 | VITE_CKB_NODE_URL=https://testnet.ckb.dev/rpc 6 | VITE_CKB_INDEXER_URL=https://testnet.ckb.dev/indexer 7 | 8 | # BTC 9 | VITE_BTC_SERVICE_URL=https://btc-assets-api.testnet.mibao.pro 10 | VITE_BTC_SERVICE_TOKEN= 11 | VITE_BTC_SERVICE_ORIGIN= 12 | -------------------------------------------------------------------------------- /packages/rgbpp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgbpp", 3 | "version": "0.7.0", 4 | "scripts": { 5 | "test": "vitest", 6 | "build": "tsup", 7 | "lint": "tsc && eslint --ext .ts src/* && prettier --check 'src/*.ts'", 8 | "lint:fix": "tsc && eslint --fix --ext .ts src/* && prettier --write 'src/*.ts'" 9 | }, 10 | "sideEffects": false, 11 | "main": "./dist/index.js", 12 | "types": "./dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": { 16 | "types": "./dist/index.d.mts", 17 | "default": "./dist/index.mjs" 18 | }, 19 | "require": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dist/index.js" 22 | } 23 | }, 24 | "./btc": { 25 | "import": { 26 | "types": "./dist/btc.d.mts", 27 | "default": "./dist/btc.mjs" 28 | }, 29 | "require": { 30 | "types": "./dist/btc.d.ts", 31 | "default": "./dist/btc.js" 32 | } 33 | }, 34 | "./ckb": { 35 | "import": { 36 | "types": "./dist/ckb.d.mts", 37 | "default": "./dist/ckb.mjs" 38 | }, 39 | "require": { 40 | "types": "./dist/ckb.d.ts", 41 | "default": "./dist/ckb.js" 42 | } 43 | }, 44 | "./service": { 45 | "import": { 46 | "types": "./dist/service.d.mts", 47 | "default": "./dist/service.mjs" 48 | }, 49 | "require": { 50 | "types": "./dist/service.d.ts", 51 | "default": "./dist/service.js" 52 | } 53 | }, 54 | "./dist/*": "./dist/*", 55 | "./package.json": "./package.json" 56 | }, 57 | "files": [ 58 | "src", 59 | "dist" 60 | ], 61 | "dependencies": { 62 | "@ckb-lumos/base": "^0.22.2", 63 | "@ckb-lumos/codec": "^0.22.2", 64 | "@nervosnetwork/ckb-sdk-utils": "0.109.5", 65 | "@rgbpp-sdk/btc": "workspace:*", 66 | "@rgbpp-sdk/ckb": "workspace:*", 67 | "@rgbpp-sdk/service": "workspace:*" 68 | }, 69 | "publishConfig": { 70 | "access": "public" 71 | }, 72 | "devDependencies": { 73 | "@types/node": "^20.3.1", 74 | "lodash": "^4.17.21", 75 | "zod": "^3.23.8" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/rgbpp/src/btc.ts: -------------------------------------------------------------------------------- 1 | export * from '@rgbpp-sdk/btc'; 2 | -------------------------------------------------------------------------------- /packages/rgbpp/src/ckb.ts: -------------------------------------------------------------------------------- 1 | export * from '@rgbpp-sdk/ckb'; 2 | -------------------------------------------------------------------------------- /packages/rgbpp/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ckb 3 | */ 4 | export { 5 | genCreateClusterCkbVirtualTx, 6 | genCreateSporeCkbVirtualTx, 7 | genLeapSporeFromBtcToCkbVirtualTx, 8 | genTransferSporeCkbVirtualTx, 9 | genBtcTransferCkbVirtualTx, 10 | genCkbJumpBtcVirtualTx, 11 | genBtcBatchTransferCkbVirtualTx, 12 | genLeapSporeFromCkbToBtcRawTx, 13 | genRgbppLaunchCkbVirtualTx, 14 | genBtcJumpCkbVirtualTx, 15 | buildBtcTimeCellsSpentTx, 16 | buildSporeBtcTimeCellsSpentTx, 17 | signBtcTimeCellSpentTx, 18 | } from '@rgbpp-sdk/ckb'; 19 | export type { 20 | CreateClusterCkbVirtualTxParams, 21 | CreateSporeCkbVirtualTxParams, 22 | LeapSporeFromBtcToCkbVirtualTxParams, 23 | TransferSporeCkbVirtualTxParams, 24 | BtcTransferVirtualTxParams, 25 | BtcJumpCkbVirtualTxParams, 26 | BtcBatchTransferVirtualTxParams, 27 | CkbJumpBtcVirtualTxParams, 28 | SporeCreateVirtualTxResult, 29 | BtcTransferVirtualTxResult, 30 | BtcJumpCkbVirtualTxResult, 31 | BtcBatchTransferVirtualTxResult, 32 | SporeTransferVirtualTxResult, 33 | SporeLeapVirtualTxResult, 34 | SporeVirtualTxResult, 35 | } from '@rgbpp-sdk/ckb'; 36 | 37 | /** 38 | * service 39 | */ 40 | export { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; 41 | 42 | /** 43 | * btc 44 | */ 45 | export { 46 | DataSource, 47 | NetworkType, 48 | AddressType, 49 | sendBtc, 50 | sendUtxos, 51 | sendRgbppUtxos, 52 | createSendBtcBuilder, 53 | createSendUtxosBuilder, 54 | createSendRgbppUtxosBuilder, 55 | } from '@rgbpp-sdk/btc'; 56 | export type { SendBtcProps, SendUtxosProps, SendRgbppUtxosProps } from '@rgbpp-sdk/btc'; 57 | 58 | /** 59 | * RGB++ 60 | */ 61 | export type { 62 | RgbppTransferTxParams, 63 | RgbppTransferTxResult, 64 | RgbppTransferAllTxsParams, 65 | RgbppTransferAllTxsResult, 66 | RgbppTransferAllTxGroup, 67 | } from './rgbpp/types/xudt'; 68 | export type { TransactionGroupSummary } from './rgbpp/summary/asset-summarizer'; 69 | export type { RgbppTxGroup, SentRgbppTxGroup } from './rgbpp/utils/transaction'; 70 | export { RgbppError, RgbppErrorCodes } from './rgbpp/error'; 71 | export { buildRgbppTransferTx } from './rgbpp/xudt/btc-transfer'; 72 | export { buildRgbppTransferAllTxs } from './rgbpp/xudt/btc-transfer-all'; 73 | export { sendRgbppTxGroups } from './rgbpp/utils/transaction'; 74 | -------------------------------------------------------------------------------- /packages/rgbpp/src/rgbpp/error.ts: -------------------------------------------------------------------------------- 1 | export enum RgbppErrorCodes { 2 | UNKNOWN, 3 | 4 | UNEXPECTED_CKB_VTX_OUTPUTS_LENGTH = 20, 5 | } 6 | 7 | export const RgbppErrorMessages = { 8 | [RgbppErrorCodes.UNKNOWN]: 'Unknown error', 9 | 10 | [RgbppErrorCodes.UNEXPECTED_CKB_VTX_OUTPUTS_LENGTH]: 'Unexpected length of the CkbVirtualTx outputs', 11 | }; 12 | 13 | export class RgbppError extends Error { 14 | public code = RgbppErrorCodes.UNKNOWN; 15 | constructor(code: RgbppErrorCodes, message = RgbppErrorMessages[code] || 'Unknown error') { 16 | super(message); 17 | this.code = code; 18 | Object.setPrototypeOf(this, RgbppError.prototype); 19 | } 20 | 21 | static withComment(code: RgbppErrorCodes, comment?: string): RgbppError { 22 | const message: string | undefined = RgbppErrorMessages[code]; 23 | return new RgbppError(code, comment ? `${message}: ${comment}` : message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/rgbpp/src/rgbpp/utils/group.ts: -------------------------------------------------------------------------------- 1 | export function groupNumbersBySum( 2 | numbers: number[], 3 | target: number, 4 | ): { 5 | indices: number[][]; 6 | numbers: number[][]; 7 | } { 8 | const groups: number[][] = []; 9 | const groupIds = new Set(); 10 | const usedIndices = new Set(); 11 | 12 | function backtrack(group: number[], sum: number, start: number): boolean { 13 | if (sum === target) { 14 | return true; 15 | } 16 | if (sum > target) { 17 | return false; 18 | } 19 | for (let i = start; i < numbers.length; i++) { 20 | if (usedIndices.has(i)) { 21 | continue; 22 | } 23 | usedIndices.add(i); 24 | group.push(i); 25 | if (backtrack(group, sum + numbers[i]!, i + 1)) { 26 | addGroup(group); 27 | return true; 28 | } 29 | usedIndices.delete(i); 30 | group.pop(); 31 | } 32 | if (group.length > 0 && sum < target) { 33 | addGroup(group); 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | function addGroup(group: number[]) { 40 | const sortedGroup = [...group].sort((a, b) => a - b); 41 | const groupId = sortedGroup.join(','); 42 | if (!groupIds.has(groupId)) { 43 | groups.push([...group]); 44 | groupIds.add(groupId); 45 | } 46 | } 47 | 48 | numbers.forEach((_, index) => { 49 | if (!usedIndices.has(index)) { 50 | backtrack([], 0, index); 51 | } 52 | }); 53 | 54 | return { 55 | indices: groups, 56 | numbers: groups.map((group) => { 57 | return group.map((index) => numbers[index]!); 58 | }), 59 | }; 60 | } 61 | 62 | export function mapGroupsByIndices(indices: number[][], getter: (index: number) => T): T[][] { 63 | return indices.map((group) => { 64 | return group.map((index) => getter(index)); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /packages/rgbpp/src/rgbpp/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { BaseCkbVirtualTxResult } from '@rgbpp-sdk/ckb'; 2 | import { BtcAssetsApi, BtcAssetsApiError } from '@rgbpp-sdk/service'; 3 | 4 | export interface RgbppTxGroup { 5 | ckbVirtualTxResult: BaseCkbVirtualTxResult | string; 6 | btcTxHex: string; 7 | } 8 | 9 | export interface SentRgbppTxGroup { 10 | btcTxId?: string; 11 | error?: string; 12 | } 13 | 14 | export async function sendRgbppTxGroups(props: { 15 | txGroups: RgbppTxGroup[]; 16 | btcService: BtcAssetsApi; 17 | }): Promise { 18 | const results: SentRgbppTxGroup[] = []; 19 | for (const group of props.txGroups) { 20 | try { 21 | const { txid } = await props.btcService.sendBtcTransaction(group.btcTxHex); 22 | await props.btcService.sendRgbppCkbTransaction({ 23 | btc_txid: txid, 24 | ckb_virtual_result: group.ckbVirtualTxResult, 25 | }); 26 | results.push({ btcTxId: txid }); 27 | } catch (e) { 28 | console.error(e); 29 | if (e instanceof BtcAssetsApiError) { 30 | results.push({ error: e.message }); 31 | } else { 32 | results.push({ error: 'Sending the RGB++ group transactions failed' }); 33 | } 34 | } 35 | } 36 | 37 | return results; 38 | } 39 | -------------------------------------------------------------------------------- /packages/rgbpp/src/rgbpp/xudt/btc-transfer.ts: -------------------------------------------------------------------------------- 1 | import { genBtcTransferCkbVirtualTx, getXudtTypeScript, serializeScript } from '@rgbpp-sdk/ckb'; 2 | import { sendRgbppUtxos } from '@rgbpp-sdk/btc'; 3 | import { RgbppTransferTxParams, RgbppTransferTxResult } from '../types/xudt'; 4 | 5 | /** 6 | * Build the CKB virtual transaction and BTC transaction to be signed for the RGB++ transfer tx 7 | * CKB parameters 8 | * @param collector The collector that collects CKB live cells and transactions 9 | * @param xudtTypeArgs The transferred xUDT type script args 10 | * @param rgbppLockArgsList The RGB++ assets cell lock script args array whose data structure is: out_index | bitcoin_tx_id 11 | * @param transferAmount The XUDT amount to be transferred, if the noMergeOutputCells is true, the transferAmount will be ignored 12 | * @param feeRate The CKB transaction fee rate, default value is 1100 13 | * @param compatibleXudtTypeScript(Optional) If the asset is compatible xUDT(not standard xUDT), the compatibleXudtTypeScript is required 14 | * 15 | * BTC parameters 16 | * @param fromAddress The sender BTC address 17 | * @param fromPubkey The public key of the sender BTC address 18 | * @param toAddress The receiver BTC address 19 | * @param dataSource The BTC data source 20 | * @param feeRate The fee rate of the BTC transaction 21 | * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet(see btcTestnetType for details about BTC Testnet) 22 | * @param testnetType(Optional) The Bitcoin Testnet type including Testnet3 and Signet, default value is Testnet3 23 | */ 24 | export const buildRgbppTransferTx = async ({ 25 | ckb: { collector, xudtTypeArgs, rgbppLockArgsList, transferAmount, feeRate: ckbFeeRate, compatibleXudtTypeScript }, 26 | btc, 27 | isMainnet, 28 | }: RgbppTransferTxParams): Promise => { 29 | const xudtType: CKBComponents.Script = compatibleXudtTypeScript ?? { 30 | ...getXudtTypeScript(isMainnet), 31 | args: xudtTypeArgs, 32 | }; 33 | 34 | const ckbVirtualTxResult = await genBtcTransferCkbVirtualTx({ 35 | collector, 36 | rgbppLockArgsList, 37 | xudtTypeBytes: serializeScript(xudtType), 38 | transferAmount, 39 | isMainnet, 40 | ckbFeeRate, 41 | btcTestnetType: btc.testnetType, 42 | }); 43 | 44 | const { commitment, ckbRawTx } = ckbVirtualTxResult; 45 | 46 | // Send BTC tx 47 | const psbt = await sendRgbppUtxos({ 48 | ckbVirtualTx: ckbRawTx, 49 | commitment, 50 | tos: [btc.toAddress], 51 | ckbCollector: collector, 52 | from: btc.fromAddress!, 53 | fromPubkey: btc.fromPubkey, 54 | source: btc.dataSource, 55 | feeRate: btc.feeRate, 56 | }); 57 | 58 | return { 59 | ckbVirtualTxResult, 60 | btcPsbtHex: psbt.toHex(), 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /packages/rgbpp/src/service.ts: -------------------------------------------------------------------------------- 1 | export * from '@rgbpp-sdk/service'; 2 | -------------------------------------------------------------------------------- /packages/rgbpp/tests/Group.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { groupNumbersBySum } from '../src/rgbpp/utils/group'; 3 | 4 | describe('Group', () => { 5 | describe('groupNumbersBySum()', () => { 6 | it('[9]', () => { 7 | const target = 10; 8 | const numbers = [9]; 9 | const groups = groupNumbersBySum(numbers, target); 10 | expect(groups.indices).toEqual([[0]]); 11 | expect(groups.numbers).toEqual([[9]]); 12 | }); 13 | it('[9, 1]', () => { 14 | const target = 10; 15 | const numbers = [9, 1]; 16 | const groups = groupNumbersBySum(numbers, target); 17 | expect(groups.indices).toEqual([[0, 1]]); 18 | expect(groups.numbers).toEqual([[9, 1]]); 19 | }); 20 | it('[10, 1]', () => { 21 | const target = 10; 22 | const numbers = [10, 1]; 23 | const groups = groupNumbersBySum(numbers, target); 24 | expect(groups.indices).toEqual([[0], [1]]); 25 | expect(groups.numbers).toEqual([[10], [1]]); 26 | }); 27 | it('[5, 5, 5]', () => { 28 | const target = 10; 29 | const numbers = [5, 5, 5]; 30 | const groups = groupNumbersBySum(numbers, target); 31 | expect(groups.indices).toEqual([[0, 1], [2]]); 32 | expect(groups.numbers).toEqual([[5, 5], [5]]); 33 | }); 34 | it('[1, 1, 1, 1, 1]', () => { 35 | const target = 10; 36 | const numbers = [1, 1, 1, 1, 1]; 37 | const groups = groupNumbersBySum(numbers, target); 38 | expect(groups.indices).toEqual([[0, 1, 2, 3, 4]]); 39 | expect(groups.numbers).toEqual([[1, 1, 1, 1, 1]]); 40 | }); 41 | it('[5, 10, 5]', () => { 42 | const target = 10; 43 | const numbers = [5, 10, 5]; 44 | const groups = groupNumbersBySum(numbers, target); 45 | expect(groups.indices).toEqual([[0, 2], [1]]); 46 | expect(groups.numbers).toEqual([[5, 5], [10]]); 47 | }); 48 | it('[10, 5, 10, 5]', () => { 49 | const target = 10; 50 | const numbers = [10, 5, 10, 5]; 51 | const groups = groupNumbersBySum(numbers, target); 52 | expect(groups.indices).toEqual([[0], [1, 3], [2]]); 53 | expect(groups.numbers).toEqual([[10], [5, 5], [10]]); 54 | }); 55 | it('[11]', () => { 56 | const target = 10; 57 | const numbers = [11]; 58 | const groups = groupNumbersBySum(numbers, target); 59 | expect(groups.indices).toEqual([]); 60 | expect(groups.numbers).toEqual([]); 61 | }); 62 | it('[]', () => { 63 | const target = 10; 64 | const numbers: number[] = []; 65 | const groups = groupNumbersBySum(numbers, target); 66 | expect(groups.indices).toEqual([]); 67 | expect(groups.numbers).toEqual([]); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/rgbpp/tests/shared/account.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NetworkType, 3 | ECPair, 4 | bitcoin, 5 | toXOnly, 6 | remove0x, 7 | tweakSigner, 8 | isP2trScript, 9 | isP2wpkhScript, 10 | networkTypeToNetwork, 11 | } from '@rgbpp-sdk/btc'; 12 | 13 | export interface BtcAccount { 14 | address: string; 15 | scriptPubkey: string; 16 | keyPair: bitcoin.Signer; 17 | payment: bitcoin.Payment; 18 | } 19 | 20 | export function createP2wpkhAccount(privateKey: string, networkType: NetworkType): BtcAccount { 21 | const privateKeyBuffer = Buffer.from(remove0x(privateKey), 'hex'); 22 | const keyPair = ECPair.fromPrivateKey(privateKeyBuffer); 23 | const payment = bitcoin.payments.p2wpkh({ 24 | pubkey: keyPair.publicKey, 25 | network: networkTypeToNetwork(networkType), 26 | }); 27 | 28 | return { 29 | keyPair, 30 | payment, 31 | address: payment.address!, 32 | scriptPubkey: payment.output!.toString('hex'), 33 | }; 34 | } 35 | 36 | export function createP2trAccount(privateKey: string, networkType: NetworkType): BtcAccount { 37 | const privateKeyBuffer = Buffer.from(remove0x(privateKey), 'hex'); 38 | const keyPair = ECPair.fromPrivateKey(privateKeyBuffer); 39 | const payment = bitcoin.payments.p2tr({ 40 | internalPubkey: toXOnly(keyPair.publicKey), 41 | network: networkTypeToNetwork(networkType), 42 | }); 43 | 44 | return { 45 | keyPair, 46 | payment, 47 | address: payment.address!, 48 | scriptPubkey: payment.output!.toString('hex'), 49 | }; 50 | } 51 | 52 | export function signPsbt(psbt: bitcoin.Psbt, account: BtcAccount): bitcoin.Psbt { 53 | // Create a tweaked signer for P2TR 54 | const tweakedSigner = tweakSigner(account.keyPair, { 55 | network: account.payment.network, 56 | }); 57 | 58 | // Sign each input 59 | psbt.data.inputs.forEach((input, index) => { 60 | if (input.witnessUtxo) { 61 | const script = input.witnessUtxo.script.toString('hex'); 62 | if (isP2wpkhScript(script) && script === account.scriptPubkey) { 63 | psbt.signInput(index, account.keyPair); 64 | } 65 | if (isP2trScript(script) && script === account.scriptPubkey) { 66 | psbt.signInput(index, tweakedSigner); 67 | } 68 | } 69 | }); 70 | 71 | return psbt; 72 | } 73 | 74 | export function signAndFinalizePsbt(psbt: bitcoin.Psbt, accounts: BtcAccount[]): bitcoin.Psbt { 75 | for (const account of accounts) { 76 | signPsbt(psbt, account); 77 | } 78 | 79 | return psbt.finalizeAllInputs(); 80 | } 81 | -------------------------------------------------------------------------------- /packages/rgbpp/tests/shared/env.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, NetworkType, networkTypeToConfig } from '@rgbpp-sdk/btc'; 2 | import { BtcAssetsApi } from '@rgbpp-sdk/service'; 3 | import { Collector } from '@rgbpp-sdk/ckb'; 4 | import { z } from 'zod'; 5 | 6 | const EnvSchema = z 7 | .object({ 8 | VITE_IS_MAINNET: z 9 | .enum(['true', 'false']) 10 | .default('false') 11 | .transform((v) => v === 'true'), 12 | VITE_CKB_NODE_URL: z.string().url(), 13 | VITE_CKB_INDEXER_URL: z.string().url(), 14 | VITE_BTC_SERVICE_URL: z.string().url(), 15 | VITE_BTC_SERVICE_TOKEN: z.string(), 16 | VITE_BTC_SERVICE_ORIGIN: z.string(), 17 | }) 18 | .transform((env) => { 19 | return { 20 | IS_MAINNET: env.VITE_IS_MAINNET, 21 | CKB_NODE_URL: env.VITE_CKB_NODE_URL, 22 | CKB_INDEXER_URL: env.VITE_CKB_INDEXER_URL, 23 | BTC_SERVICE_URL: env.VITE_BTC_SERVICE_URL, 24 | BTC_SERVICE_TOKEN: env.VITE_BTC_SERVICE_TOKEN, 25 | BTC_SERVICE_ORIGIN: env.VITE_BTC_SERVICE_ORIGIN, 26 | }; 27 | }); 28 | 29 | /** 30 | * Common 31 | */ 32 | export const env = EnvSchema.parse(process.env); 33 | export const isMainnet = env.IS_MAINNET; 34 | 35 | /** 36 | * BTC 37 | */ 38 | export const btcNetworkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; 39 | export const btcService = BtcAssetsApi.fromToken(env.BTC_SERVICE_URL, env.BTC_SERVICE_TOKEN, env.BTC_SERVICE_ORIGIN); 40 | export const btcSource = new DataSource(btcService, btcNetworkType); 41 | export const btcConfig = networkTypeToConfig(btcNetworkType); 42 | 43 | /** 44 | * CKB 45 | */ 46 | export const ckbCollector = new Collector({ 47 | ckbNodeUrl: env.CKB_NODE_URL, 48 | ckbIndexerUrl: env.CKB_INDEXER_URL, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/rgbpp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "module": "ESNext", 6 | "target": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "strict": true, 9 | "noEmit": true, 10 | "allowJs": true, 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "strictNullChecks": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/rgbpp/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | name: 'rgbpp', 5 | dts: true, 6 | clean: true, 7 | sourcemap: true, 8 | format: ['esm', 'cjs'], 9 | entry: ['src/index.ts', 'src/btc.ts', 'src/ckb.ts', 'src/service.ts'], 10 | }); 11 | -------------------------------------------------------------------------------- /packages/service/.env.example: -------------------------------------------------------------------------------- 1 | VITE_BTC_SERVICE_URL= # URL of the service 2 | VITE_BTC_SERVICE_TOKEN= # JWT token to access the service 3 | VITE_BTC_SERVICE_ORIGIN= # URL representing your token's domain 4 | -------------------------------------------------------------------------------- /packages/service/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @rgbpp-sdk/service 2 | 3 | ## 0.7.0 4 | 5 | ### Minor Changes 6 | 7 | - [#291](https://github.com/utxostack/rgbpp-sdk/pull/291): Support RGB++ compatible xUDT assets([@duanyytop](https://github.com/duanyytop)) 8 | - Add `assets/type` API to the service package 9 | 10 | - [#293](https://github.com/utxostack/rgbpp-sdk/pull/293): Add offline btc data source & ckb collector ([@fghdotio](https://github.com/fghdotio)) 11 | 12 | - [#294](https://github.com/utxostack/rgbpp-sdk/pull/294): Add UTXOAirdropBadge Testnet deployment information ([@duanyytop](https://github.com/duanyytop)) 13 | 14 | - [#298](https://github.com/utxostack/rgbpp-sdk/pull/298): Implement getRgbppSpvProof for OfflineBtcAssetsDataSource ([@fghdotio](https://github.com/fghdotio)) 15 | 16 | - [#303](https://github.com/utxostack/rgbpp-sdk/pull/303): Add offline mode support for compatible xUDT type scripts ([@fghdotio](https://github.com/fghdotio)) 17 | 18 | ### Patch Changes 19 | 20 | - [#305](https://github.com/ckb-cell/rgbpp-sdk/pull/305): Upgrade ckb-sdk-js version ([@duanyytop](https://github.com/duanyytop)) 21 | 22 | ## 0.6.0 23 | 24 | ### Minor Changes 25 | 26 | - [#281](https://github.com/ckb-cell/rgbpp-sdk/pull/281): Upgrade ckb-sdk-js to fix esm and commonjs issues ([@duanyytop](https://github.com/duanyytop)) 27 | 28 | - [#246](https://github.com/ckb-cell/rgbpp-sdk/pull/246): Export ESM packages ([@duanyytop](https://github.com/duanyytop)) 29 | 30 | ## v0.5.0 31 | 32 | ### Minor Changes 33 | 34 | - [#248](https://github.com/ckb-cell/rgbpp-sdk/pull/248): Add `type_script` to the response of `/rgbpp/v1/address/{btc_address}/balance` API, and add `typeHash` to the response of rgbpp assets-related APIs ([@ShookLyngs](https://github.com/ShookLyngs)) 35 | 36 | ## v0.4.0 37 | 38 | ### Minor Changes 39 | 40 | - [#222](https://github.com/ckb-cell/rgbpp-sdk/pull/222): Add BtcAssetsApi.getRgbppApiBalanceByAddress() API for querying RGBPP XUDT balances by a BTC address ([@ShookLyngs](https://github.com/ShookLyngs)) 41 | 42 | ## v0.3.0 43 | 44 | ### Minor Changes 45 | 46 | - [#208](https://github.com/ckb-cell/rgbpp-sdk/pull/208): Adapt btc-assets-api#154, adding new props and return values to the /balance and /unspent APIs ([@ShookLyngs](https://github.com/ShookLyngs)) 47 | 48 | - Add `available_satoshi` and `total_satoshi` to the BtcAssetsApi.getBtcBalance() API 49 | - Add `only_non_rgbpp_utxos` to the props of the BtcAssetsApi.getBtcUtxos() API 50 | - Remove `service.getRgbppAssetsByBtcUtxo()` lines from the DataCollector.collectSatoshi() 51 | - Remove `hasRgbppAssets` related variables/function from the DataCache 52 | 53 | ## v0.2.0 54 | 55 | ### Minor Changes 56 | 57 | - [#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)) 58 | 59 | ### Patch Changes 60 | 61 | - [#181](https://github.com/ckb-cell/rgbpp-sdk/pull/181): add no_cache params to btc/rgbpp service api ([@ahonn](https://github.com/ahonn)) 62 | 63 | ## v0.1.0 64 | 65 | - 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 66 | -------------------------------------------------------------------------------- /packages/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rgbpp-sdk/service", 3 | "version": "0.7.0", 4 | "scripts": { 5 | "test": "vitest", 6 | "build": "tsup", 7 | "lint": "tsc && eslint '{src,tests}/**/*.{js,ts}' && prettier --check '{src,tests}/**/*.{js,ts}'", 8 | "lint:fix": "tsc && eslint --fix '{src,tests}/**/*.{js,ts}' && prettier --write '{src,tests}/**/*.{js,ts}'" 9 | }, 10 | "sideEffects": false, 11 | "main": "./dist/index.js", 12 | "types": "./dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": { 16 | "types": "./dist/index.d.mts", 17 | "default": "./dist/index.mjs" 18 | }, 19 | "require": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dist/index.js" 22 | } 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "files": [ 27 | "src", 28 | "dist" 29 | ], 30 | "dependencies": { 31 | "@ckb-lumos/codec": "0.22.2", 32 | "@ckb-lumos/base": "0.22.2", 33 | "@nervosnetwork/ckb-types": "0.109.5", 34 | "lodash": "^4.17.21" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/service/src/ckb-types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/service/src/error.ts: -------------------------------------------------------------------------------- 1 | import { BtcAssetsApiContext } from './types'; 2 | 3 | export enum ErrorCodes { 4 | UNKNOWN, 5 | 6 | ASSETS_API_RESPONSE_ERROR, 7 | ASSETS_API_UNAUTHORIZED, 8 | ASSETS_API_INVALID_PARAM, 9 | ASSETS_API_RESOURCE_NOT_FOUND, 10 | ASSETS_API_RESPONSE_DECODE_ERROR, 11 | 12 | OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE, 13 | OFFLINE_DATA_SOURCE_SPV_PROOF_NOT_FOUND, 14 | } 15 | 16 | export const ErrorMessages = { 17 | [ErrorCodes.UNKNOWN]: 'Unknown error', 18 | 19 | [ErrorCodes.ASSETS_API_UNAUTHORIZED]: 'BtcAssetsAPI unauthorized, please check your token/origin', 20 | [ErrorCodes.ASSETS_API_INVALID_PARAM]: 'Invalid param(s) was provided to the BtcAssetsAPI', 21 | [ErrorCodes.ASSETS_API_RESPONSE_ERROR]: 'BtcAssetsAPI returned an error', 22 | [ErrorCodes.ASSETS_API_RESOURCE_NOT_FOUND]: 'Resource not found on the BtcAssetsAPI', 23 | [ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR]: 'Failed to decode the response of BtcAssetsAPI', 24 | 25 | [ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE]: 'Method not available for offline data source', 26 | [ErrorCodes.OFFLINE_DATA_SOURCE_SPV_PROOF_NOT_FOUND]: 'SPV proof not found for the given txid and confirmations', 27 | }; 28 | 29 | export class BtcAssetsApiError extends Error { 30 | public code = ErrorCodes.UNKNOWN; 31 | public message: string; 32 | public context?: BtcAssetsApiContext; 33 | 34 | constructor(payload: { code: ErrorCodes; message?: string; context?: BtcAssetsApiContext }) { 35 | const message = payload.message ?? ErrorMessages[payload.code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; 36 | 37 | super(message); 38 | this.message = message; 39 | this.code = payload.code; 40 | this.context = payload.context; 41 | Object.setPrototypeOf(this, BtcAssetsApiError.prototype); 42 | } 43 | 44 | static withComment(code: ErrorCodes, comment?: string, context?: BtcAssetsApiContext): BtcAssetsApiError { 45 | const prefixMessage = ErrorMessages[code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; 46 | const message = comment ? `${prefixMessage}: ${comment}` : undefined; 47 | return new BtcAssetsApiError({ code, message, context }); 48 | } 49 | } 50 | 51 | export class OfflineBtcAssetsDataSourceError extends Error { 52 | public code = ErrorCodes.UNKNOWN; 53 | 54 | constructor(errorCode: ErrorCodes, message?: string) { 55 | const msg = message ?? ErrorMessages[errorCode] ?? ErrorMessages[ErrorCodes.UNKNOWN]; 56 | super(msg); 57 | this.code = errorCode; 58 | Object.setPrototypeOf(this, OfflineBtcAssetsDataSourceError.prototype); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/service/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './error'; 3 | export * from './utils'; 4 | export * from './service'; 5 | -------------------------------------------------------------------------------- /packages/service/src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './service'; 3 | export * from './offline-service'; 4 | -------------------------------------------------------------------------------- /packages/service/src/types/base.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type Json = Record; 3 | 4 | export interface BaseApis { 5 | request(route: string, options?: BaseApiRequestOptions): Promise; 6 | post(route: string, options?: BaseApiRequestOptions): Promise; 7 | generateToken(): Promise; 8 | init(force?: boolean): Promise; 9 | } 10 | 11 | export interface BaseApiRequestOptions extends RequestInit { 12 | params?: Json; 13 | method?: 'GET' | 'POST'; 14 | requireToken?: boolean; 15 | allow404?: boolean; 16 | } 17 | 18 | export interface BtcAssetsApiToken { 19 | token: string; 20 | } 21 | 22 | export interface BtcAssetsApiContext { 23 | request: { 24 | url: string; 25 | body?: Json; 26 | params?: Json; 27 | }; 28 | response: { 29 | status: number; 30 | data?: Json | string; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/service/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './btc'; 3 | export * from './rgbpp'; 4 | -------------------------------------------------------------------------------- /packages/service/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if target string is a valid domain. 3 | * @exmaple 4 | * isDomain('google.com') // => true 5 | * isDomain('https://google.com') // => false 6 | * isDomain('localhost') // => false 7 | * isDomain('localhost', true) // => true 8 | */ 9 | export function isDomain(domain: string, allowLocalhost?: boolean): boolean { 10 | if (allowLocalhost && domain === 'localhost') { 11 | return true; 12 | } 13 | const regex = /^(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,}$/; 14 | return regex.test(domain); 15 | } 16 | -------------------------------------------------------------------------------- /packages/service/tests/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { BtcAssetsApiError, ErrorCodes, isDomain } from '../src'; 3 | 4 | describe('Utils', () => { 5 | it('isDomain()', () => { 6 | expect(isDomain('google.com')).toBe(true); 7 | expect(isDomain('mail.google.com')).toBe(true); 8 | expect(isDomain('https://google.com')).toBe(false); 9 | expect(isDomain('google.com/path')).toBe(false); 10 | expect(isDomain('google')).toBe(false); 11 | 12 | expect(isDomain('localhost', true)).toBe(true); 13 | expect(isDomain('localhost')).toBe(false); 14 | }); 15 | it('BtcAssetsApiError with context', () => { 16 | try { 17 | throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_INVALID_PARAM, 'param1, param2', { 18 | request: { 19 | url: 'https://api.com/api/v1/route', 20 | params: { 21 | param1: 'value1', 22 | param2: 'value2', 23 | }, 24 | }, 25 | response: { 26 | status: 400, 27 | data: { 28 | error: -10, 29 | message: 'error message about -10', 30 | }, 31 | }, 32 | }); 33 | } catch (e) { 34 | expect(e).toBeInstanceOf(BtcAssetsApiError); 35 | expect(e.toString()).toEqual('Error: Invalid param(s) was provided to the BtcAssetsAPI: param1, param2'); 36 | 37 | if (e instanceof BtcAssetsApiError) { 38 | expect(e.code).toEqual(ErrorCodes.ASSETS_API_INVALID_PARAM); 39 | expect(e.message).toEqual('Invalid param(s) was provided to the BtcAssetsAPI: param1, param2'); 40 | 41 | expect(e.context).toBeDefined(); 42 | expect(e.context.request).toBeDefined(); 43 | expect(e.context.request.url).toEqual('https://api.com/api/v1/route'); 44 | expect(e.context.request.params).toEqual({ 45 | param1: 'value1', 46 | param2: 'value2', 47 | }); 48 | 49 | expect(e.context.response).toBeDefined(); 50 | expect(e.context.response.status).toEqual(400); 51 | expect(e.context.response.data).toEqual({ 52 | error: -10, 53 | message: 'error message about -10', 54 | }); 55 | } 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Bundler", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "strict": true, 8 | "noEmit": true, 9 | "allowJs": true, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "strictNullChecks": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/service/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | name: '@rgbpp-sdk/service', 5 | dts: true, 6 | clean: true, 7 | sourcemap: true, 8 | format: ['esm', 'cjs'], 9 | entry: ['src/index.ts'], 10 | }); 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "apps/*" 4 | - "examples/*" 5 | - "tests/*" 6 | -------------------------------------------------------------------------------- /tests/rgbpp/env.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blake160, 3 | bytesToHex, 4 | privateKeyToPublicKey, 5 | scriptToAddress, 6 | systemScripts, 7 | } from '@nervosnetwork/ckb-sdk-utils'; 8 | import { DataSource, BtcAssetsApi, AddressType } from 'rgbpp'; 9 | import { ECPair, ECPairInterface, bitcoin, NetworkType } from 'rgbpp/btc'; 10 | import dotenv from 'dotenv'; 11 | import { Collector } from 'rgbpp/ckb'; 12 | import { createBtcAccount } from '../../examples/rgbpp/shared/btc-account'; 13 | 14 | dotenv.config({ path: __dirname + '/.env' }); 15 | 16 | export const isMainnet = false; 17 | 18 | export const BTC_TESTNET_TYPE = 'Testnet3'; 19 | 20 | export const collector = new Collector({ 21 | ckbNodeUrl: 'https://testnet.ckb.dev/rpc', 22 | ckbIndexerUrl: 'https://testnet.ckb.dev/indexer', 23 | }); 24 | export const CKB_PRIVATE_KEY = process.env.INTEGRATION_CKB_PRIVATE_KEY!; 25 | const secp256k1Lock: CKBComponents.Script = { 26 | ...systemScripts.SECP256K1_BLAKE160, 27 | args: bytesToHex(blake160(privateKeyToPublicKey(CKB_PRIVATE_KEY))), 28 | }; 29 | export const ckbAddress = scriptToAddress(secp256k1Lock, isMainnet); 30 | 31 | export const BTC_PRIVATE_KEY = process.env.INTEGRATION_BTC_PRIVATE_KEY!; 32 | export const BTC_SERVICE_URL = process.env.VITE_SERVICE_URL!; 33 | export const BTC_SERVICE_TOKEN = process.env.VITE_SERVICE_TOKEN!; 34 | export const BTC_SERVICE_ORIGIN = process.env.VITE_SERVICE_ORIGIN!; 35 | 36 | const network = isMainnet ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; 37 | export const btcKeyPair: ECPairInterface = ECPair.fromPrivateKey(Buffer.from(BTC_PRIVATE_KEY, 'hex'), { network }); 38 | 39 | // Read more about the available address types: 40 | // - P2WPKH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh 41 | // - P2TR: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki 42 | const addressType = AddressType.P2WPKH; 43 | const networkType = isMainnet ? NetworkType.MAINNET : NetworkType.TESTNET; 44 | export const btcAccount = createBtcAccount(BTC_PRIVATE_KEY, addressType, networkType); 45 | 46 | export const btcService = BtcAssetsApi.fromToken(BTC_SERVICE_URL, BTC_SERVICE_TOKEN, BTC_SERVICE_ORIGIN); 47 | export const btcDataSource = new DataSource(btcService, networkType); 48 | -------------------------------------------------------------------------------- /tests/rgbpp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgbpp-integration-tests", 3 | "version": "0.1.0", 4 | "description": "Test the entire process of RGBPP to ensure the proper functioning of the rgbpp-sdk package.", 5 | "private": true, 6 | "type": "commonjs", 7 | "scripts": { 8 | "format": "prettier --write '**/*.{js,ts}'", 9 | "lint": "tsc && eslint . && prettier --check '**/*.{js,ts}'", 10 | "lint:fix": "tsc && eslint --fix --ext .js,.ts . && prettier --write '**/*.{js,ts}'", 11 | "integration:xudt": "npx tsx shared/prepare-utxo.ts && npx tsx xudt/xudt-on-ckb/1-issue-xudt.ts && npx tsx xudt/xudt-on-ckb/2-transfer-xudt.ts && npx tsx xudt/1-ckb-leap-btc.ts && npx tsx xudt/2-btc-transfer.ts && npx tsx xudt/3-btc-leap-ckb.ts && npx tsx xudt/btc-transfer-all/1-btc-transfer-all.ts", 12 | "integration:spore": "npx tsx shared/prepare-utxo.ts && npx tsx spore/launch/1-prepare-cluster.ts && npx tsx spore/launch/2-create-cluster.ts && npx tsx spore/launch/3-create-spores.ts && npx tsx spore/4-transfer-spore.ts && npx tsx spore/5-leap-spore-to-ckb.ts", 13 | "integration:compatible-xudt": "npx tsx shared/prepare-utxo.ts && npx tsx xudt/compatible-xudt/1-ckb-leap-btc.ts && npx tsx xudt/compatible-xudt/2-btc-transfer.ts && npx tsx xudt/compatible-xudt/3-btc-leap-ckb.ts" 14 | }, 15 | "dependencies": { 16 | "@nervosnetwork/ckb-sdk-utils": "0.109.5", 17 | "rgbpp": "workspace:*", 18 | "zx": "^8.0.2" 19 | }, 20 | "devDependencies": { 21 | "dotenv": "^16.4.5", 22 | "@types/dotenv": "^8.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/rgbpp/shared/prepare-utxo.ts: -------------------------------------------------------------------------------- 1 | import { sendBtc } from 'rgbpp/btc'; 2 | import { getFastestFeeRate, writeStepLog } from './utils'; 3 | import { BtcAssetsApiError } from 'rgbpp/service'; 4 | import { btcAccount, btcDataSource, btcKeyPair, btcService } from '../env'; 5 | 6 | const prepareUtxo = async (index: string | number) => { 7 | console.log(btcAccount.from); 8 | const feeRate = await getFastestFeeRate(); 9 | console.log('feeRate = ', feeRate); 10 | // Send BTC tx 11 | const psbt = await sendBtc({ 12 | from: btcAccount.from!, 13 | tos: [ 14 | { 15 | address: btcAccount.from!, 16 | value: 546, 17 | minUtxoSatoshi: 546, 18 | }, 19 | ], 20 | feeRate: feeRate, 21 | source: btcDataSource, 22 | }); 23 | 24 | // Sign & finalize inputs 25 | psbt.signAllInputs(btcKeyPair); 26 | psbt.finalizeAllInputs(); 27 | 28 | // Broadcast transaction 29 | const tx = psbt.extractTransaction(); 30 | console.log(tx.toHex()); 31 | 32 | const { txid: btcTxId } = await btcService.sendBtcTransaction(tx.toHex()); 33 | console.log(`explorer: https://mempool.space/testnet/tx/${btcTxId}`); 34 | 35 | writeStepLog(String(index), { 36 | txid: btcTxId, 37 | index: 0, 38 | }); 39 | 40 | const interval = setInterval(async () => { 41 | try { 42 | console.log('Waiting for BTC tx to be confirmed'); 43 | const tx = await btcService.getBtcTransaction(btcTxId); 44 | if (tx.status.confirmed) { 45 | clearInterval(interval); 46 | console.info(`Utxo is confirmed ${btcTxId}:0`); 47 | } 48 | } catch (error) { 49 | if (!(error instanceof BtcAssetsApiError)) { 50 | console.error(error); 51 | } 52 | } 53 | }, 20 * 1000); 54 | }; 55 | 56 | prepareUtxo('prepare-utxo'); 57 | -------------------------------------------------------------------------------- /tests/rgbpp/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { btcService } from '../env'; 5 | 6 | export const network = 'testnet'; 7 | 8 | export async function getFastestFeeRate() { 9 | const fees = await btcService.getBtcRecommendedFeeRates(); 10 | // return fees.fastestFee + 1000; 11 | return Math.ceil(fees.fastestFee * 1.1); 12 | } 13 | 14 | export async function writeStepLog(step: string, data: string | object) { 15 | const file = path.join(__dirname, `../${network}/step-${step}.log`); 16 | if (typeof data !== 'string') { 17 | data = JSON.stringify(data); 18 | } 19 | 20 | fs.writeFileSync(file, data); 21 | } 22 | 23 | export function readStepLog(step: string) { 24 | const file = path.join(__dirname, `../${network}/step-${step}.log`); 25 | const retryInterval = 10000; 26 | const maxRetries = 3; 27 | 28 | for (let i = 0; i < 3; i++) { 29 | try { 30 | const data = fs.readFileSync(file, 'utf8'); 31 | return JSON.parse(data); 32 | } catch (error) { 33 | console.error(`Failed to read file ${file} on attempt ${i + 1}: ${error}`); 34 | if (i < maxRetries - 1) { 35 | console.log(`Waiting ${retryInterval / 1000} seconds before retrying...`); 36 | setTimeout(() => {}, retryInterval); 37 | } 38 | } 39 | } 40 | 41 | console.error(`Failed to read file ${file} after ${maxRetries} attempts.`); 42 | return null; 43 | } 44 | -------------------------------------------------------------------------------- /tests/rgbpp/spore/launch/0-cluster-info.ts: -------------------------------------------------------------------------------- 1 | import { RawClusterData } from 'rgbpp/ckb'; 2 | 3 | export const CLUSTER_DATA: RawClusterData = { 4 | name: 'Cluster name', 5 | description: 'Description of the cluster', 6 | }; 7 | -------------------------------------------------------------------------------- /tests/rgbpp/spore/launch/1-prepare-cluster.ts: -------------------------------------------------------------------------------- 1 | import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { 3 | MAX_FEE, 4 | NoLiveCellError, 5 | SECP256K1_WITNESS_LOCK_SIZE, 6 | append0x, 7 | buildRgbppLockArgs, 8 | calculateRgbppClusterCellCapacity, 9 | calculateTransactionFee, 10 | genRgbppLockScript, 11 | getSecp256k1CellDep, 12 | } from 'rgbpp/ckb'; 13 | import { ckbAddress, isMainnet, collector, CKB_PRIVATE_KEY, BTC_TESTNET_TYPE } from '../../env'; 14 | import { CLUSTER_DATA } from './0-cluster-info'; 15 | import { readStepLog } from '../../shared/utils'; 16 | 17 | const prepareClusterCell = async ({ outIndex, btcTxId }: { outIndex: number; btcTxId: string }) => { 18 | const { retry } = await import('zx'); 19 | await retry(20, '10s', async () => { 20 | const masterLock = addressToScript(ckbAddress); 21 | console.log('ckb address: ', ckbAddress); 22 | 23 | // The capacity required to launch cells is determined by the token info cell capacity, and transaction fee. 24 | const clusterCellCapacity = calculateRgbppClusterCellCapacity(CLUSTER_DATA); 25 | 26 | let emptyCells = await collector.getCells({ 27 | lock: masterLock, 28 | }); 29 | if (!emptyCells || emptyCells.length === 0) { 30 | throw new NoLiveCellError('The address has no empty cells'); 31 | } 32 | emptyCells = emptyCells.filter((cell) => !cell.output.type); 33 | 34 | const txFee = MAX_FEE; 35 | const { inputs, sumInputsCapacity } = collector.collectInputs(emptyCells, clusterCellCapacity, txFee); 36 | 37 | const outputs: CKBComponents.CellOutput[] = [ 38 | { 39 | lock: genRgbppLockScript(buildRgbppLockArgs(outIndex, btcTxId), isMainnet, BTC_TESTNET_TYPE), 40 | capacity: append0x(clusterCellCapacity.toString(16)), 41 | }, 42 | ]; 43 | let changeCapacity = sumInputsCapacity - clusterCellCapacity; 44 | outputs.push({ 45 | lock: masterLock, 46 | capacity: append0x(changeCapacity.toString(16)), 47 | }); 48 | const outputsData = ['0x', '0x']; 49 | 50 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 51 | const witnesses = inputs.map((_, index) => (index === 0 ? emptyWitness : '0x')); 52 | 53 | const cellDeps = [getSecp256k1CellDep(isMainnet)]; 54 | 55 | const unsignedTx = { 56 | version: '0x0', 57 | cellDeps, 58 | headerDeps: [], 59 | inputs, 60 | outputs, 61 | outputsData, 62 | witnesses, 63 | }; 64 | 65 | const txSize = getTransactionSize(unsignedTx) + SECP256K1_WITNESS_LOCK_SIZE; 66 | const estimatedTxFee = calculateTransactionFee(txSize); 67 | changeCapacity -= estimatedTxFee; 68 | unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); 69 | 70 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 71 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 72 | 73 | console.info(`Cluster cell has been prepared and the tx hash ${txHash}`); 74 | console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); 75 | }); 76 | }; 77 | 78 | // Please use your real BTC UTXO information on the BTC Testnet 79 | prepareClusterCell({ 80 | outIndex: readStepLog('prepare-utxo').index, 81 | btcTxId: readStepLog('prepare-utxo').txid, 82 | }); 83 | -------------------------------------------------------------------------------- /tests/rgbpp/testnet/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utxostack/rgbpp-sdk/2d547132ede28616647e87d603aea63daada4841/tests/rgbpp/testnet/.gitkeep -------------------------------------------------------------------------------- /tests/rgbpp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "esnext"], 5 | "module": "esnext", 6 | "composite": false, 7 | "resolveJsonModule": true, 8 | "strictNullChecks": true, 9 | "noEmit": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "inlineSources": false, 15 | "isolatedModules": true, 16 | "moduleResolution": "Bundler", 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "preserveWatchOutput": true, 20 | "skipLibCheck": true, 21 | "strict": true 22 | }, 23 | "include": ["spore", "xudt", "shared"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tests/rgbpp/xudt/1-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; 4 | import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } from '../env'; 5 | import { readStepLog } from '../shared/utils'; 6 | 7 | interface LeapToBtcParams { 8 | outIndex: number; 9 | btcTxId: string; 10 | xudtTypeArgs: string; 11 | transferAmount: bigint; 12 | } 13 | 14 | const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmount }: LeapToBtcParams) => { 15 | const { retry } = await import('zx'); 16 | await retry(20, '10s', async () => { 17 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 18 | 19 | // Warning: Please replace with your real xUDT type script here 20 | const xudtType: CKBComponents.Script = { 21 | ...getXudtTypeScript(isMainnet), 22 | args: xudtTypeArgs, 23 | }; 24 | 25 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 26 | collector, 27 | fromCkbAddress: ckbAddress, 28 | toRgbppLockArgs, 29 | xudtTypeBytes: serializeScript(xudtType), 30 | transferAmount, 31 | btcTestnetType: BTC_TESTNET_TYPE, 32 | }); 33 | 34 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 35 | const unsignedTx: CKBComponents.RawTransactionToSign = { 36 | ...ckbRawTx, 37 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 38 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 39 | }; 40 | 41 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 42 | 43 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 44 | console.info(`Rgbpp asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); 45 | console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); 46 | }); 47 | }; 48 | 49 | // Use your real BTC UTXO information on the BTC Testnet 50 | leapFromCkbToBtc({ 51 | outIndex: readStepLog('prepare-utxo').index, 52 | btcTxId: readStepLog('prepare-utxo').txid, 53 | xudtTypeArgs: readStepLog('xUDT-type-script').args, 54 | transferAmount: BigInt(800_0000_0000), 55 | }); 56 | -------------------------------------------------------------------------------- /tests/rgbpp/xudt/compatible-xudt/1-ckb-leap-btc.ts: -------------------------------------------------------------------------------- 1 | import { serializeScript } from '@nervosnetwork/ckb-sdk-utils'; 2 | import { genCkbJumpBtcVirtualTx } from 'rgbpp'; 3 | import { getSecp256k1CellDep, buildRgbppLockArgs, CompatibleXUDTRegistry } from 'rgbpp/ckb'; 4 | import { CKB_PRIVATE_KEY, isMainnet, collector, ckbAddress, BTC_TESTNET_TYPE } from '../../env'; 5 | import { readStepLog } from '../../shared/utils'; 6 | 7 | interface LeapToBtcParams { 8 | outIndex: number; 9 | btcTxId: string; 10 | compatibleXudtTypeScript: CKBComponents.Script; 11 | transferAmount: bigint; 12 | } 13 | 14 | const leapFromCkbToBtc = async ({ outIndex, btcTxId, compatibleXudtTypeScript, transferAmount }: LeapToBtcParams) => { 15 | const { retry } = await import('zx'); 16 | await retry(20, '10s', async () => { 17 | const toRgbppLockArgs = buildRgbppLockArgs(outIndex, btcTxId); 18 | 19 | // Refresh the cache by fetching the latest compatible xUDT list from the specified URL. 20 | // The default URL is: 21 | // https://raw.githubusercontent.com/utxostack/typeid-contract-cell-deps/main/compatible-udt.json 22 | // You can set your own trusted URL to fetch the compatible xUDT list. 23 | // await CompatibleXUDTRegistry.refreshCache("https://your-own-trusted-compatible-xudt-url"); 24 | await CompatibleXUDTRegistry.refreshCache(); 25 | 26 | const ckbRawTx = await genCkbJumpBtcVirtualTx({ 27 | collector, 28 | fromCkbAddress: ckbAddress, 29 | toRgbppLockArgs, 30 | xudtTypeBytes: serializeScript(compatibleXudtTypeScript), 31 | transferAmount, 32 | btcTestnetType: BTC_TESTNET_TYPE, 33 | }); 34 | 35 | const emptyWitness = { lock: '', inputType: '', outputType: '' }; 36 | const unsignedTx: CKBComponents.RawTransactionToSign = { 37 | ...ckbRawTx, 38 | cellDeps: [...ckbRawTx.cellDeps, getSecp256k1CellDep(isMainnet)], 39 | witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], 40 | }; 41 | 42 | const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); 43 | 44 | const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); 45 | console.info(`Rgbpp compatible xUDT asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); 46 | console.info(`explorer: https://pudge.explorer.nervos.org/transaction/${txHash}`); 47 | }); 48 | }; 49 | 50 | // Use your real BTC UTXO information on the BTC Testnet 51 | leapFromCkbToBtc({ 52 | outIndex: readStepLog('prepare-utxo').index, 53 | btcTxId: readStepLog('prepare-utxo').txid, 54 | compatibleXudtTypeScript: { 55 | codeHash: '0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a', 56 | hashType: 'type', 57 | args: '0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b', 58 | }, 59 | transferAmount: BigInt(100_0000), 60 | }); 61 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | watch: false, 6 | reporters: ['verbose'], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /vitest.workspace.mts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'packages/*', 3 | 'apps/*', 4 | ]; 5 | --------------------------------------------------------------------------------