├── .changeset ├── README.md ├── beige-penguins-melt.md ├── brown-carpets-learn.md ├── config.json ├── early-snakes-tan.md ├── eleven-dots-own.md ├── grumpy-schools-learn.md ├── heavy-falcons-wink.md ├── honest-rice-vanish.md ├── itchy-goats-beam.md ├── itchy-plants-kick.md ├── khaki-elephants-behave.md ├── kind-donkeys-lick.md ├── long-lobsters-walk.md ├── nine-bottles-report.md ├── old-oranges-invite.md ├── polite-donuts-begin.md ├── poor-pumpkins-learn.md ├── pre.json ├── pretty-lobsters-count.md ├── proud-phones-camp.md ├── rare-coins-notice.md ├── shaggy-snakes-collect.md ├── slimy-peas-share.md ├── slow-tools-flow.md ├── small-lobsters-jam.md ├── spotty-islands-sort.md ├── stale-ghosts-lie.md ├── strange-forks-brake.md ├── ten-knives-join.md ├── thirty-bugs-applaud.md ├── three-cows-unite.md └── twenty-planets-rest.md ├── .github └── workflows │ ├── release.yml │ └── test.yaml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── assets │ └── readme-banner.webp ├── core │ ├── composed-apis.md │ ├── joint-apis.md │ ├── setup.md │ └── utilities.md ├── recipes │ ├── capacity-margin.md │ ├── configure-spore-config.md │ ├── construct-transaction.md │ ├── create-immortal-spore.md │ └── handle-cell-data.md └── resources │ ├── demos.md │ └── examples.md ├── examples ├── acp │ ├── README.md │ ├── apis │ │ ├── createAcpCluster.ts │ │ └── createSporeWithAcpCluster.ts │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── config.ts │ │ └── wallet.ts ├── omnilock │ ├── README.md │ ├── acp │ │ ├── createAcpCluster.ts │ │ └── createSporeWithAcpCluster.ts │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── config.ts │ │ └── wallet.ts ├── secp256k1 │ ├── README.md │ ├── apis │ │ ├── createCluster.ts │ │ ├── createClusterAgent.ts │ │ ├── createClusterProxy.ts │ │ ├── createSpore.ts │ │ ├── createSporeWithCluster.ts │ │ ├── createSporeWithClusterAgent.ts │ │ ├── meltClusterAgent.ts │ │ ├── meltClusterProxy.ts │ │ ├── meltSpore.ts │ │ ├── transferCluster.ts │ │ ├── transferClusterAgent.ts │ │ ├── transferClusterProxy.ts │ │ └── transferSpore.ts │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── config.ts │ │ └── wallet.ts └── shared │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── test.jpg │ └── tsconfig.json ├── package.json ├── packages └── core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── __tests__ │ │ ├── Buffer.test.ts │ │ ├── Capacity.test.ts │ │ ├── Cluster.test.ts │ │ ├── ClusterProxyAgent.test.ts │ │ ├── Codec.test.ts │ │ ├── ContentType.test.ts │ │ ├── MimeType.test.ts │ │ ├── MultipartContent.test.ts │ │ ├── Multiple.test.ts │ │ ├── Mutant.test.ts │ │ ├── RetryWork.test.ts │ │ ├── Spore.test.ts │ │ ├── SporeConfig.test.ts │ │ ├── Vitest.test.ts │ │ ├── helpers │ │ │ ├── account.ts │ │ │ ├── check.ts │ │ │ ├── combine.ts │ │ │ ├── config.ts │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ ├── record.ts │ │ │ ├── retry.ts │ │ │ └── wallet.ts │ │ ├── resources │ │ │ ├── firstOutputMutant.lua │ │ │ ├── immortalMutant.lua │ │ │ ├── mustTransferMutant.lua │ │ │ ├── noTransferMutant.lua │ │ │ └── test.jpg │ │ └── shared │ │ │ ├── env.ts │ │ │ ├── index.ts │ │ │ └── record.ts │ ├── api │ │ ├── composed │ │ │ ├── cluster │ │ │ │ ├── createCluster.ts │ │ │ │ └── transferCluster.ts │ │ │ ├── clusterAgent │ │ │ │ ├── createClusterAgent.ts │ │ │ │ ├── meltClusterAgent.ts │ │ │ │ └── transferClusterAgent.ts │ │ │ ├── clusterProxy │ │ │ │ ├── createClusterProxy.ts │ │ │ │ ├── meltClusterProxy.ts │ │ │ │ └── transferClusterProxy.ts │ │ │ ├── mutant │ │ │ │ ├── createMutant.ts │ │ │ │ └── transferMutant.ts │ │ │ └── spore │ │ │ │ ├── createSpore.ts │ │ │ │ ├── meltSpore.ts │ │ │ │ ├── meltThenCreateSpore.ts │ │ │ │ └── transferSpore.ts │ │ ├── index.ts │ │ └── joints │ │ │ ├── cluster │ │ │ ├── getCluster.ts │ │ │ ├── injectLiveClusterCell.ts │ │ │ ├── injectLiveClusterReference.ts │ │ │ ├── injectNewClusterIds.ts │ │ │ └── injectNewClusterOutput.ts │ │ │ ├── clusterAgent │ │ │ ├── getClusterAgent.ts │ │ │ ├── injectLiveClusterAgentCell.ts │ │ │ ├── injectLiveClusterAgentReference.ts │ │ │ └── injectNewClusterAgentOutput.ts │ │ │ ├── clusterProxy │ │ │ ├── getClusterProxy.ts │ │ │ ├── injectLiveClusterProxyCell.ts │ │ │ ├── injectLiveClusterProxyReference.ts │ │ │ ├── injectNewClusterProxyIds.ts │ │ │ └── injectNewClusterProxyOutput.ts │ │ │ ├── mutant │ │ │ ├── getMutant.ts │ │ │ ├── injectLiveMutantCell.ts │ │ │ ├── injectLiveMutantReferences.ts │ │ │ ├── injectNewMutantIds.ts │ │ │ └── injectNewMutantOutput.ts │ │ │ └── spore │ │ │ ├── getSpore.ts │ │ │ ├── injectLiveSporeCell.ts │ │ │ ├── injectNewSporeIds.ts │ │ │ └── injectNewSporeOutput.ts │ ├── cobuild │ │ ├── action │ │ │ ├── cluster │ │ │ │ ├── createCluster.ts │ │ │ │ ├── referenceCluster.ts │ │ │ │ └── transferCluster.ts │ │ │ ├── clusterAgent │ │ │ │ ├── createClusterAgent.ts │ │ │ │ ├── meltClusterAgent.ts │ │ │ │ ├── referenceClusterAgent.ts │ │ │ │ └── transferClusterAgent.ts │ │ │ ├── clusterProxy │ │ │ │ ├── createClusterProxy.ts │ │ │ │ ├── meltClusterProxy.ts │ │ │ │ ├── referenceClusterProxy.ts │ │ │ │ └── transferClusterProxy.ts │ │ │ └── spore │ │ │ │ ├── createSpore.ts │ │ │ │ ├── meltSpore.ts │ │ │ │ └── transferSpore.ts │ │ ├── base │ │ │ ├── buildingPacket.ts │ │ │ ├── resolvedInputs.ts │ │ │ ├── sporeScriptInfo.ts │ │ │ └── witnessLayout.ts │ │ ├── codec │ │ │ ├── buildingPacket.ts │ │ │ ├── sporeAction.ts │ │ │ └── witnessLayout.ts │ │ └── index.ts │ ├── codec │ │ ├── cluster.ts │ │ ├── clusterAgent.ts │ │ ├── clusterProxy.ts │ │ ├── index.ts │ │ ├── mutant.ts │ │ ├── spore.ts │ │ └── utils.ts │ ├── config │ │ ├── cache.ts │ │ ├── config.ts │ │ ├── hash.ts │ │ ├── index.ts │ │ ├── predefined.ts │ │ ├── script.ts │ │ └── types.ts │ ├── env.d.ts │ ├── helpers │ │ ├── address.ts │ │ ├── buffer.ts │ │ ├── capacity.ts │ │ ├── cell.ts │ │ ├── cellDep.ts │ │ ├── contentType.ts │ │ ├── fee.ts │ │ ├── index.ts │ │ ├── lockProxy.ts │ │ ├── mimeType.ts │ │ ├── multipartContent.ts │ │ ├── retryWork.ts │ │ ├── script.ts │ │ ├── transaction.ts │ │ ├── typeId.ts │ │ └── witness.ts │ ├── index.ts │ └── types │ │ ├── async.ts │ │ ├── blockchain.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.mts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.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/beige-penguins-melt.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | passing feeRate to payFeeByOutput method 6 | -------------------------------------------------------------------------------- /.changeset/brown-carpets-learn.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | add feerate to createMultipleSpores 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@spore-sdk/*"]], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/early-snakes-tan.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Fix typo of the "assertTransactionSkeletonSize" API 6 | -------------------------------------------------------------------------------- /.changeset/eleven-dots-own.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Support Mutant related features 6 | -------------------------------------------------------------------------------- /.changeset/grumpy-schools-learn.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | fix a transaction fee calculation bug in `meltThenCreateSpore` method 6 | -------------------------------------------------------------------------------- /.changeset/heavy-falcons-wink.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | adapt for mutant functionality and add new method for spore migration 6 | -------------------------------------------------------------------------------- /.changeset/honest-rice-vanish.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | enable interface `createSpore` and `meltThenCreateSpore` to accept input cells pre-injection 6 | add `createMultipleSpores` interface to create multiple spores in one transaction 7 | -------------------------------------------------------------------------------- /.changeset/itchy-goats-beam.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | BREAKING CHANGE: Replaced v2 preview contracts 6 | -------------------------------------------------------------------------------- /.changeset/itchy-plants-kick.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Fix spore/cluster query logic, should validate target id before querying 6 | -------------------------------------------------------------------------------- /.changeset/khaki-elephants-behave.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | fix vulnerability in getSporeById interface 6 | -------------------------------------------------------------------------------- /.changeset/kind-donkeys-lick.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Support ClusterProxy and ClusterAgent type cells 6 | -------------------------------------------------------------------------------- /.changeset/long-lobsters-walk.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | solve bigint comparasion bug 6 | -------------------------------------------------------------------------------- /.changeset/nine-bottles-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Support finding SporeScripts by predefined tags 6 | -------------------------------------------------------------------------------- /.changeset/old-oranges-invite.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Remove "fromInfos" prop from the "meltSpore" API 6 | -------------------------------------------------------------------------------- /.changeset/polite-donuts-begin.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Add multipart content support 6 | -------------------------------------------------------------------------------- /.changeset/poor-pumpkins-learn.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | fix: injectNeededCapacity rare border capacity issue (#133) 6 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "alpha", 4 | "initialVersions": { 5 | "@spore-sdk/core": "0.2.1" 6 | }, 7 | "changesets": [ 8 | "beige-penguins-melt", 9 | "brown-carpets-learn", 10 | "early-snakes-tan", 11 | "eleven-dots-own", 12 | "grumpy-schools-learn", 13 | "heavy-falcons-wink", 14 | "honest-rice-vanish", 15 | "itchy-goats-beam", 16 | "itchy-plants-kick", 17 | "khaki-elephants-behave", 18 | "kind-donkeys-lick", 19 | "long-lobsters-walk", 20 | "nine-bottles-report", 21 | "old-oranges-invite", 22 | "polite-donuts-begin", 23 | "poor-pumpkins-learn", 24 | "pretty-lobsters-count", 25 | "proud-phones-camp", 26 | "rare-coins-notice", 27 | "shaggy-snakes-collect", 28 | "slimy-peas-share", 29 | "slow-tools-flow", 30 | "small-lobsters-jam", 31 | "spotty-islands-sort", 32 | "stale-ghosts-lie", 33 | "strange-forks-brake", 34 | "ten-knives-join", 35 | "thirty-bugs-applaud", 36 | "three-cows-unite", 37 | "twenty-planets-rest" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.changeset/pretty-lobsters-count.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | support exact and power mode of min_payment 6 | -------------------------------------------------------------------------------- /.changeset/proud-phones-camp.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Fix duplicated capacity collection in the "createSpore" API 6 | -------------------------------------------------------------------------------- /.changeset/rare-coins-notice.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | fix a bug for Cluster's `lockProxy` mode identification 6 | -------------------------------------------------------------------------------- /.changeset/shaggy-snakes-collect.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | add `feerate` parameter to all of interfaces 6 | -------------------------------------------------------------------------------- /.changeset/slimy-peas-share.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Add predefined SporeConfig for Mainnet 6 | -------------------------------------------------------------------------------- /.changeset/slow-tools-flow.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Remove minPayment prop from the transferClusterProxy API 6 | -------------------------------------------------------------------------------- /.changeset/small-lobsters-jam.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Add new spore type script version to support more contract features 6 | -------------------------------------------------------------------------------- /.changeset/spotty-islands-sort.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': minor 3 | --- 4 | 5 | export co-build interfaces and able to skip checking content-type 6 | -------------------------------------------------------------------------------- /.changeset/stale-ghosts-lie.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Fix and optimize the logic of capacity collection 6 | -------------------------------------------------------------------------------- /.changeset/strange-forks-brake.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | BREAKING CHANGE: Replaced v2 contracts with a v1-compatible preview version 6 | -------------------------------------------------------------------------------- /.changeset/ten-knives-join.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | slipt co-build generation interfaces to export pure assembly functions 6 | -------------------------------------------------------------------------------- /.changeset/thirty-bugs-applaud.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Support selecting v1/v2 version when creating clusters 6 | -------------------------------------------------------------------------------- /.changeset/three-cows-unite.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': patch 3 | --- 4 | 5 | Support lock proxy in spore creation 6 | -------------------------------------------------------------------------------- /.changeset/twenty-planets-rest.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@spore-sdk/core': minor 3 | --- 4 | 5 | Support basic Cobuild feature with legacy locks 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Release packages to NPM and GitHub. 2 | 3 | name: Release 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | 24 | - uses: pnpm/action-setup@v3 25 | name: Install pnpm 26 | with: 27 | version: 8 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@v3 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 --no-frozen-lockfile 45 | 46 | - name: Create Release Pull Request or Publish to npm 47 | id: changesets 48 | uses: changesets/action@v1 49 | with: 50 | publish: pnpm run release:packages 51 | env: 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # Test the functionality of the spore-sdk packages. 2 | 3 | name: Test 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | 12 | jobs: 13 | devnet: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout spore-sdk 17 | uses: actions/checkout@v4 18 | with: 19 | path: spore-sdk 20 | 21 | - name: Checkout spore-devenv 22 | uses: actions/checkout@v4 23 | with: 24 | repository: sporeprotocol/spore-devenv 25 | path: spore-devenv 26 | 27 | - name: Install Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 20.x 31 | 32 | - uses: pnpm/action-setup@v3 33 | name: Install pnpm 34 | with: 35 | version: 8 36 | run_install: false 37 | 38 | - name: Get pnpm store directory 39 | shell: bash 40 | run: | 41 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 42 | 43 | - uses: actions/cache@v3 44 | name: Setup pnpm cache 45 | with: 46 | path: ${{ env.STORE_PATH }} 47 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pnpm-store- 50 | 51 | - name: Prepare spore-devenv (contracts and stuff) 52 | working-directory: spore-devenv 53 | run: sh prepare.sh 54 | 55 | - name: Start devenv services 56 | working-directory: spore-devenv 57 | run: npm run test:start 58 | 59 | - name: Move generated config file to spore-sdk 60 | working-directory: spore-devenv 61 | run: | 62 | mkdir -p ../spore-sdk/packages/core/src/__tests__/tmp 63 | cp config.json ../spore-sdk/packages/core/src/__tests__/tmp 64 | 65 | - name: Recharge capacity for accounts 66 | working-directory: spore-devenv 67 | run: npm run test:e2e 68 | env: 69 | VITE_ACCOUNT_CHARLIE: ${{ secrets.ACCOUNT_CHARLIE }} 70 | VITE_ACCOUNT_ALICE: ${{ secrets.ACCOUNT_ALICE }} 71 | VITE_ACCOUNT_BOB: ${{ secrets.ACCOUNT_BOB }} 72 | 73 | - name: Prepare spore-sdk 74 | working-directory: spore-sdk 75 | run: pnpm install --no-frozen-lockfile 76 | 77 | - name: Run tests for @spore-sdk/core 78 | working-directory: spore-sdk/packages/core 79 | run: pnpm run test 80 | env: 81 | VITE_ACCOUNT_CHARLIE: ${{ secrets.ACCOUNT_CHARLIE }} 82 | VITE_ACCOUNT_ALICE: ${{ secrets.ACCOUNT_ALICE }} 83 | VITE_ACCOUNT_BOB: ${{ secrets.ACCOUNT_BOB }} 84 | VITE_TEST_CLUSTER_V1: true 85 | VITE_NETWORK: devnet 86 | -------------------------------------------------------------------------------- /.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.test.local 32 | .env.production.local 33 | 34 | # logs 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | node-linker=hoisted -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2023 sporeprotocol 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /docs/assets/readme-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporeprotocol/spore-sdk/5e5d886e49bfbd78253e9c14fd8baebccb488a81/docs/assets/readme-banner.webp -------------------------------------------------------------------------------- /docs/core/joint-apis.md: -------------------------------------------------------------------------------- 1 | # Joint APIs 2 | 3 | > This documentation is a work in progress. -------------------------------------------------------------------------------- /docs/core/setup.md: -------------------------------------------------------------------------------- 1 | 2 | # Start using Spore SDK 3 | 4 | ## Installation 5 | 6 | Install `@spore-sdk/core` as a dependency using any package manager, such as `npm`: 7 | 8 | ```shell 9 | npm install @spore-sdk/core 10 | ``` 11 | 12 | ## Browser environment 13 | 14 | Spore SDK is built on top of [Lumos](https://github.com/ckb-js/lumos), an open-source dapp framework for Nervos CKB. Lumos incorporates certain Node-polyfills into its implementation to provide specific functionalities, such as: 15 | 16 | - `crypto-browserify` 17 | - `buffer` 18 | 19 | If you wish to use the Spore SDK in a browser environment, it's important to manually add Node-polyfills to your application. This ensures that the Spore SDK functions properly in the browser. Visit: [CRA, Vite, Webpack or Other](https://lumos-website.vercel.app/recipes/cra-vite-webpack-or-other). 20 | -------------------------------------------------------------------------------- /docs/recipes/create-immortal-spore.md: -------------------------------------------------------------------------------- 1 | # Create immortal spores on-chain 2 | 3 | ## What is `immortal` 4 | 5 | `Immortal` serves as a Spore Extension that offers the following rules: 6 | 7 | 1. Immortal extension is enabled for a spore if the spore has a `immortal=true` in its `SporeData.contentType` 8 | 2. An immortal spore lives on-chain forever, and cannot be melted under any circumstances 9 | 10 | To create a spore with the immortal extension enabled, you need to pass the `immortal` parameter to the props of the `createSpore` API when calling it. 11 | 12 | Once an immortal spore is successfully created, any attempt from the owner to melt it will fail due to the verification by the `SporeType` script. 13 | 14 | ## Create immortal spores 15 | 16 | There are two recommended ways to set the `immortal` parameters while creating a spore. Both approaches are equally valid and will successfully achieve the intended result. Feel free to choose the one you prefer. 17 | 18 | ### Specify in the `contentTypeParameters` object 19 | 20 | ```typescript 21 | import { createSpore } from '@spore-sdk/core'; 22 | 23 | let { txSkeleton } = await createSpore({ 24 | data: { 25 | content: JPEG_AS_BYTES, 26 | contentType: 'image/jpeg', 27 | contentTypeParameters: { 28 | immortal: true, // enabling the immortal extension 29 | }, 30 | }, 31 | fromInfos: [WALLET_ADDRESS], 32 | toLock: WALLET_LOCK_SCRIPT, 33 | }); 34 | ``` 35 | 36 | ### Use the `setContentTypeParameters` function 37 | 38 | ```typescript 39 | import { createSpore, setContentTypeParameters } from '@spore-sdk/core'; 40 | 41 | let { txSkeleton } = await createSpore({ 42 | data: { 43 | content: JPEG_AS_BYTES, 44 | contentType: setContentTypeParameters( 45 | 'image/jpeg', 46 | { 47 | immortal: true, // enabling the immortal extension 48 | } 49 | ), 50 | }, 51 | fromInfos: [WALLET_ADDRESS], 52 | toLock: WALLET_LOCK_SCRIPT, 53 | }); 54 | ``` -------------------------------------------------------------------------------- /docs/resources/demos.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | sidebar_label: Demos 4 | --- 5 | 6 | # Spore Demos 7 | 8 | Spore Demos are web application demos that can be run in a browser environment. 9 | 10 | The demos are more complete projects than the [Spore Examples](./examples.md), developers can study from the demos to understand how fully functional web applications are developed. 11 | For example, how to connect wallets like [MetaMask](https://metamask.io) or [JoyID](https://joy.id). 12 | 13 | ### The gallery app 14 | 15 | A Spore Protocol Demo based on Next.js + React + Spore SDK, which implements basic functionalities, such as the creation and transfer of clusters, as well as minting, transferring, and melting of spores. 16 | 17 | The demo also shows how to connect with [MetaMask](https://metamask.io) and [JoyID](https://joy.id) wallets. 18 | 19 | - [Online A-simple-demo app](https://a-simple-demo.spore.pro) 20 | - [GitHub repository](https://github.com/sporeprotocol/spore-demo) 21 | 22 | ### Blog app with Spore 23 | 24 | A demo from the step-by-step tutorial on creating simple on-chain Blog app with Spore Protocol. Learn how to connect wallet, create your own blog cluster and post blogs within it. 25 | 26 | - [Follow the Create on-chain blog tutorial](https://docs.spore.pro/tutorials/create-on-chain-blog) 27 | - [Online on-chain blog demo](https://spore-blog-tutorial.vercel.app) 28 | - [GitHub repository](https://github.com/sporeprotocol/spore-blog-tutorial) 29 | -------------------------------------------------------------------------------- /docs/resources/examples.md: -------------------------------------------------------------------------------- 1 | # Spore Examples 2 | 3 | Here we provide several examples which are minimum viable snippets designed for a Node environment, each showcasing a specific feature implemented using the [Spore SDK](../..). 4 | 5 | These examples serve as practical guides for developers, demonstrating how to implement specific features in a straightforward manner, for instance, how to create a spore by a transaction with Spore SDK. And for those who are looking for documentation on how to develop a fully functional application, refer to: [Spore Demos](./demos). 6 | 7 | ## Scenario examples 8 | 9 | ### [Creating your first spore](https://docs.spore.pro/tutorials/create-first-spore) 10 | 11 | [`spore-first-example`](https://github.com/sporeprotocol/spore-first-example) is a hello world example for Spore SDK, showing you how to upload an image file and create a spore on [Nervos CKB](https://www.nervos.org/) in a split second. This is a well-suited code example for beginners to learn the very basics of Spore Protocol. 12 | 13 | - Follow the tutorial at [Creating your first spore](https://docs.spore.pro/tutorials/create-first-spore) 14 | - Run the example on [StackBlitz](https://stackblitz.com/github/sporeprotocol/spore-first-example?file=src%2Findex.ts) 15 | - [GitHub repository](https://github.com/sporeprotocol/spore-first-example) 16 | 17 | ## Lock script examples 18 | 19 | ### [CKB Default Lock](../../examples/secp256k1) 20 | 21 | [CKB Default Lock](https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/secp256k1_blake160_sighash_all.c) is the most commonly used lock script on [Nervos CKB](https://www.nervos.org/), also a great starting point for beginners due to its simplicity. You can create private assets with the CKB Default Lock for safeguarding ownership of your private assets. 22 | 23 | - [Check CKB Default Lock examples](../../examples/secp256k1) 24 | 25 | ### [Anyone-can-pay](../../examples/acp) 26 | 27 | [Anyone-can-pay](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0026-anyone-can-pay/0026-anyone-can-pay.md) (ACP) lock can be unlocked by anyone without signature verification and accepts any amount of CKB/UDT payment from the unlocker. You can create public clusters with the Anyone-can-pay lock and benefit from charging other users for creating spores within the public cluster. 28 | 29 | - [Check ACP examples](../../examples/acp) 30 | 31 | ### [Omnilock](../../examples/omnilock) 32 | 33 | [Omnilock](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md) is a universal and interoperable lock script supporting various blockchains (Bitcoin, Ethereum, EOS, etc.) verification methods and extensible for future additions. Omnilock also supports a [compatible anyone-can-pay mode](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md#anyone-can-pay-mode), which allows you to create public clusters using it. You can create private/public spores and clusters with the Omnilock. 34 | 35 | - [Check Omnilock examples](../../examples/omnilock) 36 | -------------------------------------------------------------------------------- /examples/acp/apis/createAcpCluster.ts: -------------------------------------------------------------------------------- 1 | import { createCluster } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | /** 8 | * Create an Anyone-can-pay lock from CHARLIE's CKB default lock, adding minimal payment requirement to the Cluster. 9 | * Anyone who references the lock cell without providing a signature to unlock it, will need to following: 10 | * - If minCkb is defined, pay at least 10^minCkb shannons to the lock cell as a fee. 11 | * - If minCkb is undefined, anyone can reference this Cluster without payment. 12 | * 13 | * Examples: 14 | * If minCkb = 10, anyone can pay 10,000,000,000 (10^10) shannons to the Cluster as a fee of referencing it. 15 | * If minCkb = 0, anyone can pay 1 (10^0) shannon to the Cluster as a fee of referencing it. 16 | * If minCkb = undefined, anyone can reference this Cluster without payment. 17 | */ 18 | const CharlieAcpLock = CHARLIE.createAcpLock({ 19 | minCkb: void 0, 20 | }); 21 | 22 | const { txSkeleton, outputIndex } = await createCluster({ 23 | data: { 24 | name: 'Test acp lock cluster', 25 | description: 'A public cluster with acp lock', 26 | }, 27 | fromInfos: [CHARLIE.address], 28 | toLock: CharlieAcpLock, 29 | config, 30 | }); 31 | 32 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 33 | console.log('CreateAcpCluster transaction sent, hash:', hash); 34 | console.log('Cluster output index:', outputIndex); 35 | 36 | const clusterCell = txSkeleton.get('outputs').get(outputIndex)!; 37 | console.log('Cluster ID:', clusterCell.cellOutput.type!.args); 38 | })(); 39 | -------------------------------------------------------------------------------- /examples/acp/apis/createSporeWithAcpCluster.ts: -------------------------------------------------------------------------------- 1 | import { BI } from '@ckb-lumos/bi'; 2 | import { number } from '@ckb-lumos/codec'; 3 | import { createSpore } from '@spore-sdk/core'; 4 | import { accounts, config } from '../utils/config'; 5 | 6 | (async function main() { 7 | const { CHARLIE } = accounts; 8 | 9 | const { txSkeleton, outputIndex } = await createSpore({ 10 | data: { 11 | contentType: 'text/plain', 12 | content: 'spore with public cluster referenced', 13 | /** 14 | * When referencing an ACP public Cluster, even if the Cluster doesn't belong to CHARLIE, 15 | * CHARLIE can still create Spores that reference the Cluster. 16 | */ 17 | clusterId: '0x4bb8ccd6dc886da947cbe8ac4d51004c9d5335ae1216fda756ac39e4bf665c22', 18 | }, 19 | toLock: CHARLIE.lock, 20 | fromInfos: [CHARLIE.address], 21 | cluster: { 22 | /** 23 | * When referencing an ACP public Cluster, 24 | * you may have to pay at least (10^minCKB) shannons to the Cluster cell as a fee. 25 | */ 26 | capacityMargin: (clusterCell, margin) => { 27 | const argsMinCkb = clusterCell.cellOutput.lock.args.slice(42, 2); 28 | const minCkb = argsMinCkb.length === 2 29 | ? BI.from(10).pow(number.Uint8.unpack(`0x${argsMinCkb}`)) 30 | : BI.from(0); 31 | 32 | return margin.add(minCkb); 33 | }, 34 | /** 35 | * When referencing an ACP public Cluster, 36 | * the Cluster's corresponding witness should be set to "0x" (empty) and shouldn't be signed. 37 | */ 38 | updateWitness: '0x', 39 | }, 40 | config, 41 | }); 42 | 43 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 44 | console.log('CreateSporeWithAcpCluster transaction sent, hash:', hash); 45 | console.log('Spore output index:', outputIndex); 46 | 47 | const sporeCell = txSkeleton.get('outputs').get(outputIndex)!; 48 | console.log('Spore ID:', sporeCell.cellOutput.type!.args); 49 | })(); 50 | -------------------------------------------------------------------------------- /examples/acp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spore-examples/acp", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "lint": "prettier --check '{apis,utils}/**/*.{js,jsx,ts,tsx}'", 7 | "lint:fix": "prettier --write '{apis,utils}/**/*.{js,jsx,ts,tsx}'" 8 | }, 9 | "dependencies": { 10 | "@ckb-lumos/lumos": "0.24.0-next.1", 11 | "@spore-examples/shared": "workspace:^", 12 | "@spore-sdk/core": "workspace:^", 13 | "ts-node": "^10.9.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/acp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/acp/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { sharedTestingPrivateKeys } from '@spore-examples/shared'; 2 | import { predefinedSporeConfigs } from '@spore-sdk/core'; 3 | import { createSecp256k1Wallet } from './wallet'; 4 | 5 | /** 6 | * SporeConfig provides spore/cluster's detailed info like ScriptIds and CellDeps. 7 | * It is a necessary part for constructing spore/cluster transactions. 8 | */ 9 | export const config = predefinedSporeConfigs.Testnet; 10 | 11 | /** 12 | * Wallets with default testing accounts for running the examples, 13 | * feel free to replace them with your own testing accounts. 14 | */ 15 | export const accounts = { 16 | CHARLIE: createSecp256k1Wallet(sharedTestingPrivateKeys.CHARLIE, config), 17 | ALICE: createSecp256k1Wallet(sharedTestingPrivateKeys.ALICE, config), 18 | }; 19 | -------------------------------------------------------------------------------- /examples/omnilock/acp/createAcpCluster.ts: -------------------------------------------------------------------------------- 1 | import { createCluster } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | import { createOmnilockAcpArgs } from '../utils/wallet'; 4 | 5 | (async function main() { 6 | const { CHARLIE } = accounts; 7 | 8 | /** 9 | * Create an Omnilock from CHARLIE's original lock, adding minimal payment requirement to the Cluster. 10 | * Anyone who references the lock cell without providing a signature to unlock it, 11 | * will have to pay at least 10^minCkb shannons to the lock cell as a fee. 12 | * 13 | * Examples: 14 | * If minCkb = 10, anyone can pay 10,000,000,000 (10^10) shannons to the Cluster as a fee of referencing it. 15 | * If minCkb = 0, anyone can pay 1 (10^0) shannon to the Cluster as a fee of referencing it. 16 | */ 17 | const CharlieOmniAcpLock = CHARLIE.createLock( 18 | createOmnilockAcpArgs({ 19 | minCkb: 0, 20 | }) 21 | ); 22 | 23 | const { txSkeleton, outputIndex } = await createCluster({ 24 | data: { 25 | name: 'Test omnilock acp cluster', 26 | description: 'An public cluster with omnilock', 27 | }, 28 | fromInfos: [CHARLIE.address], 29 | toLock: CharlieOmniAcpLock, 30 | config, 31 | }); 32 | 33 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 34 | console.log('CreateAcpCluster transaction sent, hash:', hash); 35 | console.log('Cluster output index:', outputIndex); 36 | 37 | const clusterCell = txSkeleton.get('outputs').get(outputIndex)!; 38 | console.log('Cluster ID:', clusterCell.cellOutput.type!.args); 39 | })(); 40 | -------------------------------------------------------------------------------- /examples/omnilock/acp/createSporeWithAcpCluster.ts: -------------------------------------------------------------------------------- 1 | import { BI } from '@ckb-lumos/bi'; 2 | import { createSpore } from '@spore-sdk/core'; 3 | import { accounts, config } from '../utils/config'; 4 | import { getInfoFromOmnilockArgs } from '../utils/wallet'; 5 | 6 | (async function main() { 7 | const { CHARLIE } = accounts; 8 | 9 | const { txSkeleton, outputIndex } = await createSpore({ 10 | data: { 11 | contentType: 'text/plain', 12 | content: 'spore with public cluster referenced', 13 | /** 14 | * When referencing an ACP public Cluster, even if the Cluster doesn't belong to CHARLIE, 15 | * CHARLIE can still create Spores that reference the Cluster. 16 | */ 17 | clusterId: '0x6c7df3eee9af40d4e0f27356e7dcb02a54e33f7d81a40af57d0de1f3856ab750', 18 | }, 19 | toLock: CHARLIE.lock, 20 | fromInfos: [CHARLIE.address], 21 | cluster: { 22 | /** 23 | * When referencing an Omnilock ACP public Cluster, 24 | * you must pay at least (10^minCKB) shannons to the Cluster cell as a fee. 25 | * 26 | * Every Omnilock ACP lock script has a minCkb value defined in its args. 27 | * The minimal viable minCkb is 0, which means the minimum payment is 1 (10^0) shannon. 28 | */ 29 | capacityMargin: (clusterCell, margin) => { 30 | const args = getInfoFromOmnilockArgs(clusterCell.cellOutput.lock.args); 31 | const minCkb = args.minCkb !== void 0 32 | ? BI.from(10).pow(args.minCkb) 33 | : BI.from(0); 34 | 35 | return margin.add(minCkb); 36 | }, 37 | /** 38 | * When referencing an ACP public Cluster, 39 | * the Cluster's corresponding witness should be set to "0x" (empty) and shouldn't be signed. 40 | */ 41 | updateWitness: '0x', 42 | }, 43 | config, 44 | }); 45 | 46 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 47 | console.log('CreateSporeWithAcpCluster transaction sent, hash:', hash); 48 | console.log('Spore output index:', outputIndex); 49 | 50 | const sporeCell = txSkeleton.get('outputs').get(outputIndex)!; 51 | console.log('Spore ID:', sporeCell.cellOutput.type!.args); 52 | })(); 53 | -------------------------------------------------------------------------------- /examples/omnilock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spore-examples/omnilock", 3 | "private": true, 4 | "scripts": { 5 | "lint": "prettier --check '{acp,utils}/**/*.{js,jsx,ts,tsx}'", 6 | "lint:fix": "prettier --write '{acp,utils}/**/*.{js,jsx,ts,tsx}'" 7 | }, 8 | "dependencies": { 9 | "@ckb-lumos/lumos": "0.24.0-next.1", 10 | "@spore-examples/shared": "workspace:^", 11 | "@spore-sdk/core": "workspace:^", 12 | "lodash": "^4.17.21", 13 | "ts-node": "^10.9.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/omnilock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/omnilock/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { sharedTestingPrivateKeys } from '@spore-examples/shared'; 2 | import { predefinedSporeConfigs } from '@spore-sdk/core'; 3 | import { createOmnilockSecp256k1Wallet } from './wallet'; 4 | 5 | /** 6 | * SporeConfig provides spore/cluster's detailed info like ScriptIds and CellDeps. 7 | * It is a necessary part for constructing spore/cluster transactions. 8 | */ 9 | export const config = predefinedSporeConfigs.Testnet; 10 | 11 | /** 12 | * Wallets with default testing accounts for running the examples, 13 | * feel free to replace them with your own testing accounts. 14 | */ 15 | export const accounts = { 16 | CHARLIE: createOmnilockSecp256k1Wallet({ 17 | privateKey: sharedTestingPrivateKeys.CHARLIE, 18 | config, 19 | }), 20 | ALICE: createOmnilockSecp256k1Wallet({ 21 | privateKey: sharedTestingPrivateKeys.ALICE, 22 | config, 23 | }), 24 | }; 25 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createCluster.ts: -------------------------------------------------------------------------------- 1 | import { createCluster } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | const { txSkeleton, outputIndex } = await createCluster({ 8 | data: { 9 | name: 'Test cluster', 10 | description: 'Description of the cluster', 11 | }, 12 | fromInfos: [CHARLIE.address], 13 | toLock: CHARLIE.lock, 14 | config, 15 | }); 16 | 17 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 18 | console.log('CreateCluster transaction sent, hash:', hash); 19 | console.log('Cluster output index:', outputIndex); 20 | 21 | const clusterCell = txSkeleton.get('outputs').get(outputIndex)!; 22 | console.log('Cluster ID:', clusterCell.cellOutput.type!.args); 23 | })(); 24 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { createClusterAgent, getClusterProxyById } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | import { BI } from '@ckb-lumos/bi'; 4 | 5 | (async function main() { 6 | const { ALICE } = accounts; 7 | 8 | /** 9 | * The target ClusterProxy's ID you want to create ClusterAgent from. 10 | * 11 | * Ensure that any of the following conditions can be fulfilled: 12 | * - You can unlock the ClusterProxy, or you can provide and unlock any LockProxy of the ClusterProxy 13 | * - If the ClusterProxy has minPayment defined, and you can pay the owner of the ClusterProxy a fee 14 | * 15 | * Example ClusterProxy "0x484a...e857": 16 | * - ClusterProxy ID: 0x484a439338ebe0ef6f953ead4273a59fc5972d31e67e7e51e7a9c01af810e857 17 | * - Ownership: CHARLIE 18 | */ 19 | const clusterProxyCell = await getClusterProxyById('0x484a439338ebe0ef6f953ead4273a59fc5972d31e67e7e51e7a9c01af810e857', config); 20 | 21 | const { txSkeleton, outputIndex } = await createClusterAgent({ 22 | clusterProxyOutPoint: clusterProxyCell.outPoint!, 23 | /** 24 | * Decide how to reference the target ClusterProxy: 25 | * - 'cell': Reference the ClusterProxy (Cell or LockProxy) directly 26 | * - 'payment': Pay the owner of the ClusterProxy a fee to use it without permission/signature 27 | */ 28 | referenceType: 'payment', 29 | /** 30 | * If referenceType == 'payment', you can specify the payment amount (in shannons), 31 | * and the default amount is 10^ClusterProxyArgs.minPayment if not specified. 32 | */ 33 | paymentAmount: BI.from(100_0000_0000), 34 | fromInfos: [ALICE.address], 35 | toLock: ALICE.lock, 36 | config, 37 | }); 38 | 39 | const hash = await ALICE.signAndSendTransaction(txSkeleton); 40 | console.log('CreateClusterAgent transaction sent, hash:', hash); 41 | console.log('ClusterAgent output index:', outputIndex); 42 | })(); 43 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { createClusterProxy, getClusterById, unpackToRawClusterProxyArgs } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | /** 8 | * The target Cluster's ID you want to create ClusterProxy from. 9 | * 10 | * Ensure that any of the following conditions can be fulfilled: 11 | * - You can provide a signature to unlock the Cluster 12 | * - You can provide and unlock any LockProxy of the Cluster 13 | * 14 | * Example Cluster "0x928e...8b27": 15 | * - Cluster ID: 0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27 16 | * - Ownership: CHARLIE 17 | */ 18 | const clusterCell = await getClusterById('0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27', config); 19 | 20 | const { txSkeleton, outputIndex } = await createClusterProxy({ 21 | clusterOutPoint: clusterCell.outPoint!, 22 | fromInfos: [CHARLIE.address], 23 | toLock: CHARLIE.lock, 24 | /** 25 | * Anyone who pays a minimum 10^minPayment shannons to toLock can use the ClusterProxy. 26 | * If undefined, the "pay to use" method will be disabled for others. 27 | */ 28 | minPayment: 10, 29 | config, 30 | }); 31 | 32 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 33 | console.log('CreateClusterProxy transaction sent, hash:', hash); 34 | console.log('ClusterProxy output index:', outputIndex); 35 | 36 | const clusterProxyCell = txSkeleton.get('outputs').get(outputIndex)!; 37 | const clusterProxyArgs = unpackToRawClusterProxyArgs(clusterProxyCell.cellOutput.type!.args); 38 | console.log('ClusterProxy ID:', clusterProxyArgs.id); 39 | })(); 40 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createSpore.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readFileSync } from 'fs'; 3 | import { createSpore } from '@spore-sdk/core'; 4 | import { accounts, config } from '../utils/config'; 5 | 6 | /** 7 | * Fetch local image file as Uint8Array in Node. 8 | * In browser, you can use fetch() to fetch remote image file as Uint8Array. 9 | */ 10 | export async function fetchLocalImage(src: string): Promise { 11 | const buffer = readFileSync(resolve(__dirname, src)); 12 | return new Uint8Array(buffer); 13 | } 14 | 15 | (async function main() { 16 | const { CHARLIE } = accounts; 17 | 18 | const { txSkeleton, outputIndex } = await createSpore({ 19 | data: { 20 | /** 21 | * The Spore's content type (MIME type), e.g. 'text/plain', 'image/jpeg', 'application/json', etc. 22 | * You can search for the full list of MIME types on the Internet: 23 | * https://www.iana.org/assignments/media-types/media-types.xhtml 24 | */ 25 | contentType: 'image/jpeg', 26 | /** 27 | * The Spore's content, should be a BytesLike type object, e.g. Uint8Array, ArrayBuffer, etc. 28 | * You can use bytifyRawString() to convert a string to Uint8Array if needed. 29 | */ 30 | content: await fetchLocalImage('../../shared/test.jpg'), 31 | }, 32 | fromInfos: [CHARLIE.address], 33 | toLock: CHARLIE.lock, 34 | config, 35 | }); 36 | 37 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 38 | console.log('createSpore transaction sent, hash:', hash); 39 | console.log('Spore output index:', outputIndex); 40 | 41 | const sporeCell = txSkeleton.get('outputs').get(outputIndex)!; 42 | console.log('Spore ID:', sporeCell.cellOutput.type!.args); 43 | })(); 44 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createSporeWithCluster.ts: -------------------------------------------------------------------------------- 1 | import { bytifyRawString, createSpore } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | /** 8 | * The Cluster's ID you want to reference to the new Spore. 9 | * 10 | * Ensure that any of the following conditions can be fulfilled: 11 | * - You can provide a signature to unlock the Cluster 12 | * - You can provide and unlock any LockProxy of the Cluster 13 | * 14 | * The example Cluster "0x928e...8b27": 15 | * - Cluster ID: 0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27 16 | * - Ownership: CHARLIE 17 | */ 18 | const clusterId = '0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27'; 19 | 20 | const { txSkeleton, outputIndex } = await createSpore({ 21 | data: { 22 | /** 23 | * When data.clusterId is specified, will reference the Cluster (Cell or LockProxy) in the transaction. 24 | * The Spore will be a Clustered Spore, referenced to the Cluster. 25 | */ 26 | clusterId, 27 | contentType: 'text/plain', 28 | content: bytifyRawString('spore text content'), 29 | }, 30 | fromInfos: [CHARLIE.address], 31 | toLock: CHARLIE.lock, 32 | config, 33 | }); 34 | 35 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 36 | console.log('CreateSporeWithCluster transaction sent, hash:', hash); 37 | console.log('Spore output index:', outputIndex); 38 | })(); 39 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/createSporeWithClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { bytifyRawString, createSpore } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | import { OutPoint } from '@ckb-lumos/base'; 4 | 5 | (async function main() { 6 | const { ALICE } = accounts; 7 | 8 | /** 9 | * The Cluster's ID you want to reference to the new Spore. 10 | * 11 | * The example Cluster "0x928e...8b27": 12 | * - Cluster ID: 0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27 13 | * - Ownership: CHARLIE 14 | */ 15 | const clusterId = '0x928eb52ffeb8864154b2135d57ac57b70d97ba908c5a7205ed5e5dc022468b27'; 16 | 17 | /** 18 | * The ClusterAgent you want to reference in the transaction. 19 | * 20 | * Example ClusterAgent "0xfca6...ae6b|0x1": 21 | * - Referenced Cluster: "0x928e...8b27" 22 | * - Ownership: ALICE 23 | * 24 | * ClusterAgent "0xfca6...ae6b|0x1" is owned by ALICE and is referenced to Cluster "0x928e...8b27". 25 | * It allows ALICE to reference the Cluster's ID in Spores. 26 | */ 27 | const clusterAgentOutPoint: OutPoint = { 28 | txHash: '0xfca6e903083893b143863bf3256d40fee408dae11ae359a4637d46a815f7ae6b', 29 | index: '0x1', 30 | }; 31 | 32 | const { txSkeleton, outputIndex } = await createSpore({ 33 | data: { 34 | clusterId, 35 | contentType: 'text/plain', 36 | content: bytifyRawString('spore text content'), 37 | }, 38 | /** 39 | * When clusterAgentOutpoint is specified, will reference the ClusterAgent (Cell or LockProxy) in the transaction, 40 | * instead of referencing the Cluster (Cell or LockProxy) directly. 41 | */ 42 | clusterAgentOutPoint, 43 | fromInfos: [ALICE.address], 44 | toLock: ALICE.lock, 45 | config, 46 | }); 47 | 48 | const hash = await ALICE.signAndSendTransaction(txSkeleton); 49 | console.log('CreateSporeWithClusterAgent transaction sent, hash:', hash); 50 | console.log('Spore output index:', outputIndex); 51 | })(); 52 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/meltClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { meltClusterAgent } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | const { txSkeleton } = await meltClusterAgent({ 8 | outPoint: { 9 | txHash: '0x', 10 | index: '0x', 11 | }, 12 | changeAddress: CHARLIE.address, 13 | config, 14 | }); 15 | 16 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 17 | console.log('MeltClusterAgent transaction sent, hash:', hash); 18 | })(); 19 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/meltClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { meltClusterProxy, getClusterProxyById } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | const clusterProxyCell = await getClusterProxyById('0x', config); 8 | 9 | const { txSkeleton } = await meltClusterProxy({ 10 | outPoint: clusterProxyCell.outPoint!, 11 | changeAddress: CHARLIE.address, 12 | config, 13 | }); 14 | 15 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 16 | console.log('MeltClusterProxy transaction sent, hash:', hash); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/meltSpore.ts: -------------------------------------------------------------------------------- 1 | import { getSporeById, meltSpore } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE } = accounts; 6 | 7 | const sporeCell = await getSporeById('0x', config); 8 | 9 | const { txSkeleton } = await meltSpore({ 10 | outPoint: sporeCell.outPoint!, 11 | changeAddress: CHARLIE.address, 12 | config, 13 | }); 14 | 15 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 16 | console.log('MeltSpore transaction sent, hash:', hash); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/transferCluster.ts: -------------------------------------------------------------------------------- 1 | import { getClusterById, transferCluster } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE, ALICE } = accounts; 6 | 7 | const clusterCell = await getClusterById('0x', config); 8 | 9 | const { txSkeleton, outputIndex } = await transferCluster({ 10 | outPoint: clusterCell.outPoint!, 11 | fromInfos: [CHARLIE.address], 12 | toLock: ALICE.lock, 13 | config, 14 | }); 15 | 16 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 17 | console.log('TransferCluster transaction sent, hash:', hash); 18 | console.log('Cluster output index:', outputIndex); 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/transferClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { transferClusterAgent } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE, ALICE } = accounts; 6 | 7 | const { txSkeleton, outputIndex } = await transferClusterAgent({ 8 | outPoint: { 9 | txHash: '0x', 10 | index: '', 11 | }, 12 | fromInfos: [CHARLIE.address], 13 | toLock: ALICE.lock, 14 | config, 15 | }); 16 | 17 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 18 | console.log('TransferClusterAgent transaction sent, hash:', hash); 19 | console.log('ClusterAgent output index:', outputIndex); 20 | })(); 21 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/transferClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { transferClusterProxy, getClusterProxyById } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE, ALICE } = accounts; 6 | 7 | const clusterProxyCell = await getClusterProxyById('0x', config); 8 | 9 | const { txSkeleton, outputIndex } = await transferClusterProxy({ 10 | outPoint: clusterProxyCell.outPoint!, 11 | fromInfos: [CHARLIE.address], 12 | toLock: ALICE.lock, 13 | /** 14 | * The ClusterProxyArgs.minPayment is modifiable during transfer 15 | */ 16 | minPayment: 10, 17 | config, 18 | }); 19 | 20 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 21 | console.log('TransferClusterProxy transaction sent, hash:', hash); 22 | console.log('ClusterProxy output index:', outputIndex); 23 | })(); 24 | -------------------------------------------------------------------------------- /examples/secp256k1/apis/transferSpore.ts: -------------------------------------------------------------------------------- 1 | import { getSporeById, transferSpore } from '@spore-sdk/core'; 2 | import { accounts, config } from '../utils/config'; 3 | 4 | (async function main() { 5 | const { CHARLIE, ALICE } = accounts; 6 | 7 | const sporeCell = await getSporeById('0x', config); 8 | 9 | const { txSkeleton, outputIndex } = await transferSpore({ 10 | outPoint: sporeCell.outPoint!, 11 | fromInfos: [CHARLIE.address], 12 | toLock: ALICE.lock, 13 | config, 14 | }); 15 | 16 | const hash = await CHARLIE.signAndSendTransaction(txSkeleton); 17 | console.log('TransferSpore transaction sent, hash:', hash); 18 | console.log('Spore output index:', outputIndex); 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/secp256k1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spore-examples/secp256k1", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "lint": "prettier --check '{apis,utils}/**/*.{js,jsx,ts,tsx}'", 7 | "lint:fix": "prettier --write '{apis,utils}/**/*.{js,jsx,ts,tsx}'" 8 | }, 9 | "dependencies": { 10 | "@ckb-lumos/lumos": "0.24.0-next.1", 11 | "@spore-examples/shared": "workspace:^", 12 | "@spore-sdk/core": "workspace:^", 13 | "ts-node": "^10.9.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/secp256k1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/secp256k1/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { sharedTestingPrivateKeys } from '@spore-examples/shared'; 2 | import { predefinedSporeConfigs } from '@spore-sdk/core'; 3 | import { createSecp256k1Wallet } from './wallet'; 4 | 5 | /** 6 | * SporeConfig provides spore/cluster's detailed info like ScriptIds and CellDeps. 7 | * It is a necessary part for constructing spore/cluster transactions. 8 | */ 9 | export const config = predefinedSporeConfigs.Testnet; 10 | 11 | /** 12 | * Wallets with default testing accounts for running the examples, 13 | * feel free to replace them with your own testing accounts. 14 | */ 15 | export const accounts = { 16 | CHARLIE: createSecp256k1Wallet(sharedTestingPrivateKeys.CHARLIE, config), 17 | ALICE: createSecp256k1Wallet(sharedTestingPrivateKeys.ALICE, config), 18 | }; 19 | -------------------------------------------------------------------------------- /examples/shared/README.md: -------------------------------------------------------------------------------- 1 | # Spore Examples Shared 2 | 3 | Variables and files shared in all examples. 4 | 5 | ## Files 6 | 7 | - [index.ts](./index.ts): Provides `CHARLIE` and `ALICE` as the default testing accounts. 8 | - [test.jpg](./test.jpg): An `jpeg` image as the default content of the new spores in the examples. -------------------------------------------------------------------------------- /examples/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const sharedTestingPrivateKeys = { 2 | CHARLIE: '0xc153ee57dc8ae3dac3495c828d6f8c3fef6b1d0c74fc31101c064137b3269d6d', 3 | ALICE: '0x49aa6d595ac46cc8e1a31b511754dd58f241a7d8a6ad29e83d6b0c1a82399f3d', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spore-examples/shared", 3 | "private": true, 4 | "scripts": { 5 | "lint": "prettier --check '*.{js,jsx,ts,tsx}'", 6 | "lint:fix": "prettier --write '*.{js,jsx,ts,tsx}'" 7 | }, 8 | "main": "index.ts", 9 | "dependencies": { 10 | "@ckb-lumos/lumos": "0.24.0-next.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/shared/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporeprotocol/spore-sdk/5e5d886e49bfbd78253e9c14fd8baebccb488a81/examples/shared/test.jpg -------------------------------------------------------------------------------- /examples/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spore-sdk", 3 | "private": true, 4 | "license": "MIT", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "prepare": "husky install", 10 | "test": "turbo run test", 11 | "test:packages": "turbo run test --filter=./packages/*", 12 | "build": "turbo run build", 13 | "build:packages": "turbo run build --filter=./packages/*", 14 | "lint:fix": "turbo run lint:fix", 15 | "lint:fix-all": "prettier --write '{packages,apps}/**/*.{js,jsx,ts,tsx,md,json}'", 16 | "clean": "turbo run clean", 17 | "clean:packages": "turbo run clean --filter=./packags/*", 18 | "clean:examples": "turbo run clean --filter=./examples/*", 19 | "clean:dependencies": "pnpm clean:sub-dependencies && rimraf node_modules", 20 | "clean:sub-dependencies": "rimraf packages/**/node_modules examples/**/node_modules", 21 | "release:packages": "pnpm run clean:packages && pnpm run build:packages && changeset publish" 22 | }, 23 | "dependencies": { 24 | "rimraf": "^5.0.0" 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "^2.26.1", 28 | "husky": "^8.0.0", 29 | "lint-staged": "^15.1.0", 30 | "prettier": "^3.1.0", 31 | "ts-node": "^10.9.0", 32 | "turbo": "^1.10.16", 33 | "type-fest": "^3.8.0", 34 | "typescript": "^5.3.2" 35 | }, 36 | "lint-staged": { 37 | "{packages,apps}/**/*.{js,jsx,ts,tsx,md,json}": "prettier --ignore-unknown --write" 38 | }, 39 | "packageManager": "pnpm@8.0.0", 40 | "engines": { 41 | "node": ">=18.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @spore-sdk/core 2 | 3 | ## About 4 | 5 |

6 | 7 | 8 | 9 | Version 10 | 11 | 12 | 13 | 14 | 15 | MIT License 16 | 17 | 18 | 19 | 20 | 21 | Downloads per month 22 | 23 | 24 |

25 | 26 | The `@spore-sdk/core` package provides essential tools for constructing basic and advanced transactions on spores and clusters. Additionally, it offers convenient utilities for handling [serialization](https://github.com/nervosnetwork/molecule) of spores/clusters. 27 | 28 | ## Features 29 | 30 | - ⚡ Composed APIs for efficient spores/clusters interactions with minimal time overhead 31 | - 🧩 Joint APIs for building advanced transactions as a fun block-building process 32 | - 🛠️ Utilities for encoding/decoding data of spores/clusters 33 | - 🎹 Fully written in TypeScript 34 | 35 | ## Getting started 36 | 37 | ### Installation 38 | 39 | Install `@spore-sdk/core` as a dependency using any package manager, such as `npm`: 40 | 41 | ```shell 42 | npm install @spore-sdk/core 43 | ``` 44 | 45 | ### Browser environment 46 | 47 | Spore SDK is built on top of [Lumos](https://github.com/ckb-js/lumos), an open-source dapp framework for Nervos CKB. Lumos incorporates certain Node-polyfills into its implementation to provide specific functionalities, such as: 48 | 49 | - `crypto-browserify` 50 | - `buffer` 51 | 52 | If you wish to use the Spore SDK in a browser environment, it's important to manually add Node-polyfills to your application. This ensures that the Spore SDK functions properly in the browser. Visit: [CRA, Vite, Webpack or Other](https://lumos-website.vercel.app/recipes/cra-vite-webpack-or-other). 53 | 54 | ### About the project 55 | 56 | This package is a part of the Spore SDK monorepo. 57 | 58 | For complete descriptions and instructions, visit: [Spore SDK](../../README.md). 59 | 60 | ## License 61 | 62 | [MIT](../../LICENSE) License 63 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spore-sdk/core", 3 | "version": "0.2.2-alpha.2", 4 | "license": "MIT", 5 | "scripts": { 6 | "test": "vitest", 7 | "build": "tsc -p tsconfig.build.json", 8 | "lint": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'", 9 | "lint:fix": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", 10 | "clean": "pnpm run clean:cache & pnpm run clean:build", 11 | "clean:build": "rimraf lib && pnpm run clean:buildinfo", 12 | "clean:buildinfo": "rimraf tsconfig.*tsbuildinfo", 13 | "clean:cache": "rimraf .turbo" 14 | }, 15 | "main": "lib", 16 | "files": [ 17 | "lib" 18 | ], 19 | "peerDependencies": { 20 | "@ckb-lumos/lumos": "0.24.0-next.1", 21 | "lodash": "^4.17.21" 22 | }, 23 | "devDependencies": { 24 | "vitest": "^1.4.0", 25 | "@exact-realty/multipart-parser": "^1.0.13" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/sporeprotocol/spore-sdk.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/sporeprotocol/spore-sdk/issues" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/Buffer.test.ts: -------------------------------------------------------------------------------- 1 | import { bytes } from '@ckb-lumos/codec'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { bufferToRawString, bytifyRawString } from '../helpers'; 4 | 5 | describe('Buffer', () => { 6 | it('Encode buffer from normal raw strings', () => { 7 | const strings = [ 8 | { input: '\u0041', expected: 'A' }, 9 | { input: '\u0100', expected: 'Ā' }, 10 | { input: '\u304B', expected: 'か' }, 11 | { input: '\uD800\uDF48', expected: '𐍈' }, 12 | { input: '\uD83D\uDE0A', expected: '😊' }, 13 | ]; 14 | 15 | for (const raw of strings) { 16 | const decoded = bufferToRawString(bytifyRawString(raw.input)); 17 | expect(decoded).toEqual(raw.expected); 18 | } 19 | }); 20 | it('Encode buffer from special characters', () => { 21 | const strings = [ 22 | { input: '\u0009', expected: '\t' }, 23 | { input: '\u000A', expected: '\n' }, 24 | { input: '\u0020', expected: ' ' }, 25 | ]; 26 | 27 | for (const raw of strings) { 28 | const decoded = bufferToRawString(bytifyRawString(raw.input)); 29 | expect(decoded).toEqual(raw.expected); 30 | } 31 | }); 32 | it('Encoded ascii & utf8 should be the same', () => { 33 | const raw = 'English'; 34 | const utf8Encoded = bytes.hexify(bytifyRawString(raw)); 35 | const asciiEncoded = bytes.hexify(bytes.bytifyRawString(raw)); 36 | expect(utf8Encoded).toEqual(asciiEncoded); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/MimeType.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { parseMimeType, serializeMimeType } from '../helpers'; 3 | 4 | describe('ContentType', function () { 5 | /** 6 | * Parse MIME 7 | */ 8 | it('Parse normal MIME', function () { 9 | const decoded = parseMimeType('image/png;immortal=true;ipfs="0x010aff15"'); 10 | 11 | expect(decoded).not.toBeNull(); 12 | expect(decoded!.parameters.get('immortal')).eq('true'); 13 | expect(decoded!.parameters.get('ipfs')).eq('0x010aff15'); 14 | }); 15 | it('Parse MIME with unsupported array parameter', function () { 16 | const decoded = parseMimeType('image/png;mutant[]="a,b,c"'); 17 | 18 | expect(decoded).not.toBeNull(); 19 | expect(decoded!.parameters.has('mutant')).eq(false, 'mutant should not be in parameters'); 20 | }); 21 | it('Parse MIME with supported array parameter', function () { 22 | const decoded = parseMimeType('image/png;mutant[]="a,b,c"', { 23 | arrayParameters: true, 24 | }); 25 | 26 | expect(decoded).not.toBeNull(); 27 | expect(decoded!.parameters.get('mutant')).toEqual(['a', 'b', 'c']); 28 | }); 29 | 30 | /** 31 | * Serialize MIME 32 | */ 33 | it('Serialize normal MIME', function () { 34 | const serialized = serializeMimeType({ 35 | type: 'image', 36 | subtype: 'svg+xml', 37 | parameters: new Map([ 38 | ['immortal', 'true'], 39 | ['q', '0.9'], 40 | ]), 41 | }); 42 | 43 | expect(serialized).toEqual('image/svg+xml;immortal=true;q=0.9'); 44 | }); 45 | it('Serialize MIME with unsupported array parameter', function () { 46 | expect(() => 47 | serializeMimeType({ 48 | type: 'text', 49 | subtype: 'plain', 50 | parameters: new Map([['mutant', ['a', 'b', 'c']]]), 51 | }), 52 | ).toThrow('Array parameter value is not supported'); 53 | }); 54 | it('Serialize MIME with supported array parameter', function () { 55 | const serialized = serializeMimeType( 56 | { 57 | type: 'image', 58 | subtype: 'svg+xml', 59 | parameters: new Map([['mutant', ['a', 'b', 'c']]]), 60 | }, 61 | { 62 | arrayParameters: true, 63 | }, 64 | ); 65 | 66 | expect(serialized).toEqual('image/svg+xml;mutant[]="a,b,c"'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/RetryWork.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { retryWork, waitForMilliseconds } from '../helpers'; 3 | 4 | describe('RetryWork', () => { 5 | it('Return 1', async () => { 6 | const work = await retryWork({ 7 | getter: async () => { 8 | await waitForMilliseconds(1); 9 | return 1; 10 | }, 11 | }); 12 | 13 | expect(work.success).eq(true); 14 | expect(work.retries).eq(0); 15 | expect(work.result).eq(1); 16 | }); 17 | it('Return 1 after 3 retries', async () => { 18 | let retries = 0; 19 | const work = await retryWork({ 20 | getter: async () => { 21 | await waitForMilliseconds(1); 22 | if (retries < 3) { 23 | retries++; 24 | throw new Error('Should retry 3 times'); 25 | } 26 | return 1; 27 | }, 28 | retry: 10, 29 | }); 30 | 31 | expect(work.success).eq(true); 32 | expect(work.retries).eq(retries); 33 | expect(work.result).eq(1); 34 | }); 35 | it('Fail after 3 retries', async () => { 36 | const work = await retryWork({ 37 | getter: async () => { 38 | await waitForMilliseconds(1); 39 | throw new Error('Failed'); 40 | }, 41 | retry: 3, 42 | interval: 1000, 43 | }); 44 | 45 | expect(work.success).eq(false); 46 | expect(work.result).eq(void 0); 47 | expect(work.retries).eq(3); 48 | }); 49 | it('Return 1 but fail', async () => { 50 | const work = await retryWork({ 51 | getter: async () => { 52 | await waitForMilliseconds(1); 53 | return 1; 54 | }, 55 | onComplete() { 56 | return false; 57 | }, 58 | }); 59 | 60 | expect(work.success).eq(false); 61 | expect(work.result).eq(void 0); 62 | }); 63 | it('Return 1 but stop', async () => { 64 | const work = await retryWork({ 65 | getter: async () => { 66 | await waitForMilliseconds(1); 67 | return 1; 68 | }, 69 | onComplete() { 70 | throw new Error('Should stop'); 71 | }, 72 | onError() { 73 | return false; 74 | }, 75 | }); 76 | 77 | expect(work.success).eq(false); 78 | expect(work.result).eq(void 0); 79 | expect(work.retries).eq(0); 80 | }); 81 | it('Fail and stop', async () => { 82 | const work = await retryWork({ 83 | getter: async () => { 84 | await waitForMilliseconds(1); 85 | throw new Error('Failed'); 86 | }, 87 | onError() { 88 | return false; 89 | }, 90 | }); 91 | 92 | expect(work.success).eq(false); 93 | expect(work.result).eq(void 0); 94 | expect(work.retries).eq(0); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/Vitest.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { waitForMilliseconds } from '../helpers'; 3 | 4 | describe('Vitest', () => { 5 | describe('Sequential works', () => { 6 | let result: number | null = null; 7 | it('Finish after 1 sec', async () => { 8 | await waitForMilliseconds(1000); 9 | if (result === null) { 10 | result = 1; 11 | } 12 | }); 13 | it('Finish immediately', () => { 14 | if (result === null) { 15 | result = 2; 16 | } 17 | }); 18 | it('The result should be 1', () => { 19 | expect(result).toEqual(1); 20 | }); 21 | }); 22 | describe('Concurrent works', () => { 23 | let result: number | null = null; 24 | describe.concurrent('Run works at the same time', () => { 25 | it('Finish after 1 sec', async () => { 26 | await waitForMilliseconds(1000); 27 | if (result === null) { 28 | result = 1; 29 | } 30 | }); 31 | it('Finish immediately', () => { 32 | if (result === null) { 33 | result = 2; 34 | } 35 | }); 36 | }); 37 | it('The result should be 2', () => { 38 | expect(result).toEqual(2); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/helpers/combine.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | /** 3 | * 合并两个对象中的特定数组属性,并去重。 4 | * 5 | * @param obj1 第一个对象 6 | * @param obj2 第二个对象 7 | * @param keys 需要合并和去重的属性名数组 8 | * @returns 返回合并后的新对象 9 | */ 10 | function mergeObjectsWithUniqueArrays( 11 | obj1: helpers.TransactionSkeletonType, 12 | obj2: helpers.TransactionSkeletonType, 13 | ): helpers.TransactionSkeletonType { 14 | let keys = [ 15 | 'inputs', 16 | 'outputs', 17 | 'cellDeps', 18 | 'headerDeps', 19 | 'witnesses', 20 | 'fixedEntries', 21 | 'signingEntries', 22 | 'inputSinces', 23 | ]; 24 | const result: helpers.TransactionSkeletonType = { ...obj1 }; 25 | 26 | keys.forEach((key: string) => { 27 | const array1 = Array.isArray(obj1[key]) ? obj1[key] : []; 28 | const array2 = Array.isArray(obj2[key]) ? obj2[key] : []; 29 | const mergedArray = [...array1, ...array2]; 30 | const uniqueArray = Array.from(new Set(mergedArray.map((item) => JSON.stringify(item)))).map((str) => 31 | JSON.parse(str), 32 | ); 33 | result[key] = uniqueArray; 34 | }); 35 | 36 | return result; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/helpers/file.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readFileSync } from 'fs'; 3 | import { bytes } from '@ckb-lumos/codec'; 4 | import { HexString } from '@ckb-lumos/lumos'; 5 | import { bytifyRawString } from '../../helpers'; 6 | 7 | export async function fetchLocalFile( 8 | src: string, 9 | relativePath?: string, 10 | ): Promise<{ 11 | bytes: ArrayBuffer; 12 | hex: HexString; 13 | }> { 14 | const buffer = readFileSync(resolve(relativePath ?? __dirname, src)); 15 | const uint8Array = new Uint8Array(buffer); 16 | return { 17 | bytes: uint8Array, 18 | hex: bytes.hexify(uint8Array), 19 | }; 20 | } 21 | 22 | export async function fetchLocalImage( 23 | src: string, 24 | relativePath?: string, 25 | ): Promise<{ 26 | arrayBuffer: ArrayBuffer; 27 | arrayBufferHex: HexString; 28 | base64: string; 29 | base64Hex: HexString; 30 | }> { 31 | const buffer = readFileSync(resolve(relativePath ?? __dirname, src)); 32 | const arrayBuffer = new Uint8Array(buffer).buffer; 33 | const base64 = buffer.toString('base64'); 34 | return { 35 | base64, 36 | arrayBuffer, 37 | arrayBufferHex: bytes.hexify(arrayBuffer), 38 | base64Hex: bytes.hexify(bytifyRawString(base64)), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account'; 2 | export * from './record'; 3 | export * from './check'; 4 | export * from './retry'; 5 | export * from './file'; 6 | export * from './config'; 7 | export * from './wallet'; 8 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/helpers/record.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { Hash, OutPoint } from '@ckb-lumos/base'; 3 | import { Account } from './account'; 4 | 5 | export interface TestRecord { 6 | account: Account; 7 | } 8 | export interface IdRecord extends TestRecord { 9 | id: Hash; 10 | } 11 | export interface OutPointRecord extends TestRecord { 12 | outPoint: OutPoint; 13 | } 14 | 15 | export function popRecord(records: T[], strict: true): T; 16 | export function popRecord(records: T[], strict?: false): T | undefined; 17 | export function popRecord(records: T[], strict?: unknown): T | undefined { 18 | const [record] = records.splice(records.length - 1, 1); 19 | if (strict) { 20 | expect(record).toBeDefined(); 21 | } 22 | 23 | return record; 24 | } 25 | 26 | export function unshiftRecord(records: T[], strict: true): T; 27 | export function unshiftRecord(records: T[], strict?: false): T | undefined; 28 | export function unshiftRecord(records: T[], strict?: unknown): T | undefined { 29 | const [record] = records.splice(0, 1); 30 | if (strict) { 31 | expect(record).toBeDefined(); 32 | } 33 | 34 | return record; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/helpers/retry.ts: -------------------------------------------------------------------------------- 1 | import { retryWork } from '../../helpers'; 2 | 3 | export async function retryQuery(getter: () => T | Promise): Promise { 4 | const work = await retryWork({ 5 | getter, 6 | retry: 8, 7 | interval: 10000, 8 | }); 9 | 10 | if (!work.success) { 11 | if (work.errors.length > 0) { 12 | throw new Error(`RetryWork failed for ${work.retries} times`, { 13 | cause: work.errors.pop(), 14 | }); 15 | } else { 16 | throw new Error(`RetryWork failed with no error for ${work.retries} times`); 17 | } 18 | } 19 | 20 | return work.result as T; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources/firstOutputMutant.lua: -------------------------------------------------------------------------------- 1 | if spore_ext_mode == 1 and spore_output_index > 0 then 2 | ckb.exit_script(89) 3 | end 4 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources/immortalMutant.lua: -------------------------------------------------------------------------------- 1 | -- Melt a Spore denied 2 | if spore_ext_mode == 3 then 3 | ckb.exit_script("99") 4 | end 5 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources/mustTransferMutant.lua: -------------------------------------------------------------------------------- 1 | if spore_ext_mode == 2 then 2 | local input_lock_hash, err = ckb.load_cell_by_field(spore_input_index, ckb.SOURCE_INPUT, ckb.CELL_FIELD_LOCK_HASH) 3 | local output_lock_hash, err = ckb.load_cell_by_field(spore_output_index, ckb.SOURCE_OUTPUT, ckb.CELL_FIELD_LOCK_HASH) 4 | if input_lock_hash == output_lock_hash then 5 | ckb.exit_script(87) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources/noTransferMutant.lua: -------------------------------------------------------------------------------- 1 | if spore_ext_mode == 2 then 2 | ckb.exit_script(88) 3 | end 4 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/resources/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporeprotocol/spore-sdk/5e5d886e49bfbd78253e9c14fd8baebccb488a81/packages/core/src/__tests__/resources/test.jpg -------------------------------------------------------------------------------- /packages/core/src/__tests__/shared/env.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { RPC, Indexer } from '@ckb-lumos/lumos'; 3 | import { getEnvVariable, generateTestConfig, createDefaultLockAccount } from '../helpers'; 4 | import { forkSporeConfig } from '../../config'; 5 | 6 | export const TEST_VARIABLES = { 7 | network: getEnvVariable('VITE_NETWORK', 'string', 'testnet'), 8 | configPath: getEnvVariable('VITE_CONFIG_PATH', 'string', '../tmp/config.json'), 9 | tests: { 10 | clusterV1: getEnvVariable('VITE_TEST_CLUSTER_V1', 'boolean', false), 11 | }, 12 | accounts: { 13 | charlie: getEnvVariable( 14 | 'VITE_ACCOUNT_CHARLIE', 15 | 'string', 16 | '0xd6013cd867d286ef84cc300ac6546013837df2b06c9f53c83b4c33c2417f6a07', 17 | ), 18 | alice: getEnvVariable( 19 | 'VITE_ACCOUNT_ALICE', 20 | 'string', 21 | '0x49aa6d595ac46cc8e1a31b511754dd58f241a7d8a6ad29e83d6b0c1a82399f3d', 22 | ), 23 | bob: getEnvVariable( 24 | 'VITE_ACCOUNT_BOB', 25 | 'string', 26 | '0xee638e49a61bdc7fda63c412c29d5185eec2913f1122ab59b5d362ee9ef9bb50', 27 | ), 28 | }, 29 | }; 30 | 31 | const config = generateTestConfig(TEST_VARIABLES.network, resolve(__dirname, TEST_VARIABLES.configPath)); 32 | export const TEST_ENV = { 33 | config, 34 | v1Config: forkSporeConfig(config, { 35 | defaultTags: ['v1'], 36 | }), 37 | rpc: new RPC(config.ckbNodeUrl), 38 | indexer: new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl), 39 | }; 40 | 41 | export const TEST_ACCOUNTS = { 42 | CHARLIE: createDefaultLockAccount(TEST_VARIABLES.accounts.charlie, config), 43 | ALICE: createDefaultLockAccount(TEST_VARIABLES.accounts.alice, config), 44 | BOB: createDefaultLockAccount(TEST_VARIABLES.accounts.bob, config), 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/src/__tests__/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | export * from './record'; 3 | -------------------------------------------------------------------------------- /packages/core/src/api/composed/clusterAgent/meltClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { Address, OutPoint } from '@ckb-lumos/base'; 2 | import { helpers, HexString, Indexer } from '@ckb-lumos/lumos'; 3 | import { returnExceededCapacityAndPayFee } from '../../../helpers'; 4 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 5 | import { generateMeltClusterAgentAction, injectCommonCobuildProof } from '../../../cobuild'; 6 | import { getClusterAgentByOutPoint, injectLiveClusterAgentCell } from '../..'; 7 | 8 | export async function meltClusterAgent(props: { 9 | outPoint: OutPoint; 10 | changeAddress?: Address; 11 | updateWitness?: HexString | ((witness: HexString) => HexString); 12 | config?: SporeConfig; 13 | }): Promise<{ 14 | txSkeleton: helpers.TransactionSkeletonType; 15 | inputIndex: number; 16 | }> { 17 | // Env 18 | const config = props.config ?? getSporeConfig(); 19 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 20 | 21 | // TransactionSkeleton 22 | let txSkeleton = helpers.TransactionSkeleton({ 23 | cellProvider: indexer, 24 | }); 25 | 26 | // Get ClusterAgent cell 27 | const clusterAgentCell = await getClusterAgentByOutPoint(props.outPoint, config); 28 | const clusterAgentScript = getSporeScript(config, 'ClusterAgent', clusterAgentCell.cellOutput.type!); 29 | 30 | // Inject target cell to Transaction.inputs 31 | const injectLiveClusterAgentCellResult = await injectLiveClusterAgentCell({ 32 | txSkeleton, 33 | cell: clusterAgentCell, 34 | updateWitness: props.updateWitness, 35 | config, 36 | }); 37 | txSkeleton = injectLiveClusterAgentCellResult.txSkeleton; 38 | 39 | // Inject CobuildProof 40 | if (clusterAgentScript.behaviors?.cobuild) { 41 | const actionResult = generateMeltClusterAgentAction({ 42 | txSkeleton, 43 | inputIndex: injectLiveClusterAgentCellResult.inputIndex, 44 | }); 45 | const injectCobuildProofResult = injectCommonCobuildProof({ 46 | txSkeleton, 47 | actions: actionResult.actions, 48 | }); 49 | txSkeleton = injectCobuildProofResult.txSkeleton; 50 | } 51 | 52 | // Redeem occupied capacity from the melted cell 53 | const targetCellAddress = helpers.encodeToAddress(clusterAgentCell.cellOutput.lock, { config: config.lumos }); 54 | const returnExceededCapacityAndPayFeeResult = await returnExceededCapacityAndPayFee({ 55 | changeAddress: props.changeAddress ?? targetCellAddress, 56 | txSkeleton, 57 | config, 58 | }); 59 | txSkeleton = returnExceededCapacityAndPayFeeResult.txSkeleton; 60 | 61 | return { 62 | txSkeleton, 63 | inputIndex: injectLiveClusterAgentCellResult.inputIndex, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/api/composed/clusterProxy/meltClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { Address, OutPoint } from '@ckb-lumos/base'; 2 | import { helpers, HexString, Indexer } from '@ckb-lumos/lumos'; 3 | import { returnExceededCapacityAndPayFee } from '../../../helpers'; 4 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 5 | import { generateMeltClusterProxyAction, injectCommonCobuildProof } from '../../../cobuild'; 6 | import { getClusterProxyByOutPoint, injectLiveClusterProxyCell } from '../..'; 7 | 8 | export async function meltClusterProxy(props: { 9 | outPoint: OutPoint; 10 | changeAddress?: Address; 11 | updateWitness?: HexString | ((witness: HexString) => HexString); 12 | config?: SporeConfig; 13 | }): Promise<{ 14 | txSkeleton: helpers.TransactionSkeletonType; 15 | inputIndex: number; 16 | }> { 17 | // Env 18 | const config = props.config ?? getSporeConfig(); 19 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 20 | 21 | // TransactionSkeleton 22 | let txSkeleton = helpers.TransactionSkeleton({ 23 | cellProvider: indexer, 24 | }); 25 | 26 | // Get ClusterProxy cell 27 | const clusterProxyCell = await getClusterProxyByOutPoint(props.outPoint, config); 28 | const clusterProxyScript = getSporeScript(config, 'ClusterProxy', clusterProxyCell.cellOutput.type!); 29 | 30 | // Inject live spore to Transaction.inputs 31 | const injectLiveClusterProxyCellResult = await injectLiveClusterProxyCell({ 32 | txSkeleton, 33 | cell: clusterProxyCell, 34 | updateWitness: props.updateWitness, 35 | config, 36 | }); 37 | txSkeleton = injectLiveClusterProxyCellResult.txSkeleton; 38 | 39 | // Inject CobuildProof 40 | if (clusterProxyScript.behaviors?.cobuild) { 41 | const actionResult = generateMeltClusterProxyAction({ 42 | txSkeleton, 43 | inputIndex: injectLiveClusterProxyCellResult.inputIndex, 44 | }); 45 | const injectCobuildProofResult = injectCommonCobuildProof({ 46 | txSkeleton, 47 | actions: actionResult.actions, 48 | }); 49 | txSkeleton = injectCobuildProofResult.txSkeleton; 50 | } 51 | 52 | // Redeem occupied capacity from the melted cell 53 | const targetCellAddress = helpers.encodeToAddress(clusterProxyCell.cellOutput.lock, { config: config.lumos }); 54 | const returnExceededCapacityAndPayFeeResult = await returnExceededCapacityAndPayFee({ 55 | changeAddress: props.changeAddress ?? targetCellAddress, 56 | txSkeleton, 57 | config, 58 | }); 59 | txSkeleton = returnExceededCapacityAndPayFeeResult.txSkeleton; 60 | 61 | return { 62 | txSkeleton, 63 | inputIndex: injectLiveClusterProxyCellResult.inputIndex, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/api/composed/mutant/createMutant.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { BytesLike } from '@ckb-lumos/codec'; 3 | import { Address, Script } from '@ckb-lumos/base'; 4 | import { FromInfo } from '@ckb-lumos/common-scripts'; 5 | import { BI, Cell, helpers, Indexer } from '@ckb-lumos/lumos'; 6 | import { getSporeConfig, SporeConfig } from '../../../config'; 7 | import { assertTransactionSkeletonSize, injectCapacityAndPayFee } from '../../../helpers'; 8 | import { injectNewMutantOutput, injectNewMutantIds } from '../..'; 9 | 10 | export async function createMutant(props: { 11 | data: BytesLike; 12 | minPayment?: BIish; 13 | toLock: Script; 14 | fromInfos: FromInfo[]; 15 | changeAddress?: Address; 16 | updateOutput?(cell: Cell): Cell; 17 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 18 | maxTransactionSize?: number | false; 19 | feeRate?: BIish | undefined; 20 | config?: SporeConfig; 21 | }): Promise<{ 22 | txSkeleton: helpers.TransactionSkeletonType; 23 | outputIndex: number; 24 | }> { 25 | // Env 26 | const config = props.config ?? getSporeConfig(); 27 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 28 | const capacityMargin = BI.from(props.capacityMargin ?? 1_0000_0000); 29 | const maxTransactionSize = props.maxTransactionSize ?? config.maxTransactionSize ?? false; 30 | 31 | // TransactionSkeleton 32 | let txSkeleton = helpers.TransactionSkeleton({ 33 | cellProvider: indexer, 34 | }); 35 | 36 | // Create and inject a new Mutant cell 37 | const injectNewMutantResult = injectNewMutantOutput({ 38 | txSkeleton, 39 | data: props.data, 40 | toLock: props.toLock, 41 | minPayment: props.minPayment, 42 | updateOutput: props.updateOutput, 43 | capacityMargin, 44 | config, 45 | }); 46 | txSkeleton = injectNewMutantResult.txSkeleton; 47 | 48 | // Inject needed capacity and pay fee 49 | const injectCapacityAndPayFeeResult = await injectCapacityAndPayFee({ 50 | txSkeleton, 51 | fromInfos: props.fromInfos, 52 | changeAddress: props.changeAddress, 53 | feeRate: props.feeRate, 54 | config, 55 | }); 56 | txSkeleton = injectCapacityAndPayFeeResult.txSkeleton; 57 | 58 | // Generate and inject ID for the new Mutant 59 | txSkeleton = injectNewMutantIds({ 60 | outputIndices: [injectNewMutantResult.outputIndex], 61 | txSkeleton, 62 | config, 63 | }); 64 | 65 | // Make sure the tx size is in range (if needed) 66 | if (typeof maxTransactionSize === 'number') { 67 | assertTransactionSkeletonSize(txSkeleton, void 0, maxTransactionSize); 68 | } 69 | 70 | return { 71 | txSkeleton, 72 | outputIndex: injectNewMutantResult.outputIndex, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/src/api/composed/spore/meltSpore.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { Address, OutPoint } from '@ckb-lumos/base'; 3 | import { Indexer, helpers, HexString, PackedSince } from '@ckb-lumos/lumos'; 4 | import { returnExceededCapacityAndPayFee } from '../../../helpers'; 5 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 6 | import { getSporeByOutPoint, injectLiveSporeCell } from '../..'; 7 | import { generateMeltSporeAction, injectCommonCobuildProof } from '../../../cobuild'; 8 | 9 | export async function meltSpore(props: { 10 | outPoint: OutPoint; 11 | changeAddress?: Address; 12 | updateWitness?: HexString | ((witness: HexString) => HexString); 13 | defaultWitness?: HexString; 14 | since?: PackedSince; 15 | config?: SporeConfig; 16 | feeRate?: BIish | undefined; 17 | }): Promise<{ 18 | txSkeleton: helpers.TransactionSkeletonType; 19 | inputIndex: number; 20 | }> { 21 | // Env 22 | const config = props.config ?? getSporeConfig(); 23 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 24 | 25 | // TransactionSkeleton 26 | let txSkeleton = helpers.TransactionSkeleton({ 27 | cellProvider: indexer, 28 | }); 29 | 30 | // Inject live spore to Transaction.inputs 31 | const sporeCell = await getSporeByOutPoint(props.outPoint, config); 32 | const injectLiveSporeCellResult = await injectLiveSporeCell({ 33 | txSkeleton, 34 | cell: sporeCell, 35 | updateWitness: props.updateWitness, 36 | defaultWitness: props.defaultWitness, 37 | since: props.since, 38 | config, 39 | }); 40 | txSkeleton = injectLiveSporeCellResult.txSkeleton; 41 | 42 | // Inject CobuildProof 43 | const sporeScript = getSporeScript(config, 'Spore', sporeCell.cellOutput.type!); 44 | if (sporeScript.behaviors?.cobuild) { 45 | const actionResult = generateMeltSporeAction({ 46 | txSkeleton: txSkeleton, 47 | inputIndex: injectLiveSporeCellResult.inputIndex, 48 | }); 49 | const injectCobuildProofResult = injectCommonCobuildProof({ 50 | txSkeleton: txSkeleton, 51 | actions: actionResult.actions, 52 | }); 53 | txSkeleton = injectCobuildProofResult.txSkeleton; 54 | } 55 | 56 | // Redeem capacity from the melted spore 57 | const sporeAddress = helpers.encodeToAddress(sporeCell.cellOutput.lock, { config: config.lumos }); 58 | const returnExceededCapacityAndPayFeeResult = await returnExceededCapacityAndPayFee({ 59 | changeAddress: props.changeAddress ?? sporeAddress, 60 | txSkeleton, 61 | config, 62 | feeRate: props.feeRate, 63 | }); 64 | txSkeleton = returnExceededCapacityAndPayFeeResult.txSkeleton; 65 | 66 | return { 67 | txSkeleton, 68 | inputIndex: injectLiveSporeCellResult.inputIndex, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Composed APIs 3 | */ 4 | 5 | // Cluster 6 | export * from './composed/cluster/createCluster'; 7 | export * from './composed/cluster/transferCluster'; 8 | 9 | // Spore 10 | export * from './composed/spore/createSpore'; 11 | export * from './composed/spore/transferSpore'; 12 | export * from './composed/spore/meltSpore'; 13 | export * from './composed/spore/meltThenCreateSpore'; 14 | 15 | // ClusterProxy 16 | export * from './composed/clusterProxy/createClusterProxy'; 17 | export * from './composed/clusterProxy/transferClusterProxy'; 18 | export * from './composed/clusterProxy/meltClusterProxy'; 19 | 20 | // ClusterAgent 21 | export * from './composed/clusterAgent/createClusterAgent'; 22 | export * from './composed/clusterAgent/transferClusterAgent'; 23 | export * from './composed/clusterAgent/meltClusterAgent'; 24 | 25 | // Mutant 26 | export * from './composed/mutant/createMutant'; 27 | export * from './composed/mutant/transferMutant'; 28 | 29 | /** 30 | * Joint APIs 31 | */ 32 | 33 | // Cluster 34 | export * from './joints/cluster/injectNewClusterOutput'; 35 | export * from './joints/cluster/injectNewClusterIds'; 36 | export * from './joints/cluster/injectLiveClusterCell'; 37 | export * from './joints/cluster/injectLiveClusterReference'; 38 | export * from './joints/cluster/getCluster'; 39 | 40 | // Spore 41 | export * from './joints/spore/injectNewSporeOutput'; 42 | export * from './joints/spore/injectLiveSporeCell'; 43 | export * from './joints/spore/injectNewSporeIds'; 44 | export * from './joints/spore/getSpore'; 45 | 46 | // ClusterProxy 47 | export * from './joints/clusterProxy/injectNewClusterProxyOutput'; 48 | export * from './joints/clusterProxy/injectNewClusterProxyIds'; 49 | export * from './joints/clusterProxy/injectLiveClusterProxyCell'; 50 | export * from './joints/clusterProxy/injectLiveClusterProxyReference'; 51 | export * from './joints/clusterProxy/getClusterProxy'; 52 | 53 | // ClusterAgent 54 | export * from './joints/clusterAgent/injectNewClusterAgentOutput'; 55 | export * from './joints/clusterAgent/injectLiveClusterAgentCell'; 56 | export * from './joints/clusterAgent/injectLiveClusterAgentReference'; 57 | export * from './joints/clusterAgent/getClusterAgent'; 58 | 59 | // Mutant 60 | export * from './joints/mutant/injectNewMutantOutput'; 61 | export * from './joints/mutant/injectNewMutantIds'; 62 | export * from './joints/mutant/injectLiveMutantCell'; 63 | export * from './joints/mutant/injectLiveMutantReferences'; 64 | export * from './joints/mutant/getMutant'; 65 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/cluster/getCluster.ts: -------------------------------------------------------------------------------- 1 | import { OutPoint, Script } from '@ckb-lumos/base'; 2 | import { Cell, HexString, Indexer, RPC } from '@ckb-lumos/lumos'; 3 | import { getCellByType, getCellWithStatusByOutPoint, isTypeId } from '../../../helpers'; 4 | import { getSporeConfig, getSporeScriptCategory, isSporeScriptSupported, SporeConfig } from '../../../config'; 5 | 6 | export async function getClusterByType(type: Script, config?: SporeConfig): Promise { 7 | // Env 8 | config = config ?? getSporeConfig(); 9 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 10 | 11 | // Check if the cluster's id is TypeID 12 | if (!isTypeId(type.args)) { 13 | throw new Error(`Target Cluster Id is invalid: ${type.args}`); 14 | } 15 | 16 | // Get cell by type 17 | const cell = await getCellByType({ type, indexer }); 18 | if (cell === void 0) { 19 | throw new Error('Cannot find Cluster by Type because target cell does not exist'); 20 | } 21 | 22 | // Check target cell's type script 23 | const cellType = cell.cellOutput.type; 24 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Cluster')) { 25 | throw new Error('Cannot find Cluster by Type because target cell is not a supported version of Cluster'); 26 | } 27 | 28 | return cell; 29 | } 30 | 31 | export async function getClusterByOutPoint(outPoint: OutPoint, config?: SporeConfig): Promise { 32 | // Env 33 | config = config ?? getSporeConfig(); 34 | const rpc = new RPC(config.ckbNodeUrl); 35 | 36 | // Get cell from rpc 37 | const cellWithStatus = await getCellWithStatusByOutPoint({ 38 | outPoint, 39 | rpc, 40 | }); 41 | if (!cellWithStatus.cell) { 42 | throw new Error('Cannot find Cluster by OutPoint because target cell was not found'); 43 | } 44 | if (cellWithStatus.status !== 'live') { 45 | throw new Error('Cannot find Cluster by OutPoint because target cell is not lived'); 46 | } 47 | 48 | // Check target cell's type script 49 | const cellType = cellWithStatus.cell.cellOutput.type; 50 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Cluster')) { 51 | throw new Error('Cannot find Cluster by OutPoint because target cell is not a supported version of Cluster'); 52 | } 53 | 54 | return cellWithStatus.cell; 55 | } 56 | 57 | export async function getClusterById(id: HexString, config?: SporeConfig): Promise { 58 | // Env 59 | config = config ?? getSporeConfig(); 60 | 61 | // Check if the cluster's id is TypeID 62 | if (!isTypeId(id)) { 63 | throw new Error(`Target ClusterId is invalid: ${id}`); 64 | } 65 | 66 | // Get cluster script 67 | const clusterScript = getSporeScriptCategory(config, 'Cluster'); 68 | const scripts = (clusterScript.versions ?? []).map((r) => r.script); 69 | 70 | // Search target cluster from the latest version to the oldest 71 | for (const script of scripts) { 72 | try { 73 | return await getClusterByType( 74 | { 75 | ...script, 76 | args: id, 77 | }, 78 | config, 79 | ); 80 | } catch (e) { 81 | // Not found in the script, don't have to do anything 82 | console.error('getClusterById error:', e); 83 | } 84 | } 85 | 86 | throw new Error( 87 | `Cannot find Cluster by Id because target cell does not exist or it's not a supported version of Cluster`, 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/cluster/injectLiveClusterCell.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { PackedSince } from '@ckb-lumos/base'; 3 | import { BI, Cell, helpers, HexString } from '@ckb-lumos/lumos'; 4 | import { addCellDep } from '@ckb-lumos/lumos/helpers'; 5 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 6 | import { assetCellMinimalCapacity, setAbsoluteCapacityMargin, setupCell } from '../../../helpers'; 7 | 8 | export async function injectLiveClusterCell(props: { 9 | txSkeleton: helpers.TransactionSkeletonType; 10 | cell: Cell; 11 | addOutput?: boolean; 12 | updateOutput?: (cell: Cell) => Cell; 13 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 14 | updateWitness?: HexString | ((witness: HexString) => HexString); 15 | defaultWitness?: HexString; 16 | since?: PackedSince; 17 | config?: SporeConfig; 18 | }): Promise<{ 19 | txSkeleton: helpers.TransactionSkeletonType; 20 | inputIndex: number; 21 | outputIndex: number; 22 | }> { 23 | // Env 24 | const clusterCell = props.cell; 25 | const config = props.config ?? getSporeConfig(); 26 | 27 | // Get TransactionSkeleton 28 | let txSkeleton = props.txSkeleton; 29 | 30 | // Check target cell's type 31 | const clusterCellType = clusterCell.cellOutput.type; 32 | const clusterScript = getSporeScript(config, 'Cluster', clusterCellType!); 33 | if (!clusterCellType || !clusterScript) { 34 | throw new Error('Cannot inject Cluster because target cell is not a supported version of Cluster'); 35 | } 36 | 37 | // Add cluster cell to Transaction.inputs 38 | const setupCellResult = await setupCell({ 39 | txSkeleton, 40 | input: props.cell, 41 | config: config.lumos, 42 | addOutput: props.addOutput, 43 | updateOutput(cell) { 44 | if (props.capacityMargin !== void 0) { 45 | cell = setAbsoluteCapacityMargin(cell, props.capacityMargin); 46 | } 47 | if (props.updateOutput instanceof Function) { 48 | cell = props.updateOutput(cell); 49 | } 50 | return cell; 51 | }, 52 | defaultWitness: props.defaultWitness, 53 | updateWitness: props.updateWitness, 54 | since: props.since, 55 | }); 56 | txSkeleton = setupCellResult.txSkeleton; 57 | 58 | // If the cluster is added to Transaction.outputs 59 | if (props.addOutput) { 60 | // Make sure the cell's output has declared enough capacity 61 | const output = txSkeleton.get('outputs').get(setupCellResult.outputIndex)!; 62 | assetCellMinimalCapacity(output); 63 | 64 | // Fix the cell's output index 65 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 66 | return fixedEntries.push({ 67 | field: 'outputs', 68 | index: setupCellResult.outputIndex, 69 | }); 70 | }); 71 | } 72 | 73 | // Add cluster required cellDeps 74 | txSkeleton = addCellDep(txSkeleton, clusterScript.cellDep); 75 | 76 | return { 77 | txSkeleton, 78 | inputIndex: setupCellResult.inputIndex, 79 | outputIndex: setupCellResult.outputIndex, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/cluster/injectLiveClusterReference.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { Cell, PackedSince, Script } from '@ckb-lumos/base'; 3 | import { BI, helpers, HexString } from '@ckb-lumos/lumos'; 4 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 5 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 6 | import { referenceCellOrLockProxy } from '../../../helpers'; 7 | import { injectLiveClusterCell } from './injectLiveClusterCell'; 8 | 9 | export async function injectLiveClusterReference(props: { 10 | txSkeleton: helpers.TransactionSkeletonType; 11 | cell: Cell; 12 | inputLocks: Script[]; 13 | outputLocks: Script[]; 14 | updateOutput?: (cell: Cell) => Cell; 15 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 16 | updateWitness?: HexString | ((witness: HexString) => HexString); 17 | defaultWitness?: HexString; 18 | since?: PackedSince; 19 | config?: SporeConfig; 20 | }): Promise<{ 21 | txSkeleton: helpers.TransactionSkeletonType; 22 | referenceType: 'cell' | 'lockProxy'; 23 | cluster?: { 24 | inputIndex: number; 25 | outputIndex: number; 26 | }; 27 | }> { 28 | // Env 29 | const config = props.config ?? getSporeConfig(); 30 | const clusterCell = props.cell; 31 | 32 | // TransactionSkeleton 33 | let txSkeleton = props.txSkeleton; 34 | 35 | // Injection status & hooks 36 | let injectLiveClusterResult: Awaited> | undefined; 37 | 38 | // Inject referenced cluster directly or inject LockProxy only 39 | const referenceResult = await referenceCellOrLockProxy({ 40 | txSkeleton, 41 | cell: clusterCell, 42 | inputLocks: props.inputLocks, 43 | outputLocks: props.outputLocks, 44 | async referenceCell(tx) { 45 | injectLiveClusterResult = await injectLiveClusterCell({ 46 | txSkeleton: tx, 47 | cell: clusterCell, 48 | addOutput: true, 49 | updateOutput: props.updateOutput, 50 | updateWitness: props.updateWitness, 51 | capacityMargin: props.capacityMargin, 52 | defaultWitness: props.defaultWitness, 53 | since: props.since, 54 | config, 55 | }); 56 | 57 | return injectLiveClusterResult.txSkeleton; 58 | }, 59 | async referenceLockProxy(tx) { 60 | const clusterType = clusterCell.cellOutput.type; 61 | const clusterScript = getSporeScript(config, 'Cluster', clusterType!); 62 | if (!clusterScript.behaviors?.lockProxy) { 63 | throw new Error('Cannot reference Cluster because target Cluster does not supported lockProxy'); 64 | } 65 | 66 | tx = addCellDep(tx, clusterScript.cellDep); 67 | 68 | return tx; 69 | }, 70 | }); 71 | txSkeleton = referenceResult.txSkeleton; 72 | 73 | return { 74 | txSkeleton, 75 | referenceType: referenceResult.referencedCell ? 'cell' : 'lockProxy', 76 | cluster: 77 | referenceResult.referencedCell && injectLiveClusterResult !== void 0 78 | ? { 79 | inputIndex: injectLiveClusterResult.inputIndex, 80 | outputIndex: injectLiveClusterResult.outputIndex, 81 | } 82 | : void 0, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/cluster/injectNewClusterIds.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { generateTypeIdsByOutputs } from '../../../helpers'; 3 | import { getSporeConfig, isSporeScriptSupported, SporeConfig } from '../../../config'; 4 | 5 | export function injectNewClusterIds(props: { 6 | txSkeleton: helpers.TransactionSkeletonType; 7 | outputIndices?: number[]; 8 | config?: SporeConfig; 9 | }): helpers.TransactionSkeletonType { 10 | // Env 11 | const config = props.config ?? getSporeConfig(); 12 | 13 | // Get TransactionSkeleton 14 | let txSkeleton = props.txSkeleton; 15 | 16 | // Get the first input 17 | const inputs = txSkeleton.get('inputs'); 18 | const firstInput = inputs.get(0); 19 | if (!firstInput) { 20 | throw new Error('Cannot generate Cluster Id because Transaction.inputs[0] does not exist'); 21 | } 22 | 23 | // Calculates TypeIds by the outputs' indices 24 | let outputs = txSkeleton.get('outputs'); 25 | let typeIdGroup = generateTypeIdsByOutputs(firstInput, outputs.toArray(), (cell) => { 26 | return !!cell.cellOutput.type && isSporeScriptSupported(config, cell.cellOutput.type, 'Cluster'); 27 | }); 28 | 29 | // If `clusterOutputIndices` is provided, filter the result 30 | if (props.outputIndices) { 31 | typeIdGroup = typeIdGroup.filter(([outputIndex]) => { 32 | const index = props.outputIndices!.findIndex((index) => index === outputIndex); 33 | return index >= 0; 34 | }); 35 | if (typeIdGroup.length !== props.outputIndices.length) { 36 | throw new Error('Cannot generate Cluster Id because clusterOutputIndices cannot be fully handled'); 37 | } 38 | } 39 | 40 | // Update results 41 | for (const [index, typeId] of typeIdGroup) { 42 | const output = outputs.get(index); 43 | if (!output) { 44 | throw new Error(`Cannot generate Cluster Id because Transaction.outputs[${index}] does not exist`); 45 | } 46 | 47 | output.cellOutput.type!.args = typeId; 48 | outputs = outputs.set(index, output); 49 | } 50 | 51 | return txSkeleton.set('outputs', outputs); 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/cluster/injectNewClusterOutput.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { bytes } from '@ckb-lumos/codec'; 3 | import { Script } from '@ckb-lumos/base'; 4 | import { BI, Cell, helpers } from '@ckb-lumos/lumos'; 5 | import { addCellDep } from '@ckb-lumos/lumos/helpers'; 6 | import { RawClusterData, packRawClusterData } from '../../../codec'; 7 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 8 | import { correctCellMinimalCapacity, setAbsoluteCapacityMargin } from '../../../helpers'; 9 | import { injectNewClusterIds } from './injectNewClusterIds'; 10 | 11 | export function injectNewClusterOutput(props: { 12 | txSkeleton: helpers.TransactionSkeletonType; 13 | data: RawClusterData; 14 | toLock: Script; 15 | config?: SporeConfig; 16 | updateOutput?(cell: Cell): Cell; 17 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 18 | }): { 19 | txSkeleton: helpers.TransactionSkeletonType; 20 | outputIndex: number; 21 | hasId: boolean; 22 | } { 23 | // Env 24 | const config = props.config ?? getSporeConfig(); 25 | 26 | // Get TransactionSkeleton 27 | let txSkeleton = props.txSkeleton; 28 | 29 | // Check the referenced Mutant's ID format 30 | if (props.data.mutantId !== void 0) { 31 | const packedMutantId = bytes.bytify(props.data.mutantId!); 32 | if (packedMutantId.byteLength !== 32) { 33 | throw new Error(`Invalid Mutant Id length, expected 32, actually: ${packedMutantId.byteLength}`); 34 | } 35 | } 36 | 37 | // Create Cluster cell (the latest version) 38 | const clusterScript = getSporeScript(config, 'Cluster'); 39 | const clusterData = packRawClusterData(props.data, clusterScript.behaviors?.clusterDataVersion as any); 40 | let clusterCell: Cell = correctCellMinimalCapacity({ 41 | cellOutput: { 42 | capacity: '0x0', 43 | lock: props.toLock, 44 | type: { 45 | ...clusterScript.script, 46 | args: '0x' + '0'.repeat(64), // Fill 32-byte TypeId placeholder 47 | }, 48 | }, 49 | data: bytes.hexify(clusterData), 50 | }); 51 | 52 | // Add to Transaction.outputs 53 | const outputIndex = txSkeleton.get('outputs').size; 54 | txSkeleton = txSkeleton.update('outputs', (outputs) => { 55 | if (props.capacityMargin !== void 0) { 56 | clusterCell = setAbsoluteCapacityMargin(clusterCell, props.capacityMargin); 57 | } 58 | if (props.updateOutput instanceof Function) { 59 | clusterCell = props.updateOutput(clusterCell); 60 | } 61 | return outputs.push(clusterCell); 62 | }); 63 | 64 | // Fix the output's index to prevent it from future reduction 65 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 66 | return fixedEntries.push({ 67 | field: 'outputs', 68 | index: outputIndex, 69 | }); 70 | }); 71 | 72 | // Generate ID for the new Cluster if possible 73 | const firstInput = txSkeleton.get('inputs').first(); 74 | if (firstInput) { 75 | txSkeleton = injectNewClusterIds({ 76 | outputIndices: [outputIndex], 77 | txSkeleton, 78 | config, 79 | }); 80 | } 81 | 82 | // Add Cluster required cellDeps 83 | txSkeleton = addCellDep(txSkeleton, clusterScript.cellDep); 84 | // Add Mutant cellDeps if ClusterData.mutantId is specified 85 | if (props.data.mutantId !== void 0) { 86 | const mutantScript = getSporeScript(config, 'Mutant'); 87 | txSkeleton = addCellDep(txSkeleton, mutantScript.cellDep); 88 | } 89 | 90 | return { 91 | txSkeleton, 92 | outputIndex, 93 | hasId: firstInput !== void 0, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterAgent/getClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { OutPoint } from '@ckb-lumos/base'; 2 | import { Cell, RPC } from '@ckb-lumos/lumos'; 3 | import { getCellWithStatusByOutPoint } from '../../../helpers'; 4 | import { SporeConfig, getSporeConfig, isSporeScriptSupported } from '../../../config'; 5 | 6 | export async function getClusterAgentByOutPoint(outPoint: OutPoint, config?: SporeConfig): Promise { 7 | // Env 8 | config = config ?? getSporeConfig(); 9 | const rpc = new RPC(config.ckbNodeUrl); 10 | 11 | // Get cell from rpc 12 | const cellWithStatus = await getCellWithStatusByOutPoint({ 13 | outPoint, 14 | rpc, 15 | }); 16 | if (!cellWithStatus.cell) { 17 | throw new Error('Cannot find ClusterAgent by OutPoint because target cell was not found'); 18 | } 19 | if (cellWithStatus.status !== 'live') { 20 | throw new Error('Cannot find ClusterAgent by OutPoint because target cell is not lived'); 21 | } 22 | 23 | // Check target cell's type script 24 | const cellType = cellWithStatus.cell.cellOutput.type; 25 | if (!cellType || !isSporeScriptSupported(config, cellType, 'ClusterAgent')) { 26 | throw new Error( 27 | 'Cannot find ClusterAgent by OutPoint because target cell is not a supported version of ClusterAgent', 28 | ); 29 | } 30 | 31 | return cellWithStatus.cell; 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterAgent/injectLiveClusterAgentCell.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { PackedSince } from '@ckb-lumos/base'; 3 | import { BI, Cell, helpers, HexString } from '@ckb-lumos/lumos'; 4 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 5 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 6 | import { assetCellMinimalCapacity, setAbsoluteCapacityMargin, setupCell } from '../../../helpers'; 7 | 8 | export async function injectLiveClusterAgentCell(props: { 9 | txSkeleton: helpers.TransactionSkeletonType; 10 | cell: Cell; 11 | addOutput?: boolean; 12 | config?: SporeConfig; 13 | updateOutput?(cell: Cell): Cell; 14 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 15 | updateWitness?: HexString | ((witness: HexString) => HexString); 16 | defaultWitness?: HexString; 17 | since?: PackedSince; 18 | }): Promise<{ 19 | txSkeleton: helpers.TransactionSkeletonType; 20 | inputIndex: number; 21 | outputIndex: number; 22 | }> { 23 | // Env 24 | const config = props.config ?? getSporeConfig(); 25 | const clusterAgentCell = props.cell; 26 | 27 | // TransactionSkeleton 28 | let txSkeleton = props.txSkeleton; 29 | 30 | // Check the target cell's type 31 | const cellType = clusterAgentCell.cellOutput.type; 32 | const clusterAgentScript = getSporeScript(config, 'ClusterAgent', cellType!); 33 | if (!cellType || !clusterAgentScript) { 34 | throw new Error('Cannot inject ClusterAgent because target cell is not a supported version of ClusterAgent'); 35 | } 36 | 37 | // Add the target cell to Transaction.inputs (and outputs if needed) 38 | const setupCellResult = await setupCell({ 39 | txSkeleton, 40 | input: clusterAgentCell, 41 | config: config.lumos, 42 | addOutput: props.addOutput, 43 | updateOutput(cell) { 44 | if (props.capacityMargin !== void 0) { 45 | cell = setAbsoluteCapacityMargin(cell, props.capacityMargin); 46 | } 47 | if (props.updateOutput instanceof Function) { 48 | cell = props.updateOutput(cell); 49 | } 50 | return cell; 51 | }, 52 | defaultWitness: props.defaultWitness, 53 | updateWitness: props.updateWitness, 54 | since: props.since, 55 | }); 56 | txSkeleton = setupCellResult.txSkeleton; 57 | 58 | // If the target cell has been added to Transaction.outputs 59 | if (props.addOutput) { 60 | // Make sure the cell's output has declared enough capacity 61 | const output = txSkeleton.get('outputs').get(setupCellResult.outputIndex)!; 62 | assetCellMinimalCapacity(output); 63 | 64 | // Fix the cell's output index 65 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 66 | return fixedEntries.push({ 67 | field: 'outputs', 68 | index: setupCellResult.outputIndex, 69 | }); 70 | }); 71 | } 72 | 73 | // Add ClusterAgent required cellDeps 74 | txSkeleton = addCellDep(txSkeleton, clusterAgentScript.cellDep); 75 | 76 | return { 77 | txSkeleton, 78 | inputIndex: setupCellResult.inputIndex, 79 | outputIndex: setupCellResult.outputIndex, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterAgent/injectLiveClusterAgentReference.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { PackedSince, Script } from '@ckb-lumos/base'; 3 | import { BI, Cell, helpers, HexString } from '@ckb-lumos/lumos'; 4 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 5 | import { referenceCellOrLockProxy } from '../../../helpers'; 6 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 7 | import { injectLiveClusterAgentCell } from './injectLiveClusterAgentCell'; 8 | 9 | export async function injectLiveClusterAgentReference(props: { 10 | txSkeleton: helpers.TransactionSkeletonType; 11 | cell: Cell; 12 | inputLocks: Script[]; 13 | outputLocks: Script[]; 14 | updateOutput?: (cell: Cell) => Cell; 15 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 16 | updateWitness?: HexString | ((witness: HexString) => HexString); 17 | defaultWitness?: HexString; 18 | since?: PackedSince; 19 | config?: SporeConfig; 20 | }): Promise<{ 21 | txSkeleton: helpers.TransactionSkeletonType; 22 | referenceType: 'cell' | 'lockProxy'; 23 | clusterAgent?: { 24 | inputIndex: number; 25 | outputIndex: number; 26 | }; 27 | }> { 28 | // Env 29 | const config = props.config ?? getSporeConfig(); 30 | let txSkeleton = props.txSkeleton; 31 | 32 | // Get ClusterAgent cell 33 | const clusterAgentCell = props.cell; 34 | if (!clusterAgentCell.outPoint) { 35 | throw new Error(`Cannot inject ClusterAgent as reference because target cell has no OutPoint`); 36 | } 37 | 38 | // Inject reference cell or LockProxy 39 | let injectLiveClusterAgentResult: Awaited> | undefined; 40 | const referenceResult = await referenceCellOrLockProxy({ 41 | txSkeleton, 42 | cell: clusterAgentCell, 43 | inputLocks: props.inputLocks, 44 | outputLocks: props.outputLocks, 45 | async referenceCell(tx) { 46 | injectLiveClusterAgentResult = await injectLiveClusterAgentCell({ 47 | txSkeleton: tx, 48 | cell: clusterAgentCell, 49 | addOutput: true, 50 | updateOutput: props.updateOutput, 51 | updateWitness: props.updateWitness, 52 | capacityMargin: props.capacityMargin, 53 | defaultWitness: props.defaultWitness, 54 | since: props.since, 55 | config, 56 | }); 57 | 58 | return injectLiveClusterAgentResult.txSkeleton; 59 | }, 60 | referenceLockProxy(tx) { 61 | const cellType = clusterAgentCell.cellOutput.type; 62 | const clusterAgentScript = getSporeScript(config, 'ClusterAgent', cellType!); 63 | if (!clusterAgentScript.behaviors?.lockProxy) { 64 | throw new Error('Cannot reference ClusterAgent because target cell does not supported lockProxy'); 65 | } 66 | 67 | // Add ClusterAgent required cellDeps 68 | tx = addCellDep(tx, clusterAgentScript.cellDep); 69 | 70 | return tx; 71 | }, 72 | }); 73 | txSkeleton = referenceResult.txSkeleton; 74 | 75 | return { 76 | txSkeleton, 77 | referenceType: referenceResult.referencedCell ? 'cell' : 'lockProxy', 78 | clusterAgent: 79 | referenceResult.referencedCell && injectLiveClusterAgentResult !== void 0 80 | ? { 81 | inputIndex: injectLiveClusterAgentResult.inputIndex, 82 | outputIndex: injectLiveClusterAgentResult.outputIndex, 83 | } 84 | : void 0, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterProxy/getClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { bytes } from '@ckb-lumos/codec'; 2 | import { Cell, Indexer, RPC } from '@ckb-lumos/lumos'; 3 | import { Hash, OutPoint, Script } from '@ckb-lumos/base'; 4 | import { getCellByType, getCellWithStatusByOutPoint } from '../../../helpers'; 5 | import { getSporeConfig, getSporeScriptCategory, isSporeScriptSupported, SporeConfig } from '../../../config'; 6 | import { packRawClusterProxyArgs } from '../../../codec'; 7 | 8 | export async function getClusterProxyByType(type: Script, config?: SporeConfig): Promise { 9 | // Env 10 | config = config ?? getSporeConfig(); 11 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 12 | 13 | // Get cell by type 14 | const cell = await getCellByType({ type, indexer }); 15 | if (cell === void 0) { 16 | throw new Error('Cannot find ClusterProxy by Type because target cell does not exist'); 17 | } 18 | 19 | // Check target cell's type script 20 | const cellType = cell.cellOutput.type; 21 | if (!cellType || !isSporeScriptSupported(config, cellType, 'ClusterProxy')) { 22 | throw new Error('Cannot find ClusterProxy by Type because target cell is not a supported version of ClusterProxy'); 23 | } 24 | 25 | return cell; 26 | } 27 | 28 | export async function getClusterProxyByOutPoint(outPoint: OutPoint, config?: SporeConfig): Promise { 29 | // Env 30 | config = config ?? getSporeConfig(); 31 | const rpc = new RPC(config.ckbNodeUrl); 32 | 33 | // Get cell from rpc 34 | const cellWithStatus = await getCellWithStatusByOutPoint({ 35 | outPoint, 36 | rpc, 37 | }); 38 | if (!cellWithStatus.cell) { 39 | throw new Error('Cannot find ClusterProxy by OutPoint because target cell was not found'); 40 | } 41 | if (cellWithStatus.status !== 'live') { 42 | throw new Error('Cannot find ClusterProxy by OutPoint because target cell is not lived'); 43 | } 44 | 45 | // Check target cell's type script 46 | const cellType = cellWithStatus.cell.cellOutput.type; 47 | if (!cellType || !isSporeScriptSupported(config, cellType, 'ClusterProxy')) { 48 | throw new Error( 49 | 'Cannot find ClusterProxy by OutPoint because target cell is not a supported version of ClusterProxy', 50 | ); 51 | } 52 | 53 | return cellWithStatus.cell; 54 | } 55 | 56 | export async function getClusterProxyById(id: Hash, config?: SporeConfig): Promise { 57 | // Env 58 | config = config ?? getSporeConfig(); 59 | 60 | // Get ClusterProxy script 61 | const clusterProxyScript = getSporeScriptCategory(config, 'ClusterProxy'); 62 | const scripts = (clusterProxyScript.versions ?? []).map((r) => r.script); 63 | 64 | // Search target cluster proxy from the latest version to the oldest 65 | const args = bytes.hexify( 66 | packRawClusterProxyArgs({ 67 | id, 68 | }), 69 | ); 70 | for (const script of scripts) { 71 | try { 72 | return await getClusterProxyByType( 73 | { 74 | ...script, 75 | args, 76 | }, 77 | config, 78 | ); 79 | } catch (e) { 80 | // Not found in the script, don't have to do anything 81 | console.error('getClusterProxyById error:', e); 82 | } 83 | } 84 | 85 | throw new Error( 86 | `Cannot find ClusterProxy by ID because target cell does not exist or it's not a supported version of ClusterProxy`, 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterProxy/injectLiveClusterProxyCell.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { PackedSince } from '@ckb-lumos/base'; 3 | import { BI, Cell, helpers, HexString } from '@ckb-lumos/lumos'; 4 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 5 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 6 | import { assetCellMinimalCapacity, setAbsoluteCapacityMargin, setupCell } from '../../../helpers'; 7 | 8 | export async function injectLiveClusterProxyCell(props: { 9 | txSkeleton: helpers.TransactionSkeletonType; 10 | cell: Cell; 11 | addOutput?: boolean; 12 | updateOutput?: (cell: Cell) => Cell; 13 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 14 | updateWitness?: HexString | ((witness: HexString) => HexString); 15 | defaultWitness?: HexString; 16 | since?: PackedSince; 17 | config?: SporeConfig; 18 | }): Promise<{ 19 | txSkeleton: helpers.TransactionSkeletonType; 20 | inputIndex: number; 21 | outputIndex: number; 22 | }> { 23 | // Env 24 | const config = props.config ?? getSporeConfig(); 25 | const clusterProxyCell = props.cell; 26 | 27 | // TransactionSkeleton 28 | let txSkeleton = props.txSkeleton; 29 | 30 | // Check target cell's type 31 | const cellType = clusterProxyCell.cellOutput.type; 32 | const clusterProxyScript = getSporeScript(config, 'ClusterProxy', cellType!); 33 | if (!cellType || !clusterProxyScript) { 34 | throw new Error('Cannot inject ClusterProxy because target cell is not a supported version of ClusterProxy'); 35 | } 36 | 37 | // Add target cell to Transaction.inputs (and outputs if needed) 38 | const setupCellResult = await setupCell({ 39 | txSkeleton, 40 | input: clusterProxyCell, 41 | addOutput: props.addOutput, 42 | updateOutput(cell) { 43 | if (props.capacityMargin !== void 0) { 44 | cell = setAbsoluteCapacityMargin(cell, props.capacityMargin); 45 | } 46 | if (props.updateOutput instanceof Function) { 47 | cell = props.updateOutput(cell); 48 | } 49 | return cell; 50 | }, 51 | defaultWitness: props.defaultWitness, 52 | updateWitness: props.updateWitness, 53 | since: props.since, 54 | config: config.lumos, 55 | }); 56 | txSkeleton = setupCellResult.txSkeleton; 57 | 58 | // If the target cell is added to Transaction.outputs 59 | if (props.addOutput) { 60 | // Make sure the cell's output has declared enough capacity 61 | const output = txSkeleton.get('outputs').get(setupCellResult.outputIndex)!; 62 | assetCellMinimalCapacity(output); 63 | 64 | // Fix the cell's output index 65 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 66 | return fixedEntries.push({ 67 | field: 'outputs', 68 | index: setupCellResult.outputIndex, 69 | }); 70 | }); 71 | } 72 | 73 | // Add ClusterProxy required cellDeps 74 | txSkeleton = addCellDep(txSkeleton, clusterProxyScript.cellDep); 75 | 76 | return { 77 | txSkeleton, 78 | inputIndex: setupCellResult.inputIndex, 79 | outputIndex: setupCellResult.outputIndex, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/clusterProxy/injectNewClusterProxyIds.ts: -------------------------------------------------------------------------------- 1 | import { bytes } from '@ckb-lumos/codec'; 2 | import { helpers } from '@ckb-lumos/lumos'; 3 | import { generateTypeIdsByOutputs } from '../../../helpers'; 4 | import { packRawClusterProxyArgs, unpackToRawClusterProxyArgs } from '../../../codec'; 5 | import { getSporeConfig, isSporeScriptSupported, SporeConfig } from '../../../config'; 6 | 7 | export function injectNewClusterProxyIds(props: { 8 | txSkeleton: helpers.TransactionSkeletonType; 9 | outputIndices?: number[]; 10 | config?: SporeConfig; 11 | }): helpers.TransactionSkeletonType { 12 | // Env 13 | const config = props.config ?? getSporeConfig(); 14 | 15 | // TransactionSkeleton 16 | let txSkeleton = props.txSkeleton; 17 | 18 | // Get the Transaction.inputs[0] 19 | const firstInput = txSkeleton.get('inputs').get(0); 20 | if (!firstInput) { 21 | throw new Error('Cannot generate ClusterProxy Id because Transaction.inputs[0] does not exist'); 22 | } 23 | 24 | // Generate TypeIds by the output indices 25 | let outputs = txSkeleton.get('outputs'); 26 | let typeIdGroup = generateTypeIdsByOutputs(firstInput, outputs.toArray(), (cell) => { 27 | return !!cell.cellOutput.type && isSporeScriptSupported(config, cell.cellOutput.type, 'ClusterProxy'); 28 | }); 29 | 30 | // Only keep the TypeIDs corresponding to the specified output indices 31 | if (props.outputIndices) { 32 | typeIdGroup = typeIdGroup.filter(([outputIndex]) => { 33 | const index = props.outputIndices!.findIndex((index) => index === outputIndex); 34 | return index >= 0; 35 | }); 36 | if (typeIdGroup.length !== props.outputIndices.length) { 37 | throw new Error('Cannot generate ClusterProxy Id because outputIndices cannot be fully handled'); 38 | } 39 | } 40 | 41 | // Update results 42 | for (const [index, typeId] of typeIdGroup) { 43 | const output = outputs.get(index); 44 | if (!output) { 45 | throw new Error(`Cannot generate ClusterProxy Id because Transaction.outputs[${index}] does not exist`); 46 | } 47 | 48 | const unpackedArgs = unpackToRawClusterProxyArgs(output.cellOutput.type!.args); 49 | const packedNewArgs = packRawClusterProxyArgs({ 50 | id: typeId, 51 | minPayment: unpackedArgs.minPayment, 52 | }); 53 | 54 | output.cellOutput.type!.args = bytes.hexify(packedNewArgs); 55 | outputs = outputs.set(index, output); 56 | } 57 | 58 | return txSkeleton.set('outputs', outputs); 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/mutant/getMutant.ts: -------------------------------------------------------------------------------- 1 | import { bytes } from '@ckb-lumos/codec'; 2 | import { Cell, Indexer, RPC } from '@ckb-lumos/lumos'; 3 | import { Hash, OutPoint, Script } from '@ckb-lumos/base'; 4 | import { packRawMutantArgs } from '../../../codec'; 5 | import { getCellByType, getCellWithStatusByOutPoint } from '../../../helpers'; 6 | import { getSporeConfig, getSporeScriptCategory, isSporeScriptSupported, SporeConfig } from '../../../config'; 7 | 8 | export async function getMutantByType(type: Script, config?: SporeConfig): Promise { 9 | // Env 10 | config = config ?? getSporeConfig(); 11 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 12 | 13 | // Get cell by type 14 | const cell = await getCellByType({ type, indexer }); 15 | if (cell === void 0) { 16 | throw new Error('Cannot find Mutant by Type because target cell does not exist'); 17 | } 18 | 19 | // Check target cell's type script 20 | const cellType = cell.cellOutput.type; 21 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Mutant')) { 22 | throw new Error('Cannot find Mutant by Type because target cell is not a supported version of Mutant'); 23 | } 24 | 25 | return cell; 26 | } 27 | 28 | export async function getMutantByOutPoint(outPoint: OutPoint, config?: SporeConfig): Promise { 29 | // Env 30 | config = config ?? getSporeConfig(); 31 | const rpc = new RPC(config.ckbNodeUrl); 32 | 33 | // Get cell from rpc 34 | const cellWithStatus = await getCellWithStatusByOutPoint({ 35 | outPoint, 36 | rpc, 37 | }); 38 | if (!cellWithStatus.cell) { 39 | throw new Error('Cannot find Mutant by OutPoint because target cell was not found'); 40 | } 41 | if (cellWithStatus.status !== 'live') { 42 | throw new Error('Cannot find Mutant by OutPoint because target cell is not lived'); 43 | } 44 | 45 | // Check target cell's type script 46 | const cellType = cellWithStatus.cell.cellOutput.type; 47 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Mutant')) { 48 | throw new Error('Cannot find Mutant by OutPoint because target cell is not a supported version of Mutant'); 49 | } 50 | 51 | return cellWithStatus.cell; 52 | } 53 | 54 | export async function getMutantById(id: Hash, config?: SporeConfig): Promise { 55 | // Env 56 | config = config ?? getSporeConfig(); 57 | 58 | // Get Mutant script 59 | const mutantScript = getSporeScriptCategory(config, 'Mutant'); 60 | const scripts = (mutantScript.versions ?? []).map((r) => r.script); 61 | 62 | // Search target cluster proxy from the latest version to the oldest 63 | const args = bytes.hexify( 64 | packRawMutantArgs({ 65 | id, 66 | }), 67 | ); 68 | for (const script of scripts) { 69 | try { 70 | return await getMutantByType( 71 | { 72 | ...script, 73 | args, 74 | }, 75 | config, 76 | ); 77 | } catch (e) { 78 | // Not found in the script, don't have to do anything 79 | console.error('getMutantById error:', e); 80 | } 81 | } 82 | 83 | throw new Error( 84 | `Cannot find Mutant by ID because target cell does not exist or it's not a supported version of Mutant`, 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/mutant/injectLiveMutantCell.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { bytes } from '@ckb-lumos/codec'; 3 | import { PackedSince } from '@ckb-lumos/base'; 4 | import { BI, Cell, helpers, HexString } from '@ckb-lumos/lumos'; 5 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 6 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 7 | import { assetCellMinimalCapacity, setAbsoluteCapacityMargin, setupCell } from '../../../helpers'; 8 | import { packRawMutantArgs, unpackToRawMutantArgs } from '../../../codec'; 9 | 10 | export async function injectLiveMutantCell(props: { 11 | txSkeleton: helpers.TransactionSkeletonType; 12 | cell: Cell; 13 | minPayment?: BIish; 14 | addOutput?: boolean; 15 | updateOutput?: (cell: Cell) => Cell; 16 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 17 | updateWitness?: HexString | ((witness: HexString) => HexString); 18 | defaultWitness?: HexString; 19 | since?: PackedSince; 20 | config?: SporeConfig; 21 | }): Promise<{ 22 | txSkeleton: helpers.TransactionSkeletonType; 23 | inputIndex: number; 24 | outputIndex: number; 25 | }> { 26 | // Env 27 | const mutantCell = props.cell; 28 | const config = props.config ?? getSporeConfig(); 29 | 30 | // Get TransactionSkeleton 31 | let txSkeleton = props.txSkeleton; 32 | 33 | // Check target cell's type 34 | const mutantCellType = mutantCell.cellOutput.type; 35 | const mutantScript = getSporeScript(config, 'Mutant', mutantCellType!); 36 | if (!mutantCellType || !mutantScript) { 37 | throw new Error('Cannot inject Mutant because target cell is not a supported version of Mutant'); 38 | } 39 | 40 | // Add Mutant cell to Transaction.inputs 41 | const setupCellResult = await setupCell({ 42 | txSkeleton, 43 | input: props.cell, 44 | config: config.lumos, 45 | addOutput: props.addOutput, 46 | updateOutput(cell) { 47 | if (props.minPayment !== void 0) { 48 | const unpackedArgs = unpackToRawMutantArgs(cell.cellOutput.type!.args!); 49 | const newArgs = packRawMutantArgs({ 50 | ...unpackedArgs, 51 | minPayment: BI.from(props.minPayment), 52 | }); 53 | cell.cellOutput.type!.args = bytes.hexify(newArgs); 54 | } 55 | if (props.capacityMargin !== void 0) { 56 | cell = setAbsoluteCapacityMargin(cell, props.capacityMargin); 57 | } 58 | if (props.updateOutput instanceof Function) { 59 | cell = props.updateOutput(cell); 60 | } 61 | return cell; 62 | }, 63 | defaultWitness: props.defaultWitness, 64 | updateWitness: props.updateWitness, 65 | since: props.since, 66 | }); 67 | txSkeleton = setupCellResult.txSkeleton; 68 | 69 | // If the Mutant is added to Transaction.outputs 70 | if (props.addOutput) { 71 | // Make sure the cell's output has declared enough capacity 72 | const output = txSkeleton.get('outputs').get(setupCellResult.outputIndex)!; 73 | assetCellMinimalCapacity(output); 74 | 75 | // Fix the cell's output index 76 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 77 | return fixedEntries.push({ 78 | field: 'outputs', 79 | index: setupCellResult.outputIndex, 80 | }); 81 | }); 82 | } 83 | 84 | // Add Mutant required cellDeps 85 | txSkeleton = addCellDep(txSkeleton, mutantScript.cellDep); 86 | 87 | return { 88 | txSkeleton, 89 | inputIndex: setupCellResult.inputIndex, 90 | outputIndex: setupCellResult.outputIndex, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/mutant/injectNewMutantIds.ts: -------------------------------------------------------------------------------- 1 | import { bytes } from '@ckb-lumos/codec'; 2 | import { helpers } from '@ckb-lumos/lumos'; 3 | import { generateTypeIdsByOutputs } from '../../../helpers'; 4 | import { packRawMutantArgs, unpackToRawMutantArgs } from '../../../codec'; 5 | import { getSporeConfig, isSporeScriptSupported, SporeConfig } from '../../../config'; 6 | 7 | export function injectNewMutantIds(props: { 8 | txSkeleton: helpers.TransactionSkeletonType; 9 | outputIndices?: number[]; 10 | config?: SporeConfig; 11 | }): helpers.TransactionSkeletonType { 12 | // Env 13 | const config = props.config ?? getSporeConfig(); 14 | 15 | // TransactionSkeleton 16 | let txSkeleton = props.txSkeleton; 17 | 18 | // Get the Transaction.inputs[0] 19 | const firstInput = txSkeleton.get('inputs').get(0); 20 | if (!firstInput) { 21 | throw new Error('Cannot generate Mutant Id because Transaction.inputs[0] does not exist'); 22 | } 23 | 24 | // Generate TypeIds by the output indices 25 | let outputs = txSkeleton.get('outputs'); 26 | let typeIdGroup = generateTypeIdsByOutputs(firstInput, outputs.toArray(), (cell) => { 27 | return !!cell.cellOutput.type && isSporeScriptSupported(config, cell.cellOutput.type, 'Mutant'); 28 | }); 29 | 30 | // Only keep the TypeIDs corresponding to the specified output indices 31 | if (props.outputIndices) { 32 | typeIdGroup = typeIdGroup.filter(([outputIndex]) => { 33 | const index = props.outputIndices!.findIndex((index) => index === outputIndex); 34 | return index >= 0; 35 | }); 36 | if (typeIdGroup.length !== props.outputIndices.length) { 37 | throw new Error('Cannot generate Mutant Id because outputIndices cannot be fully handled'); 38 | } 39 | } 40 | 41 | // Update results 42 | for (const [index, typeId] of typeIdGroup) { 43 | const output = outputs.get(index); 44 | if (!output) { 45 | throw new Error(`Cannot generate Mutant Id because Transaction.outputs[${index}] does not exist`); 46 | } 47 | 48 | const unpackedArgs = unpackToRawMutantArgs(output.cellOutput.type!.args); 49 | const packedNewArgs = packRawMutantArgs({ 50 | id: typeId, 51 | minPayment: unpackedArgs.minPayment, 52 | }); 53 | 54 | output.cellOutput.type!.args = bytes.hexify(packedNewArgs); 55 | outputs = outputs.set(index, output); 56 | } 57 | 58 | return txSkeleton.set('outputs', outputs); 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/mutant/injectNewMutantOutput.ts: -------------------------------------------------------------------------------- 1 | import { BIish } from '@ckb-lumos/bi'; 2 | import { Script } from '@ckb-lumos/base'; 3 | import { bytes, BytesLike } from '@ckb-lumos/codec'; 4 | import { BI, Cell, helpers } from '@ckb-lumos/lumos'; 5 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 6 | import { packRawMutantArgs } from '../../../codec'; 7 | import { getSporeConfig, getSporeScript, SporeConfig } from '../../../config'; 8 | import { correctCellMinimalCapacity, setAbsoluteCapacityMargin } from '../../../helpers'; 9 | import { injectNewMutantIds } from './injectNewMutantIds'; 10 | 11 | export function injectNewMutantOutput(props: { 12 | txSkeleton: helpers.TransactionSkeletonType; 13 | minPayment?: BIish; 14 | data: BytesLike; 15 | toLock: Script; 16 | config?: SporeConfig; 17 | updateOutput?(cell: Cell): Cell; 18 | capacityMargin?: BIish | ((cell: Cell, margin: BI) => BIish); 19 | }): { 20 | txSkeleton: helpers.TransactionSkeletonType; 21 | outputIndex: number; 22 | hasId: boolean; 23 | } { 24 | // Env 25 | const config = props.config ?? getSporeConfig(); 26 | 27 | // Get TransactionSkeleton 28 | let txSkeleton = props.txSkeleton; 29 | 30 | // Create Mutant cell (the latest version) 31 | const mutantScript = getSporeScript(config, 'Mutant'); 32 | let mutantCell: Cell = correctCellMinimalCapacity({ 33 | cellOutput: { 34 | capacity: '0x0', 35 | lock: props.toLock, 36 | type: { 37 | ...mutantScript.script, 38 | args: bytes.hexify( 39 | packRawMutantArgs({ 40 | id: '0x' + '0'.repeat(64), // Fill 32-byte TypeId placeholder 41 | minPayment: props.minPayment !== void 0 ? BI.from(props.minPayment) : void 0, 42 | }), 43 | ), 44 | }, 45 | }, 46 | data: bytes.hexify(props.data), 47 | }); 48 | 49 | // Add to Transaction.outputs 50 | const outputIndex = txSkeleton.get('outputs').size; 51 | txSkeleton = txSkeleton.update('outputs', (outputs) => { 52 | if (props.capacityMargin !== void 0) { 53 | mutantCell = setAbsoluteCapacityMargin(mutantCell, props.capacityMargin); 54 | } 55 | if (props.updateOutput instanceof Function) { 56 | mutantCell = props.updateOutput(mutantCell); 57 | } 58 | return outputs.push(mutantCell); 59 | }); 60 | 61 | // Fix the output's index to prevent it from future reduction 62 | txSkeleton = txSkeleton.update('fixedEntries', (fixedEntries) => { 63 | return fixedEntries.push({ 64 | field: 'outputs', 65 | index: outputIndex, 66 | }); 67 | }); 68 | 69 | // Generate ID for the new Mutant if possible 70 | const firstInput = txSkeleton.get('inputs').first(); 71 | if (firstInput) { 72 | txSkeleton = injectNewMutantIds({ 73 | outputIndices: [outputIndex], 74 | txSkeleton, 75 | config, 76 | }); 77 | } 78 | 79 | // Add Lua lib script as cellDep 80 | const luaScript = getSporeScript(config, 'Lua'); 81 | txSkeleton = addCellDep(txSkeleton, luaScript.cellDep); 82 | // Add Mutant script as cellDep 83 | txSkeleton = addCellDep(txSkeleton, mutantScript.cellDep); 84 | 85 | return { 86 | txSkeleton, 87 | outputIndex, 88 | hasId: firstInput !== void 0, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/spore/getSpore.ts: -------------------------------------------------------------------------------- 1 | import { OutPoint, Script } from '@ckb-lumos/base'; 2 | import { Cell, HexString, Indexer, RPC } from '@ckb-lumos/lumos'; 3 | import { getCellByType, getCellWithStatusByOutPoint, isTypeId } from '../../../helpers'; 4 | import { getSporeConfig, getSporeScriptCategory, isSporeScriptSupported, SporeConfig } from '../../../config'; 5 | 6 | export async function getSporeByType(type: Script, config?: SporeConfig): Promise { 7 | // Env 8 | config = config ?? getSporeConfig(); 9 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 10 | 11 | // Check if the spore's id is TypeID 12 | if (!isTypeId(type.args)) { 13 | throw new Error(`Target Spore ID is invalid: ${type.args}`); 14 | } 15 | 16 | // Get cell by type 17 | const cell = await getCellByType({ type, indexer }); 18 | if (cell === void 0) { 19 | throw new Error('Cannot find Spore by Type because target cell does not exist'); 20 | } 21 | 22 | // Check target cell's type script 23 | const cellType = cell.cellOutput.type; 24 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Spore')) { 25 | throw new Error('Cannot find spore by Type because target cell type is not a supported version of Spore'); 26 | } 27 | 28 | return cell; 29 | } 30 | 31 | export async function getSporeByOutPoint(outPoint: OutPoint, config?: SporeConfig): Promise { 32 | // Env 33 | config = config ?? getSporeConfig(); 34 | const rpc = new RPC(config.ckbNodeUrl); 35 | 36 | // Get cell from rpc 37 | const cellWithStatus = await getCellWithStatusByOutPoint({ outPoint, rpc }); 38 | if (!cellWithStatus.cell) { 39 | throw new Error('Cannot find spore by OutPoint because target cell was not found'); 40 | } 41 | if (cellWithStatus.status !== 'live') { 42 | throw new Error('Cannot find spore by OutPoint because target cell is not lived'); 43 | } 44 | 45 | // Check target cell's type script 46 | const cellType = cellWithStatus.cell.cellOutput.type; 47 | if (!cellType || !isSporeScriptSupported(config, cellType, 'Spore')) { 48 | throw new Error('Cannot find spore by OutPoint because target cell type is not a supported version of Spore'); 49 | } 50 | 51 | return cellWithStatus.cell; 52 | } 53 | 54 | export async function getSporeById(id: HexString, config?: SporeConfig): Promise { 55 | // Env 56 | config = config ?? getSporeConfig(); 57 | 58 | // Check if the spore's id is TypeID 59 | if (!isTypeId(id)) { 60 | throw new Error('Cannot find spore because target SporeId is not valid'); 61 | } 62 | 63 | // Get SporeType script 64 | const sporeScript = getSporeScriptCategory(config, 'Spore'); 65 | const scripts = (sporeScript.versions ?? []).map((r) => r.script); 66 | const indexer = new Indexer(config.ckbIndexerUrl, config.ckbNodeUrl); 67 | 68 | // Search target spore from the latest version to the oldest 69 | for (const script of scripts) { 70 | const cell = await getCellByType({ type: { ...script, args: id }, indexer }); 71 | if (cell !== void 0) { 72 | return cell; 73 | } 74 | } 75 | 76 | throw new Error( 77 | `Cannot find spore by SporeId because target cell does not exist or it's not a supported version of Spore`, 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/api/joints/spore/injectNewSporeIds.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { generateTypeIdsByOutputs } from '../../../helpers'; 3 | import { getSporeConfig, isSporeScriptSupported, SporeConfig } from '../../../config'; 4 | 5 | export function injectNewSporeIds(props: { 6 | txSkeleton: helpers.TransactionSkeletonType; 7 | outputIndices: number[]; 8 | config?: SporeConfig; 9 | }): helpers.TransactionSkeletonType { 10 | // Env 11 | const config = props.config ?? getSporeConfig(); 12 | 13 | // Get TransactionSkeleton 14 | let txSkeleton = props.txSkeleton; 15 | 16 | // Get the first input 17 | const inputs = txSkeleton.get('inputs'); 18 | const firstInput = inputs.get(0); 19 | if (!firstInput) { 20 | throw new Error('Cannot generate Spore Id because Transaction.inputs[0] does not exist'); 21 | } 22 | 23 | // Calculates TypeIds by the outputs' indices 24 | let outputs = txSkeleton.get('outputs'); 25 | let typeIdGroup = generateTypeIdsByOutputs(firstInput, outputs.toArray(), (cell) => { 26 | return !!cell.cellOutput.type && isSporeScriptSupported(config, cell.cellOutput.type, 'Spore'); 27 | }); 28 | 29 | // If `sporeOutputIndices` is provided, filter the result 30 | if (props.outputIndices) { 31 | typeIdGroup = typeIdGroup.filter(([outputIndex]) => { 32 | const index = props.outputIndices!.findIndex((index) => index === outputIndex); 33 | return index >= 0; 34 | }); 35 | if (typeIdGroup.length !== props.outputIndices.length) { 36 | throw new Error('Cannot generate Spore Id because sporeOutputIndices cannot be fully handled'); 37 | } 38 | } 39 | 40 | for (const [index, typeId] of typeIdGroup) { 41 | const output = outputs.get(index); 42 | if (!output) { 43 | throw new Error(`Cannot generate Spore Id because Transaction.outputs[${index}] does not exist`); 44 | } 45 | 46 | output.cellOutput.type!.args = typeId; 47 | outputs = outputs.set(index, output); 48 | } 49 | 50 | return txSkeleton.set('outputs', outputs); 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/cluster/createCluster.ts: -------------------------------------------------------------------------------- 1 | import { Cell, helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function assembleCreateClusterAction(clusterOutput: Cell | undefined): { 9 | actions: UnpackResult[]; 10 | scriptInfos: UnpackResult[]; 11 | } { 12 | const actions: UnpackResult[] = []; 13 | const scriptInfos: UnpackResult[] = []; 14 | 15 | const clusterType = clusterOutput!.cellOutput.type!; 16 | const clusterTypeHash = utils.computeScriptHash(clusterType); 17 | const scriptInfo = createSporeScriptInfoFromTemplate({ 18 | scriptHash: clusterTypeHash, 19 | }); 20 | scriptInfos.push(scriptInfo); 21 | 22 | const actionData = SporeAction.pack({ 23 | type: 'CreateCluster', 24 | value: { 25 | clusterId: clusterType.args, 26 | dataHash: utils.ckbHash(clusterOutput!.data), 27 | to: { 28 | type: 'Script', 29 | value: clusterOutput!.cellOutput.lock, 30 | }, 31 | }, 32 | }); 33 | actions.push({ 34 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 35 | scriptHash: clusterTypeHash, 36 | data: bytes.hexify(actionData), 37 | }); 38 | 39 | return { 40 | actions, 41 | scriptInfos, 42 | }; 43 | } 44 | 45 | export function generateCreateClusterAction(props: { 46 | txSkeleton: helpers.TransactionSkeletonType; 47 | outputIndex: number; 48 | }): { 49 | actions: UnpackResult[]; 50 | scriptInfos: UnpackResult[]; 51 | } { 52 | let txSkeleton = props.txSkeleton; 53 | const clusterOutput = txSkeleton.get('outputs').get(props.outputIndex); 54 | return assembleCreateClusterAction(clusterOutput); 55 | } 56 | 57 | export function generateCreateClusterBuildingPacket(props: { 58 | txSkeleton: helpers.TransactionSkeletonType; 59 | outputIndex: number; 60 | }) { 61 | let txSkeleton = props.txSkeleton; 62 | 63 | const action = generateCreateClusterAction(props); 64 | return createRawBuildingPacket({ 65 | txSkeleton, 66 | actions: action.actions, 67 | scriptInfos: action.scriptInfos, 68 | changeOutput: txSkeleton.get('outputs').size - 1, 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/cluster/referenceCluster.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 3 | import { generateTransferClusterAction } from './transferCluster'; 4 | import { UnpackResult } from '@ckb-lumos/codec'; 5 | 6 | export function generateReferenceClusterAction(props: { 7 | txSkeleton: helpers.TransactionSkeletonType; 8 | referenceType: 'cell' | 'lockProxy'; 9 | cluster?: { 10 | inputIndex: number; 11 | outputIndex: number; 12 | }; 13 | }): { 14 | actions: UnpackResult[]; 15 | scriptInfos: UnpackResult[]; 16 | } { 17 | if (props.referenceType === 'lockProxy') { 18 | return { 19 | actions: [], 20 | scriptInfos: [], 21 | }; 22 | } 23 | if (!props.cluster) { 24 | throw new Error('Cannot generate TransferCluster Action without cluster info'); 25 | } 26 | 27 | return generateTransferClusterAction({ 28 | txSkeleton: props.txSkeleton, 29 | inputIndex: props.cluster.inputIndex, 30 | outputIndex: props.cluster.outputIndex, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/cluster/transferCluster.ts: -------------------------------------------------------------------------------- 1 | import { Cell, helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function assembleTransferClusterAction( 9 | clusterInput: Cell | undefined, 10 | clusterOutput: Cell | undefined, 11 | ): { 12 | actions: UnpackResult[]; 13 | scriptInfos: UnpackResult[]; 14 | } { 15 | const actions: UnpackResult[] = []; 16 | const scriptInfos: UnpackResult[] = []; 17 | 18 | const clusterType = clusterOutput!.cellOutput.type!; 19 | const clusterTypeHash = utils.computeScriptHash(clusterType); 20 | const scriptInfo = createSporeScriptInfoFromTemplate({ 21 | scriptHash: clusterTypeHash, 22 | }); 23 | scriptInfos.push(scriptInfo); 24 | 25 | const actionData = SporeAction.pack({ 26 | type: 'TransferCluster', 27 | value: { 28 | clusterId: clusterType.args, 29 | from: { 30 | type: 'Script', 31 | value: clusterInput!.cellOutput.lock, 32 | }, 33 | to: { 34 | type: 'Script', 35 | value: clusterOutput!.cellOutput.lock, 36 | }, 37 | }, 38 | }); 39 | actions.push({ 40 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 41 | scriptHash: clusterTypeHash, 42 | data: bytes.hexify(actionData), 43 | }); 44 | 45 | return { 46 | actions, 47 | scriptInfos, 48 | }; 49 | } 50 | 51 | export function generateTransferClusterAction(props: { 52 | txSkeleton: helpers.TransactionSkeletonType; 53 | inputIndex: number; 54 | outputIndex: number; 55 | }): { 56 | actions: UnpackResult[]; 57 | scriptInfos: UnpackResult[]; 58 | } { 59 | let txSkeleton = props.txSkeleton; 60 | const clusterInput = txSkeleton.get('inputs').get(props.inputIndex); 61 | const clusterOutput = txSkeleton.get('outputs').get(props.outputIndex); 62 | return assembleTransferClusterAction(clusterInput, clusterOutput); 63 | } 64 | 65 | export function generateTransferClusterBuildingPacket(props: { 66 | txSkeleton: helpers.TransactionSkeletonType; 67 | inputIndex: number; 68 | outputIndex: number; 69 | useCapacityMarginAsFee: boolean; 70 | }) { 71 | let txSkeleton = props.txSkeleton; 72 | 73 | const action = generateTransferClusterAction(props); 74 | return createRawBuildingPacket({ 75 | txSkeleton, 76 | actions: action.actions, 77 | scriptInfos: action.scriptInfos, 78 | changeOutput: props.useCapacityMarginAsFee ? props.outputIndex : txSkeleton.get('outputs').size - 1, 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterAgent/createClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { injectNewClusterAgentOutput } from '../../../api'; 5 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 6 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 7 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 8 | import { generateReferenceClusterProxyAction } from '../clusterProxy/referenceClusterProxy'; 9 | import { Hash } from '@ckb-lumos/base'; 10 | 11 | export function generateCreateClusterAgentAction(props: { 12 | txSkeleton: helpers.TransactionSkeletonType; 13 | outputIndex: number; 14 | clusterProxyId: Hash; 15 | reference: Awaited>['reference']; 16 | }): { 17 | actions: UnpackResult[]; 18 | scriptInfos: UnpackResult[]; 19 | } { 20 | const actions: UnpackResult[] = []; 21 | const scriptInfos: UnpackResult[] = []; 22 | 23 | let txSkeleton = props.txSkeleton; 24 | const clusterOutput = txSkeleton.get('outputs').get(props.outputIndex); 25 | 26 | const clusterAgentType = clusterOutput!.cellOutput.type!; 27 | const clusterAgentTypeHash = utils.computeScriptHash(clusterAgentType); 28 | const scriptInfo = createSporeScriptInfoFromTemplate({ 29 | scriptHash: clusterAgentTypeHash, 30 | }); 31 | scriptInfos.push(scriptInfo); 32 | 33 | const actionData = SporeAction.pack({ 34 | type: 'CreateClusterAgent', 35 | value: { 36 | clusterId: clusterAgentType.args, 37 | clusterProxyId: props.clusterProxyId, 38 | to: { 39 | type: 'Script', 40 | value: clusterOutput!.cellOutput.lock, 41 | }, 42 | }, 43 | }); 44 | actions.push({ 45 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 46 | scriptHash: clusterAgentTypeHash, 47 | data: bytes.hexify(actionData), 48 | }); 49 | 50 | const clusterAction = generateReferenceClusterProxyAction({ 51 | txSkeleton, 52 | referenceType: props.reference.referenceType, 53 | clusterProxy: props.reference.clusterProxy, 54 | }); 55 | actions.push(...clusterAction.actions); 56 | scriptInfos.push(...clusterAction.scriptInfos); 57 | 58 | return { 59 | actions, 60 | scriptInfos, 61 | }; 62 | } 63 | 64 | export function generateCreateClusterAgentBuildingPacket(props: { 65 | txSkeleton: helpers.TransactionSkeletonType; 66 | outputIndex: number; 67 | clusterProxyId: Hash; 68 | reference: Awaited>['reference']; 69 | }) { 70 | let txSkeleton = props.txSkeleton; 71 | 72 | const action = generateCreateClusterAgentAction(props); 73 | return createRawBuildingPacket({ 74 | txSkeleton, 75 | actions: action.actions, 76 | scriptInfos: action.scriptInfos, 77 | changeOutput: txSkeleton.get('outputs').size - 1, 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterAgent/meltClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function generateMeltClusterAgentAction(props: { 9 | txSkeleton: helpers.TransactionSkeletonType; 10 | inputIndex: number; 11 | }): { 12 | actions: UnpackResult[]; 13 | scriptInfos: UnpackResult[]; 14 | } { 15 | const actions: UnpackResult[] = []; 16 | const scriptInfos: UnpackResult[] = []; 17 | 18 | let txSkeleton = props.txSkeleton; 19 | const clusterAgentInput = txSkeleton.get('inputs').get(props.inputIndex); 20 | 21 | const clusterAgentType = clusterAgentInput!.cellOutput.type!; 22 | const clusterAgentTypeHash = utils.computeScriptHash(clusterAgentType); 23 | const scriptInfo = createSporeScriptInfoFromTemplate({ 24 | scriptHash: clusterAgentTypeHash, 25 | }); 26 | scriptInfos.push(scriptInfo); 27 | 28 | const actionData = SporeAction.pack({ 29 | type: 'MeltClusterAgent', 30 | value: { 31 | clusterId: clusterAgentInput!.cellOutput.type!.args, 32 | from: { 33 | type: 'Script', 34 | value: clusterAgentInput!.cellOutput.lock, 35 | }, 36 | }, 37 | }); 38 | actions.push({ 39 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 40 | scriptHash: clusterAgentTypeHash, 41 | data: bytes.hexify(actionData), 42 | }); 43 | 44 | return { 45 | actions, 46 | scriptInfos, 47 | }; 48 | } 49 | 50 | export function generateMeltClusterAgentBuildingPacket(props: { 51 | txSkeleton: helpers.TransactionSkeletonType; 52 | inputIndex: number; 53 | }) { 54 | let txSkeleton = props.txSkeleton; 55 | 56 | const action = generateMeltClusterAgentAction(props); 57 | return createRawBuildingPacket({ 58 | txSkeleton, 59 | actions: action.actions, 60 | scriptInfos: action.scriptInfos, 61 | changeOutput: txSkeleton.get('outputs').size - 1, 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterAgent/referenceClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { UnpackResult } from '@ckb-lumos/codec'; 3 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 4 | import { generateTransferClusterAgentAction } from './transferClusterAgent'; 5 | 6 | export function generateReferenceClusterAgentAction(props: { 7 | txSkeleton: helpers.TransactionSkeletonType; 8 | referenceType: 'cell' | 'lockProxy'; 9 | clusterAgent?: { 10 | inputIndex: number; 11 | outputIndex: number; 12 | }; 13 | }): { 14 | actions: UnpackResult[]; 15 | scriptInfos: UnpackResult[]; 16 | } { 17 | if (props.referenceType === 'lockProxy') { 18 | return { 19 | actions: [], 20 | scriptInfos: [], 21 | }; 22 | } 23 | if (!props.clusterAgent) { 24 | throw new Error('Cannot generate TransferClusterAgent Action without clusterAgent info'); 25 | } 26 | 27 | return generateTransferClusterAgentAction({ 28 | txSkeleton: props.txSkeleton, 29 | inputIndex: props.clusterAgent.inputIndex, 30 | outputIndex: props.clusterAgent.outputIndex, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterAgent/transferClusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function generateTransferClusterAgentAction(props: { 9 | txSkeleton: helpers.TransactionSkeletonType; 10 | inputIndex: number; 11 | outputIndex: number; 12 | }): { 13 | actions: UnpackResult[]; 14 | scriptInfos: UnpackResult[]; 15 | } { 16 | const actions: UnpackResult[] = []; 17 | const scriptInfos: UnpackResult[] = []; 18 | 19 | let txSkeleton = props.txSkeleton; 20 | const clusterAgentInput = txSkeleton.get('inputs').get(props.inputIndex); 21 | const clusterAgentOutput = txSkeleton.get('outputs').get(props.outputIndex); 22 | 23 | const clusterAgentType = clusterAgentOutput!.cellOutput.type!; 24 | const clusterAgentTypeHash = utils.computeScriptHash(clusterAgentType); 25 | const scriptInfo = createSporeScriptInfoFromTemplate({ 26 | scriptHash: clusterAgentTypeHash, 27 | }); 28 | scriptInfos.push(scriptInfo); 29 | 30 | const actionData = SporeAction.pack({ 31 | type: 'TransferClusterAgent', 32 | value: { 33 | clusterId: clusterAgentType.args.slice(0, 66), 34 | from: { 35 | type: 'Script', 36 | value: clusterAgentInput!.cellOutput.lock, 37 | }, 38 | to: { 39 | type: 'Script', 40 | value: clusterAgentOutput!.cellOutput.lock, 41 | }, 42 | }, 43 | }); 44 | actions.push({ 45 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 46 | scriptHash: clusterAgentTypeHash, 47 | data: bytes.hexify(actionData), 48 | }); 49 | 50 | return { 51 | actions, 52 | scriptInfos, 53 | }; 54 | } 55 | 56 | export function generateTransferClusterAgentBuildingPacket(props: { 57 | txSkeleton: helpers.TransactionSkeletonType; 58 | inputIndex: number; 59 | outputIndex: number; 60 | useCapacityMarginAsFee: boolean; 61 | }) { 62 | let txSkeleton = props.txSkeleton; 63 | 64 | const action = generateTransferClusterAgentAction(props); 65 | return createRawBuildingPacket({ 66 | txSkeleton, 67 | actions: action.actions, 68 | scriptInfos: action.scriptInfos, 69 | changeOutput: props.useCapacityMarginAsFee ? props.outputIndex : txSkeleton.get('outputs').size - 1, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterProxy/createClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { injectNewClusterProxyOutput } from '../../../api'; 4 | import { SporeAction } from '../../codec/sporeAction'; 5 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 6 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 7 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 8 | import { generateReferenceClusterAction } from '../cluster/referenceCluster'; 9 | import { unpackToRawClusterProxyArgs } from '../../../codec'; 10 | 11 | export function generateCreateClusterProxyAction(props: { 12 | txSkeleton: helpers.TransactionSkeletonType; 13 | outputIndex: number; 14 | reference: Awaited>['reference']; 15 | }): { 16 | actions: UnpackResult[]; 17 | scriptInfos: UnpackResult[]; 18 | } { 19 | const actions: UnpackResult[] = []; 20 | const scriptInfos: UnpackResult[] = []; 21 | 22 | let txSkeleton = props.txSkeleton; 23 | const clusterProxyOutput = txSkeleton.get('outputs').get(props.outputIndex); 24 | 25 | const clusterProxyType = clusterProxyOutput!.cellOutput.type!; 26 | const clusterProxyTypeHash = utils.computeScriptHash(clusterProxyType); 27 | const scriptInfo = createSporeScriptInfoFromTemplate({ 28 | scriptHash: clusterProxyTypeHash, 29 | }); 30 | scriptInfos.push(scriptInfo); 31 | 32 | const actionData = SporeAction.pack({ 33 | type: 'CreateClusterProxy', 34 | value: { 35 | clusterId: clusterProxyOutput!.data, 36 | clusterProxyId: unpackToRawClusterProxyArgs(clusterProxyType.args).id, 37 | to: { 38 | type: 'Script', 39 | value: clusterProxyOutput!.cellOutput.lock, 40 | }, 41 | }, 42 | }); 43 | actions.push({ 44 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 45 | scriptHash: clusterProxyTypeHash, 46 | data: bytes.hexify(actionData), 47 | }); 48 | 49 | const clusterAction = generateReferenceClusterAction({ 50 | txSkeleton, 51 | referenceType: props.reference.referenceType, 52 | cluster: props.reference.cluster, 53 | }); 54 | actions.push(...clusterAction.actions); 55 | scriptInfos.push(...clusterAction.scriptInfos); 56 | 57 | return { 58 | actions, 59 | scriptInfos, 60 | }; 61 | } 62 | 63 | export function generateCreateClusterProxyBuildingPacket(props: { 64 | txSkeleton: helpers.TransactionSkeletonType; 65 | outputIndex: number; 66 | reference: Awaited>['reference']; 67 | }) { 68 | let txSkeleton = props.txSkeleton; 69 | 70 | const action = generateCreateClusterProxyAction(props); 71 | return createRawBuildingPacket({ 72 | txSkeleton, 73 | actions: action.actions, 74 | scriptInfos: action.scriptInfos, 75 | changeOutput: txSkeleton.get('outputs').size - 1, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterProxy/meltClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | import { unpackToRawClusterProxyArgs } from '../../../codec'; 8 | 9 | export function generateMeltClusterProxyAction(props: { 10 | txSkeleton: helpers.TransactionSkeletonType; 11 | inputIndex: number; 12 | }): { 13 | actions: UnpackResult[]; 14 | scriptInfos: UnpackResult[]; 15 | } { 16 | const actions: UnpackResult[] = []; 17 | const scriptInfos: UnpackResult[] = []; 18 | 19 | let txSkeleton = props.txSkeleton; 20 | const clusterProxyInput = txSkeleton.get('inputs').get(props.inputIndex); 21 | 22 | const clusterProxyType = clusterProxyInput!.cellOutput.type!; 23 | const clusterProxyTypeHash = utils.computeScriptHash(clusterProxyType); 24 | const scriptInfo = createSporeScriptInfoFromTemplate({ 25 | scriptHash: clusterProxyTypeHash, 26 | }); 27 | scriptInfos.push(scriptInfo); 28 | 29 | const actionData = SporeAction.pack({ 30 | type: 'MeltClusterProxy', 31 | value: { 32 | clusterId: clusterProxyInput!.data, 33 | clusterProxyId: unpackToRawClusterProxyArgs(clusterProxyType.args).id, 34 | from: { 35 | type: 'Script', 36 | value: clusterProxyInput!.cellOutput.lock, 37 | }, 38 | }, 39 | }); 40 | actions.push({ 41 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 42 | scriptHash: clusterProxyTypeHash, 43 | data: bytes.hexify(actionData), 44 | }); 45 | 46 | return { 47 | actions, 48 | scriptInfos, 49 | }; 50 | } 51 | 52 | export function generateMeltClusterProxyBuildingPacket(props: { 53 | txSkeleton: helpers.TransactionSkeletonType; 54 | inputIndex: number; 55 | }) { 56 | let txSkeleton = props.txSkeleton; 57 | 58 | const action = generateMeltClusterProxyAction(props); 59 | return createRawBuildingPacket({ 60 | txSkeleton, 61 | actions: action.actions, 62 | scriptInfos: action.scriptInfos, 63 | changeOutput: txSkeleton.get('outputs').size - 1, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterProxy/referenceClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { UnpackResult } from '@ckb-lumos/codec'; 3 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 4 | import { generateTransferClusterProxyAction } from './transferClusterProxy'; 5 | 6 | export function generateReferenceClusterProxyAction(props: { 7 | txSkeleton: helpers.TransactionSkeletonType; 8 | referenceType: 'payment' | 'cell'; 9 | clusterProxy?: { 10 | inputIndex: number; 11 | outputIndex: number; 12 | }; 13 | }): { 14 | actions: UnpackResult[]; 15 | scriptInfos: UnpackResult[]; 16 | } { 17 | if (props.referenceType === 'payment') { 18 | return { 19 | actions: [], 20 | scriptInfos: [], 21 | }; 22 | } 23 | if (!props.clusterProxy) { 24 | throw new Error('Cannot generate TransferClusterProxy Action without clusterProxy info'); 25 | } 26 | 27 | return generateTransferClusterProxyAction({ 28 | txSkeleton: props.txSkeleton, 29 | inputIndex: props.clusterProxy.inputIndex, 30 | outputIndex: props.clusterProxy.outputIndex, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/clusterProxy/transferClusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | import { unpackToRawClusterProxyArgs } from '../../../codec'; 8 | 9 | export function generateTransferClusterProxyAction(props: { 10 | txSkeleton: helpers.TransactionSkeletonType; 11 | inputIndex: number; 12 | outputIndex: number; 13 | }): { 14 | actions: UnpackResult[]; 15 | scriptInfos: UnpackResult[]; 16 | } { 17 | const actions: UnpackResult[] = []; 18 | const scriptInfos: UnpackResult[] = []; 19 | 20 | let txSkeleton = props.txSkeleton; 21 | const clusterProxyInput = txSkeleton.get('inputs').get(props.inputIndex); 22 | const clusterProxyOutput = txSkeleton.get('outputs').get(props.outputIndex); 23 | 24 | const clusterProxyType = clusterProxyOutput!.cellOutput.type!; 25 | const clusterProxyTypeHash = utils.computeScriptHash(clusterProxyType); 26 | const scriptInfo = createSporeScriptInfoFromTemplate({ 27 | scriptHash: clusterProxyTypeHash, 28 | }); 29 | scriptInfos.push(scriptInfo); 30 | 31 | const actionData = SporeAction.pack({ 32 | type: 'TransferClusterProxy', 33 | value: { 34 | clusterId: clusterProxyOutput!.data, 35 | clusterProxyId: unpackToRawClusterProxyArgs(clusterProxyType.args).id, 36 | from: { 37 | type: 'Script', 38 | value: clusterProxyInput!.cellOutput.lock, 39 | }, 40 | to: { 41 | type: 'Script', 42 | value: clusterProxyOutput!.cellOutput.lock, 43 | }, 44 | }, 45 | }); 46 | actions.push({ 47 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 48 | scriptHash: clusterProxyTypeHash, 49 | data: bytes.hexify(actionData), 50 | }); 51 | 52 | return { 53 | actions, 54 | scriptInfos, 55 | }; 56 | } 57 | 58 | export function generateTransferClusterProxyBuildingPacket(props: { 59 | txSkeleton: helpers.TransactionSkeletonType; 60 | inputIndex: number; 61 | outputIndex: number; 62 | useCapacityMarginAsFee: boolean; 63 | }) { 64 | let txSkeleton = props.txSkeleton; 65 | 66 | const action = generateTransferClusterProxyAction(props); 67 | return createRawBuildingPacket({ 68 | txSkeleton, 69 | actions: action.actions, 70 | scriptInfos: action.scriptInfos, 71 | changeOutput: props.useCapacityMarginAsFee ? props.outputIndex : txSkeleton.get('outputs').size - 1, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/spore/meltSpore.ts: -------------------------------------------------------------------------------- 1 | import { Cell, helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function assembleMeltSporeAction(sporeInput: Cell | undefined): { 9 | actions: UnpackResult[]; 10 | scriptInfos: UnpackResult[]; 11 | } { 12 | const actions: UnpackResult[] = []; 13 | const scriptInfos: UnpackResult[] = []; 14 | 15 | const sporeType = sporeInput!.cellOutput.type!; 16 | const sporeTypeHash = utils.computeScriptHash(sporeType); 17 | const scriptInfo = createSporeScriptInfoFromTemplate({ 18 | scriptHash: sporeTypeHash, 19 | }); 20 | scriptInfos.push(scriptInfo); 21 | 22 | const actionData = SporeAction.pack({ 23 | type: 'MeltSpore', 24 | value: { 25 | sporeId: sporeType.args, 26 | from: { 27 | type: 'Script', 28 | value: sporeInput!.cellOutput.lock, 29 | }, 30 | }, 31 | }); 32 | actions.push({ 33 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 34 | scriptHash: sporeTypeHash, 35 | data: bytes.hexify(actionData), 36 | }); 37 | 38 | return { 39 | actions, 40 | scriptInfos, 41 | }; 42 | } 43 | 44 | export function generateMeltSporeAction(props: { txSkeleton: helpers.TransactionSkeletonType; inputIndex: number }): { 45 | actions: UnpackResult[]; 46 | scriptInfos: UnpackResult[]; 47 | } { 48 | let txSkeleton = props.txSkeleton; 49 | const sporeInput = txSkeleton.get('inputs').get(props.inputIndex); 50 | return assembleMeltSporeAction(sporeInput); 51 | } 52 | 53 | export function generateMeltSporeBuildingPacket(props: { 54 | txSkeleton: helpers.TransactionSkeletonType; 55 | inputIndex: number; 56 | }) { 57 | let txSkeleton = props.txSkeleton; 58 | 59 | const action = generateMeltSporeAction(props); 60 | return createRawBuildingPacket({ 61 | txSkeleton, 62 | actions: action.actions, 63 | scriptInfos: action.scriptInfos, 64 | changeOutput: txSkeleton.get('outputs').size - 1, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/action/spore/transferSpore.ts: -------------------------------------------------------------------------------- 1 | import { Cell, helpers, utils } from '@ckb-lumos/lumos'; 2 | import { bytes, UnpackResult } from '@ckb-lumos/codec'; 3 | import { SporeAction } from '../../codec/sporeAction'; 4 | import { Action, ScriptInfo } from '../../codec/buildingPacket'; 5 | import { createRawBuildingPacket } from '../../base/buildingPacket'; 6 | import { createSporeScriptInfoFromTemplate } from '../../base/sporeScriptInfo'; 7 | 8 | export function assembleTransferSporeAction( 9 | sporeInput: Cell | undefined, 10 | sporeOutput: Cell | undefined, 11 | ): { 12 | actions: UnpackResult[]; 13 | scriptInfos: UnpackResult[]; 14 | } { 15 | const actions: UnpackResult[] = []; 16 | const scriptInfos: UnpackResult[] = []; 17 | 18 | const sporeType = sporeOutput!.cellOutput.type!; 19 | const sporeTypeHash = utils.computeScriptHash(sporeType); 20 | const scriptInfo = createSporeScriptInfoFromTemplate({ 21 | scriptHash: sporeTypeHash, 22 | }); 23 | scriptInfos.push(scriptInfo); 24 | 25 | const actionData = SporeAction.pack({ 26 | type: 'TransferSpore', 27 | value: { 28 | sporeId: sporeType.args, 29 | from: { 30 | type: 'Script', 31 | value: sporeInput!.cellOutput.lock, 32 | }, 33 | to: { 34 | type: 'Script', 35 | value: sporeOutput!.cellOutput.lock, 36 | }, 37 | }, 38 | }); 39 | actions.push({ 40 | scriptInfoHash: utils.ckbHash(ScriptInfo.pack(scriptInfo)), 41 | scriptHash: sporeTypeHash, 42 | data: bytes.hexify(actionData), 43 | }); 44 | 45 | return { 46 | actions, 47 | scriptInfos, 48 | }; 49 | } 50 | 51 | export function generateTransferSporeAction(props: { 52 | txSkeleton: helpers.TransactionSkeletonType; 53 | inputIndex: number; 54 | outputIndex: number; 55 | }): { 56 | actions: UnpackResult[]; 57 | scriptInfos: UnpackResult[]; 58 | } { 59 | let txSkeleton = props.txSkeleton; 60 | const sporeInput = txSkeleton.get('inputs').get(props.inputIndex); 61 | const sporeOutput = txSkeleton.get('outputs').get(props.outputIndex); 62 | return assembleTransferSporeAction(sporeInput, sporeOutput); 63 | } 64 | 65 | export function generateTransferSporeBuildingPacket(props: { 66 | txSkeleton: helpers.TransactionSkeletonType; 67 | inputIndex: number; 68 | outputIndex: number; 69 | useCapacityMarginAsFee: boolean; 70 | }) { 71 | let txSkeleton = props.txSkeleton; 72 | 73 | const action = generateTransferSporeAction(props); 74 | return createRawBuildingPacket({ 75 | txSkeleton, 76 | actions: action.actions, 77 | scriptInfos: action.scriptInfos, 78 | changeOutput: props.useCapacityMarginAsFee ? props.outputIndex : txSkeleton.get('outputs').size - 1, 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/base/buildingPacket.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { UnpackResult } from '@ckb-lumos/codec'; 3 | import { ActionVec, BuildingPacket, ScriptInfoVec } from '../codec/buildingPacket'; 4 | import { inputCellsToResolvedInputs } from './resolvedInputs'; 5 | 6 | export function createRawBuildingPacket(props: { 7 | txSkeleton: helpers.TransactionSkeletonType; 8 | scriptInfos?: UnpackResult; 9 | actions?: UnpackResult; 10 | changeOutput?: number; 11 | }): UnpackResult { 12 | const txSkeleton = props.txSkeleton; 13 | return { 14 | type: 'BuildingPacketV1', 15 | value: { 16 | message: { 17 | actions: props.actions ?? [], 18 | }, 19 | payload: helpers.createTransactionFromSkeleton(txSkeleton), 20 | resolvedInputs: inputCellsToResolvedInputs(txSkeleton.get('inputs')), 21 | changeOutput: props.changeOutput ?? txSkeleton.get('outputs').size - 1, 22 | scriptInfos: props.scriptInfos ?? [], 23 | lockActions: [], 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/base/resolvedInputs.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import { Cell, Input } from '@ckb-lumos/lumos'; 3 | import { UnpackResult } from '@ckb-lumos/codec'; 4 | import { ResolvedInputs } from '../codec/buildingPacket'; 5 | 6 | export function inputCellsToResolvedInputs( 7 | inputs: List, 8 | filter?: (value: Cell, index: number, iter: List) => boolean, 9 | ): UnpackResult { 10 | if (filter instanceof Function) { 11 | inputs = inputs.filter(filter); 12 | } 13 | 14 | return inputs.reduce>( 15 | (sum, input) => { 16 | sum.outputs.push(input.cellOutput); 17 | sum.outputsData.push(input.data); 18 | return sum; 19 | }, 20 | { 21 | outputs: [], 22 | outputsData: [], 23 | }, 24 | ); 25 | } 26 | 27 | export function resolvedInputsToInputCells( 28 | inputs: Input[], 29 | resolvedInputs: UnpackResult, 30 | ): Cell[] { 31 | return inputs.map((input, index) => { 32 | return { 33 | cellOutput: resolvedInputs.outputs[index], 34 | data: resolvedInputs.outputsData[index], 35 | outPoint: input.previousOutput, 36 | }; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/base/sporeScriptInfo.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from '@ckb-lumos/base'; 2 | import { UnpackResult } from '@ckb-lumos/codec'; 3 | import { PackParam } from '@ckb-lumos/codec/src/base'; 4 | import { ScriptInfo } from '../codec/buildingPacket'; 5 | 6 | export const sporeScriptInfoMessageType = 'SporeAction'; 7 | 8 | export const sporeScriptInfoSchema = ` 9 | array Byte32 [byte; 32]; 10 | vector Bytes ; 11 | 12 | table Script { 13 | code_hash: Byte32, 14 | hash_type: byte, 15 | args: Bytes, 16 | } 17 | 18 | union Address { 19 | Script, 20 | } 21 | 22 | /* Actions for Spore */ 23 | 24 | table MintSpore { 25 | spore_id: Byte32, 26 | to: Address, 27 | data_hash: Byte32, 28 | } 29 | 30 | table TransferSpore { 31 | spore_id: Byte32, 32 | from: Address, 33 | to: Address, 34 | } 35 | 36 | table BurnSpore { 37 | spore_id: Byte32, 38 | from: Address, 39 | } 40 | 41 | /* Actions for Cluster */ 42 | 43 | table MintCluster { 44 | cluster_id: Byte32, 45 | to: Address, 46 | data_hash: Byte32, 47 | } 48 | 49 | table TransferCluster { 50 | cluster_id: Byte32, 51 | from: Address, 52 | to: Address, 53 | } 54 | 55 | /* Actions for Cluster/Proxy */ 56 | 57 | table MintProxy { 58 | cluster_id: Byte32, 59 | proxy_id: Byte32, 60 | to: Address, 61 | } 62 | 63 | table TransferProxy { 64 | cluster_id: Byte32, 65 | proxy_id: Byte32, 66 | from: Address, 67 | to: Address, 68 | } 69 | 70 | table BurnProxy { 71 | cluster_id: Byte32, 72 | proxy_id: Byte32, 73 | from: Address, 74 | } 75 | 76 | /* Actions for Cluster/Agent */ 77 | 78 | table MintAgent { 79 | cluster_id: Byte32, 80 | proxy_id: Byte32, 81 | to: Address, 82 | } 83 | 84 | table TransferAgent { 85 | cluster_id: Byte32, 86 | from: Address, 87 | to: Address, 88 | } 89 | 90 | table BurnAgent { 91 | cluster_id: Byte32, 92 | from: Address, 93 | } 94 | 95 | /* Action in ScriptInfo */ 96 | 97 | union SporeAction { 98 | MintSpore, 99 | TransferSpore, 100 | BurnSpore, 101 | 102 | MintCluster, 103 | TransferCluster, 104 | 105 | MintProxy, 106 | TransferProxy, 107 | BurnProxy, 108 | 109 | MintAgent, 110 | TransferAgent, 111 | BurnAgent, 112 | } 113 | `; 114 | 115 | export const sporeScriptInfoTemplate: Omit, 'scriptHash'> = { 116 | name: 'spore', 117 | url: 'https://spore.pro', 118 | schema: sporeScriptInfoSchema, 119 | messageType: sporeScriptInfoMessageType, 120 | }; 121 | 122 | export function createSporeScriptInfoFromTemplate(props: { scriptHash: Hash }): UnpackResult { 123 | return { 124 | ...sporeScriptInfoTemplate, 125 | scriptHash: props.scriptHash, 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/base/witnessLayout.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@ckb-lumos/lumos'; 2 | import { blockchain } from '@ckb-lumos/base'; 3 | import { bytes, BytesLike, number, UnpackResult } from '@ckb-lumos/codec'; 4 | import { WitnessLayout, WitnessLayoutFieldTags } from '../codec/witnessLayout'; 5 | import { ActionVec } from '../codec/buildingPacket'; 6 | 7 | export function getWitnessType(witness?: BytesLike) { 8 | const buf = bytes.bytify(witness ?? []); 9 | if (buf.length > 4) { 10 | const typeIndex = number.Uint32LE.unpack(buf.slice(0, 4)); 11 | if (typeIndex >= WitnessLayoutFieldTags.SighashAll) { 12 | for (const [name, index] of Object.entries(WitnessLayoutFieldTags)) { 13 | if (index === typeIndex) { 14 | return name; 15 | } 16 | } 17 | } else { 18 | return 'WitnessArgs'; 19 | } 20 | } 21 | 22 | throw new Error('Unknown witness format'); 23 | } 24 | 25 | export function unpackWitness(witness?: BytesLike) { 26 | const buf = bytes.bytify(witness ?? []); 27 | if (buf.length > 4) { 28 | const typeIndex = number.Uint32LE.unpack(buf.slice(0, 4)); 29 | try { 30 | if (typeIndex >= WitnessLayoutFieldTags.SighashAll) { 31 | return WitnessLayout.unpack(buf); 32 | } else { 33 | return { 34 | type: 'WitnessArgs', 35 | value: blockchain.WitnessArgs.unpack(buf), 36 | }; 37 | } 38 | } catch (_err) { 39 | // passthrough 40 | } 41 | } 42 | 43 | throw new Error('Unknown witness format'); 44 | } 45 | 46 | export function assembleCobuildWitnessLayout(actions: UnpackResult): string { 47 | const witness = bytes.hexify( 48 | WitnessLayout.pack({ 49 | type: 'SighashAll', 50 | value: { 51 | seal: '0x', 52 | message: { 53 | actions, 54 | }, 55 | }, 56 | }), 57 | ); 58 | return witness; 59 | } 60 | 61 | export function injectCommonCobuildProof(props: { 62 | txSkeleton: helpers.TransactionSkeletonType; 63 | actions: UnpackResult; 64 | }): { 65 | txSkeleton: helpers.TransactionSkeletonType; 66 | witnessIndex: number; 67 | } { 68 | let txSkeleton = props.txSkeleton; 69 | 70 | // TODO: add Cobuild witness-check: If it's in legacy mode, manually add WitnessLayout 71 | if (txSkeleton.get('inputs').size > 0) { 72 | // Generate WitnessLayout 73 | 74 | // Append the witness to the end of the witnesses 75 | let witnessIndex: number | undefined; 76 | txSkeleton = txSkeleton.update('witnesses', (witnesses) => { 77 | witnessIndex = witnesses.size; 78 | const witness = assembleCobuildWitnessLayout(props.actions); 79 | return witnesses.push(witness); 80 | }); 81 | 82 | return { 83 | txSkeleton, 84 | witnessIndex: witnessIndex!, 85 | }; 86 | } 87 | 88 | throw new Error('Cannot inject CobuildProof into a Transaction without witnesses'); 89 | } 90 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/codec/buildingPacket.ts: -------------------------------------------------------------------------------- 1 | import { molecule } from '@ckb-lumos/codec'; 2 | import { blockchain } from '@ckb-lumos/base'; 3 | import { Hash, RawString, Uint32Opt } from '../../codec'; 4 | 5 | export const Action = molecule.table( 6 | { 7 | scriptInfoHash: Hash, 8 | scriptHash: Hash, 9 | data: blockchain.Bytes, 10 | }, 11 | ['scriptInfoHash', 'scriptHash', 'data'], 12 | ); 13 | 14 | export const ActionVec = molecule.vector(Action); 15 | 16 | export const Message = molecule.table( 17 | { 18 | actions: ActionVec, 19 | }, 20 | ['actions'], 21 | ); 22 | 23 | export const ResolvedInputs = molecule.table( 24 | { 25 | outputs: blockchain.CellOutputVec, 26 | outputsData: blockchain.BytesVec, 27 | }, 28 | ['outputs', 'outputsData'], 29 | ); 30 | 31 | export const ScriptInfo = molecule.table( 32 | { 33 | name: RawString, 34 | url: RawString, 35 | scriptHash: Hash, 36 | schema: RawString, 37 | messageType: RawString, 38 | }, 39 | ['name', 'url', 'scriptHash', 'schema', 'messageType'], 40 | ); 41 | 42 | export const ScriptInfoVec = molecule.vector(ScriptInfo); 43 | 44 | export const BuildingPacketV1 = molecule.table( 45 | { 46 | message: Message, 47 | payload: blockchain.Transaction, 48 | resolvedInputs: ResolvedInputs, 49 | changeOutput: Uint32Opt, 50 | scriptInfos: ScriptInfoVec, 51 | lockActions: ActionVec, 52 | }, 53 | ['message', 'payload', 'resolvedInputs', 'changeOutput', 'scriptInfos', 'lockActions'], 54 | ); 55 | 56 | export const BuildingPacket = molecule.union( 57 | { 58 | BuildingPacketV1, 59 | }, 60 | ['BuildingPacketV1'], 61 | ); 62 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/codec/sporeAction.ts: -------------------------------------------------------------------------------- 1 | import { molecule } from '@ckb-lumos/codec'; 2 | import { blockchain } from '@ckb-lumos/base'; 3 | import { Hash } from '../../codec'; 4 | 5 | export const Address = molecule.union( 6 | { 7 | Script: blockchain.Script, 8 | }, 9 | ['Script'], 10 | ); 11 | 12 | /** 13 | * Spore 14 | */ 15 | export const CreateSpore = molecule.table( 16 | { 17 | sporeId: Hash, 18 | to: Address, 19 | dataHash: Hash, 20 | }, 21 | ['sporeId', 'to', 'dataHash'], 22 | ); 23 | export const TransferSpore = molecule.table( 24 | { 25 | sporeId: Hash, 26 | from: Address, 27 | to: Address, 28 | }, 29 | ['sporeId', 'from', 'to'], 30 | ); 31 | export const MeltSpore = molecule.table( 32 | { 33 | sporeId: Hash, 34 | from: Address, 35 | }, 36 | ['sporeId', 'from'], 37 | ); 38 | 39 | /** 40 | * Cluster 41 | */ 42 | export const CreateCluster = molecule.table( 43 | { 44 | clusterId: Hash, 45 | to: Address, 46 | dataHash: Hash, 47 | }, 48 | ['clusterId', 'to', 'dataHash'], 49 | ); 50 | export const TransferCluster = molecule.table( 51 | { 52 | clusterId: Hash, 53 | from: Address, 54 | to: Address, 55 | }, 56 | ['clusterId', 'from', 'to'], 57 | ); 58 | 59 | /** 60 | * ClusterProxy 61 | */ 62 | export const CreateClusterProxy = molecule.table( 63 | { 64 | clusterId: Hash, 65 | clusterProxyId: Hash, 66 | to: Address, 67 | }, 68 | ['clusterId', 'clusterProxyId', 'to'], 69 | ); 70 | export const TransferClusterProxy = molecule.table( 71 | { 72 | clusterId: Hash, 73 | clusterProxyId: Hash, 74 | from: Address, 75 | to: Address, 76 | }, 77 | ['clusterId', 'clusterProxyId', 'from', 'to'], 78 | ); 79 | export const MeltClusterProxy = molecule.table( 80 | { 81 | clusterId: Hash, 82 | clusterProxyId: Hash, 83 | from: Address, 84 | }, 85 | ['clusterId', 'clusterProxyId', 'from'], 86 | ); 87 | 88 | /** 89 | * ClusterAgent 90 | */ 91 | export const CreateClusterAgent = molecule.table( 92 | { 93 | clusterId: Hash, 94 | clusterProxyId: Hash, 95 | to: Address, 96 | }, 97 | ['clusterId', 'clusterProxyId', 'to'], 98 | ); 99 | export const TransferClusterAgent = molecule.table( 100 | { 101 | clusterId: Hash, 102 | from: Address, 103 | to: Address, 104 | }, 105 | ['clusterId', 'from', 'to'], 106 | ); 107 | export const MeltClusterAgent = molecule.table( 108 | { 109 | clusterId: Hash, 110 | from: Address, 111 | }, 112 | ['clusterId', 'from'], 113 | ); 114 | 115 | /** 116 | * Spore ScriptInfo Actions 117 | */ 118 | export const SporeAction = molecule.union( 119 | { 120 | // Spore 121 | CreateSpore, 122 | TransferSpore, 123 | MeltSpore, 124 | 125 | // Cluster 126 | CreateCluster, 127 | TransferCluster, 128 | 129 | // ClusterProxy 130 | CreateClusterProxy, 131 | TransferClusterProxy, 132 | MeltClusterProxy, 133 | 134 | // ClusterAgent 135 | CreateClusterAgent, 136 | TransferClusterAgent, 137 | MeltClusterAgent, 138 | }, 139 | [ 140 | 'CreateSpore', 141 | 'TransferSpore', 142 | 'MeltSpore', 143 | 'CreateCluster', 144 | 'TransferCluster', 145 | 'CreateClusterProxy', 146 | 'TransferClusterProxy', 147 | 'MeltClusterProxy', 148 | 'CreateClusterAgent', 149 | 'TransferClusterAgent', 150 | 'MeltClusterAgent', 151 | ], 152 | ); 153 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/codec/witnessLayout.ts: -------------------------------------------------------------------------------- 1 | import { blockchain } from '@ckb-lumos/base'; 2 | import { molecule } from '@ckb-lumos/codec'; 3 | import { Message } from './buildingPacket'; 4 | 5 | export const SighashAll = molecule.table( 6 | { 7 | seal: blockchain.Bytes, 8 | message: Message, 9 | }, 10 | ['seal', 'message'], 11 | ); 12 | export const SighashAllOnly = molecule.table( 13 | { 14 | seal: blockchain.Bytes, 15 | }, 16 | ['seal'], 17 | ); 18 | 19 | /** 20 | * Otx related are not implemented yet, so just placeholders. 21 | */ 22 | export const Otx = molecule.table({}, []); 23 | export const OtxStart = molecule.table({}, []); 24 | 25 | export const WitnessLayoutFieldTags = { 26 | SighashAll: 4278190081, 27 | SighashAllOnly: 4278190082, 28 | Otx: 4278190083, 29 | OtxStart: 4278190084, 30 | } as const; 31 | 32 | export const WitnessLayout = molecule.union( 33 | { 34 | SighashAll, 35 | SighashAllOnly, 36 | Otx, 37 | OtxStart, 38 | }, 39 | WitnessLayoutFieldTags, 40 | ); 41 | -------------------------------------------------------------------------------- /packages/core/src/cobuild/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base/buildingPacket'; 2 | export * from './base/resolvedInputs'; 3 | export * from './base/sporeScriptInfo'; 4 | export * from './base/witnessLayout'; 5 | 6 | export * from './codec/buildingPacket'; 7 | export * from './codec/witnessLayout'; 8 | export * from './codec/sporeAction'; 9 | 10 | export * from './action/spore/createSpore'; 11 | export * from './action/spore/transferSpore'; 12 | export * from './action/spore/meltSpore'; 13 | export * from './action/cluster/createCluster'; 14 | export * from './action/cluster/transferCluster'; 15 | export * from './action/cluster/referenceCluster'; 16 | export * from './action/clusterProxy/createClusterProxy'; 17 | export * from './action/clusterProxy/transferClusterProxy'; 18 | export * from './action/clusterProxy/referenceClusterProxy'; 19 | export * from './action/clusterProxy/meltClusterProxy'; 20 | export * from './action/clusterAgent/createClusterAgent'; 21 | export * from './action/clusterAgent/transferClusterAgent'; 22 | export * from './action/clusterAgent/referenceClusterAgent'; 23 | export * from './action/clusterAgent/meltClusterAgent'; 24 | -------------------------------------------------------------------------------- /packages/core/src/codec/clusterAgent.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@ckb-lumos/lumos'; 2 | import { bytes } from '@ckb-lumos/codec'; 3 | import { Hash, Script } from '@ckb-lumos/base'; 4 | 5 | export type RawClusterAgentData = Script; 6 | 7 | export function packRawClusterAgentDataToHash(packable: RawClusterAgentData): Hash { 8 | return utils.computeScriptHash(packable); 9 | } 10 | 11 | export function packRawClusterAgentData(packable: RawClusterAgentData): Uint8Array { 12 | const hash = packRawClusterAgentDataToHash(packable); 13 | return bytes.bytify(hash); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/codec/clusterProxy.ts: -------------------------------------------------------------------------------- 1 | import { BIish, BI } from '@ckb-lumos/bi'; 2 | import { blockchain, Hash } from '@ckb-lumos/base'; 3 | import { BytesLike, createBytesCodec } from '@ckb-lumos/codec'; 4 | import { Uint64Opt, Uint8Opt } from './utils'; 5 | 6 | export interface PackableClusterProxyArgs { 7 | id: BytesLike; 8 | minPayment?: BIish; 9 | } 10 | 11 | export interface RawClusterProxyArgs { 12 | id: Hash; 13 | minPayment?: BI; 14 | } 15 | 16 | export const ClusterProxyArgsPower = createBytesCodec({ 17 | pack(packable: PackableClusterProxyArgs): Uint8Array { 18 | const id = blockchain.Byte32.pack(packable.id); 19 | const minPayment = Uint8Opt.pack(packable.minPayment); 20 | 21 | const composed = new Uint8Array(id.length + minPayment.length); 22 | composed.set(id, 0); 23 | composed.set(minPayment, id.length); 24 | 25 | return composed; 26 | }, 27 | unpack(unpackable: Uint8Array): RawClusterProxyArgs { 28 | const id = blockchain.Byte32.unpack(unpackable.slice(0, 32)); 29 | const minPayment = Uint8Opt.unpack(unpackable.slice(32, 33)); 30 | return { 31 | id, 32 | minPayment: typeof minPayment === 'number' ? BI.from(minPayment) : void 0, 33 | }; 34 | }, 35 | }); 36 | 37 | export const ClusterProxyArgsExact = createBytesCodec({ 38 | pack(packable: PackableClusterProxyArgs): Uint8Array { 39 | const id = blockchain.Byte32.pack(packable.id); 40 | const minPayment = Uint64Opt.pack(packable.minPayment); 41 | 42 | const composed = new Uint8Array(id.length + minPayment.length); 43 | composed.set(id, 0); 44 | composed.set(minPayment, id.length); 45 | 46 | return composed; 47 | }, 48 | unpack(unpackable: Uint8Array): RawClusterProxyArgs { 49 | const id = blockchain.Byte32.unpack(unpackable.slice(0, 32)); 50 | const minPayment = Uint64Opt.unpack(unpackable.slice(32, 40)); 51 | return { 52 | id, 53 | minPayment: typeof BI.isBI(minPayment) ? minPayment : void 0, 54 | }; 55 | }, 56 | }); 57 | 58 | export function packRawClusterProxyArgs(packable: PackableClusterProxyArgs): Uint8Array; 59 | export function packRawClusterProxyArgs(packable: PackableClusterProxyArgs, version: 'power'): Uint8Array; 60 | export function packRawClusterProxyArgs(packable: PackableClusterProxyArgs, version: 'exact'): Uint8Array; 61 | export function packRawClusterProxyArgs(packable: PackableClusterProxyArgs, version?: unknown): unknown { 62 | switch (version) { 63 | case 'power': 64 | return ClusterProxyArgsPower.pack(packable); 65 | case 'exact': 66 | case void 0: 67 | return ClusterProxyArgsExact.pack(packable); 68 | default: 69 | throw new Error(`Unsupported ClusterProxy version: ${version}`); 70 | } 71 | } 72 | 73 | export function unpackToRawClusterProxyArgs(unpackable: BytesLike): RawClusterProxyArgs; 74 | export function unpackToRawClusterProxyArgs(unpackable: BytesLike, version: 'power'): RawClusterProxyArgs; 75 | export function unpackToRawClusterProxyArgs(unpackable: BytesLike, version: 'exact'): RawClusterProxyArgs; 76 | export function unpackToRawClusterProxyArgs(unpackable: BytesLike, version?: unknown): unknown { 77 | switch (version) { 78 | case 'power': 79 | return ClusterProxyArgsPower.unpack(unpackable); 80 | case 'exact': 81 | case void 0: 82 | return ClusterProxyArgsExact.unpack(unpackable); 83 | default: 84 | throw new Error(`Unsupported ClusterProxy version: ${version}`); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/codec/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | 3 | export * from './spore'; 4 | export * from './cluster'; 5 | export * from './clusterProxy'; 6 | export * from './clusterAgent'; 7 | export * from './mutant'; 8 | -------------------------------------------------------------------------------- /packages/core/src/codec/mutant.ts: -------------------------------------------------------------------------------- 1 | import { BIish, BI } from '@ckb-lumos/bi'; 2 | import { blockchain, Hash } from '@ckb-lumos/base'; 3 | import { BytesLike, createBytesCodec } from '@ckb-lumos/codec'; 4 | import { Uint64Opt, Uint8Opt } from './utils'; 5 | 6 | export interface PackableMutantArgs { 7 | id: BytesLike; 8 | minPayment?: BIish; 9 | } 10 | 11 | export interface RawMutantArgs { 12 | id: Hash; 13 | minPayment?: BI; 14 | } 15 | 16 | export const MutantArgsPower = createBytesCodec({ 17 | pack(packable: PackableMutantArgs): Uint8Array { 18 | const id = blockchain.Byte32.pack(packable.id); 19 | const minPayment = Uint8Opt.pack(packable.minPayment); 20 | 21 | const composed = new Uint8Array(id.length + minPayment.length); 22 | composed.set(id, 0); 23 | composed.set(minPayment, id.length); 24 | 25 | return composed; 26 | }, 27 | unpack(unpackable: Uint8Array): RawMutantArgs { 28 | const id = blockchain.Byte32.unpack(unpackable.slice(0, 32)); 29 | const minPayment = Uint8Opt.unpack(unpackable.slice(32, 33)); 30 | return { 31 | id, 32 | minPayment: typeof minPayment === 'number' ? BI.from(minPayment) : void 0, 33 | }; 34 | }, 35 | }); 36 | 37 | export const MutantArgsExact = createBytesCodec({ 38 | pack(packable: PackableMutantArgs): Uint8Array { 39 | const id = blockchain.Byte32.pack(packable.id); 40 | const minPayment = Uint64Opt.pack(packable.minPayment); 41 | 42 | const composed = new Uint8Array(id.length + minPayment.length); 43 | composed.set(id, 0); 44 | composed.set(minPayment, id.length); 45 | 46 | return composed; 47 | }, 48 | unpack(unpackable: Uint8Array): RawMutantArgs { 49 | const id = blockchain.Byte32.unpack(unpackable.slice(0, 32)); 50 | const minPayment = Uint64Opt.unpack(unpackable.slice(32, 40)); 51 | return { 52 | id, 53 | minPayment: typeof BI.isBI(minPayment) ? minPayment : void 0, 54 | }; 55 | }, 56 | }); 57 | 58 | export function packRawMutantArgs(packable: PackableMutantArgs): Uint8Array; 59 | export function packRawMutantArgs(packable: PackableMutantArgs, version: 'power'): Uint8Array; 60 | export function packRawMutantArgs(packable: PackableMutantArgs, version: 'exact'): Uint8Array; 61 | export function packRawMutantArgs(packable: PackableMutantArgs, version?: unknown): unknown { 62 | switch (version) { 63 | case 'power': 64 | return MutantArgsPower.pack(packable); 65 | case 'exact': 66 | case void 0: 67 | return MutantArgsExact.pack(packable); 68 | default: 69 | throw new Error(`Unsupported Mutant version: ${version}`); 70 | } 71 | } 72 | 73 | export function unpackToRawMutantArgs(unpackable: BytesLike): RawMutantArgs; 74 | export function unpackToRawMutantArgs(unpackable: BytesLike, version: 'power'): RawMutantArgs; 75 | export function unpackToRawMutantArgs(unpackable: BytesLike, version: 'exact'): RawMutantArgs; 76 | export function unpackToRawMutantArgs(unpackable: BytesLike, version?: unknown): unknown { 77 | switch (version) { 78 | case 'power': 79 | return MutantArgsPower.unpack(unpackable); 80 | case 'exact': 81 | case void 0: 82 | return MutantArgsExact.unpack(unpackable); 83 | default: 84 | throw new Error(`Unsupported Mutant version: ${version}`); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/core/src/codec/spore.ts: -------------------------------------------------------------------------------- 1 | import { blockchain } from '@ckb-lumos/base'; 2 | import { BytesLike, molecule } from '@ckb-lumos/codec'; 3 | import { bufferToRawString, bytifyRawString } from '../helpers'; 4 | import { Hash } from '@ckb-lumos/lumos'; 5 | 6 | export const SporeData = molecule.table( 7 | { 8 | contentType: blockchain.Bytes, 9 | content: blockchain.Bytes, 10 | clusterId: blockchain.BytesOpt, 11 | }, 12 | ['contentType', 'content', 'clusterId'], 13 | ); 14 | 15 | export interface RawSporeData { 16 | contentType: string; 17 | content: BytesLike; 18 | clusterId?: Hash; 19 | } 20 | 21 | export function packRawSporeData(packable: RawSporeData): Uint8Array { 22 | return SporeData.pack({ 23 | contentType: bytifyRawString(packable.contentType), 24 | content: packable.content, 25 | clusterId: packable.clusterId, 26 | }); 27 | } 28 | 29 | export function unpackToRawSporeData(unpackable: BytesLike): RawSporeData { 30 | const unpacked = SporeData.unpack(unpackable); 31 | return { 32 | contentType: bufferToRawString(unpacked.contentType), 33 | content: unpacked.content, 34 | clusterId: unpacked.clusterId, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/codec/utils.ts: -------------------------------------------------------------------------------- 1 | import { blockchain } from '@ckb-lumos/base'; 2 | import { BytesLike, molecule, number } from '@ckb-lumos/codec'; 3 | import { bufferToRawString, bytifyRawString } from '../helpers'; 4 | 5 | export const ScriptId = molecule.struct( 6 | { 7 | codeHash: blockchain.Byte32, 8 | hashType: blockchain.HashType, 9 | }, 10 | ['codeHash', 'hashType'], 11 | ); 12 | 13 | export const Uint8Opt = molecule.option(number.Uint8); 14 | export const Uint32Opt = molecule.option(number.Uint32LE); 15 | export const Uint64Opt = molecule.option(number.Uint64LE); 16 | 17 | export const Hash = blockchain.Byte32; 18 | 19 | /** 20 | * The codec for packing/unpacking UTF-8 raw strings. 21 | * Should be packed like so: String.pack('something') 22 | */ 23 | export const RawString = molecule.byteVecOf({ 24 | pack: (packable: string) => bytifyRawString(packable), 25 | unpack: (unpackable: BytesLike) => bufferToRawString(unpackable), 26 | }); 27 | -------------------------------------------------------------------------------- /packages/core/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import { predefinedSporeConfigs } from './predefined'; 3 | import { SporeConfig, SporeScriptCategories } from './types'; 4 | 5 | let configStore: SporeConfig = predefinedSporeConfigs.Aggron4; 6 | 7 | /** 8 | * Set the global default SporeConfig. 9 | * The default config is "predefinedSporeConfigs.Aggron4". 10 | */ 11 | export function setSporeConfig(config: SporeConfig): void { 12 | configStore = config; 13 | } 14 | 15 | /** 16 | * Get the global default SporeConfig. 17 | * The default config is "predefinedSporeConfigs.Aggron4". 18 | */ 19 | export function getSporeConfig(): SporeConfig { 20 | return configStore as SporeConfig; 21 | } 22 | 23 | /** 24 | * Clone and create a new SporeConfig. 25 | */ 26 | export function forkSporeConfig( 27 | origin: SporeConfig, 28 | change: Partial>, 29 | ): SporeConfig { 30 | origin = cloneDeep(origin); 31 | 32 | const scripts = { 33 | ...origin.scripts, 34 | ...change.scripts, 35 | } as SporeScriptCategories; 36 | 37 | return { 38 | ...origin, 39 | ...change, 40 | scripts, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/config/hash.ts: -------------------------------------------------------------------------------- 1 | import { Hash, utils } from '@ckb-lumos/base'; 2 | import { bytifyRawString } from '../helpers'; 3 | import { SporeConfig } from './types'; 4 | 5 | const configHashStore: Map = new Map(); 6 | 7 | /** 8 | * Get the hash of a SporeConfig, calculated by the JSON string of the config. 9 | * Generated hashes will be stored in cache to save time for later searching. 10 | */ 11 | export function getSporeConfigHash(config: SporeConfig): Hash { 12 | const string = JSON.stringify(config, null, 0); 13 | if (configHashStore.has(string)) { 14 | return configHashStore.get(string)!; 15 | } 16 | 17 | const hash = utils.ckbHash(bytifyRawString(string)); 18 | configHashStore.set(string, hash); 19 | return hash; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | 3 | export * from './hash'; 4 | export * from './cache'; 5 | export * from './config'; 6 | export * from './script'; 7 | 8 | export * from './predefined'; 9 | -------------------------------------------------------------------------------- /packages/core/src/config/types.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@ckb-lumos/config-manager'; 2 | import { CellDep } from '@ckb-lumos/base'; 3 | import { ScriptId } from '../types'; 4 | import { ClusterDataVersion } from '../codec'; 5 | import { HexString, Script } from '@ckb-lumos/lumos'; 6 | 7 | export interface SporeConfig { 8 | lumos: Config; 9 | ckbNodeUrl: string; 10 | ckbIndexerUrl: string; 11 | maxTransactionSize?: number; 12 | defaultTags?: string[]; 13 | scripts: SporeScriptCategories; 14 | } 15 | 16 | export type SporeScriptCategories = Record; 17 | 18 | export interface SporeScriptCategory { 19 | versions: SporeScript[]; 20 | } 21 | 22 | export interface SporeVersionedScript extends SporeScript { 23 | versions?: SporeScript[]; 24 | } 25 | 26 | export type SporeScripts = Record; 27 | 28 | export interface SporeScript { 29 | tags: string[]; 30 | script: ScriptId; 31 | cellDep: CellDep; 32 | behaviors?: SporeScriptBehaviors; 33 | } 34 | 35 | export interface SporeScriptBehaviors { 36 | lockProxy?: boolean; 37 | cobuild?: boolean; 38 | clusterDataVersion?: ClusterDataVersion; 39 | dynamicCelldep?: Script; 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_NETWORK: string; 5 | readonly VITE_CONFIG_PATH: string; 6 | readonly VITE_TESTS_CLUSTER_V1: string; 7 | readonly VITE_ACCOUNT_CHARLIE: string; 8 | readonly VITE_ACCOUNT_ALICE: string; 9 | readonly VITE_ACCOUNT_BOB: string; 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/helpers/address.ts: -------------------------------------------------------------------------------- 1 | import { Address } from '@ckb-lumos/base'; 2 | import { helpers } from '@ckb-lumos/lumos'; 3 | import { Config } from '@ckb-lumos/config-manager'; 4 | import { FromInfo, parseFromInfo } from '@ckb-lumos/common-scripts'; 5 | 6 | /** 7 | * Check if the target address is valid. 8 | */ 9 | export function isAddressValid(address: Address, config?: Config) { 10 | try { 11 | helpers.parseAddress(address, { config }); 12 | return true; 13 | } catch { 14 | return false; 15 | } 16 | } 17 | 18 | /** 19 | * Convert a FromInfo to a CKB address. 20 | */ 21 | export function fromInfoToAddress(fromInfo: FromInfo, config?: Config): Address { 22 | if (typeof fromInfo === 'string' && isAddressValid(fromInfo)) { 23 | return fromInfo as Address; 24 | } 25 | 26 | const parsed = parseFromInfo(fromInfo, { config }); 27 | return helpers.encodeToAddress(parsed.fromScript, { config }); 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/helpers/buffer.ts: -------------------------------------------------------------------------------- 1 | import { bytes, BytesLike } from '@ckb-lumos/codec'; 2 | 3 | const encoder = new TextEncoder(); 4 | const decoder = new TextDecoder(); 5 | 6 | export function bytifyRawString(text: string): Uint8Array { 7 | return encoder.encode(text); 8 | } 9 | 10 | export function bufferToRawString(source: BytesLike, options?: TextDecodeOptions): string { 11 | const buffer = bytes.bytify(source); 12 | return decoder.decode(buffer, options); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/helpers/cellDep.ts: -------------------------------------------------------------------------------- 1 | import { Script } from '@ckb-lumos/base'; 2 | import { helpers, RPC } from '@ckb-lumos/lumos'; 3 | import { getSporeConfig, SporeConfig } from '../config'; 4 | import { getCellWithStatusByOutPoint } from './cell'; 5 | import { isScriptValueEquals } from './script'; 6 | 7 | export async function findCellDepIndexByTypeFromTransactionSkeleton(props: { 8 | txSkeleton: helpers.TransactionSkeletonType; 9 | type: Script; 10 | config?: SporeConfig; 11 | }) { 12 | const config = props.config ?? getSporeConfig(); 13 | 14 | const rpc = new RPC(config.ckbNodeUrl); 15 | const cellDeps = props.txSkeleton.get('cellDeps'); 16 | 17 | for await (const [index, cellDep] of cellDeps.toArray().entries()) { 18 | const target = await getCellWithStatusByOutPoint({ 19 | outPoint: cellDep.outPoint, 20 | rpc, 21 | }); 22 | 23 | if (target.cell?.cellOutput.type && isScriptValueEquals(target.cell.cellOutput.type, props.type)) { 24 | return index; 25 | } 26 | } 27 | 28 | return -1; 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | // Bytes 2 | export * from './buffer'; 3 | 4 | // Utils 5 | export * from './retryWork'; 6 | 7 | // Blockchain 8 | export * from './cell'; 9 | export * from './cellDep'; 10 | export * from './script'; 11 | export * from './address'; 12 | export * from './typeId'; 13 | export * from './capacity'; 14 | export * from './transaction'; 15 | export * from './witness'; 16 | export * from './fee'; 17 | 18 | // Core 19 | export * from './mimeType'; 20 | export * from './contentType'; 21 | export * from './multipartContent'; 22 | export * from './lockProxy'; 23 | -------------------------------------------------------------------------------- /packages/core/src/helpers/lockProxy.ts: -------------------------------------------------------------------------------- 1 | import { Cell, Script } from '@ckb-lumos/base'; 2 | import { TransactionSkeletonType } from '@ckb-lumos/helpers'; 3 | import { addCellDep } from '@ckb-lumos/common-scripts/lib/helper'; 4 | import { isScriptValueEquals } from './script'; 5 | import { PromiseOr } from '../types'; 6 | 7 | export async function referenceCellOrLockProxy(props: { 8 | txSkeleton: TransactionSkeletonType; 9 | cell: Cell; 10 | inputLocks: Script[]; 11 | outputLocks: Script[]; 12 | referenceCell: (txSkeleton: TransactionSkeletonType) => PromiseOr; 13 | referenceLockProxy: (txSkeleton: TransactionSkeletonType) => PromiseOr; 14 | addCellToCellDeps?: boolean; 15 | }): Promise<{ 16 | txSkeleton: TransactionSkeletonType; 17 | referencedCell: boolean; 18 | referencedLockProxy: boolean; 19 | }> { 20 | // Env 21 | const addCellToCellDeps = props.addCellToCellDeps ?? true; 22 | 23 | // TransactionSkeleton 24 | let txSkeleton = props.txSkeleton; 25 | 26 | // The reference cell 27 | const cell = props.cell; 28 | const cellLock = cell.cellOutput.lock; 29 | if (!cell.outPoint) { 30 | throw new Error('Cannot reference cell because target cell has no OutPoint'); 31 | } 32 | 33 | // Inputs/Outputs conditions 34 | const hasTargetLockInInputs = props.inputLocks.some((script) => { 35 | return isScriptValueEquals(cellLock, script); 36 | }); 37 | const hasTargetLockInOutputs = props.outputLocks.some((script) => { 38 | return isScriptValueEquals(cellLock, script); 39 | }); 40 | 41 | // Summarize conditions 42 | const referencedLockProxy = hasTargetLockInInputs && hasTargetLockInOutputs; 43 | const referencedCell = !hasTargetLockInInputs || !hasTargetLockInOutputs; 44 | console.log('referencedLockProxy = ', referencedLockProxy); 45 | 46 | // Inject the target cell's LockProxy to the transaction 47 | if (referencedLockProxy) { 48 | txSkeleton = await props.referenceLockProxy(txSkeleton); 49 | } 50 | 51 | // Inject the target cell directly to inputs & outputs 52 | if (referencedCell) { 53 | txSkeleton = await props.referenceCell(txSkeleton); 54 | } 55 | 56 | // Add the target cell as cell dep to cellDeps 57 | if (addCellToCellDeps) { 58 | txSkeleton = addCellDep(txSkeleton, { 59 | outPoint: cell.outPoint!, 60 | depType: 'code', 61 | }); 62 | } 63 | 64 | return { 65 | txSkeleton, 66 | referencedCell, 67 | referencedLockProxy, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/src/helpers/retryWork.ts: -------------------------------------------------------------------------------- 1 | export interface RetryWorkResult { 2 | result: T | undefined; 3 | success: boolean; 4 | errors: E[]; 5 | retries: number; 6 | duration: number; 7 | } 8 | 9 | export interface RetryWorkIntervalContext { 10 | retries: number; 11 | } 12 | 13 | /** 14 | * A util function to run a getter code and retry if it fails. 15 | * This is useful when you're fetching changes from the internet. 16 | */ 17 | export function retryWork(props: { 18 | getter: () => T | Promise; 19 | retry?: number; 20 | delay?: number; 21 | interval?: number | ((context: RetryWorkIntervalContext) => number); 22 | onError?: (e: E) => boolean | Promise; 23 | onComplete?: (value: T) => boolean | Promise; 24 | }): Promise> { 25 | const isDynamicInterval = props.interval instanceof Function; 26 | const staticInterval = isDynamicInterval ? 100 : (props.interval as number | undefined) ?? 100; 27 | 28 | const delay = props.delay ?? 0; 29 | const maxRetry = props.retry ?? 3; 30 | const onError = props.onError ?? (() => true); 31 | const onComplete = props.onComplete ?? (() => true); 32 | 33 | return new Promise>(async (resolve) => { 34 | // Record 35 | const startTime = Date.now(); 36 | const errors: E[] = []; 37 | 38 | // Status 39 | let retries = 0; 40 | let result: T | undefined; 41 | let isRejected = false; 42 | let isCompleted = false; 43 | 44 | function dynamicInterval() { 45 | return (props.interval as (context: RetryWorkIntervalContext) => number)({ 46 | retries, 47 | }); 48 | } 49 | async function event(): Promise { 50 | try { 51 | result = await props.getter(); 52 | if (onComplete(result)) { 53 | isCompleted = true; 54 | } else { 55 | retries++; 56 | } 57 | } catch (e: any) { 58 | errors.push(e); 59 | if (await onError(e)) { 60 | retries++; 61 | } else { 62 | isRejected = true; 63 | } 64 | } 65 | 66 | if (isCompleted) { 67 | return resolve({ 68 | success: true, 69 | result, 70 | errors, 71 | retries, 72 | duration: Date.now() - startTime, 73 | }); 74 | } 75 | if (isRejected || retries >= maxRetry) { 76 | return resolve({ 77 | success: false, 78 | result: void 0, 79 | errors, 80 | retries, 81 | duration: Date.now() - startTime, 82 | }); 83 | } 84 | 85 | const interval = isDynamicInterval ? dynamicInterval() : staticInterval; 86 | setTimeout(() => event(), interval); 87 | } 88 | 89 | if (delay > 0) { 90 | await waitForMilliseconds(delay); 91 | } 92 | await event(); 93 | }); 94 | } 95 | 96 | export function waitForMilliseconds(ms: number): Promise { 97 | return new Promise((resolve) => { 98 | setTimeout(() => resolve(), ms); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /packages/core/src/helpers/script.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import { helpers } from '@ckb-lumos/lumos'; 3 | import { Config } from '@ckb-lumos/config-manager'; 4 | import { Address, Script, values } from '@ckb-lumos/base'; 5 | import { FromInfo, parseFromInfo } from '@ckb-lumos/common-scripts'; 6 | import { ScriptId } from '../types'; 7 | 8 | /** 9 | * Compare two scripts to see if they are identical. 10 | */ 11 | export function isScriptValueEquals(a: Script, b: Script) { 12 | return a.codeHash === b.codeHash && a.hashType === b.hashType && a.args === b.args; 13 | } 14 | 15 | /** 16 | * Compare two scripts to see if their 'codeHash' and 'hashType' are the same. 17 | */ 18 | export function isScriptIdEquals(a: ScriptId, b: ScriptId) { 19 | return a.codeHash === b.codeHash && a.hashType === b.hashType; 20 | } 21 | 22 | /** 23 | * Get change lock of a transaction. 24 | */ 25 | export function getChangeLock(fromInfos: FromInfo[], changeAddress?: Address, config?: Config): Script { 26 | const firstFromInfo = parseFromInfo(fromInfos[0], { config }); 27 | const changeAddressLock = changeAddress ? helpers.parseAddress(changeAddress, { config }) : void 0; 28 | 29 | return changeAddressLock ?? firstFromInfo.fromScript; 30 | } 31 | 32 | /** 33 | * Assemble locks of Transaction.inputs. 34 | */ 35 | export function composeInputLocks(props: { fromInfos: FromInfo[]; inputLocks?: Script[]; config?: Config }): Script[] { 36 | const config = props.config; 37 | const inputLocks = Array.isArray(props.inputLocks) ? cloneDeep(props.inputLocks) : []; 38 | 39 | const fromInfoLocks = props.fromInfos.map((fromInfo) => { 40 | return parseFromInfo(fromInfo, { config }).fromScript; 41 | }); 42 | 43 | return [...inputLocks, ...fromInfoLocks]; 44 | } 45 | 46 | /** 47 | * Assemble possible locks of Transaction.outputs. 48 | */ 49 | export function composeOutputLocks(props: { 50 | fromInfos: FromInfo[]; 51 | outputLocks?: Script[]; 52 | changeAddress?: Address; 53 | config?: Config; 54 | }): Script[] { 55 | const config = props.config; 56 | const outputLocks = Array.isArray(props.outputLocks) ? cloneDeep(props.outputLocks) : []; 57 | 58 | let changeLock: Script; 59 | if (props.changeAddress) { 60 | changeLock = helpers.parseAddress(props.changeAddress, { config }); 61 | } else { 62 | changeLock = parseFromInfo(props.fromInfos[0], { config }).fromScript; 63 | } 64 | 65 | const foundIndex = outputLocks.findIndex((script) => isScriptValueEquals(script, changeLock)); 66 | if (foundIndex < 0) { 67 | outputLocks.push(changeLock); 68 | } 69 | 70 | return outputLocks; 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/helpers/typeId.ts: -------------------------------------------------------------------------------- 1 | import { BI, Cell, utils } from '@ckb-lumos/lumos'; 2 | import { BIish } from '@ckb-lumos/bi'; 3 | import { Hash } from '@ckb-lumos/base'; 4 | import { BytesLike } from '@ckb-lumos/codec'; 5 | import { bytes } from '@ckb-lumos/codec'; 6 | 7 | /** 8 | * Generate a TypeId based on the first input in Transaction.inputs, 9 | * and the index of the target cell in Transaction.outputs. 10 | */ 11 | export function generateTypeId(firstInput: Cell, outputIndex: BIish): Hash { 12 | if (!firstInput.outPoint) { 13 | throw new Error('Cannot generate TypeId because Transaction.inputs[0] has no OutPoint'); 14 | } 15 | 16 | const script = utils.generateTypeIdScript( 17 | { 18 | previousOutput: firstInput.outPoint, 19 | since: '0x0', 20 | }, 21 | BI.from(outputIndex).toHexString(), 22 | ); 23 | 24 | return script.args; 25 | } 26 | 27 | /** 28 | * Generate TypeIds for a group of cells in Transaction.outputs. 29 | */ 30 | export function generateTypeIdGroup( 31 | firstInput: Cell, 32 | outputs: Cell[], 33 | filter: (cell: Cell) => boolean, 34 | ): [number, Hash][] { 35 | const group: [number, Hash][] = []; 36 | 37 | for (let i = 0; i < outputs.length; i++) { 38 | const isTarget = filter(outputs[i]); 39 | if (isTarget) { 40 | const groupIndex = group.length; 41 | group.push([i, generateTypeId(firstInput, groupIndex)]); 42 | } 43 | } 44 | 45 | return group; 46 | } 47 | 48 | /** 49 | * Generate TypeIds from a Transaction.outputs. 50 | * 51 | * This function is different from the `generateTypeIdGroup` function, 52 | * because this function generates TypeIds based on each output's original index in the list, 53 | * instead of generating them by each output's index in a group. 54 | */ 55 | export function generateTypeIdsByOutputs( 56 | firstInput: Cell, 57 | outputs: Cell[], 58 | filter?: (cell: Cell) => boolean, 59 | ): [number, Hash][] { 60 | function filterOutput(cell: Cell): boolean { 61 | return filter instanceof Function ? filter(cell) : true; 62 | } 63 | 64 | const result: [number, Hash][] = []; 65 | for (let i = 0; i < outputs.length; i++) { 66 | if (filterOutput(outputs[i])) { 67 | result.push([i, generateTypeId(firstInput, i)]); 68 | } 69 | } 70 | 71 | return result; 72 | } 73 | 74 | /** 75 | * Check if the target string is a Type ID 76 | */ 77 | export function isTypeId(target: BytesLike): boolean { 78 | try { 79 | const buf = bytes.bytify(target); 80 | return buf.byteLength === 32; 81 | } catch { 82 | return false; 83 | } 84 | } 85 | 86 | /** 87 | * Check if the target string at least contains is a Type ID 88 | */ 89 | export function isTypeIdLengthMatch(target: BytesLike): boolean { 90 | try { 91 | const buf = bytes.bytify(target); 92 | return buf.byteLength >= 32; 93 | } catch { 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/core/src/helpers/witness.ts: -------------------------------------------------------------------------------- 1 | import { bytes, BytesLike } from '@ckb-lumos/codec'; 2 | import { blockchain } from '@ckb-lumos/base'; 3 | 4 | type WitnessArgsObject = Parameters<(typeof blockchain.WitnessArgs)['pack']>[0]; 5 | type WitnessArgsKey = keyof WitnessArgsObject; 6 | 7 | export const defaultEmptyWitnessArgs = bytes.hexify(blockchain.WitnessArgs.pack({})); 8 | 9 | /** 10 | * Update a property value of a WitnessArgs (in hex). 11 | */ 12 | export function updateWitnessArgs(witness: string | undefined, key: WitnessArgsKey, value: BytesLike) { 13 | witness = witness && witness !== '0x' ? witness : defaultEmptyWitnessArgs; 14 | return bytes.hexify( 15 | blockchain.WitnessArgs.pack({ 16 | ...blockchain.WitnessArgs.unpack(witness), 17 | [key]: value, 18 | }), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './codec'; 2 | export * from './config'; 3 | export * from './helpers'; 4 | export * from './api'; 5 | export * from './cobuild'; 6 | -------------------------------------------------------------------------------- /packages/core/src/types/async.ts: -------------------------------------------------------------------------------- 1 | export type PromiseOr = T | Promise; 2 | -------------------------------------------------------------------------------- /packages/core/src/types/blockchain.ts: -------------------------------------------------------------------------------- 1 | import { Script } from '@ckb-lumos/base'; 2 | 3 | export type ScriptId = Omit; 4 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blockchain'; 2 | export * from './async'; 3 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "module": "CommonJS" 6 | }, 7 | "exclude": ["src/__tests__"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "outDir": "lib", 6 | "rootDir": "src", 7 | "composite": true, 8 | "noEmit": false, 9 | "lib": ["ESNext"] 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["**/lib"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | watch: false, 6 | fileParallelism: false, 7 | poolOptions: { 8 | threads: { 9 | singleThread: true, 10 | }, 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "lib": ["ES6", "DOM"], 6 | "strict": true, 7 | "allowJs": true, 8 | "sourceMap": true, 9 | "moduleResolution": "Node", 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "composite": true, 22 | "noEmit": true, 23 | "jsx": "react" 24 | }, 25 | "exclude": [ 26 | "**/node_modules", 27 | "**/dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "dev": { 6 | "cache": false, 7 | "persistent": true, 8 | "dependsOn": ["clean", "^build"] 9 | }, 10 | "build": { 11 | "outputs": ["lib/**", "dist/**", ".next/**", "!.next/cache/**"], 12 | "dependsOn": ["^build"], 13 | "cache": false 14 | }, 15 | "test": { 16 | "outputs": ["coverage/**"], 17 | "dependsOn": [] 18 | }, 19 | "lint:fix": { 20 | "cache": false 21 | }, 22 | "clean": { 23 | "cache": false 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------