Repository: scallop-io/sui-kit Branch: main Commit: 0d1adfe4adda Files: 42 Total size: 153.6 KB Directory structure: gitextract_54u8gdjh/ ├── .github/ │ └── workflows/ │ └── publish-package.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── CHANGELOG.md ├── LICENSE ├── README.md ├── document/ │ ├── README_cn.md │ ├── how-to-achieve-max-performance-on-sui.md │ └── migration-guide-v2.md ├── package.json ├── src/ │ ├── index.ts │ ├── libs/ │ │ ├── multiSig/ │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ └── publickey.ts │ │ ├── suiAccountManager/ │ │ │ ├── crypto.ts │ │ │ ├── index.ts │ │ │ ├── keypair.ts │ │ │ └── util.ts │ │ ├── suiInteractor/ │ │ │ ├── index.ts │ │ │ ├── suiInteractor.ts │ │ │ └── util.ts │ │ ├── suiModel/ │ │ │ ├── index.ts │ │ │ ├── suiOwnedObject.ts │ │ │ └── suiSharedObject.ts │ │ └── suiTxBuilder/ │ │ ├── index.ts │ │ └── util.ts │ ├── suiKit.ts │ └── types/ │ └── index.ts ├── test/ │ ├── integration/ │ │ ├── index.spec.ts │ │ └── multiSig.spec.ts │ ├── tsconfig.json │ └── unit/ │ └── libs/ │ ├── multiSig/ │ │ └── publickey.spec.ts │ ├── suiAccountManager/ │ │ ├── crypto.spec.ts │ │ └── util.spec.ts │ ├── suiInteractor/ │ │ └── suiInteractor.spec.ts │ ├── suiModel/ │ │ ├── suiOwnedObject.spec.ts │ │ └── suiSharedObject.spec.ts │ └── suiTxBuilder/ │ ├── index.spec.ts │ └── utils.spec.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/publish-package.yml ================================================ name: Publish package to GitHub Packages on: release: types: [published] jobs: release: runs-on: ubuntu-22.04 permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v4 with: version: 9.12.3 - uses: actions/setup-node@v3.6.0 with: node-version: '18' cache: 'pnpm' registry-url: 'https://registry.npmjs.org' scope: '@scallop-io' - run: | pnpm install --frozen-lockfile pnpm run build env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - name: Determine npm tag id: npm_tag run: | if [[ "${{ github.ref_name }}" == *"alpha."* ]]; then echo "tag=alpha" >> $GITHUB_ENV elif [[ "${{ github.ref_name }}" == *"rc."* ]]; then echo "tag=rc" >> $GITHUB_ENV else echo "tag=latest" >> $GITHUB_ENV fi - run: pnpm publish --no-git-checks --tag ${{ env.tag }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - uses: actions/setup-node@v3.6.0 with: node-version: '18' cache: 'pnpm' registry-url: 'https://npm.pkg.github.com' scope: '@scallop-io' - run: pnpm publish --no-git-checks --access public --registry https://npm.pkg.github.com --tag ${{ env.tag }} env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Dependency directories node_modules/ # build / generate output dist/ docs/ #misc .DS_Store .rollup.cache *.pem # dotenv environment variables file .env .env.local .env.test .env.development.local .env.test.local .env.production.local # typescript *.tsbuildinfo # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* logs/ *.log* # coverage coverage/ ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ### [2.0.1](https://github.com/scallop-io/sui-kit/compare/v2.0.0...v2.0.1) (2026-03-01) ### Changed - Include `SuiObjectArg` and `SuiAmountsArg` in `moveCall` argument types ([ba16009](https://github.com/scallop-io/sui-kit/commit/ba16009)) ### [2.0.0](https://github.com/scallop-io/sui-kit/compare/v1.4.3...v2.0.0) (2026-02-05) ### ⚠ BREAKING CHANGES - Migrate to `@mysten/sui@2` and `@mysten/bcs@2` with gRPC support ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) - Minimum Node.js version changed to >=22 ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) - Migrate to ESM-only package, CommonJS support removed ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) ### Added - gRPC client support using `SuiGrpcClient` ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) - Export `getFullnodeUrl` and `SimulateTransactionResponse` ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) ### Changed - Client type from `SuiClient` to `ClientWithCoreApi` ([5bcae16](https://github.com/scallop-io/sui-kit/commit/5bcae16)) ### [1.4.3](https://github.com/scallop-io/sui-kit/compare/v1.4.2...v1.4.3) (2025-10-24) ### Changed - Use `instanceof Uint8Array` instead of `instanceof Transaction` ([82e1e27](https://github.com/scallop-io/sui-kit/pull/53/commits/82e1e276a31838387d1fb5c58397607d49063784)) ### [1.4.2](https://github.com/scallop-io/sui-kit/compare/v1.4.1...v1.4.2) (2025-07-21) ### Features - Add more unit tests and integration tests ([PR](https://github.com/scallop-io/sui-kit/pull/47)) - Add deepwiki badge ([ebb6c18](https://github.com/scallop-io/sui-kit/pull/48/commits/ebb6c18fc293c17a82eb9f6e781c4b57bb6b9e6f)) ### [1.4.1](https://github.com/scallop-io/sui-kit/compare/v1.4.0...v1.4.1) (2025-06-01) ### Features - Better handling on coin objects in `SuiTxBlock` class ([3b7d6bf](https://github.com/scallop-io/sui-kit/commit/3b7d6bfbcfd98660fa6f3245867a40f386d8dccf)) ### [1.4.0](https://github.com/scallop-io/sui-kit/compare/v1.3.7...v1.4.0) (2025-05-03) ### Features - Add batching for `multiGetObjects` ([6228c03](https://github.com/scallop-io/sui-kit/pull/43/commits/6228c03057450c452f8d07c38d7e2324e541f085)) - Improve `SuiInteractor` to allow fullnode switching ([5e56a46](https://github.com/scallop-io/sui-kit/pull/43/commits/5e56a46811d9efee63f7f26397a834fcd0fa3af7)) ### [1.3.7](https://github.com/scallop-io/sui-kit/compare/v1.3.6...v1.3.7) (2025-04-20) ### Features - bump version ### [1.3.6](https://github.com/scallop-io/sui-kit/compare/v1.3.5...v1.3.6) (2025-04-20) ### Features - update `vitest` package ([e2bd98b](https://github.com/scallop-io/sui-kit/pull/41/commits/e2bd98b5e0fc72e86968598b8bc9098d5b84326b)) - add more tests ([f18c3d4](https://github.com/scallop-io/sui-kit/pull/41/commits/f18c3d487f1bb9ebe87d5573fd784ad14abc1803)) ### [1.3.5](https://github.com/scallop-io/sui-kit/compare/v1.3.4...v1.3.5) (2025-03-19) ### [1.3.4](https://github.com/scallop-io/sui-kit/compare/v1.3.3...v1.3.4) (2025-03-19) ### [1.3.3](https://github.com/scallop-io/sui-kit/compare/v1.3.2...v1.3.3) (2025-03-19) ### Features - add suiClients to class params ([f23186c](https://github.com/scallop-io/sui-kit/commit/f23186c0430149145b9ed2dcc9ea118481f53245)) - allow multiple tag for npm package ([aecd0c5](https://github.com/scallop-io/sui-kit/commit/aecd0c5a659aaf4b7c2722010ad5002b88f0ed7e)) ### Bug Fixes - correct typo in README and package.json ([39d1ec7](https://github.com/scallop-io/sui-kit/commit/39d1ec7942502f345c6904b112b7f7a48fd47302)) - github workflow ([4f125c4](https://github.com/scallop-io/sui-kit/commit/4f125c44f39ca3cd2d2128afb4da9cccf6be18c7)) - minor ([100aa02](https://github.com/scallop-io/sui-kit/commit/100aa02646652f5d04eeb7270e8d0e4328080cd7)) - typo and prettier ([3b9760f](https://github.com/scallop-io/sui-kit/commit/3b9760fb4fbb0f7c701ab60704f6fabecb799af5)) ### [1.3.2](https://github.com/scallop-io/sui-kit/compare/v1.3.1...v1.3.2) (2024-12-21) ### Features - Bump version ### [1.3.1](https://github.com/scallop-io/sui-kit/compare/v1.3.0...v1.3.1) (2024-12-14) ### Features - Export `SuiInteractor` class ([e7109e0](https://github.com/scallop-io/sui-kit/pull/35/commits/e7109e0324e6ffb028d2ab894d2859a2b79041af)) - Upgrade `@mysten/sui` to version `1.3.1` ([5925815](https://github.com/scallop-io/sui-kit/pull/35/commits/59258155689456736fc05a3c73c52d12680ad5b1)) - Add `createTxBlock`; update `selectCoinsWithAmount` to return `version` and also `digest` instead of `objectId` only ([569c043](https://github.com/scallop-io/sui-kit/pull/35/commits/569c043a6c7e920a743506941d980e4288e969a7)) - Upgrade `@mysten/sui` sdk to 1.7.0 and other related packages ([f17e669](https://github.com/scallop-io/sui-kit/pull/33/commits/f17e669099550854ead93edd37f70eafc5400456)) ### [1.3.0](https://github.com/scallop-io/sui-kit/compare/v1.0.1...v1.3.0) (2024-07-25) ### Features - Update `mysten/sui` sdk ([c0a4691](https://github.com/scallop-io/sui-kit/pull/31/commits/c0a469153b306f4502f8634ee3a49a63b33ba6e1)) - Bump version to match `mysten/sui` version ### [1.0.2](https://github.com/scallop-io/sui-kit/compare/v1.0.1...v1.0.2) (2024-07-12) ### Bug Fixes - Add `number` and `bigint` check on `convertArgs` ([c73ccb3](https://github.com/scallop-io/sui-kit/pull/30/commits/c73ccb34840e6556e0aaf45ea978a7db99056a6b)) - Fix types and `pure` getter on `SuiTxBlock` ([6cae48f](https://github.com/scallop-io/sui-kit/pull/30/commits/6cae48f1898d91ced89c0446804196efc9c0daa2)) ### [1.0.1](https://github.com/scallop-io/sui-kit/compare/v1.0.0...v1.0.1) (2024-07-12) ### Bug Fixes - Minor fixes ([060761c](https://github.com/scallop-io/sui-kit/pull/28/commits/060761cc32f6c13b541c08c367e1c37ccaad3f2e)) ### [1.0.0](https://github.com/scallop-io/sui-kit/compare/v0.45.0...v1.0.0) (2024-07-9) ### ⚠ BREAKING CHANGES - Upgrade to `@mysten/sui@1` (https://github.com/scallop-io/sui-kit/pull/23) ### [0.52.0](https://github.com/scallop-io/sui-kit/compare/v0.45.0...v0.52.0) (2024-06-14) ### Features - Add `balance` property to `selectCoins` method in `SuiInteractor` class ([c61c7d8](https://github.com/scallop-io/sui-kit/pull/24/commits/c61c7d86e86bfb213271b9c7c4c32768a072df7f)) - Match version number with `mysten/sui.js` library version ### [0.44.45](https://github.com/scallop-io/sui-kit/compare/v0.44.2...v0.45.0) (2024-05-14) ### [0.44.2](https://github.com/scallop-io/sui-kit/compare/v0.44.1...v0.44.2) (2023-12-30) ### Bug Fixes - correct VecArg for convertArgs ([c213a7b](https://github.com/scallop-io/sui-kit/commit/c213a7bf670ecb28ad8698b130e27e0240fedd36)) ## [0.44.0](https://github.com/scallop-io/sui-kit/compare/v0.42.2...v0.44.0) (2023-10-22) ## [0.42.0](https://github.com/scallop-io/sui-kit/compare/v0.41.0...v0.42.0) (2023-09-27) ### ⚠ BREAKING CHANGES - change `JsonRpcProvider` to `SuiClient`, and change `getSinger` to `getKeypair` in SuiKit class ### Features - export transactions from sui sdk ([4bfa0f2](https://github.com/scallop-io/sui-kit/commit/4bfa0f2580c34592bcb7b0b507d94e6daa1f00bc)) - upgrade sui sdk to refactored version ([925f731](https://github.com/scallop-io/sui-kit/commit/925f73138501a40b650059be8d3601b5144cd08f)) ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 Scallop Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Toolkit for interacting with SUI network ## Features - [x] Transfer SUI, Custom Coin and objects. - [x] Move call - [x] Programmable transaction - [x] Query on-chain data - [x] HD wallet multi-accounts ## Pre-requisites ```bash npm install @scallop-io/sui-kit ``` ## How to use ### Init SuiKit ```typescript import { SuiKit } from '@scallop-io/sui-kit'; // The following types of secret key are supported: // 1. base64 key from SUI cli keystore file // 2. 32 bytes hex key // 3. 64 bytes legacy hex key const secretKey = ''; const suiKit1 = new SuiKit({ secretKey }); // 12 or 24 words mnemonics const mnemonics = ''; const suiKit2 = new SuiKit({ mnemonics }); // It will create a HD wallet with a random mnemonics const suiKit3 = new SuiKit(); // Override options const suiKit = new SuiKit({ mnemonics: '', // 'testnet' | 'mainnet' | 'devnet', default is 'devnet' networkType: 'testnet', // the fullnode url, default is the preconfig fullnode url for the given network type // It will rotate the fullnode when the current fullnode is not available fullnodeUrls: '[, ]', // the faucet url, default is the preconfig faucet url for the given network type faucetUrl: '', }); ``` ### Transfer You can use SuiKit to transfer SUI, custom coins, and any objects. ```typescript const recipient1 = '0x123'; // replace with real address const recipient2 = '0x456'; // replace with real address // transfer SUI to single recipient await suiKit.transferSui(recipient1, 1000); // transfer SUI to multiple recipients await suiKit.transferSuiToMany([recipient1, recipient2], [1000, 2000]); const coinType = '::custom_coin::CUSTOM_COIN'; // Transfer custom coin to single recipient await suiKit.transferCoin(recipient1, 1000, coinType); // Transfer custom coin to multiple recipients await suiKit.transferCoinToMany( [recipient1, recipient2], [1000, 2000], coinType ); // Transfer objects const objectIds = ['', '']; await suiKit.transferObjects(objectIds, recipient1); ``` ### Stake SUI You can use SuiKit to stake SUI. ```typescript /** * This is an example of using SuiKit to stake SUI */ const stakeAmount = 1000; const validatorAddress = '0x123'; // replace with real address suiKit.stakeSui(stakeAmount, validatorAddress).then(() => { console.log('Stake SUI success'); }); ``` ### Move call You can use SuiKit to call move functions. ```typescript const res = await suiKit.moveCall({ target: '0x2::coin::join', arguments: [coin0, coin1], typeArguments: [coinType], }); console.log(res); ``` How to pass arguments? Suppose you have a move function like this: ```move public entry fun test_args( addrs: vector
, name: vector, numbers: vector, bools: vector, coins: vector>, ctx: &mut TxContext, ) { // ... } ``` You can pass the arguments like this: ```typescript const addr1 = '0x656b875c9c072a465048fc10643470a39ba331727719df46c004973fcfb53c95'; const addr2 = '0x10651e50cdbb4944a8fd77665d5af27f8abde6eb76a12b97444809ae4ddb1aad'; const coin1 = '0xd4a01b597b87986b04b65e04049499b445c0ee901fe8ba310b1cf29feaa86876'; const coin2 = '0x4d4a01b597b87986b04b65e04049499b445c0ee901fe8ba310b1cf29feaa8687'; suiKit.moveCall({ target: `${pkgId}::module::test_args`, arguments: [ // pass vector
, need to specify the vecType as 'address' { value: [addr1, addr2], vecType: 'address' }, // pass vector, need to specify the vecType as 'u8' { value: [10, 20], vecType: 'u8' }, // pass vector, default vecType for number array is 'u64', so no need to specify [34324, 234234], // pass vector, default vecType for boolean array is 'bool', so no need to specify [true, false], // pass vector>, no need to specify the vecType for object array [coin1, coin2], ], }); ``` All the supported types are: - address - u8 - u16 - u32 - u64 - u128 - u256 - bool - object ### Programmable transaction With programmable transaction, you can send a transaction with multiple actions. The following is an example using flashloan to make arbitrage. (check [here](./examples/sample_move/custom_coin/sources/dex.move) for the corresponding Move contract code) ```typescript import { SuiKit, SuiTxBlock } from '@scallop-io/sui-kit'; import * as process from 'process'; import * as dotenv from 'dotenv'; dotenv.config(); const treasuryA = '0xe5042357d2c2bb928f37e4d12eac594e6d02327d565e801eaf9aca4c7340c28c'; const treasuryB = '0xdd2f53171b8c886fad20e0bfecf1d4eede9d6c75762f169a9f3c3022e5ce7293'; const dexPool = '0x8a13859a8d930f3238ddd31180a5f0914e5b8dbaa31e18387066b61a563fedf9'; const pkgId = '0x3c316b6af0586343ce8e6b4be890305a1f83b7e196366f6435b22b6e3fc8e3d9'; (async () => { const mnemonics = process.env.MNEMONICS; const suiKit = new SuiKit({ mnemonics }); const sender = suiKit.currentAddress; const tx = new SuiTxBlock(); // 1. Make a flash loan for coinB const [coinB, loan] = tx.moveCall(`${pkgId}::custom_coin_b::flash_loan`, [ treasuryB, 10 ** 9, ]); // 2. Swap from coinB to coinA, ratio is 1:1 const coinA = tx.moveCall(`${pkgId}::dex::swap_a`, [dexPool, coinB]); // 3. Swap from coinA back to coinB, ratio is 1:2 const coinB2 = tx.moveCall(`${pkgId}::dex::swap_b`, [dexPool, coinA]); // 4. Repay flash loan const [paybackCoinB] = tx.splitCoins(coinB2, [10 ** 9]); tx.moveCall(`${pkgId}::custom_coin_b::payback_loan`, [ treasuryB, paybackCoinB, loan, ]); // 5. Transfer profits to sender tx.transferObjects([coinB2], sender); // 5. Execute transaction const res = await suiKit.signAndSendTxn(tx); console.log(res); })(); ``` ### Multi-accounts SuiKit follows bip32 & bip39 standard, so you can use it to manage multiple accounts. When init SuiKit, you can pass in your mnemonics to create a wallet with multiple accounts. ```typescript /** * This is an example of using SuiKit to manage multiple accounts. */ import { SuiKit } from '@scallop-io/sui-kit'; async function checkAccounts(suiKit: SuiKit) { const displayAccounts = async (suiKit: SuiKit, accountIndex: number) => { const coinType = '0x2::sui::SUI'; const addr = suiKit.getAddress({ accountIndex }); const balance = (await suiKit.getBalance(coinType, { accountIndex })) .balance; console.log(`Account ${accountIndex}: ${addr} has ${balance} SUI`); }; // log the first 10 accounts const numAccounts = 10; for (let i = 0; i < numAccounts; i++) { await displayAccounts(suiKit, i); } } async function internalTransferSui( suiKit: SuiKit, fromAccountIndex: number, toAccountIndex: number, amount: number ) { const toAddr = suiKit.getAddress({ accountIndex: toAccountIndex }); console.log( `Transfer ${amount} SUI from account ${fromAccountIndex} to account ${toAccountIndex}` ); return await suiKit.transferSui(toAddr, amount, { accountIndex: fromAccountIndex, }); } const mnemonics = process.env.MNEMONICS; const suiKit = new SuiKit({ mnemonics }); checkAccounts(suiKit).then(() => {}); // transfer 1000 SUI from account 0 to account 1 internalTransferSui(suiKit, 0, 1, 1000).then(() => {}); ``` ### Publish & upgrade Move package We have a standalone npm package to help you publish and upgrade Move package based on sui-kit. Please refer to the repository: [sui-package-kit](https://github.com/scallop-io/sui-package-kit) ## Migration Guide If you're upgrading from v1.x to v2.0.0, please refer to the [Migration Guide](./document/migration-guide-v2.md). [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/scallop-io/sui-kit) ================================================ FILE: document/README_cn.md ================================================ # SUI 网络交互工具箱 ## 特点 - [x] 相比于 Mystenlab 的 SDK,更加易于使用 - [x] 支持转账 SUI 和自定义代币 - [x] 从开发网络和测试网络请求水龙头 - [x] 质押 SUI - [x] 兼容可编程交易 - [x] 交易检查(无需 gas 的交易检查) - [x] 高级特性:新增多账户支持 ## 先决条件 1. 安装包 ```bash npm install @scallop-io/sui-kit ``` 2. 安装 SUI cli(可选:仅在发布包时需要) 请参考官方文档:[如何安装 SUI cli](https://docs.sui.io/devnet/build/install) ## 如何使用 ### 转账 你可以使用 SuiKit 来转账 SUI 和其他代币。 ```typescript /** * 这是使用 SuiKit 将代币从一个账户转到另一个账户的示例。 */ import { SuiKit } from '@scallop-io/sui-kit'; const secretKey = '<秘钥>'; const suiKit = new SuiKit({ secretKey }); const recipient = '0xCAFE'; suiKit.transferSui(recipient, 1000).then(() => console.log('转账了 1000 SUI')); suiKit .transferCoin(recipient, 1000, '0xCOFFEE::coin::COIN') .then(() => console.log('转账了 1000 COIN')); ``` ### 请求水龙头 你可以使用 SuiKit 来从开发网络和测试网络请求水龙头。 ```typescript import { SuiKit } from '@scallop-io/sui-kit'; const secretKey = '<密钥>'; const suiKit = new SuiKit({ secretKey, networkType: 'devnet' }); suiKit.requestFaucet().then(() => { console.log('请求水龙头成功'); }); ``` ### 质押 SUI 你可以使用 SuiKit 来质押 SUI。 ```typescript /** * 这是一个使用 SuiKit 质押 SUI 的示例 */ import { SuiKit } from '@scallop-io/sui-kit'; const secretKey = '<密钥>'; const suiKit = new SuiKit({ secretKey, networkType: 'devnet' }); const stakeAmount = 1000; const validatorAddress = '0x123'; suiKit.stakeSui(stakeAmount, validatorAddress).then(() => { console.log('质押成功'); }); ``` ### 可编程交易 通过可编程交易,您可以在一个交易中发送多个操作。下面的示例演示如何在一笔交易中向多个账户转账 SUI。 ```typescript /** * 这个示例演示如何使用 SuiKit 进行可编程交易 */ import { SuiKit, SuiTxBlock } from '@scallop-io/sui-kit'; const secretKey = '<密钥>'; const suiKit = new SuiKit({ secretKey }); // 构建一个交易块以将代币发送到多个账户 const tx = new SuiTxBlock(); const recipients = ['0x123', '0x456', '0x789']; recipients.forEach((recipient) => { const [coin] = tx.splitCoins(tx.gas, [1000]); tx.transferObjects([coin], recipient); }); // 发送交易 suiKit.signAndSendTxn(tx).then((response) => { console.log('交易摘要: ' + response.digest); }); ``` ## 高级特性 ### 多账户支持 SuiKit 遵循 bip32 和 bip39 标准,因此您可以使用它来管理多个账户。 下面这段代码展示了如何使用 SuiKit 来管理多个账户。在初始化 SuiKit 时,您可以传入助记词以创建具有多个账户的钱包。 代码中,`checkAccounts` 函数打印了前十个账户的余额信息。在循环中,它使用 getAddress 和 getBalance 函数获取特定账户的地址和余额。 `internalTransferSui` 函数实现了内部账户之间的转账。 ```typescript /** * 这是一个使用 SuiKit 管理多个账户的示例代码 */ import { SuiKit } from '@scallop-io/sui-kit'; // 展示 SUI 在多个账户中的余额 async function checkAccounts(suiKit: SuiKit) { const displayAccounts = async (suiKit: SuiKit, accountIndex: number) => { const coinType = '0x2::sui::SUI'; const addr = suiKit.getAddress({ accountIndex }); const balance = (await suiKit.getBalance(coinType, { accountIndex })) .balance; console.log(`账户 ${accountIndex}: ${addr} 余额为 ${balance} SUI`); }; // 显示前10个账户 const numAccounts = 10; for (let i = 0; i < numAccounts; i++) { await displayAccounts(suiKit, i); } } // 在多个账户之间进行 SUI 转账 async function internalTransferSui( suiKit: SuiKit, fromAccountIndex: number, toAccountIndex: number, amount: number ) { const toAddr = suiKit.getAddress({ accountIndex: toAccountIndex }); console.log( `从账户 ${fromAccountIndex} 转账 ${amount} SUI 到账户 ${toAccountIndex}` ); return await suiKit.transferSui(toAddr, amount, { accountIndex: fromAccountIndex, }); } // 读取环境变量 MNEMONICS,生成 SuiKit 实例 const mnemonics = process.env.MNEMONICS; const suiKit = new SuiKit({ mnemonics }); checkAccounts(suiKit).then(() => {}); // 从账户 0 转账 1000 SUI 到账户 1 internalTransferSui(suiKit, 0, 1, 1000).then(() => {}); ``` ================================================ FILE: document/how-to-achieve-max-performance-on-sui.md ================================================ # How to achieve max performance on SUI network? ## 1. Pre-build the transaction When sending transactions, there's a process to build the transaction which needs to call the nodes to get the latest data, such as object version, gas price, gas budget. There'll be multiple forth and back calls between the client and the nodes, if you can pre-build the transaction, you can save a lot of time when you send it. For simple transactions, you can pre-build the whole transaction and send it anytime you want. For complex transactions, you can pre-build the other parts and leave dynamic part to be built when sending the transaction. ## 2. Choose high quality fullnode By default, people will use the public fullnode provided by SUI, which is a good choice for most of the cases. But, it's usually not the best choice for the high performance applications, since the public fullnode is shared by many users, and it's not optimized for the high performance applications. By choosing a node that is optimized and used less by others, you can build & send your transaction significantly faster. ## 3. Set higher gas price By default, the gas price will be set to the reference gas price returned by the node. You should set a higher gas price to make sure your transaction will get executed as soon as possible. ## 4. Use programmable transaction Programmable transaction is a feature that allows you to include multiple move calls in one transaction. Instead of sending multiple transactions, you can send one transaction with multiple move calls, which will save a lot of time. SuiKit supports programmable transaction. ## 5. Batch transactions Suppose you want to send multiple transactions in a short period of time, let's say 10 transactions in a second. If you send them in parallel, by default you'll end up with 10 transactions referencing the same gas coins, which will cause the transactions to fail. How to solve this problem? 1, Manually set different gas coins for each transaction. 2, Use programmable transaction if you can batch the transactions together. 3, Split your assets into multiple accounts, and send the transactions in parallel. Here I think solution 3 is the best one, with least effort and best compatibility. You can use SuiKit to create multiple accounts, and send the transactions in parallel. ================================================ FILE: document/migration-guide-v2.md ================================================ # Migration Guide: v1.x to v2.0.0 This guide helps you migrate your project from sui-kit v1.x to v2.0.0. ## Breaking Changes Overview ### 1. Node.js Version Requirement **v2.0.0 requires Node.js >= 22** ```bash # Check your Node.js version node --version # If needed, upgrade Node.js to v22 or later ``` ### 2. ESM-Only Package v2.0.0 is now an ESM-only package. CommonJS support has been removed. **Before (CommonJS):** ```javascript const { SuiKit } = require('@scallop-io/sui-kit'); ``` **After (ESM):** ```javascript import { SuiKit } from '@scallop-io/sui-kit'; ``` If your project uses CommonJS, you need to either: - Convert your project to ESM by adding `"type": "module"` to `package.json` - Use dynamic imports: `const { SuiKit } = await import('@scallop-io/sui-kit')` ### 3. Dependency Updates v2.0.0 migrates to the latest Mysten SDK: | Package | v1.x | v2.0.0 | |---------|------|--------| | @mysten/sui | ^1.x | ^2.0.0 | | @mysten/bcs | ^1.x | ^2.0.0 | ### 4. Client Type Changes The client type has changed from `SuiClient` to `ClientWithCoreApi`. **Before:** ```typescript import { SuiClient } from '@mysten/sui/client'; ``` **After:** ```typescript import { ClientWithCoreApi } from '@mysten/sui/client'; ``` ### 5. gRPC Support v2.0.0 adds gRPC client support alongside the existing REST client. **Using gRPC client:** ```typescript import { SuiKit, SuiGrpcClient } from '@scallop-io/sui-kit'; // The SDK now supports gRPC for improved performance const suiKit = new SuiKit({ mnemonics: '', networkType: 'mainnet', }); ``` ### 6. New Exports v2.0.0 exports additional utilities: ```typescript import { SuiKit, SuiTxBlock, getFullnodeUrl, // New in v2.0.0 SimulateTransactionResponse // New in v2.0.0 } from '@scallop-io/sui-kit'; ``` ## Migration Steps ### Step 1: Update Node.js Ensure you're running Node.js v22 or later. ### Step 2: Update package.json ```json { "type": "module", "engines": { "node": ">=22" }, "dependencies": { "@scallop-io/sui-kit": "^2.0.0" } } ``` ### Step 3: Update Import Statements Convert all `require()` statements to ESM `import` syntax. ### Step 4: Update TypeScript Configuration (if applicable) ```json { "compilerOptions": { "module": "ESNext", "moduleResolution": "bundler", "target": "ES2022" } } ``` ### Step 5: Test Your Application Run your test suite to ensure everything works correctly: ```bash npm test ``` ## FAQ ### Q: Can I still use CommonJS? A: No, v2.0.0 is ESM-only. You must migrate to ESM or use dynamic imports. ### Q: Is the API backward compatible? A: Yes, the SuiKit API remains largely the same. The main changes are in the module system and underlying dependencies. ### Q: What are the benefits of v2.0.0? - gRPC support for better performance - Latest @mysten/sui SDK features - Smaller bundle size with ESM - Improved type definitions ## Need Help? If you encounter issues during migration, please: 1. Check the [CHANGELOG](../CHANGELOG.md) for detailed changes 2. Open an issue on [GitHub](https://github.com/scallop-io/sui-kit/issues) ================================================ FILE: package.json ================================================ { "name": "@scallop-io/sui-kit", "version": "2.0.1", "description": "Toolkit for interacting with SUI network", "keywords": [ "sui", "scallop labs", "move", "blockchain", "sui-kit" ], "author": "team@scallop.io", "homepage": "https://github.com/scallop-io/sui-kit#readme", "bugs": "https://github.com/scallop-io/sui-kit/issues", "repository": { "type": "git", "url": "https://github.com/scallop-io/sui-kit.git" }, "license": "Apache-2.0", "publishConfig": { "access": "public" }, "engines": { "node": ">=22" }, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "files": [ "dist", "src" ], "scripts": { "clean": "rm -rf tsconfig.tsbuildinfo ./dist", "build": "npm run build:types && npm run build:prod", "build:prod": "tsup --env.NODE_ENV production", "build:dev": "tsup", "build:types": "tsc --build", "watch:tsup": "tsup --watch", "watch": "pnpm run clean && pnpm run watch:tsup", "test": "pnpm test:typecheck && pnpm test:unit && pnpm test:integration", "test:typecheck": "tsc -p ./test", "test:unit": "vitest run test/unit --test-timeout=60000", "test:integration": "vitest run test/integration --test-timeout=60000", "test:watch": "vitest", "test:coverage-unit": "vitest run test/unit --coverage --test-timeout=60000", "test:coverage-integration": "vitest run test/integration --coverage --test-timeout=60000", "test:coverage": "vitest run --coverage --test-timeout=60000", "format:fix": "prettier --ignore-path 'dist/* docs/*' --write '**/*.{ts,json,md}'", "lint:fix": "eslint . --ignore-pattern dist --ext .ts --fix", "prepare": "husky install", "commit": "commit", "release": "standard-version -f", "release:major": "standard-version -r major", "release:minor": "standard-version -r minor", "release:patch": "standard-version -r patch", "doc": "typedoc --out docs src/index.ts" }, "dependencies": { "@mysten/bcs": "^2.0.0", "@mysten/sui": "^2.0.0", "@scure/bip39": "^1.5.4", "assert": "^2.1.0", "bech32": "^2.0.0" }, "devDependencies": { "@commitlint/cli": "^18.0.0", "@commitlint/config-conventional": "^18.0.0", "@commitlint/prompt-cli": "^18.0.0", "@protobuf-ts/grpcweb-transport": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "@types/node": "^20.8.7", "@types/tmp": "^0.2.5", "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "8.10.0", "@vitest/coverage-v8": "3.1.1", "@vitest/expect": "^3.1.1", "@vitest/runner": "^3.1.1", "@vitest/spy": "^3.1.1", "dotenv": "^16.3.1", "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", "husky": "^8.0.3", "lint-staged": "^15.0.2", "prettier": "^3.0.3", "standard-version": "^9.5.0", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "tsup": "^7.2.0", "typedoc": "^0.25.2", "typescript": "5.5.4", "valibot": "^0.36.0", "vitest": "^3.1.1" }, "lint-staged": { "**/*.ts": [ "pnpm run format:fix", "pnpm run lint:fix" ], "**/*.json|md": [ "pnpm run format:fix" ] }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] }, "prettier": { "trailingComma": "es5", "tabWidth": 2, "semi": true, "singleQuote": true, "useTabs": false, "quoteProps": "as-needed", "bracketSpacing": true, "arrowParens": "always", "endOfLine": "lf" }, "eslintConfig": { "root": true, "env": { "browser": true, "node": true, "es2022": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended" ], "plugins": [ "@typescript-eslint", "prettier" ], "parser": "@typescript-eslint/parser", "rules": { "prettier/prettier": "warn", "@typescript-eslint/no-explicit-any": "off", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ] } } } ================================================ FILE: src/index.ts ================================================ export * from '@mysten/sui/utils'; export * from '@mysten/sui/transactions'; export { SuiKit } from './suiKit.js'; export { SuiAccountManager } from './libs/suiAccountManager/index.js'; export { SuiTxBlock } from './libs/suiTxBuilder/index.js'; export { MultiSigClient } from './libs/multiSig/index.js'; export { SuiInteractor, getFullnodeUrl, type SimulateTransactionResponse, } from './libs/suiInteractor/index.js'; export type * from './types/index.js'; ================================================ FILE: src/libs/multiSig/client.ts ================================================ import { MultiSigPublicKey } from '@mysten/sui/multisig'; import type { PublicKey } from '@mysten/sui/cryptography'; import { ed25519PublicKeyFromBase64 } from './publickey.js'; export type PublicKeyWeightPair = { publicKey: PublicKey; weight: number; }; export class MultiSigClient { public readonly pksWeightPairs: PublicKeyWeightPair[]; public readonly threshold: number; public readonly multiSigPublicKey: MultiSigPublicKey; constructor(pks: PublicKeyWeightPair[], threshold: number) { this.pksWeightPairs = pks; this.threshold = threshold; this.multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({ threshold: this.threshold, publicKeys: this.pksWeightPairs, }); } static fromRawEd25519PublicKeys( rawPublicKeys: string[], weights: number[], threshold: number ): MultiSigClient { const pks = rawPublicKeys.map((rawPublicKey, i) => { return { publicKey: ed25519PublicKeyFromBase64(rawPublicKey), weight: weights[i], }; }); return new MultiSigClient(pks, threshold); } multiSigAddress(): string { return this.multiSigPublicKey.toSuiAddress(); } combinePartialSigs(sigs: string[]): string { return this.multiSigPublicKey.combinePartialSignatures(sigs); } } ================================================ FILE: src/libs/multiSig/index.ts ================================================ export { MultiSigClient } from './client.js'; ================================================ FILE: src/libs/multiSig/publickey.ts ================================================ import { PublicKey } from '@mysten/sui/cryptography'; import { Ed25519PublicKey } from '@mysten/sui/keypairs/ed25519'; import { fromBase64 } from '@mysten/bcs'; export function ed25519PublicKeyFromBase64(rawPubkey: string): PublicKey { let bytes = fromBase64(rawPubkey); // raw public keys should either be 32 bytes or 33 bytes (with the first byte being flag) if (bytes.length !== 32 && bytes.length !== 33) throw 'invalid pubkey length'; bytes = bytes.length === 33 ? bytes.slice(1) : bytes; return new Ed25519PublicKey(bytes); } ================================================ FILE: src/libs/suiAccountManager/crypto.ts ================================================ import { generateMnemonic as genMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; export const generateMnemonic = (numberOfWords: 12 | 24 = 24) => { const strength = numberOfWords === 12 ? 128 : 256; return genMnemonic(wordlist, strength); }; ================================================ FILE: src/libs/suiAccountManager/index.ts ================================================ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; import { getKeyPair } from './keypair.js'; import { hexOrBase64ToUint8Array, normalizePrivateKey } from './util.js'; import { generateMnemonic } from './crypto.js'; import type { AccountManagerParams, DerivePathParams, } from '../../types/index.js'; import { SUI_PRIVATE_KEY_PREFIX, decodeSuiPrivateKey, } from '@mysten/sui/cryptography'; export class SuiAccountManager { private mnemonics: string; private secretKey: string; public currentKeyPair: Ed25519Keypair; public currentAddress: string; /** * Support the following ways to init the SuiToolkit: * 1. mnemonics * 2. secretKey (base64 or hex) * If none of them is provided, will generate a random mnemonics with 24 words. * * @param mnemonics, 12 or 24 mnemonics words, separated by space * @param secretKey, base64 or hex string or Bech32 string, when mnemonics is provided, secretKey will be ignored */ constructor({ mnemonics, secretKey }: AccountManagerParams = {}) { // If the mnemonics or secretKey is provided, use it // Otherwise, generate a random mnemonics with 24 words this.mnemonics = mnemonics || ''; this.secretKey = secretKey || ''; if (!this.mnemonics && !this.secretKey) { this.mnemonics = generateMnemonic(24); } // Init the current account this.currentKeyPair = this.secretKey ? this.parseSecretKey(this.secretKey) : getKeyPair(this.mnemonics); this.currentAddress = this.currentKeyPair.getPublicKey().toSuiAddress(); } /** * Check if the secretKey starts with bench32 format */ parseSecretKey(secretKey: string) { if (secretKey.startsWith(SUI_PRIVATE_KEY_PREFIX)) { const { secretKey: uint8ArraySecretKey } = decodeSuiPrivateKey(secretKey); return Ed25519Keypair.fromSecretKey( normalizePrivateKey(uint8ArraySecretKey) ); } return Ed25519Keypair.fromSecretKey( normalizePrivateKey(hexOrBase64ToUint8Array(secretKey)) ); } /** * if derivePathParams is not provided or mnemonics is empty, it will return the currentKeyPair. * else: * it will generate keyPair from the mnemonic with the given derivePathParams. */ getKeyPair(derivePathParams?: DerivePathParams) { if (!derivePathParams || !this.mnemonics) return this.currentKeyPair; return getKeyPair(this.mnemonics, derivePathParams); } /** * if derivePathParams is not provided or mnemonics is empty, it will return the currentAddress. * else: * it will generate address from the mnemonic with the given derivePathParams. */ getAddress(derivePathParams?: DerivePathParams) { if (!derivePathParams || !this.mnemonics) return this.currentAddress; return getKeyPair(this.mnemonics, derivePathParams) .getPublicKey() .toSuiAddress(); } /** * Switch the current account with the given derivePathParams. * This is only useful when the mnemonics is provided. For secretKey mode, it will always use the same account. */ switchAccount(derivePathParams: DerivePathParams) { if (this.mnemonics) { this.currentKeyPair = getKeyPair(this.mnemonics, derivePathParams); this.currentAddress = this.currentKeyPair.getPublicKey().toSuiAddress(); } } } ================================================ FILE: src/libs/suiAccountManager/keypair.ts ================================================ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; import type { DerivePathParams } from '../../types/index.js'; /** * @description Get ed25519 derive path for SUI * @param derivePathParams */ export const getDerivePathForSUI = ( derivePathParams: DerivePathParams = {} ) => { const { accountIndex = 0, isExternal = false, addressIndex = 0, } = derivePathParams; return `m/44'/784'/${accountIndex}'/${isExternal ? 1 : 0}'/${addressIndex}'`; }; /** * the format is m/44'/784'/accountIndex'/${isExternal ? 1 : 0}'/addressIndex' * * accountIndex is the index of the account, default is 0. * * isExternal is the type of the address, default is false. Usually, the external address is used to receive coins. The internal address is used to change coins. * * addressIndex is the index of the address, default is 0. It's used to generate multiple addresses for one account. * * @description Get keypair from mnemonics and derive path * @param mnemonics * @param derivePathParams */ export const getKeyPair = ( mnemonics: string, derivePathParams: DerivePathParams = {} ) => { const derivePath = getDerivePathForSUI(derivePathParams); return Ed25519Keypair.deriveKeypair(mnemonics, derivePath); }; ================================================ FILE: src/libs/suiAccountManager/util.ts ================================================ import { fromBase64, fromHex } from '@mysten/bcs'; /** * @description This regular expression matches any string that contains only hexadecimal digits (0-9, A-F, a-f). * @param str */ export const isHex = (str: string) => /^0x[0-9a-fA-F]+$|^[0-9a-fA-F]+$/.test(str); /** * @description This regular expression matches any string that contains only base64 digits (0-9, A-Z, a-z, +, /, =). * Note that the "=" signs at the end are optional padding characters that may be present in some base64 encoded strings. * @param str */ export const isBase64 = (str: string) => /^[a-zA-Z0-9+/]+={0,2}$/g.test(str); /** * Use fromHex or fromBase64 from @mysten/bcs directly instead. * @description Convert a hex or base64 string to Uint8Array */ export const hexOrBase64ToUint8Array = (str: string): Uint8Array => { if (isHex(str)) { return fromHex(str); } if (isBase64(str)) { return fromBase64(str); } throw new Error('The string is not a valid hex or base64 string.'); }; const PRIVATE_KEY_SIZE = 32; const LEGACY_PRIVATE_KEY_SIZE = 64; /** * normalize a private key * A private key is a 32-byte array. * But there are two different formats for private keys: * 1. A 32-byte array * 2. A 64-byte array with the first 32 bytes being the private key and the last 32 bytes being the public key * 3. A 33-byte array with the first byte being 0x00 (sui.keystore key is a Base64 string with scheme flag 0x00 at the beginning) */ export const normalizePrivateKey = (key: Uint8Array): Uint8Array => { if (key.length === LEGACY_PRIVATE_KEY_SIZE) { return key.slice(0, PRIVATE_KEY_SIZE); } if (key.length === PRIVATE_KEY_SIZE + 1 && key[0] === 0) { return key.slice(1); } if (key.length === PRIVATE_KEY_SIZE) { return key; } throw new Error('invalid secret key'); }; /** * @deprecated Please use fromHex and fromBase64 from '@mysten/bcs' directly. */ export { fromHex, fromBase64 } from '@mysten/bcs'; ================================================ FILE: src/libs/suiInteractor/index.ts ================================================ export { SuiInteractor, getFullnodeUrl, type SuiObjectData, type SuiObjectDataOptions, type SimulateTransactionResponse, } from './suiInteractor.js'; ================================================ FILE: src/libs/suiInteractor/suiInteractor.ts ================================================ import { SuiInteractorParams, NetworkType } from '../../types/index.js'; import { SuiOwnedObject, SuiSharedObject } from '../suiModel/index.js'; import { batch, delay } from './util.js'; import { SuiGrpcClient, type SuiGrpcClientOptions } from '@mysten/sui/grpc'; import type { ClientWithCoreApi, SuiClientTypes } from '@mysten/sui/client'; const MAX_OBJECTS_PER_REQUEST = 50; // Helper to create gRPC client options with baseUrl function createGrpcClientOptions( url: string, network: NetworkType ): SuiGrpcClientOptions { return { baseUrl: url, network } satisfies SuiGrpcClientOptions; } // Helper to get fullnode URLs for each network function getFullnodeUrl(network: NetworkType): string { switch (network) { case 'mainnet': return 'https://fullnode.mainnet.sui.io:443'; case 'testnet': return 'https://fullnode.testnet.sui.io:443'; case 'devnet': return 'https://fullnode.devnet.sui.io:443'; case 'localnet': return 'http://127.0.0.1:9000'; default: throw new Error(`Unknown network: ${network}`); } } // Object data type from SDK v2 export type SuiObjectData = SuiClientTypes.Object<{ content: true; json: true; }>; // Options for getObjects (SDK v2 naming) export type SuiObjectDataOptions = SuiClientTypes.ObjectInclude; // Simulate transaction response type export type SimulateTransactionResponse = SuiClientTypes.SimulateTransactionResult<{ effects: true; events: true; balanceChanges: true; commandResults: true; }>; /** * Encapsulates all functions that interact with the sui sdk */ export class SuiInteractor { private clients: ClientWithCoreApi[] = []; public currentClient: ClientWithCoreApi; private fullNodes: string[] = []; private network: NetworkType; constructor(params: Partial) { // Default network this.network = 'mainnet'; if ('fullnodeUrls' in params && params.fullnodeUrls) { this.network = params.network ?? 'mainnet'; this.fullNodes = params.fullnodeUrls; this.clients = this.fullNodes.map( (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network)) ); } else if ('suiClients' in params && params.suiClients) { this.clients = params.suiClients; } else { this.fullNodes = [getFullnodeUrl(this.network)]; this.clients = [ new SuiGrpcClient( createGrpcClientOptions(this.fullNodes[0], this.network) ), ]; } this.currentClient = this.clients[0]; } switchToNextClient() { const currentClientIdx = this.clients.indexOf(this.currentClient); this.currentClient = this.clients[(currentClientIdx + 1) % this.clients.length]; } switchFullNodes(fullNodes: string[], network?: NetworkType) { if (fullNodes.length === 0) { throw new Error('fullNodes cannot be empty'); } this.fullNodes = fullNodes; if (network) { this.network = network; } this.clients = fullNodes.map( (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network)) ); this.currentClient = this.clients[0]; } get currentFullNode() { if (this.fullNodes.length === 0) { throw new Error('No full nodes available'); } const clientIdx = this.clients.indexOf(this.currentClient); if (clientIdx === -1) { throw new Error('Current client not found'); } return this.fullNodes[clientIdx]; } async sendTx( transactionBlock: Uint8Array | string, signature: string | string[] ): Promise< SuiClientTypes.TransactionResult<{ balanceChanges: true; effects: true; events: true; objectTypes: true; }> > { const txBytes = typeof transactionBlock === 'string' ? Uint8Array.from(Buffer.from(transactionBlock, 'base64')) : transactionBlock; const signatures = Array.isArray(signature) ? signature : [signature]; for (const clientIdx in this.clients) { try { return await this.clients[clientIdx].core.executeTransaction({ transaction: txBytes, signatures, include: { balanceChanges: true, effects: true, events: true, objectTypes: true, }, }); } catch (err) { console.warn( `Failed to send transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}` ); await delay(2000); } } throw new Error('Failed to send transaction with all fullnodes'); } async dryRunTx( transactionBlock: Uint8Array ): Promise { for (const clientIdx in this.clients) { try { return await this.clients[clientIdx].core.simulateTransaction({ transaction: transactionBlock, include: { effects: true, events: true, balanceChanges: true, commandResults: true, }, }); } catch (err) { console.warn( `Failed to dry run transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}` ); await delay(2000); } } throw new Error('Failed to dry run transaction with all fullnodes'); } async getObjects( ids: string[], options?: { include?: SuiObjectDataOptions; batchSize?: number; switchClientDelay?: number; } ): Promise { const include = options?.include ?? { content: true, json: true }; const batchIds = batch( ids, Math.max( options?.batchSize ?? MAX_OBJECTS_PER_REQUEST, MAX_OBJECTS_PER_REQUEST ) ); const results: SuiObjectData[] = []; let lastError = null; for (const batchChunk of batchIds) { for (const clientIdx in this.clients) { try { const response = await this.clients[clientIdx].core.getObjects({ objectIds: batchChunk, include, }); const parsedObjects = response.objects .map((obj) => { if (obj instanceof Error) { return null; } return obj as SuiObjectData; }) .filter((object): object is SuiObjectData => object !== null); results.push(...parsedObjects); lastError = null; break; // Exit the client loop if successful } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); await delay(options?.switchClientDelay ?? 2000); console.warn( `Failed to get objects with fullnode ${this.fullNodes[clientIdx]}: ${err}` ); } } if (lastError) { throw new Error( `Failed to get objects with all fullnodes: ${lastError}` ); } } return results; } async getObject(id: string, options?: { include?: SuiObjectDataOptions }) { const objects = await this.getObjects([id], options); return objects[0]; } /** * @description Update objects in a batch * @param suiObjects */ async updateObjects(suiObjects: (SuiOwnedObject | SuiSharedObject)[]) { const objectIds = suiObjects.map((obj) => obj.objectId); const objects = await this.getObjects(objectIds); for (const object of objects) { const suiObject = suiObjects.find( (obj) => obj.objectId === object?.objectId ); if (suiObject instanceof SuiSharedObject) { const owner = object.owner; if (owner && typeof owner === 'object' && 'Shared' in owner) { suiObject.initialSharedVersion = ( owner as { Shared: { initialSharedVersion: string } } ).Shared.initialSharedVersion; } else { suiObject.initialSharedVersion = undefined; } } else if (suiObject instanceof SuiOwnedObject) { suiObject.version = object?.version; suiObject.digest = object?.digest; } } } /** * @description Select coins that add up to the given amount. * @param addr the address of the owner * @param amount the amount that is needed for the coin * @param coinType the coin type, default is '0x2::SUI::SUI' */ async selectCoins( addr: string, amount: number, coinType: string = '0x2::SUI::SUI' ) { const selectedCoins: { objectId: string; digest: string; version: string; balance: string; }[] = []; let totalAmount = 0; let hasNext = true, nextCursor: string | null | undefined = null; while (hasNext && totalAmount < amount) { const { objects, hasNextPage, cursor } = await this.currentClient.core.listCoins({ owner: addr, coinType: coinType, cursor: nextCursor, }); // Sort the coins by balance in descending order objects.sort((a, b) => parseInt(b.balance) - parseInt(a.balance)); for (const coinData of objects) { selectedCoins.push({ objectId: coinData.objectId, digest: coinData.digest, version: coinData.version, balance: coinData.balance, }); totalAmount = totalAmount + parseInt(coinData.balance); if (totalAmount >= amount) { break; } } nextCursor = cursor; hasNext = hasNextPage; } if (!selectedCoins.length) { throw new Error('No valid coins found for the transaction.'); } return selectedCoins; } } export { getFullnodeUrl }; ================================================ FILE: src/libs/suiInteractor/util.ts ================================================ export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const batch = (arr: T[], size: number): T[][] => { const batches = []; for (let i = 0; i < arr.length; i += size) { batches.push(arr.slice(i, i + size)); } return batches; }; ================================================ FILE: src/libs/suiModel/index.ts ================================================ export { SuiOwnedObject } from './suiOwnedObject.js'; export { SuiSharedObject } from './suiSharedObject.js'; ================================================ FILE: src/libs/suiModel/suiOwnedObject.ts ================================================ // TODO: I think we can remove this file or update to NormalizedCallArg import type { SuiClientTypes } from '@mysten/sui/client'; import type { CallArg } from '@mysten/sui/transactions'; // Transaction result type with effects type TransactionResultWithEffects = SuiClientTypes.TransactionResult<{ effects: true; }>; export class SuiOwnedObject { public readonly objectId: string; public version?: string; public digest?: string; constructor(param: { objectId: string; version?: string; digest?: string }) { this.objectId = param.objectId; this.version = param.version; this.digest = param.digest; } /** * Check if the object is fully initialized. * So that when it's used as an input, it won't be necessary to fetch from fullnode again. * Which can save time when sending transactions. */ isFullObject(): boolean { return !!this.version && !!this.digest; } asCallArg(): CallArg | string { if (!this.version || !this.digest) { return this.objectId; } return { $kind: 'Object', Object: { $kind: 'ImmOrOwnedObject', ImmOrOwnedObject: { objectId: this.objectId, version: this.version, digest: this.digest, }, }, }; } /** * Update object version & digest based on the transaction response. * @param txResponse */ updateFromTxResponse(txResponse: TransactionResultWithEffects) { const tx = txResponse.Transaction ?? txResponse.FailedTransaction; if (!tx) { throw new Error('Bad transaction response!'); } const effects = tx.effects; if (!effects) { throw new Error('Transaction response has no effects!'); } for (const change of effects.changedObjects) { if (change.objectId === this.objectId && change.outputDigest) { this.digest = change.outputDigest; this.version = change.outputVersion ?? undefined; return; } } throw new Error('Could not find object in transaction response!'); } } ================================================ FILE: src/libs/suiModel/suiSharedObject.ts ================================================ // TODO: I think we can remove this file or update to NormalizedCallArg import type { CallArg } from '@mysten/sui/transactions'; export class SuiSharedObject { public readonly objectId: string; public initialSharedVersion?: string; constructor(param: { objectId: string; initialSharedVersion?: string; mutable?: boolean; }) { this.objectId = param.objectId; this.initialSharedVersion = param.initialSharedVersion; } asCallArg(mutable: boolean = false): CallArg | string { if (!this.initialSharedVersion) { return this.objectId; } return { $kind: 'Object', Object: { $kind: 'SharedObject', SharedObject: { objectId: this.objectId, initialSharedVersion: this.initialSharedVersion, mutable, }, }, }; } } ================================================ FILE: src/libs/suiTxBuilder/index.ts ================================================ import { Transaction, TransactionObjectInput } from '@mysten/sui/transactions'; import { SUI_SYSTEM_STATE_OBJECT_ID } from '@mysten/sui/utils'; import { convertArgs, convertAddressArg, convertObjArg, convertAmounts, partitionArray, } from './util.js'; import type { ClientWithCoreApi } from '@mysten/sui/client'; import type { Keypair } from '@mysten/sui/cryptography'; import type { SuiTxArg, SuiAddressArg, SuiObjectArg, SuiVecTxArg, SuiAmountsArg, } from '../../types/index.js'; import type { bcs } from '@mysten/sui/bcs'; // Object reference type interface SuiObjectRef { objectId: string; version: number | string; digest: string; } export class SuiTxBlock { public txBlock: Transaction; constructor(transaction?: Transaction) { this.txBlock = transaction ? Transaction.from(transaction) : new Transaction(); } /* Directly wrap methods and properties of TransactionBlock */ get gas() { return this.txBlock.gas; } getData() { return this.txBlock.getData(); } address(value: string) { return this.txBlock.pure.address(value); } get pure(): typeof this.txBlock.pure { return this.txBlock.pure; } object(value: string | TransactionObjectInput) { return this.txBlock.object(value); } objectRef(ref: SuiObjectRef) { return this.txBlock.objectRef(ref); } sharedObjectRef(ref: typeof bcs.SharedObjectRef.$inferType) { return this.txBlock.sharedObjectRef(ref); } setSender(sender: string) { return this.txBlock.setSender(sender); } setSenderIfNotSet(sender: string) { return this.txBlock.setSenderIfNotSet(sender); } setExpiration(expiration?: Parameters[0]) { return this.txBlock.setExpiration(expiration); } setGasPrice(price: number | bigint) { return this.txBlock.setGasPrice(price); } setGasBudget(budget: number | bigint) { return this.txBlock.setGasBudget(budget); } setGasOwner(owner: string) { return this.txBlock.setGasOwner(owner); } setGasPayment(payments: SuiObjectRef[]) { return this.txBlock.setGasPayment(payments); } /** * @deprecated Use toJSON instead. * For synchronous serialization, you can use `getData()` * */ serialize() { // TODO: need to update this method to use the new serialize method return this.txBlock.serialize(); } toJSON() { return this.txBlock.toJSON(); } sign(params: { signer: Keypair; client?: ClientWithCoreApi; onlyTransactionKind?: boolean; }) { return this.txBlock.sign(params); } build( params: { client?: ClientWithCoreApi; onlyTransactionKind?: boolean; } = {} ) { return this.txBlock.build(params); } getDigest(params: { client?: ClientWithCoreApi } = {}) { return this.txBlock.getDigest(params); } add(...args: Parameters) { return this.txBlock.add(...args); } publish({ modules, dependencies, }: { modules: number[][] | string[]; dependencies: string[]; }) { return this.txBlock.publish({ modules, dependencies }); } upgrade(...args: Parameters) { return this.txBlock.upgrade(...args); } makeMoveVec(...args: Parameters) { return this.txBlock.makeMoveVec(...args); } /* Override methods of TransactionBlock */ transferObjects(objects: SuiObjectArg[], address: SuiAddressArg) { return this.txBlock.transferObjects( objects.map((object) => convertObjArg(this.txBlock, object)), convertAddressArg(this.txBlock, address) ); } splitCoins(coin: SuiObjectArg, amounts: SuiAmountsArg[]) { const res = this.txBlock.splitCoins( convertObjArg(this.txBlock, coin), convertAmounts(this.txBlock, amounts) ); return amounts.map((_, i) => res[i]); } mergeCoins(destination: SuiObjectArg, sources: SuiObjectArg[]) { const destinationObject = convertObjArg(this.txBlock, destination); const sourceObjects = sources.map((source) => convertObjArg(this.txBlock, source) ); return this.txBlock.mergeCoins(destinationObject, sourceObjects); } /** * @description Move call * @param target `${string}::${string}::${string}`, e.g. `0x3::sui_system::request_add_stake` * @param args the arguments of the move call, such as `['0x1', '0x2']` * @param typeArgs the type arguments of the move call, such as `['0x2::sui::SUI']` */ moveCall( target: string, args: (SuiTxArg | SuiVecTxArg | SuiObjectArg | SuiAmountsArg)[] = [], typeArgs: string[] = [] ) { // a regex for pattern `${string}::${string}::${string}` const regex = /(?[a-zA-Z0-9]+)::(?[a-zA-Z0-9_]+)::(?[a-zA-Z0-9_]+)/; const match = target.match(regex); if (match === null) throw new Error( 'Invalid target format. Expected `${string}::${string}::${string}`' ); const convertedArgs = convertArgs(this.txBlock, args); return this.txBlock.moveCall({ target: target as `${string}::${string}::${string}`, arguments: convertedArgs, typeArguments: typeArgs, }); } /* Enhance methods of TransactionBlock */ transferSuiToMany(recipients: SuiAddressArg[], amounts: SuiAmountsArg[]) { // require recipients.length === amounts.length if (recipients.length !== amounts.length) { throw new Error( 'transferSuiToMany: recipients.length !== amounts.length' ); } const coins = this.txBlock.splitCoins( this.txBlock.gas, convertAmounts(this.txBlock, amounts) ); const recipientObjects = recipients.map((recipient) => convertAddressArg(this.txBlock, recipient) ); // Transfer splitted coins to recipients recipientObjects.forEach((address, index) => { this.txBlock.transferObjects([coins[index]], address); }); return this; } transferSui(address: SuiAddressArg, amount: SuiAmountsArg) { return this.transferSuiToMany([address], [amount]); } takeAmountFromCoins(coins: SuiObjectArg[], amount: SuiAmountsArg) { const { splitedCoins, mergedCoin } = this.splitMultiCoins( coins, convertAmounts(this.txBlock, [amount]) ); return [splitedCoins, mergedCoin]; } splitSUIFromGas(amounts: SuiAmountsArg[]) { return this.txBlock.splitCoins( this.txBlock.gas, convertAmounts(this.txBlock, amounts) ); } splitMultiCoins(coins: SuiObjectArg[], amounts: SuiAmountsArg[]) { if (coins.length === 0) { throw new Error('takeAmountFromCoins: coins array is empty'); } const partitions = partitionArray(coins.slice(1), 511); const mergedCoin = convertObjArg(this.txBlock, coins[0]); for (const partition of partitions) { const coinObjects = partition.map((coin) => convertObjArg(this.txBlock, coin) ); this.txBlock.mergeCoins(mergedCoin, coinObjects); } const splitedCoins = this.txBlock.splitCoins( mergedCoin, convertAmounts(this.txBlock, amounts) ); return { splitedCoins, mergedCoin }; } transferCoinToMany( coins: SuiObjectArg[], sender: SuiAddressArg, recipients: SuiAddressArg[], amounts: SuiAmountsArg[] ) { // require recipients.length === amounts.length if (recipients.length !== amounts.length) { throw new Error( 'transferCoinToMany: recipients.length !== amounts.length' ); } const coinObjects = coins.map((coin) => convertObjArg(this.txBlock, coin)); const { splitedCoins, mergedCoin } = this.splitMultiCoins( coinObjects, convertAmounts(this.txBlock, amounts) ); const recipientObjects = recipients.map((recipient) => convertAddressArg(this.txBlock, recipient) ); // Transfer splitted coins to recipients recipientObjects.forEach((address, index) => { this.txBlock.transferObjects([splitedCoins[index]], address); }); // Return the remaining coin back to sender this.txBlock.transferObjects( [mergedCoin], convertAddressArg(this.txBlock, sender) ); return this; } transferCoin( coins: SuiObjectArg[], sender: SuiAddressArg, recipient: SuiAddressArg, amount: SuiAmountsArg ) { return this.transferCoinToMany(coins, sender, [recipient], [amount]); } stakeSui(amount: SuiAmountsArg, validatorAddr: SuiAddressArg) { const [stakeCoin] = this.txBlock.splitCoins( this.txBlock.gas, convertAmounts(this.txBlock, [amount]) ); return this.txBlock.moveCall({ target: '0x3::sui_system::request_add_stake', arguments: convertArgs(this.txBlock, [ this.txBlock.object(SUI_SYSTEM_STATE_OBJECT_ID), stakeCoin, convertAddressArg(this.txBlock, validatorAddr), ]), }); } } ================================================ FILE: src/libs/suiTxBuilder/util.ts ================================================ import { normalizeSuiObjectId, normalizeSuiAddress, isValidSuiObjectId, isValidSuiAddress, } from '@mysten/sui/utils'; import { Inputs, getPureBcsSchema } from '@mysten/sui/transactions'; import { SerializedBcs, bcs, isSerializedBcs } from '@mysten/bcs'; import type { TransactionArgument, Transaction, TransactionObjectArgument, } from '@mysten/sui/transactions'; import type { SuiClientTypes } from '@mysten/sui/client'; import type { SuiObjectArg, SuiAddressArg, SuiTxArg, SuiVecTxArg, SuiInputTypes, SuiAmountsArg, } from '../../types/index.js'; // Object reference type interface SuiObjectRef { objectId: string; version: number | string; digest: string; } // Simple types that can be converted to OpenSignatureBody const SIMPLE_BCS_TYPES = [ 'u8', 'u16', 'u32', 'u64', 'u128', 'u256', 'bool', 'address', ] as const; type SimpleBcsType = (typeof SIMPLE_BCS_TYPES)[number]; // Convert simple type string to OpenSignatureBody function toOpenSignatureBody(type: string): SuiClientTypes.OpenSignatureBody { if (!SIMPLE_BCS_TYPES.includes(type as SimpleBcsType)) { throw new Error(`Invalid SimpleBcsType: ${type}`); } return { $kind: type } as SuiClientTypes.OpenSignatureBody; } // TODO: unclear why we need this function and types export const getDefaultSuiInputType = ( value: SuiTxArg ): 'u64' | 'bool' | 'object' | undefined => { if (typeof value === 'string' && isValidSuiObjectId(value)) { return 'object'; } if (typeof value === 'number' || typeof value === 'bigint') { return 'u64'; } if (typeof value === 'boolean') { return 'bool'; } return undefined; }; // =========== TYPE GUARD ============ /** * Check whether it is an valid input amount; * * @param arg * @returns boolean. */ function isAmountArg(arg: any): arg is bigint | number | string { return ( typeof arg === 'number' || typeof arg === 'bigint' || (typeof arg === 'string' && !isValidSuiAddress(arg) && !isNaN(Number(arg))) ); } /** * Check whether it is an valid move vec input. * * @param arg The argument to check. * @returns boolean. */ function isMoveVecArg( arg: SuiTxArg | SuiVecTxArg | SuiObjectArg | SuiAmountsArg ): arg is SuiVecTxArg { if ( arg !== null && typeof arg === 'object' && 'vecType' in arg && 'value' in arg ) { return true; } else if (Array.isArray(arg)) { return true; } return false; } /** * Check whether it is an valid object reference. * @param arg The argument to check * @returns boolean */ function isObjectRef(arg: SuiObjectArg): arg is SuiObjectRef { return ( typeof arg === 'object' && 'digest' in arg && 'version' in arg && 'objectId' in arg ); } /** * Check whether it is an valid shared object reference. * @param arg The argument to check * @returns */ function isSharedObjectRef( arg: SuiObjectArg ): arg is Parameters[0] { return ( typeof arg === 'object' && 'objectId' in arg && 'initialSharedVersion' in arg && 'mutable' in arg ); } // =================================== /** * Since we know the elements in the array are the same type * If type is not provided, we will try to infer the type from the first element * By default, * * string is hex and its length equal to 32 =====> object id * number, bigint ====> u64 * boolean =====> bool * * If type is provided, we will use the type to convert the array * @param args * @param type 'address' | 'bool' | 'u8' | 'u16' | 'u32' | 'u64' | 'u128' | 'u256' | 'signer' | 'object' | string */ export function makeVecParam( txBlock: Transaction, args: SuiTxArg[], type?: SuiInputTypes ): TransactionArgument { if (args.length === 0) throw new Error('Transaction builder error: Empty array is not allowed'); // Using first element value as default type // TODO: unclear why we need this function and types const defaultSuiType = getDefaultSuiInputType(args[0]); const VECTOR_REGEX = /^vector<(.+)>$/; const STRUCT_REGEX = /^([^:]+)::([^:]+)::([^<]+)(<(.+)>)?/; type = type || defaultSuiType; if (type === 'object') { const elements = args.map((arg) => typeof arg === 'string' && isValidSuiObjectId(arg) ? txBlock.object(normalizeSuiObjectId(arg)) : convertObjArg(txBlock, arg as SuiObjectArg) ); return txBlock.makeMoveVec({ elements }); } else if ( typeof type === 'string' && !VECTOR_REGEX.test(type) && !STRUCT_REGEX.test(type) ) { // Convert simple type to OpenSignatureBody for BCS schema const signatureBody = toOpenSignatureBody(type as SimpleBcsType); const bcsSchema = getPureBcsSchema(signatureBody); if (!bcsSchema) { throw new Error(`Unknown type: ${type}`); } return txBlock.pure(bcs.vector(bcsSchema).serialize(args)); } else { const elements = args.map((arg) => convertObjArg(txBlock, arg as SuiObjectArg) ); return txBlock.makeMoveVec({ elements, type: type as string }); } } /** * Convert any valid input into array of TransactionArgument. * * @param txb The Transaction Block * @param args The array of argument to convert. * @returns The converted array of TransactionArgument. */ export function convertArgs( txBlock: Transaction, args: (SuiTxArg | SuiVecTxArg | SuiObjectArg | SuiAmountsArg)[] ): TransactionArgument[] { return args.map((arg) => { if (arg instanceof SerializedBcs || isSerializedBcs(arg)) { return txBlock.pure(arg); } if (isMoveVecArg(arg)) { const vecType = 'vecType' in arg; return vecType ? makeVecParam(txBlock, arg.value, arg.vecType) : makeVecParam(txBlock, arg); } if (isAmountArg(arg)) { return convertAmounts(txBlock, [arg as unknown as SuiAmountsArg])[0]; } return convertObjArg(txBlock, arg as SuiObjectArg); }); } /** * Convert any valid address input into a TransactionArgument. * * @param txb The Transaction Block * @param arg The address argument to convert. * @returns The converted TransactionArgument. */ export function convertAddressArg( txBlock: Transaction, arg: SuiAddressArg ): SuiTxArg { if (typeof arg === 'string' && isValidSuiAddress(arg)) { return txBlock.pure.address(normalizeSuiAddress(arg)); } else { return convertArgs(txBlock, [arg])[0]; } } /** * Convert any valid object input into a TransactionArgument. * * @param txb The Transaction Block * @param arg The object argument to convert. * @returns The converted TransactionArgument. */ export function convertObjArg( txb: Transaction, arg: SuiObjectArg ): TransactionObjectArgument { if (typeof arg === 'string') { return txb.object(arg); } if (isObjectRef(arg)) { return txb.objectRef(arg); } if (isSharedObjectRef(arg)) { return txb.sharedObjectRef(arg); } if ('Object' in arg) { if ('ImmOrOwnedObject' in arg.Object) { return txb.object(Inputs.ObjectRef(arg.Object.ImmOrOwnedObject)); } else if ('SharedObject' in arg.Object) { return txb.object(Inputs.SharedObjectRef(arg.Object.SharedObject)); } else { throw new Error('Invalid argument type'); } } if (typeof arg === 'function') { return arg; } if ( 'GasCoin' in arg || 'Input' in arg || 'Result' in arg || 'NestedResult' in arg ) { return arg; } throw new Error('Invalid argument type'); } export function convertAmounts( txBlock: Transaction, amounts: SuiAmountsArg[] ): TransactionArgument[] { return amounts.map((amount) => { if (isAmountArg(amount)) { return txBlock.pure.u64(amount); } else { return convertArgs(txBlock, [amount])[0]; } }); } export const partitionArray = (array: T[], chunkSize: number) => { const result: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { result.push(array.slice(i, i + chunkSize)); } return result; }; ================================================ FILE: src/suiKit.ts ================================================ /** * @description This file is used to aggregate the tools that used to interact with SUI network. */ import { Transaction } from '@mysten/sui/transactions'; import { SuiAccountManager } from './libs/suiAccountManager/index.js'; import { SuiTxBlock } from './libs/suiTxBuilder/index.js'; import { SuiInteractor, getFullnodeUrl, type SuiObjectDataOptions, type SimulateTransactionResponse, } from './libs/suiInteractor/index.js'; import type { SuiSharedObject, SuiOwnedObject } from './libs/suiModel/index.js'; import type { SuiKitParams, DerivePathParams, SuiTxArg, SuiVecTxArg, SuiKitReturnType, SuiObjectArg, SuiAmountsArg, SuiTransactionBlockResponse, } from './types/index.js'; import { normalizeStructTag, SUI_TYPE_ARG } from '@mysten/sui/utils'; /** * @class SuiKit * @description This class is used to aggregate the tools that used to interact with SUI network. */ export class SuiKit { public accountManager: SuiAccountManager; public suiInteractor: SuiInteractor; /** * Support the following ways to init the SuiToolkit: * 1. mnemonics * 2. secretKey (base64 or hex) * If none of them is provided, will generate a random mnemonics with 24 words. * * @param mnemonics, 12 or 24 mnemonics words, separated by space * @param secretKey, base64 or hex string or bech32, when mnemonics is provided, secretKey will be ignored * @param networkType, 'testnet' | 'mainnet' | 'devnet' | 'localnet', default is 'mainnet' * @param fullnodeUrls, the fullnode url, default is the preconfig fullnode url for the given network type */ constructor(params: SuiKitParams) { const { mnemonics, secretKey, networkType } = params; // Init the account manager this.accountManager = new SuiAccountManager({ mnemonics, secretKey }); const network = networkType ?? 'mainnet'; let suiInteractorParams; if ('fullnodeUrls' in params) { suiInteractorParams = { fullnodeUrls: params.fullnodeUrls, network, }; } else if ('suiClients' in params) { suiInteractorParams = { suiClients: params.suiClients }; } else { suiInteractorParams = { fullnodeUrls: [getFullnodeUrl(network)], network, }; } this.suiInteractor = new SuiInteractor(suiInteractorParams); } /** * Create SuiTxBlock with sender set to the current signer * @returns SuiTxBlock with sender set to the current signer */ createTxBlock(): SuiTxBlock { const txb = new SuiTxBlock(); txb.setSender(this.accountManager.currentAddress); return txb; } /** * if derivePathParams is not provided or mnemonics is empty, it will return the keypair. * else: * it will generate signer from the mnemonic with the given derivePathParams. * @param derivePathParams, such as { accountIndex: 2, isExternal: false, addressIndex: 10 }, comply with the BIP44 standard */ getKeypair(derivePathParams?: DerivePathParams) { return this.accountManager.getKeyPair(derivePathParams); } /** * @description Switch the current account with the given derivePathParams * @param derivePathParams, such as { accountIndex: 2, isExternal: false, addressIndex: 10 }, comply with the BIP44 standard */ switchAccount(derivePathParams: DerivePathParams) { this.accountManager.switchAccount(derivePathParams); } /** * @description Get the address of the account for the given derivePathParams * @param derivePathParams, such as { accountIndex: 2, isExternal: false, addressIndex: 10 }, comply with the BIP44 standard */ getAddress(derivePathParams?: DerivePathParams) { return this.accountManager.getAddress(derivePathParams); } get currentAddress() { return this.accountManager.currentAddress; } async getBalance(coinType?: string, derivePathParams?: DerivePathParams) { const owner = this.accountManager.getAddress(derivePathParams); const { balance } = await this.suiInteractor.currentClient.core.getBalance({ owner, coinType, }); return balance; } get client() { return this.suiInteractor.currentClient; } async getObjects( objectIds: string[], options?: { include?: SuiObjectDataOptions; batchSize?: number; switchClientDelay?: number; } ) { return this.suiInteractor.getObjects(objectIds, options); } /** * @description Update objects in a batch * @param suiObjects */ async updateObjects(suiObjects: (SuiSharedObject | SuiOwnedObject)[]) { return this.suiInteractor.updateObjects(suiObjects); } async signTxn( tx: Uint8Array | Transaction | SuiTxBlock, derivePathParams?: DerivePathParams ) { if (tx instanceof SuiTxBlock) { tx.setSender(this.getAddress(derivePathParams)); } const txBlock = tx instanceof SuiTxBlock ? tx.txBlock : tx; const txBytes = txBlock instanceof Uint8Array ? txBlock : await txBlock.build({ client: this.client }); const keyPair = this.getKeypair(derivePathParams); return await keyPair.signTransaction(txBytes); } async signAndSendTxn( tx: Uint8Array | Transaction | SuiTxBlock, derivePathParams?: DerivePathParams ): Promise { const { bytes, signature } = await this.signTxn(tx, derivePathParams); return this.suiInteractor.sendTx(bytes, signature); } async dryRunTxn( tx: Uint8Array | Transaction | SuiTxBlock, derivePathParams?: DerivePathParams ): Promise { if (tx instanceof SuiTxBlock) { tx.setSender(this.getAddress(derivePathParams)); } const txBlock = tx instanceof SuiTxBlock ? tx.txBlock : tx; const txBytes = txBlock instanceof Uint8Array ? txBlock : await txBlock.build({ client: this.client }); return this.suiInteractor.dryRunTx(txBytes); } /** * Transfer the given amount of SUI to the recipient * @param recipient * @param amount * @param derivePathParams */ async transferSui( recipient: string, amount: number, derivePathParams?: DerivePathParams ): Promise; async transferSui( recipient: string, amount: number, sign?: S, derivePathParams?: DerivePathParams ): Promise>; async transferSui( recipient: string, amount: number, sign: S = true as S, derivePathParams?: DerivePathParams ) { const tx = new SuiTxBlock(); tx.transferSui(recipient, amount); return sign ? ((await this.signAndSendTxn( tx, derivePathParams )) as SuiKitReturnType) : (tx as SuiKitReturnType); } /** * Transfer to mutliple recipients * @param recipients the recipients addresses * @param amounts the amounts of SUI to transfer to each recipient, the length of amounts should be the same as the length of recipients * @param derivePathParams */ async transferSuiToMany( recipients: string[], amounts: number[], derivePathParams?: DerivePathParams ): Promise; async transferSuiToMany( recipients: string[], amounts: number[], sign?: S, derivePathParams?: DerivePathParams ): Promise>; async transferSuiToMany( recipients: string[], amounts: number[], sign: S = true as S, derivePathParams?: DerivePathParams ) { const tx = new SuiTxBlock(); tx.transferSuiToMany(recipients, amounts); return sign ? ((await this.signAndSendTxn( tx, derivePathParams )) as SuiKitReturnType) : (tx as SuiKitReturnType); } /** * Transfer the given amounts of coin to multiple recipients * @param recipients the list of recipient address * @param amounts the amounts to transfer for each recipient * @param coinType any custom coin type but not SUI * @param derivePathParams the derive path params for the current signer */ async transferCoinToMany( recipients: string[], amounts: number[], coinType: string, derivePathParams?: DerivePathParams ): Promise; async transferCoinToMany( recipients: string[], amounts: number[], coinType: string, sign?: S, derivePathParams?: DerivePathParams ): Promise>; async transferCoinToMany( recipients: string[], amounts: number[], coinType: string, sign: S = true as S, derivePathParams?: DerivePathParams ) { const tx = new SuiTxBlock(); const owner = this.accountManager.getAddress(derivePathParams); const totalAmount = amounts.reduce((a, b) => a + b, 0); if (normalizeStructTag(coinType) === normalizeStructTag(SUI_TYPE_ARG)) { tx.transferSuiToMany(recipients, amounts); } else { const coins = await this.suiInteractor.selectCoins( owner, totalAmount, coinType ); tx.transferCoinToMany( coins.map((coin) => ('objectId' in coin ? tx.objectRef(coin) : coin)), owner, recipients, amounts ); } return sign ? ((await this.signAndSendTxn( tx, derivePathParams )) as SuiKitReturnType) : (tx as SuiKitReturnType); } async transferCoin( recipient: string, amount: number, coinType: string, derivePathParams?: DerivePathParams ): Promise; async transferCoin( recipient: string, amount: number, coinType: string, sign?: S, derivePathParams?: DerivePathParams ): Promise>; async transferCoin( recipient: string, amount: number, coinType: string, sign: S = true as S, derivePathParams?: DerivePathParams ) { return this.transferCoinToMany( [recipient], [amount], coinType, sign, derivePathParams ); } async transferObjects( objects: SuiObjectArg[], recipient: string, derivePathParams?: DerivePathParams ): Promise; async transferObjects( objects: SuiObjectArg[], recipient: string, sign?: S, derivePathParams?: DerivePathParams ): Promise>; async transferObjects( objects: SuiObjectArg[], recipient: string, sign: S = true as S, derivePathParams?: DerivePathParams ) { const tx = new SuiTxBlock(); tx.transferObjects(objects, recipient); return sign ? await this.signAndSendTxn(tx, derivePathParams) : tx; } async moveCall(callParams: { target: string; arguments?: (SuiTxArg | SuiVecTxArg | SuiObjectArg | SuiAmountsArg)[]; typeArguments?: string[]; derivePathParams?: DerivePathParams; }) { const { target, arguments: args = [], typeArguments = [], derivePathParams, } = callParams; const tx = new SuiTxBlock(); tx.moveCall(target, args, typeArguments); return this.signAndSendTxn(tx, derivePathParams); } /** * Select coins with the given amount and coin type, the total amount is greater than or equal to the given amount * @param amount * @param coinType * @param owner */ async selectCoinsWithAmount( amount: number, coinType: string, owner?: string ) { owner = owner || this.accountManager.currentAddress; const coins = await this.suiInteractor.selectCoins(owner, amount, coinType); return coins; } /** * stake the given amount of SUI to the validator * @param amount the amount of SUI to stake * @param validatorAddr the validator address * @param sign whether to sign and send the transaction, default is true * @param derivePathParams the derive path params for the current signer */ async stakeSui( amount: number, validatorAddr: string, derivePathParams?: DerivePathParams ): Promise; async stakeSui( amount: number, validatorAddr: string, sign?: S, derivePathParams?: DerivePathParams ): Promise>; async stakeSui( amount: number, validatorAddr: string, sign: S = true as S, derivePathParams?: DerivePathParams ) { const tx = new SuiTxBlock(); tx.stakeSui(amount, validatorAddr); return sign ? ((await this.signAndSendTxn( tx, derivePathParams )) as SuiKitReturnType) : (tx as SuiKitReturnType); } /** * Execute the transaction with on-chain data but without really submitting. Useful for querying the effects of a transaction. * Since the transaction is not submitted, its gas cost is not charged. * @param tx the transaction to execute * @param derivePathParams the derive path params * @returns the effects and events of the transaction, such as object changes, gas cost, event emitted. */ async inspectTxn( tx: Uint8Array | Transaction | SuiTxBlock, derivePathParams?: DerivePathParams ): Promise { if (tx instanceof SuiTxBlock) { tx.setSender(this.getAddress(derivePathParams)); } const txBlock = tx instanceof SuiTxBlock ? tx.txBlock : tx; const txBytes = txBlock instanceof Uint8Array ? txBlock : await txBlock.build({ client: this.client }); return this.suiInteractor.currentClient.core.simulateTransaction({ transaction: txBytes, include: { effects: true, events: true, balanceChanges: true, commandResults: true, }, }); } } ================================================ FILE: src/types/index.ts ================================================ import type { Transaction, TransactionObjectArgument, Argument, Inputs, TransactionArgument, } from '@mysten/sui/transactions'; import type { SerializedBcs } from '@mysten/bcs'; import type { ClientWithCoreApi, SuiClientTypes } from '@mysten/sui/client'; import { SuiTxBlock } from 'src/libs/suiTxBuilder/index.js'; export type SuiKitParams = (AccountManagerParams & { faucetUrl?: string; networkType?: NetworkType; }) & Partial; export type SuiInteractorParams = | { fullnodeUrls: string[]; network?: NetworkType; } | { suiClients: ClientWithCoreApi[]; }; export type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet'; export type AccountManagerParams = { mnemonics?: string; secretKey?: string; }; export type DerivePathParams = { accountIndex?: number; isExternal?: boolean; addressIndex?: number; }; type TransactionBlockType = InstanceType; export type PureCallArg = { Pure: number[]; }; type SharedObjectRef = { /** Hex code as string representing the object id */ objectId: string; /** The version the object was shared at */ initialSharedVersion: number | string; /** Whether reference is mutable */ mutable: boolean; }; type SuiObjectRef = { /** Base64 string representing the object digest */ objectId: string; /** Object version */ version: number | string; /** Hex code as string representing the object id */ digest: string; }; /** * An object argument. */ type ObjectArg = | { ImmOrOwnedObject: SuiObjectRef } | { SharedObject: SharedObjectRef } | { Receiving: SuiObjectRef }; export type ObjectCallArg = { Object: ObjectArg; }; export type TransactionType = Parameters; export type TransactionPureArgument = Extract< Argument, { $kind: 'Input'; type?: 'pure'; } >; export type SuiTxArg = TransactionArgument | SerializedBcs; export type SuiAddressArg = Argument | SerializedBcs | string; export type SuiAmountsArg = SuiTxArg | number | bigint; export type SuiObjectArg = | TransactionObjectArgument | string | Parameters[0] | Parameters[0] | ObjectCallArg; export type SuiVecTxArg = | { value: SuiTxArg[]; vecType: SuiInputTypes } | SuiTxArg[]; /** * These are the basics types that can be used in the SUI */ export type SuiBasicTypes = | 'address' | 'bool' | 'u8' | 'u16' | 'u32' | 'u64' | 'u128' | 'u256'; export type SuiInputTypes = 'object' | SuiBasicTypes; // Transaction result type from SDK v2 export type SuiTransactionResult< Include extends SuiClientTypes.TransactionInclude = {}, > = SuiClientTypes.TransactionResult; // Full transaction response with all includes enabled export type SuiTransactionBlockResponse = SuiClientTypes.TransactionResult<{ balanceChanges: true; effects: true; events: true; objectTypes: true; }>; export type SuiKitReturnType = T extends true ? SuiTransactionBlockResponse : SuiTxBlock; ================================================ FILE: test/integration/index.spec.ts ================================================ import { config as dotenvConfig } from 'dotenv'; import { describe, it, expect } from 'vitest'; import { SUI_TYPE_ARG, SuiKit, SuiTxBlock, getFullnodeUrl, normalizeStructTag, } from 'src/index.js'; import { getDerivePathForSUI } from 'src/libs/suiAccountManager/keypair.js'; import type { SuiTransactionBlockResponse, SimulateTransactionResponse, } from 'src/index.js'; const ENABLE_LOG = false; // Helper to check if transaction was successful in v2 SDK response function isTransactionSuccess( result: SuiTransactionBlockResponse | SimulateTransactionResponse ): boolean { const tx = result.Transaction ?? result.FailedTransaction; return tx?.status?.success === true; } dotenvConfig(); describe('Test Scallop Kit with secret key', () => { const suiKit = new SuiKit({ secretKey: process.env.SECRET_KEY, }); it('Test Manage Account', async () => { const coinType = '0x2::sui::SUI'; const currentAddress = suiKit.currentAddress; const derivePathParams = { accountIndex: 0, isExternal: false, addressIndex: 0, }; const deriveAddress = suiKit.getAddress(derivePathParams); const currentAddressBalance = (await suiKit.getBalance()).balance; const deriveAddressBalance = ( await suiKit.getBalance(coinType, derivePathParams) ).balance; const currentPrivateKey = suiKit.getKeypair().getSecretKey(); if (ENABLE_LOG) { console.log( `Current Account: ${currentAddress} has ${currentAddressBalance} SUI` ); console.log( `Account ${getDerivePathForSUI( derivePathParams )}: ${deriveAddress} has ${deriveAddressBalance} SUI` ); console.log(`Current Account PrivateKey: ${currentPrivateKey}`); } expect(!!currentAddress).toBe(true); expect(!!deriveAddress).toBe(true); expect(!!currentPrivateKey).toBe(true); }); it('Test Interactor with Sui: sign and send txn', async () => { const tx = new SuiTxBlock(); tx.setSender(suiKit.currentAddress); const signAndSendTxnRes = await suiKit.signAndSendTxn(tx); if (ENABLE_LOG) { console.log(signAndSendTxnRes); } expect(isTransactionSuccess(signAndSendTxnRes)).toBe(true); }); it('Test Interactor with Sui: get objects', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: get objects with batching', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, batchSize: 2, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: select coins', async () => { const coinType = '0x2::sui::SUI'; const coins = await suiKit.selectCoinsWithAmount(10 ** 8, coinType); if (ENABLE_LOG) { console.log(`Select coins: ${coins}`); } expect(coins.length > 0).toBe(true); }); it('Test Interactor with Sui: transfer coin', async () => { const coinType = '0x2::sui::SUI'; const receiver = suiKit.currentAddress; console.log(`Receiver: ${receiver}`); const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoin(receiver, amount, coinType, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test interactor with sui: transfer coin to many', async () => { const coinType = '0x2::sui::SUI'; const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoinToMany( receiver, [amount, amount], coinType, false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui', async () => { const receiver = suiKit.currentAddress; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSui(receiver, amount, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui to many', async () => { const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSuiToMany( receiver, [amount, amount], false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with sui: stake sui', async () => { const validatorAddress = '0x8ecaf4b95b3c82c712d3ddb22e7da88d2286c4653f3753a86b6f7a216a3ca518'; const amount = 10 ** 9; const tx = await suiKit.stakeSui( amount, validatorAddress, false, undefined ); const stakeSuiRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Stake sui response: ${stakeSuiRes}`); } expect(isTransactionSuccess(stakeSuiRes)).toBe(true); }); it('Test Interactor with sui: transfer object', async () => { const objectsResult = await suiKit.client.core.listOwnedObjects({ owner: suiKit.currentAddress, limit: 2, }); const object = objectsResult.objects.find( (t) => t.type !== normalizeStructTag( `0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<${SUI_TYPE_ARG}>` ) ); if (!object) throw new Error( `No object found for wallet address: ${suiKit.currentAddress}` ); const receiver = suiKit.currentAddress; const tx = await suiKit.transferObjects([object.objectId], receiver, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); }); describe('Test Scallop Kit with mnemonics', () => { const suiKit = new SuiKit({ mnemonics: process.env.MNEMONICS, }); it('Test Manage Account', async () => { const coinType = '0x2::sui::SUI'; const currentAddress = suiKit.currentAddress; const derivePathParams = { accountIndex: 0, isExternal: false, addressIndex: 0, }; const deriveAddress = suiKit.getAddress(derivePathParams); const currentAddressBalance = (await suiKit.getBalance()).balance; const deriveAddressBalance = ( await suiKit.getBalance(coinType, derivePathParams) ).balance; const currentPrivateKey = suiKit.getKeypair().getSecretKey(); if (ENABLE_LOG) { console.log( `Current Account: ${currentAddress} has ${currentAddressBalance} SUI` ); console.log( `Account ${getDerivePathForSUI( derivePathParams )}: ${deriveAddress} has ${deriveAddressBalance} SUI` ); console.log(`Current Account PrivateKey: ${currentPrivateKey}`); } expect(!!currentAddress).toBe(true); expect(!!deriveAddress).toBe(true); expect(!!currentPrivateKey).toBe(true); }); it('Test Interactor with Sui: sign and send txn', async () => { const tx = new SuiTxBlock(); tx.setSender(suiKit.currentAddress); const signAndSendTxnRes = await suiKit.signAndSendTxn(tx); if (ENABLE_LOG) { console.log(signAndSendTxnRes); } expect(isTransactionSuccess(signAndSendTxnRes)).toBe(true); }); it('Test Interactor with Sui: get objects', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: get objects with batching', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, batchSize: 2, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: select coins', async () => { const coinType = '0x2::sui::SUI'; const coins = await suiKit.selectCoinsWithAmount(10 ** 8, coinType); if (ENABLE_LOG) { console.log(`Select coins: ${coins}`); } expect(coins.length > 0).toBe(true); }); it('Test Interactor with Sui: transfer coin', async () => { const coinType = '0x2::sui::SUI'; const receiver = suiKit.currentAddress; console.log(`Receiver: ${receiver}`); const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoin(receiver, amount, coinType, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test interactor with sui: transfer coin to many', async () => { const coinType = '0x2::sui::SUI'; const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoinToMany( receiver, [amount, amount], coinType, false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui', async () => { const receiver = suiKit.currentAddress; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSui(receiver, amount, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui to many', async () => { const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSuiToMany( receiver, [amount, amount], false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with sui: stake sui', async () => { const validatorAddress = '0x8ecaf4b95b3c82c712d3ddb22e7da88d2286c4653f3753a86b6f7a216a3ca518'; const amount = 10 ** 9; const tx = await suiKit.stakeSui( amount, validatorAddress, false, undefined ); const stakeSuiRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Stake sui response: ${stakeSuiRes}`); } expect(isTransactionSuccess(stakeSuiRes)).toBe(true); }); it('Test Interactor with sui: transfer object', async () => { const objectsResult = await suiKit.client.core.listOwnedObjects({ owner: suiKit.currentAddress, limit: 2, }); const object = objectsResult.objects.find( (t) => t.type !== normalizeStructTag( `0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<${SUI_TYPE_ARG}>` ) ); if (!object) throw new Error( `No object found for wallet address: ${suiKit.currentAddress}` ); const receiver = suiKit.currentAddress; const tx = await suiKit.transferObjects([object.objectId], receiver, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); }); describe('Test Scallop Kit with sui clients', () => { const fullnodeUrls = [getFullnodeUrl('mainnet')]; const suiKit = new SuiKit({ secretKey: process.env.SECRET_KEY, fullnodeUrls, }); it('Test Manage Account', async () => { const coinType = '0x2::sui::SUI'; const currentAddress = suiKit.currentAddress; const derivePathParams = { accountIndex: 0, isExternal: false, addressIndex: 0, }; const deriveAddress = suiKit.getAddress(derivePathParams); const currentAddressBalance = (await suiKit.getBalance()).balance; const deriveAddressBalance = ( await suiKit.getBalance(coinType, derivePathParams) ).balance; const currentPrivateKey = suiKit.getKeypair().getSecretKey(); if (ENABLE_LOG) { console.log( `Current Account: ${currentAddress} has ${currentAddressBalance} SUI` ); console.log( `Account ${getDerivePathForSUI( derivePathParams )}: ${deriveAddress} has ${deriveAddressBalance} SUI` ); console.log(`Current Account PrivateKey: ${currentPrivateKey}`); } expect(!!currentAddress).toBe(true); expect(!!deriveAddress).toBe(true); expect(!!currentPrivateKey).toBe(true); }); it('Test Interactor with Sui: sign and send txn', async () => { const tx = new SuiTxBlock(); tx.setSender(suiKit.currentAddress); const signAndSendTxnRes = await suiKit.signAndSendTxn(tx); if (ENABLE_LOG) { console.log(signAndSendTxnRes); } expect(isTransactionSuccess(signAndSendTxnRes)).toBe(true); }); it('Test Interactor with Sui: get objects', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: get objects with batching', async () => { const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const getObjectsRes = await suiKit.getObjects(objIds, { include: { content: false }, batchSize: 2, }); if (ENABLE_LOG) { console.info(`Get Objects Response:`); console.dir(getObjectsRes); } expect(getObjectsRes.length).toBe(objIds.length); }); it('Test Interactor with Sui: select coins', async () => { const coinType = '0x2::sui::SUI'; const coins = await suiKit.selectCoinsWithAmount(10 ** 8, coinType); if (ENABLE_LOG) { console.log(`Select coins: ${coins}`); } expect(coins.length > 0).toBe(true); }); it('Test Interactor with Sui: transfer coin', async () => { const coinType = '0x2::sui::SUI'; const receiver = suiKit.currentAddress; console.log(`Receiver: ${receiver}`); const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoin(receiver, amount, coinType, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test interactor with sui: transfer coin to many', async () => { const coinType = '0x2::sui::SUI'; const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferCoinToMany( receiver, [amount, amount], coinType, false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui', async () => { const receiver = suiKit.currentAddress; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSui(receiver, amount, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with Sui: transfer sui to many', async () => { const receiver = [ suiKit.accountManager.getAddress({ accountIndex: 1, }), suiKit.accountManager.getAddress({ accountIndex: 2, }), ]; const amount = 10 ** 7; // 0.01 SUI const tx = await suiKit.transferSuiToMany( receiver, [amount, amount], false ); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test Interactor with sui: stake sui', async () => { const validatorAddress = '0x8ecaf4b95b3c82c712d3ddb22e7da88d2286c4653f3753a86b6f7a216a3ca518'; const amount = 10 ** 9; const tx = await suiKit.stakeSui( amount, validatorAddress, false, undefined ); const stakeSuiRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Stake sui response: ${stakeSuiRes}`); } expect(isTransactionSuccess(stakeSuiRes)).toBe(true); }); it('Test Interactor with sui: transfer object', async () => { const objectsResult = await suiKit.client.core.listOwnedObjects({ owner: suiKit.currentAddress, limit: 2, }); const object = objectsResult.objects.find( (t) => t.type !== normalizeStructTag( `0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<${SUI_TYPE_ARG}>` ) ); if (!object) throw new Error( `No object found for wallet address: ${suiKit.currentAddress}` ); const receiver = suiKit.currentAddress; const tx = await suiKit.transferObjects([object.objectId], receiver, false); const transferCoinsRes = await suiKit.inspectTxn(tx); // inspect txn should be enough to check if the txn is valid if (ENABLE_LOG) { console.log(`Transfer coins response: ${transferCoinsRes}`); } expect(isTransactionSuccess(transferCoinsRes)).toBe(true); }); it('Test switching fullnodes', async () => { const fullNode = getFullnodeUrl('mainnet'); suiKit.suiInteractor.switchFullNodes([fullNode]); expect(suiKit.suiInteractor.currentFullNode).toEqual(fullNode); }); }); ================================================ FILE: test/integration/multiSig.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { Transaction } from '@mysten/sui/transactions'; import { SuiGrpcClient, type SuiGrpcClientOptions } from '@mysten/sui/grpc'; import { SuiAccountManager } from 'src/libs/suiAccountManager/index.js'; import { MultiSigClient } from 'src/libs/multiSig/index.js'; import { getFullnodeUrl } from 'src/index.js'; const ENABLE_LOG = false; describe('Test MultiSigClient', async () => { const mnemonics = 'elite balcony laundry unique quit flee farm dry buddy outside airport service'; const accountManager = new SuiAccountManager({ mnemonics }); const suiClient = new SuiGrpcClient({ baseUrl: getFullnodeUrl('mainnet'), network: 'mainnet', } as SuiGrpcClientOptions); const rawPubkeys: string[] = []; for (let i = 0; i < 5; i++) { const keypair = accountManager.getKeyPair({ accountIndex: i }); const pubkey = keypair.getPublicKey().toSuiPublicKey(); rawPubkeys.push(pubkey); } const weights = [2, 1, 1, 1, 1]; const threshold = 3; const expectedMultiSigAddress = '0x9beec666af1077857edc0172d0f5624f6f4f15d02159769b3f4935a41985ebf4'; const multiSigClient = MultiSigClient.fromRawEd25519PublicKeys( rawPubkeys, weights, threshold ); it('Test multiSig address', async () => { const multiSigAddress = multiSigClient.multiSigAddress(); if (ENABLE_LOG) { console.log(`Calculated multiSig address: ${multiSigAddress}`); console.log(`Expected multiSig address: ${expectedMultiSigAddress}`); } expect(multiSigAddress).toEqual(expectedMultiSigAddress); }); it('Test multiSig combine with weight 2 + 1 should success', async () => { const tx = new Transaction(); const [suiCoin] = tx.splitCoins(tx.gas, [1]); tx.transferObjects([suiCoin], expectedMultiSigAddress); tx.setSender(expectedMultiSigAddress); const txBytes = await tx.build({ client: suiClient }); const sig1 = await accountManager .getKeyPair({ accountIndex: 0 }) .signTransaction(txBytes); const sig2 = await accountManager .getKeyPair({ accountIndex: 1 }) .signTransaction(txBytes); const sigs = [sig1.signature, sig2.signature]; const signature = multiSigClient.combinePartialSigs(sigs); const result = await suiClient.core.executeTransaction({ transaction: txBytes, signatures: [signature], }); if (ENABLE_LOG) { console.log(result); } const txResult = result.Transaction ?? result.FailedTransaction; expect(txResult?.status?.success === true).toBe(true); }); it.skip('Test multiSig combine with weight 1 + 1 + 1 should success', async () => { const tx = new Transaction(); const [suiCoin] = tx.splitCoins(tx.gas, [1]); tx.transferObjects([suiCoin], expectedMultiSigAddress); tx.setSender(expectedMultiSigAddress); const txBytes = await tx.build({ client: suiClient }); const sig1 = await accountManager .getKeyPair({ accountIndex: 1 }) .signTransaction(txBytes); const sig2 = await accountManager .getKeyPair({ accountIndex: 2 }) .signTransaction(txBytes); const sig3 = await accountManager .getKeyPair({ accountIndex: 3 }) .signTransaction(txBytes); const sigs = [sig1.signature, sig2.signature, sig3.signature]; const signature = multiSigClient.combinePartialSigs(sigs); const result = await suiClient.core.executeTransaction({ transaction: txBytes, signatures: [signature], }); if (ENABLE_LOG) { console.log(result); } const txResult = result.Transaction ?? result.FailedTransaction; expect(txResult?.status?.success === true).toBe(true); }); }); ================================================ FILE: test/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["./", "../src"], "compilerOptions": { "rootDir": "..", "noEmit": true, "emitDeclarationOnly": false } } ================================================ FILE: test/unit/libs/multiSig/publickey.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { ed25519PublicKeyFromBase64 } from 'src/libs/multiSig/publickey.js'; import { toBase64 } from '@mysten/sui/utils'; describe('ed25519PublicKeyFromBase64', () => { it('should return Ed25519PublicKey for 32 bytes', () => { const bytes = new Uint8Array(32); const b64 = toBase64(bytes); expect(() => ed25519PublicKeyFromBase64(b64)).not.toThrow(); }); it('should return Ed25519PublicKey for 33 bytes', () => { const bytes = new Uint8Array(33); const b64 = toBase64(bytes); expect(() => ed25519PublicKeyFromBase64(b64)).not.toThrow(); }); it('should throw for invalid length', () => { const bytes = new Uint8Array(10); // 不是 32 也不是 33 const b64 = toBase64(bytes); expect(() => ed25519PublicKeyFromBase64(b64)).toThrow( 'invalid pubkey length' ); }); }); ================================================ FILE: test/unit/libs/suiAccountManager/crypto.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { generateMnemonic } from 'src/libs/suiAccountManager/crypto.js'; describe('generateMnemonic', () => { it('should generate a 24-word mnemonic by default', () => { const mnemonic = generateMnemonic(); expect(typeof mnemonic).toBe('string'); expect(mnemonic.split(' ').length).toBe(24); }); it('should generate a 12-word mnemonic when specified', () => { const mnemonic = generateMnemonic(12); expect(typeof mnemonic).toBe('string'); expect(mnemonic.split(' ').length).toBe(12); }); }); ================================================ FILE: test/unit/libs/suiAccountManager/util.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { isHex, isBase64, hexOrBase64ToUint8Array, normalizePrivateKey, fromHex, fromBase64, } from 'src/libs/suiAccountManager/util.js'; describe('isHex', () => { it('should return true for 0x hex', () => { expect(isHex('0x123abc')).toBe(true); }); it('should return true for pure hex', () => { expect(isHex('123abc')).toBe(true); }); it('should return false for non-hex', () => { expect(isHex('xyz')).toBe(false); }); }); describe('isBase64', () => { it('should return true for valid base64', () => { expect(isBase64('AQI=')).toBe(true); }); it('should return false for invalid base64', () => { expect(isBase64('!@#$')).toBe(false); }); }); describe('hexOrBase64ToUint8Array', () => { it('should parse hex', () => { expect(hexOrBase64ToUint8Array('0x0102')).toEqual(new Uint8Array([1, 2])); }); it('should parse base64', () => { expect(hexOrBase64ToUint8Array('AQI=')).toEqual(new Uint8Array([1, 2])); }); it('should throw on invalid string', () => { expect(() => hexOrBase64ToUint8Array('!@#$')).toThrow(); }); }); describe('normalizePrivateKey', () => { it('should handle legacy 64 bytes', () => { const arr = new Uint8Array(64).fill(1); expect(normalizePrivateKey(arr)).toEqual(new Uint8Array(32).fill(1)); }); it('should handle 33 bytes with 0 prefix', () => { const arr = new Uint8Array(33); arr[0] = 0; arr.fill(2, 1); expect(normalizePrivateKey(arr)).toEqual(new Uint8Array(32).fill(2)); }); it('should handle 32 bytes', () => { const arr = new Uint8Array(32).fill(3); expect(normalizePrivateKey(arr)).toEqual(arr); }); it('should throw on invalid length', () => { expect(() => normalizePrivateKey(new Uint8Array(10))).toThrow(); }); }); describe('fromHex (re-export)', () => { it('should parse valid hex', () => { expect(fromHex('0x0102')).toEqual(new Uint8Array([1, 2])); }); it('should throw on invalid hex', () => { expect(() => fromHex('0xZZ')).toThrow(); }); }); describe('fromBase64 (re-export)', () => { it('should parse valid base64', () => { expect(fromBase64('AQI=')).toEqual(new Uint8Array([1, 2])); }); it('should throw on invalid base64', () => { expect(() => fromBase64('!@#$')).toThrow(); }); }); ================================================ FILE: test/unit/libs/suiInteractor/suiInteractor.spec.ts ================================================ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SuiInteractor } from 'src/libs/suiInteractor/suiInteractor.js'; import { SuiOwnedObject, SuiSharedObject } from 'src/libs/suiModel/index.js'; import { batch, delay } from 'src/libs/suiInteractor/util.js'; vi.mock('@mysten/sui/grpc', () => { return { SuiGrpcClient: vi.fn().mockImplementation(({ baseUrl, network }) => { const client: any = { baseUrl, network, core: { executeTransaction: vi.fn(), simulateTransaction: vi.fn(), getObjects: vi.fn(), listCoins: vi.fn(), getBalance: vi.fn(), }, }; return client; }), }; }); describe('SuiInteractor', () => { let interactor: SuiInteractor; let client0: any, client1: any; beforeEach(() => { interactor = new SuiInteractor({ fullnodeUrls: ['url1', 'url2'], network: 'testnet', }); client0 = interactor['clients'][0]; client1 = interactor['clients'][1]; }); it('should construct with suiClients param', () => { const fakeClient = { core: { foo: 'bar' } }; const i = new SuiInteractor({ suiClients: [fakeClient as any] }); expect(i['clients'][0]).toBe(fakeClient); expect(i.currentClient).toBe(fakeClient); }); it('should switch to next client', () => { const first = interactor.currentClient; interactor.switchToNextClient(); expect(interactor.currentClient).not.toBe(first); interactor.switchToNextClient(); expect(interactor.currentClient).toBe(first); }); it('should switch full nodes', () => { interactor.switchFullNodes(['a', 'b']); expect(interactor['fullNodes']).toEqual(['a', 'b']); expect(interactor['clients'].length).toBe(2); expect(interactor.currentClient).toBe(interactor['clients'][0]); }); it('should throw if switchFullNodes is called with empty array', () => { expect(() => interactor.switchFullNodes([])).toThrow( 'fullNodes cannot be empty' ); }); it('should throw if currentFullNode is called with no fullNodes', () => { interactor['fullNodes'] = []; expect(() => interactor.currentFullNode).toThrow('No full nodes available'); }); it('should throw if current client not found', () => { interactor['clients'] = []; expect(() => interactor.currentFullNode).toThrow( 'Current client not found' ); }); it('should try all clients and throw if all fail in sendTx', async () => { client0.core.executeTransaction.mockRejectedValue(new Error('fail')); client1.core.executeTransaction.mockRejectedValue(new Error('fail')); await expect( interactor.sendTx(new Uint8Array([1, 2, 3]), 'sig') ).rejects.toThrow('Failed to send transaction with all fullnodes'); }); it('should return result if a client succeeds in sendTx', async () => { const result = { $kind: 'Transaction', Transaction: { digest: 'ok' } }; client0.core.executeTransaction.mockRejectedValue(new Error('fail')); client1.core.executeTransaction.mockResolvedValue(result); await expect( interactor.sendTx(new Uint8Array([1, 2, 3]), 'sig') ).resolves.toBe(result); }); it('should try all clients and throw if all fail in dryRunTx', async () => { client0.core.simulateTransaction.mockRejectedValue(new Error('fail')); client1.core.simulateTransaction.mockRejectedValue(new Error('fail')); await expect(interactor.dryRunTx(new Uint8Array())).rejects.toThrow( 'Failed to dry run transaction with all fullnodes' ); }); it('should return result if a client succeeds in dryRunTx', async () => { const result = { $kind: 'Transaction', Transaction: { digest: 'ok' } }; client0.core.simulateTransaction.mockRejectedValue(new Error('fail')); client1.core.simulateTransaction.mockResolvedValue(result); await expect(interactor.dryRunTx(new Uint8Array())).resolves.toBe(result); }); it('should get objects from core.getObjects', async () => { client0.core.getObjects.mockResolvedValue({ objects: [{ objectId: 'a', version: '1', digest: 'd1' }], }); const res = await interactor.getObjects(['a']); expect(res).toEqual([{ objectId: 'a', version: '1', digest: 'd1' }]); }); it('should filter out Error objects in getObjects', async () => { client0.core.getObjects.mockResolvedValue({ objects: [ new Error('not found'), { objectId: 'b', version: '2', digest: 'd2' }, ], }); const res = await interactor.getObjects(['a', 'b']); expect(res).toEqual([{ objectId: 'b', version: '2', digest: 'd2' }]); }); it('should throw if all clients fail in getObjects', async () => { client0.core.getObjects.mockRejectedValue(new Error('fail')); client1.core.getObjects.mockRejectedValue(new Error('fail')); await expect(interactor.getObjects(['id1'])).rejects.toThrow( 'Failed to get objects with all fullnodes' ); }); it('should call getObjects and return first in getObject', async () => { const obj = { objectId: 'x', version: '1', digest: 'd1' }; interactor.getObjects = vi.fn().mockResolvedValue([obj]); const res = await interactor.getObject('x'); expect(res).toBe(obj); expect(interactor.getObjects).toHaveBeenCalledWith(['x'], undefined); }); it('should update SuiSharedObject initialSharedVersion', async () => { const sharedObj = new SuiSharedObject({ objectId: 'id1' }); interactor.getObjects = vi.fn().mockResolvedValue([ { objectId: 'id1', owner: { Shared: { initialSharedVersion: '123' } }, }, ]); await interactor.updateObjects([sharedObj]); expect(sharedObj.initialSharedVersion).toBe('123'); }); it('should set SuiSharedObject initialSharedVersion to undefined if not Shared', async () => { const sharedObj = new SuiSharedObject({ objectId: 'id1' }); interactor.getObjects = vi .fn() .mockResolvedValue([{ objectId: 'id1', owner: { NotShared: {} } }]); await interactor.updateObjects([sharedObj]); expect(sharedObj.initialSharedVersion).toBeUndefined(); }); it('should update SuiOwnedObject version and digest', async () => { const ownedObj = new SuiOwnedObject({ objectId: 'id2' }); interactor.getObjects = vi .fn() .mockResolvedValue([{ objectId: 'id2', version: 'v1', digest: 'd1' }]); await interactor.updateObjects([ownedObj]); expect(ownedObj.version).toBe('v1'); expect(ownedObj.digest).toBe('d1'); }); it('should select coins and sum up to amount', async () => { client0.core.listCoins = vi.fn().mockResolvedValueOnce({ objects: [ { objectId: 'a', digest: 'd', version: '1', balance: '60' }, { objectId: 'b', digest: 'e', version: '2', balance: '50' }, ], hasNextPage: false, cursor: null, }); interactor.currentClient = client0; const coins = await interactor.selectCoins('addr', 100); expect(coins.length).toBe(2); expect(coins[0].objectId).toBe('a'); expect(coins[1].objectId).toBe('b'); }); it('should throw if no coins found in selectCoins', async () => { client0.core.listCoins = vi .fn() .mockResolvedValue({ objects: [], hasNextPage: false, cursor: null }); interactor.currentClient = client0; await expect(interactor.selectCoins('addr', 100)).rejects.toThrow( 'No valid coins found for the transaction.' ); }); }); describe('SuiInteractor Utils', () => { it('delay should resolve after given time', async () => { // Enable fake timers vi.useFakeTimers(); const start = Date.now(); const delayPromise = delay(100); // Start the delay // Fast-forward time vi.advanceTimersToNextTimer(); await delayPromise; // Wait for the delay to complete const duration = Date.now() - start; expect(duration).toBeGreaterThanOrEqual(100); // Restore real timers vi.useRealTimers(); }); it('batch should split array into chunks of given size', () => { const arr = [1, 2, 3, 4, 5]; const result = batch(arr, 2); expect(result).toEqual([[1, 2], [3, 4], [5]]); }); }); ================================================ FILE: test/unit/libs/suiModel/suiOwnedObject.spec.ts ================================================ import { SuiOwnedObject } from 'src/libs/suiModel/suiOwnedObject.js'; import { describe, it, expect } from 'vitest'; describe('SuiOwnedObject', () => { it('should initialize with objectId', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); expect(obj.objectId).toBe('0x123'); expect(obj.version).toBeUndefined(); expect(obj.digest).toBeUndefined(); }); it('isFullObject returns false if missing version or digest', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); expect(obj.isFullObject()).toBe(false); obj.version = '1'; expect(obj.isFullObject()).toBe(false); obj.digest = 'abc'; expect(obj.isFullObject()).toBe(true); }); it('asCallArg returns objectId if not full', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); expect(obj.asCallArg()).toBe('0x123'); }); it('asCallArg returns CallArg if full', () => { const obj = new SuiOwnedObject({ objectId: '0x123', version: '1', digest: 'abc', }); expect(obj.asCallArg()).toEqual({ $kind: 'Object', Object: { $kind: 'ImmOrOwnedObject', ImmOrOwnedObject: { objectId: '0x123', version: '1', digest: 'abc', }, }, }); }); it('updateFromTxResponse updates version and digest', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); const txResponse = { $kind: 'Transaction', Transaction: { effects: { changedObjects: [ { objectId: '0x123', outputVersion: '2', outputDigest: 'def', }, ], }, }, } as any; obj.updateFromTxResponse(txResponse); expect(obj.version).toBe('2'); expect(obj.digest).toBe('def'); }); it('updateFromTxResponse throws if object not found', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); const txResponse = { $kind: 'Transaction', Transaction: { effects: { changedObjects: [ { objectId: '0x456', outputVersion: '2', outputDigest: 'def', }, ], }, }, } as any; expect(() => obj.updateFromTxResponse(txResponse)).toThrow(); }); it('updateFromTxResponse throws if no transaction', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); const txResponse = {} as any; expect(() => obj.updateFromTxResponse(txResponse)).toThrow(); }); it('updateFromTxResponse throws if no effects', () => { const obj = new SuiOwnedObject({ objectId: '0x123' }); const txResponse = { $kind: 'Transaction', Transaction: {}, } as any; expect(() => obj.updateFromTxResponse(txResponse)).toThrow(); }); }); ================================================ FILE: test/unit/libs/suiModel/suiSharedObject.spec.ts ================================================ import { SuiSharedObject } from 'src/libs/suiModel/suiSharedObject.js'; import { describe, it, expect } from 'vitest'; describe('SuiSharedObject', () => { it('should initialize with objectId', () => { const obj = new SuiSharedObject({ objectId: '0xabc' }); expect(obj.objectId).toBe('0xabc'); expect(obj.initialSharedVersion).toBeUndefined(); }); it('should initialize with objectId and initialSharedVersion', () => { const obj = new SuiSharedObject({ objectId: '0xabc', initialSharedVersion: '1', }); expect(obj.objectId).toBe('0xabc'); expect(obj.initialSharedVersion).toBe('1'); }); it('asCallArg returns objectId if initialSharedVersion is missing', () => { const obj = new SuiSharedObject({ objectId: '0xabc' }); expect(obj.asCallArg()).toBe('0xabc'); }); it('asCallArg returns correct CallArg with default mutable=false', () => { const obj = new SuiSharedObject({ objectId: '0xabc', initialSharedVersion: '1', }); expect(obj.asCallArg()).toEqual({ $kind: 'Object', Object: { $kind: 'SharedObject', SharedObject: { objectId: '0xabc', initialSharedVersion: '1', mutable: false, }, }, }); }); it('asCallArg returns correct CallArg with mutable=true', () => { const obj = new SuiSharedObject({ objectId: '0xabc', initialSharedVersion: '1', }); expect(obj.asCallArg(true)).toEqual({ $kind: 'Object', Object: { $kind: 'SharedObject', SharedObject: { objectId: '0xabc', initialSharedVersion: '1', mutable: true, }, }, }); }); }); ================================================ FILE: test/unit/libs/suiTxBuilder/index.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { SuiKit } from 'src/suiKit.js'; import { SuiTxBlock } from 'src/libs/suiTxBuilder/index.js'; function createTxBlock() { return new SuiTxBlock(); } describe('SuiTxBlock', () => { const suiKit = new SuiKit({ mnemonics: 'test test test test test test test test test test test test', }); it('makeMoveVec should call underlying txBlock.makeMoveVec', () => { const tx = createTxBlock(); expect(() => tx.makeMoveVec({ elements: [] })).not.toThrow(); }); it('transferObjects should call underlying txBlock.transferObjects', () => { const tx = createTxBlock(); const result = tx.transferObjects( ['0x1234567890abcdef1234567890abcdef12345678'], suiKit.currentAddress ); expect(result).toBeDefined(); }); it('moveCall should call underlying txBlock.moveCall', () => { const tx = createTxBlock(); const result = tx.moveCall('0x1::module::func', []); expect(result).toBeDefined(); }); it('transferSuiToMany should transfer to multiple recipients', () => { const tx = createTxBlock(); const recipients = [suiKit.currentAddress, suiKit.currentAddress]; const amounts = [1, 2]; const result = tx.transferSuiToMany(recipients, amounts); expect(result).toBe(tx); }); it('transferSui should transfer to one recipient', () => { const tx = createTxBlock(); const result = tx.transferSui(suiKit.currentAddress, 1); expect(result).toBe(tx); }); it('takeAmountFromCoins should return splitedCoins and mergedCoin', () => { const tx = createTxBlock(); const coins = ['0x1234567890abcdef1234567890abcdef12345678']; const [splitedCoins, mergedCoin] = tx.takeAmountFromCoins(coins, 1); expect(splitedCoins).toBeDefined(); expect(mergedCoin).toBeDefined(); }); it('splitSUIFromGas should split coins from gas', () => { const tx = createTxBlock(); const result = tx.splitSUIFromGas([1, 2]); expect(result).toBeDefined(); }); it('splitMultiCoins should split and merge coins', () => { const tx = createTxBlock(); const coins = ['0x1234567890abcdef1234567890abcdef12345678']; const result = tx.splitMultiCoins(coins, [1]); expect(result).toHaveProperty('splitedCoins'); expect(result).toHaveProperty('mergedCoin'); }); it('transferCoinToMany should transfer coins to many', () => { const tx = createTxBlock(); const coins = ['0x1234567890abcdef1234567890abcdef12345678']; const sender = suiKit.currentAddress; const recipients = [suiKit.currentAddress]; const amounts = [1]; const result = tx.transferCoinToMany(coins, sender, recipients, amounts); expect(result).toBe(tx); }); it('stakeSui should call moveCall for staking', () => { const tx = createTxBlock(); const result = tx.stakeSui(1, suiKit.currentAddress); expect(result).toBeDefined(); }); }); describe('SuiTxBlock (simple coverage)', () => { const suiKit = new SuiKit({ mnemonics: 'test test test test test test test test test test test test', }); it('should get gas', () => { const tx = new SuiTxBlock(); expect(tx.gas).toBeDefined(); }); it('should get getData', () => { const tx = new SuiTxBlock(); expect(tx.getData()).toBeDefined(); }); it('should get pure', () => { const tx = new SuiTxBlock(); expect(tx.pure).toBeDefined(); }); it('should call object', () => { const tx = new SuiTxBlock(); expect(() => tx.object('0x' + '1'.repeat(64))).not.toThrow(); }); it('should call objectRef', () => { const tx = new SuiTxBlock(); expect(() => tx.objectRef({ objectId: '0x' + '1'.repeat(64), version: '1', digest: 'abc', }) ).not.toThrow(); }); it('should call sharedObjectRef', () => { const tx = new SuiTxBlock(); expect(() => tx.sharedObjectRef({ objectId: '0x' + '1'.repeat(64), initialSharedVersion: '1', mutable: true, }) ).not.toThrow(); }); it('should call setSender', () => { const tx = new SuiTxBlock(); expect(() => tx.setSender('0x' + '1'.repeat(64))).not.toThrow(); }); it('should call setSenderIfNotSet', () => { const tx = new SuiTxBlock(); expect(() => tx.setSenderIfNotSet('0x' + '1'.repeat(64))).not.toThrow(); }); it('should call setExpiration', () => { const tx = new SuiTxBlock(); expect(() => tx.setExpiration()).not.toThrow(); }); it('should call setGasPrice', () => { const tx = new SuiTxBlock(); expect(() => tx.setGasPrice(1)).not.toThrow(); }); it('should call setGasBudget', () => { const tx = new SuiTxBlock(); expect(() => tx.setGasBudget(1)).not.toThrow(); }); it('should call setGasOwner', () => { const tx = new SuiTxBlock(); expect(() => tx.setGasOwner('0x' + '1'.repeat(64))).not.toThrow(); }); it('should call setGasPayment', () => { const tx = new SuiTxBlock(); expect(() => tx.setGasPayment([ { objectId: '0x' + '1'.repeat(64), version: '1', digest: 'abc' }, ]) ).not.toThrow(); }); it('should call serialize', () => { const tx = createTxBlock(); expect(() => tx.serialize()).not.toThrow(); }); it('should call toJSON', () => { const tx = createTxBlock(); expect(() => tx.toJSON()).not.toThrow(); }); it('should call add with invalid input throws in SDK v2', () => { const tx = createTxBlock(); // SDK v2 validates transaction commands and throws for invalid input expect(() => tx.add({} as any)).toThrow(); }); it('should call publish', () => { const tx = createTxBlock(); expect(() => tx.publish({ modules: [[1, 2, 3]], dependencies: ['0x' + '1'.repeat(64)], }) ).not.toThrow(); }); it('should call upgrade', () => { const tx = createTxBlock(); const args = [ { modules: [ [1, 2, 3], [4, 5, 6], ], dependencies: ['0x' + '1'.repeat(64)], package: '0x' + '1'.repeat(64), ticket: '0x' + '1'.repeat(64), }, ]; expect(() => tx.upgrade(args[0])).not.toThrow(); }); it('should call getDigest', () => { const tx = new SuiTxBlock(); tx.setSender('0x' + '1'.repeat(64)); tx.setGasPayment([ { objectId: '0x' + '4'.repeat(64), version: '1', digest: 'abc' }, ]); tx.transferObjects(['0x' + '2'.repeat(64)], '0x' + '3'.repeat(64)); expect(() => tx.getDigest({ client: suiKit.client })).not.toThrow(); }); it('should call build', () => { const tx = new SuiTxBlock(); tx.setSender('0x' + '1'.repeat(64)); tx.setGasPayment([ { objectId: '0x' + '4'.repeat(64), version: '1', digest: 'abc' }, ]); tx.transferObjects(['0x' + '2'.repeat(64)], '0x' + '3'.repeat(64)); expect(() => tx.build({ client: suiKit.client })).not.toThrow(); }); }); ================================================ FILE: test/unit/libs/suiTxBuilder/utils.spec.ts ================================================ import { describe, it, expect } from 'vitest'; import { getDefaultSuiInputType, makeVecParam, convertArgs, convertAddressArg, convertObjArg, convertAmounts, partitionArray, } from 'src/libs/suiTxBuilder/util.js'; import { Transaction } from '@mysten/sui/transactions'; // Mock types const mockObjectRef = { objectId: '0x1', version: '1', digest: 'abc' }; const mockSharedObjectRef = { objectId: '0x2', initialSharedVersion: '1', mutable: true, }; const mockImmOrOwnedObject = { Object: { ImmOrOwnedObject: mockObjectRef } }; const mockSharedObject = { Object: { SharedObject: mockSharedObjectRef } }; // Helper for Transaction mock function createTx() { return new Transaction(); } describe('util.ts', () => { describe('getDefaultSuiInputType', () => { it('should detect primitive types', () => { expect(getDefaultSuiInputType(123 as any)).toBe('u64'); expect(getDefaultSuiInputType(BigInt(123) as any)).toBe('u64'); expect(getDefaultSuiInputType(true as any)).toBe('bool'); expect(getDefaultSuiInputType(false as any)).toBe('bool'); }); it('should detect object', () => { expect( getDefaultSuiInputType( '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab' as any ) ).toBe('object'); }); it('should return undefined for unknown', () => { expect(getDefaultSuiInputType({} as any)).toBeUndefined(); expect(getDefaultSuiInputType('not-an-object-id' as any)).toBeUndefined(); }); }); describe('makeVecParam', () => { it('should throw on empty array', () => { const tx = createTx(); expect(() => makeVecParam(tx, [], 'u64')).toThrow(); }); it('should handle object type', () => { const tx = createTx(); const objIds = [ '0x5dec622733a204ca27f5a90d8c2fad453cc6665186fd5dff13a83d0b6c9027ab', '0x24c0247fb22457a719efac7f670cdc79be321b521460bd6bd2ccfa9f80713b14', '0x7c5b7837c44a69b469325463ac0673ac1aa8435ff44ddb4191c9ae380463647f', '0x9d0d275efbd37d8a8855f6f2c761fa5983293dd8ce202ee5196626de8fcd4469', '0x9a62b4863bdeaabdc9500fce769cf7e72d5585eeb28a6d26e4cafadc13f76ab2', '0x9193fd47f9a0ab99b6e365a464c8a9ae30e6150fc37ed2a89c1586631f6fc4ab', ]; const arr = objIds.map((id) => tx.object(id)); const result = makeVecParam(tx, arr, 'object'); expect(result).toBeDefined(); }); it('should handle u64 type', () => { const tx = createTx(); const arr = [1, 2]; const result = makeVecParam(tx, arr as any[], 'u64'); expect(result).toBeDefined(); }); }); describe('convertArgs', () => { it('should handle SerializedBcs', () => { const tx = createTx(); const arg = tx.pure.u8(1); expect(convertArgs(tx, [arg])[0]).toBeDefined(); }); it('should handle move vec arg (array)', () => { const tx = createTx(); const arg = [1, 2].map((n) => tx.pure.u64(n)); expect(convertArgs(tx, [arg])[0]).toBeDefined(); }); it('should handle amount arg', () => { const tx = createTx(); const arg = tx.pure.u64(123); expect(convertArgs(tx, [arg])[0]).toBeDefined(); }); it('should handle TransactionArgument as object arg', () => { const tx = createTx(); const arg = tx.pure.u64(1); expect(convertArgs(tx, [arg])[0]).toBeDefined(); }); }); describe('convertAddressArg', () => { it('should handle valid address', () => { const tx = createTx(); expect( convertAddressArg( tx, '0xd9612ec5cb1d13bcd955ad4b8936d41824dc478a37bff6b0619e994279def7f3' ) ).toBeDefined(); }); it('should handle TransactionArgument', () => { const tx = createTx(); const arg = tx.pure.address( '0xd9612ec5cb1d13bcd955ad4b8936d41824dc478a37bff6b0619e994279def7f3' ); expect(convertAddressArg(tx, arg)).toBeDefined(); }); }); describe('convertObjArg', () => { it('should handle string', () => { const tx = createTx(); expect( convertObjArg( tx, '0xd9612ec5cb1d13bcd955ad4b8936d41824dc478a37bff6b0619e994279def7f3' ) ).toBeDefined(); }); it('should handle object ref', () => { const tx = createTx(); expect(convertObjArg(tx, mockObjectRef)).toBeDefined(); }); it('should handle shared object ref', () => { const tx = createTx(); expect(convertObjArg(tx, mockSharedObjectRef)).toBeDefined(); }); it('should handle ImmOrOwnedObject', () => { const tx = createTx(); expect(convertObjArg(tx, mockImmOrOwnedObject)).toBeDefined(); }); it('should handle SharedObject', () => { const tx = createTx(); expect(convertObjArg(tx, mockSharedObject)).toBeDefined(); }); it('should handle function', () => { const tx = createTx(); const fn = () => tx.object( '0xd9612ec5cb1d13bcd955ad4b8936d41824dc478a37bff6b0619e994279def7f3' ); expect(convertObjArg(tx, fn)).toBe(fn); }); it('should handle special objects', () => { const tx = createTx(); expect(convertObjArg(tx, { GasCoin: true })).toBeDefined(); }); it('should throw on invalid type', () => { const tx = createTx(); expect(() => convertObjArg(tx, {} as any)).toThrow(); }); }); describe('convertAmounts', () => { it('should handle amount arg (number)', () => { const tx = createTx(); expect(convertAmounts(tx, [tx.pure.u64(123)])).toHaveLength(1); }); it('should handle amount arg (TransactionArgument)', () => { const tx = createTx(); const arg = tx.pure.u64(1); expect(convertAmounts(tx, [arg])).toHaveLength(1); }); }); describe('partitionArray', () => { it('should partition array correctly', () => { const arr = [1, 2, 3, 4, 5]; const result = partitionArray(arr, 2); expect(result).toHaveLength(3); expect(result[0]).toHaveLength(2); expect(result[1]).toHaveLength(2); expect(result[2]).toHaveLength(1); }); }); }); ================================================ FILE: tsconfig.json ================================================ { "ts-node": { "require": ["tsconfig-paths/register"] }, "compilerOptions": { "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "baseUrl": ".", "downlevelIteration": true, "rootDir": "src", "outDir": "dist", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "sourceMap": true, "declaration": true, "esModuleInterop": true, "resolveJsonModule": true, "isolatedModules": true, "skipLibCheck": true, "preserveSymlinks": true, "forceConsistentCasingInFileNames": true, "allowJs": true, "noEmit": false, "emitDeclarationOnly": true, "incremental": false, "typeRoots": ["./node_modules/@types"] }, "include": ["./src/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: tsup.config.ts ================================================ import { defineConfig } from 'tsup'; export default defineConfig((options) => { const isProduction = options.env?.NODE_ENV === 'production'; return { // Define entry points for your library. // This typically points to your main source file, e.g., 'src/index.ts'. entry: ['src/index.ts'], // Generate TypeScript declaration files (.d.ts) for type safety. dts: true, // Clean the 'dist' directory before each build to ensure a fresh output. clean: true, // Generate source maps for easier debugging in development. sourcemap: isProduction ? false : true, // Output formats: ESM (ECMAScript Modules) for modern environments // and CJS (CommonJS) for Node.js and older toolchains. format: ['esm', 'cjs'], // Minify the output for production builds to reduce file size. minify: true, // Target a specific ECMAScript version for broader compatibility. // 'esnext' is often suitable for modern libraries. target: 'esnext', // Specify the output directory for the bundled files. outDir: 'dist', // Optionally, define modules that should not be bundled but treated as external dependencies. // This is crucial for libraries to avoid bundling their dependencies into the output. // external: ["react", "react-dom"], treeshake: 'recommended', }; }); ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; import path from 'path'; export default defineConfig({ resolve: { alias: { src: path.resolve(__dirname, './src'), }, extensions: ['.ts', '.js', '.mts', '.mjs'], }, test: { globals: true, environment: 'node', testTimeout: 60000, include: ['test/**/*.spec.ts'], }, esbuild: { target: 'es2022', }, });