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 = '<Secret key>';
const suiKit1 = new SuiKit({ secretKey });
// 12 or 24 words mnemonics
const mnemonics = '<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: '<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: '[<SUI fullnode1>, <SUI fullnode2>]',
// the faucet url, default is the preconfig faucet url for the given network type
faucetUrl: '<SUI faucet url>',
});
```
### 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 = '<pkgId>::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 = ['<objId1>', '<objId2>'];
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<address>,
name: vector<u8>,
numbers: vector<u64>,
bools: vector<bool>,
coins: vector<Coin<SUI>>,
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<address>, need to specify the vecType as 'address'
{ value: [addr1, addr2], vecType: 'address' },
// pass vector<u8>, need to specify the vecType as 'u8'
{ value: [10, 20], vecType: 'u8' },
// pass vector<u64>, default vecType for number array is 'u64', so no need to specify
[34324, 234234],
// pass vector<bool>, default vecType for boolean array is 'bool', so no need to specify
[true, false],
// pass vector<Coin<SUI>>, 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).
[](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: '<your-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<SuiInteractorParams>) {
// 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<SimulateTransactionResponse> {
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<SuiObjectData[]> {
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 = <T>(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<typeof this.txBlock.setExpiration>[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<typeof this.txBlock.add>) {
return this.txBlock.add(...args);
}
publish({
modules,
dependencies,
}: {
modules: number[][] | string[];
dependencies: string[];
}) {
return this.txBlock.publish({ modules, dependencies });
}
upgrade(...args: Parameters<typeof this.txBlock.upgrade>) {
return this.txBlock.upgrade(...args);
}
makeMoveVec(...args: Parameters<typeof this.txBlock.makeMoveVec>) {
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 =
/(?<package>[a-zA-Z0-9]+)::(?<module>[a-zA-Z0-9_]+)::(?<function>[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<typeof Inputs.SharedObjectRef>[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 = <T>(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<SuiTransactionBlockResponse> {
const { bytes, signature } = await this.signTxn(tx, derivePathParams);
return this.suiInteractor.sendTx(bytes, signature);
}
async dryRunTxn(
tx: Uint8Array | Transaction | SuiTxBlock,
derivePathParams?: DerivePathParams
): Promise<SimulateTransactionResponse> {
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<SuiTransactionBlockResponse>;
async transferSui<S extends boolean>(
recipient: string,
amount: number,
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async transferSui<S extends boolean>(
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<S>)
: (tx as SuiKitReturnType<S>);
}
/**
* 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<SuiTransactionBlockResponse>;
async transferSuiToMany<S extends boolean>(
recipients: string[],
amounts: number[],
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async transferSuiToMany<S extends boolean>(
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<S>)
: (tx as SuiKitReturnType<S>);
}
/**
* 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<SuiTransactionBlockResponse>;
async transferCoinToMany<S extends boolean>(
recipients: string[],
amounts: number[],
coinType: string,
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async transferCoinToMany<S extends boolean>(
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<S>)
: (tx as SuiKitReturnType<S>);
}
async transferCoin(
recipient: string,
amount: number,
coinType: string,
derivePathParams?: DerivePathParams
): Promise<SuiTransactionBlockResponse>;
async transferCoin<S extends boolean>(
recipient: string,
amount: number,
coinType: string,
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async transferCoin<S extends boolean>(
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<SuiTransactionBlockResponse>;
async transferObjects<S extends boolean>(
objects: SuiObjectArg[],
recipient: string,
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async transferObjects<S extends boolean>(
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<SuiTransactionBlockResponse>;
async stakeSui<S extends boolean>(
amount: number,
validatorAddr: string,
sign?: S,
derivePathParams?: DerivePathParams
): Promise<SuiKitReturnType<S>>;
async stakeSui<S extends boolean>(
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<S>)
: (tx as SuiKitReturnType<S>);
}
/**
* 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<SimulateTransactionResponse> {
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<SuiInteractorParams>;
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<typeof Transaction>;
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<TransactionBlockType['add']>;
export type TransactionPureArgument = Extract<
Argument,
{
$kind: 'Input';
type?: 'pure';
}
>;
export type SuiTxArg = TransactionArgument | SerializedBcs<any>;
export type SuiAddressArg = Argument | SerializedBcs<any> | string;
export type SuiAmountsArg = SuiTxArg | number | bigint;
export type SuiObjectArg =
| TransactionObjectArgument
| string
| Parameters<typeof Inputs.ObjectRef>[0]
| Parameters<typeof Inputs.SharedObjectRef>[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<Include>;
// 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 boolean> = 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',
},
});
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
SYMBOL INDEX (143 symbols across 15 files)
FILE: src/libs/multiSig/client.ts
type PublicKeyWeightPair (line 5) | type PublicKeyWeightPair = {
class MultiSigClient (line 10) | class MultiSigClient {
method constructor (line 14) | constructor(pks: PublicKeyWeightPair[], threshold: number) {
method fromRawEd25519PublicKeys (line 23) | static fromRawEd25519PublicKeys(
method multiSigAddress (line 37) | multiSigAddress(): string {
method combinePartialSigs (line 41) | combinePartialSigs(sigs: string[]): string {
FILE: src/libs/multiSig/publickey.ts
function ed25519PublicKeyFromBase64 (line 5) | function ed25519PublicKeyFromBase64(rawPubkey: string): PublicKey {
FILE: src/libs/suiAccountManager/index.ts
class SuiAccountManager (line 14) | class SuiAccountManager {
method constructor (line 29) | constructor({ mnemonics, secretKey }: AccountManagerParams = {}) {
method parseSecretKey (line 48) | parseSecretKey(secretKey: string) {
method getKeyPair (line 66) | getKeyPair(derivePathParams?: DerivePathParams) {
method getAddress (line 76) | getAddress(derivePathParams?: DerivePathParams) {
method switchAccount (line 87) | switchAccount(derivePathParams: DerivePathParams) {
FILE: src/libs/suiAccountManager/util.ts
constant PRIVATE_KEY_SIZE (line 31) | const PRIVATE_KEY_SIZE = 32;
constant LEGACY_PRIVATE_KEY_SIZE (line 32) | const LEGACY_PRIVATE_KEY_SIZE = 64;
FILE: src/libs/suiInteractor/suiInteractor.ts
constant MAX_OBJECTS_PER_REQUEST (line 7) | const MAX_OBJECTS_PER_REQUEST = 50;
function createGrpcClientOptions (line 10) | function createGrpcClientOptions(
function getFullnodeUrl (line 18) | function getFullnodeUrl(network: NetworkType): string {
type SuiObjectData (line 34) | type SuiObjectData = SuiClientTypes.Object<{
type SuiObjectDataOptions (line 40) | type SuiObjectDataOptions = SuiClientTypes.ObjectInclude;
type SimulateTransactionResponse (line 43) | type SimulateTransactionResponse =
class SuiInteractor (line 54) | class SuiInteractor {
method constructor (line 60) | constructor(params: Partial<SuiInteractorParams>) {
method switchToNextClient (line 83) | switchToNextClient() {
method switchFullNodes (line 89) | switchFullNodes(fullNodes: string[], network?: NetworkType) {
method currentFullNode (line 103) | get currentFullNode() {
method sendTx (line 116) | async sendTx(
method dryRunTx (line 156) | async dryRunTx(
method getObjects (line 180) | async getObjects(
method getObject (line 238) | async getObject(id: string, options?: { include?: SuiObjectDataOptions...
method updateObjects (line 247) | async updateObjects(suiObjects: (SuiOwnedObject | SuiSharedObject)[]) {
method selectCoins (line 276) | async selectCoins(
FILE: src/libs/suiModel/suiOwnedObject.ts
type TransactionResultWithEffects (line 6) | type TransactionResultWithEffects = SuiClientTypes.TransactionResult<{
class SuiOwnedObject (line 10) | class SuiOwnedObject {
method constructor (line 15) | constructor(param: { objectId: string; version?: string; digest?: stri...
method isFullObject (line 26) | isFullObject(): boolean {
method asCallArg (line 30) | asCallArg(): CallArg | string {
method updateFromTxResponse (line 51) | updateFromTxResponse(txResponse: TransactionResultWithEffects) {
FILE: src/libs/suiModel/suiSharedObject.ts
class SuiSharedObject (line 4) | class SuiSharedObject {
method constructor (line 8) | constructor(param: {
method asCallArg (line 17) | asCallArg(mutable: boolean = false): CallArg | string {
FILE: src/libs/suiTxBuilder/index.ts
type SuiObjectRef (line 22) | interface SuiObjectRef {
class SuiTxBlock (line 28) | class SuiTxBlock {
method constructor (line 31) | constructor(transaction?: Transaction) {
method gas (line 38) | get gas() {
method getData (line 42) | getData() {
method address (line 46) | address(value: string) {
method pure (line 50) | get pure(): typeof this.txBlock.pure {
method object (line 54) | object(value: string | TransactionObjectInput) {
method objectRef (line 58) | objectRef(ref: SuiObjectRef) {
method sharedObjectRef (line 61) | sharedObjectRef(ref: typeof bcs.SharedObjectRef.$inferType) {
method setSender (line 64) | setSender(sender: string) {
method setSenderIfNotSet (line 67) | setSenderIfNotSet(sender: string) {
method setExpiration (line 70) | setExpiration(expiration?: Parameters<typeof this.txBlock.setExpiratio...
method setGasPrice (line 73) | setGasPrice(price: number | bigint) {
method setGasBudget (line 76) | setGasBudget(budget: number | bigint) {
method setGasOwner (line 79) | setGasOwner(owner: string) {
method setGasPayment (line 82) | setGasPayment(payments: SuiObjectRef[]) {
method serialize (line 89) | serialize() {
method toJSON (line 94) | toJSON() {
method sign (line 98) | sign(params: {
method build (line 105) | build(
method getDigest (line 113) | getDigest(params: { client?: ClientWithCoreApi } = {}) {
method add (line 116) | add(...args: Parameters<typeof this.txBlock.add>) {
method publish (line 119) | publish({
method upgrade (line 128) | upgrade(...args: Parameters<typeof this.txBlock.upgrade>) {
method makeMoveVec (line 132) | makeMoveVec(...args: Parameters<typeof this.txBlock.makeMoveVec>) {
method transferObjects (line 138) | transferObjects(objects: SuiObjectArg[], address: SuiAddressArg) {
method splitCoins (line 145) | splitCoins(coin: SuiObjectArg, amounts: SuiAmountsArg[]) {
method mergeCoins (line 153) | mergeCoins(destination: SuiObjectArg, sources: SuiObjectArg[]) {
method moveCall (line 167) | moveCall(
method transferSuiToMany (line 189) | transferSuiToMany(recipients: SuiAddressArg[], amounts: SuiAmountsArg[...
method transferSui (line 213) | transferSui(address: SuiAddressArg, amount: SuiAmountsArg) {
method takeAmountFromCoins (line 217) | takeAmountFromCoins(coins: SuiObjectArg[], amount: SuiAmountsArg) {
method splitSUIFromGas (line 226) | splitSUIFromGas(amounts: SuiAmountsArg[]) {
method splitMultiCoins (line 233) | splitMultiCoins(coins: SuiObjectArg[], amounts: SuiAmountsArg[]) {
method transferCoinToMany (line 253) | transferCoinToMany(
method transferCoin (line 288) | transferCoin(
method stakeSui (line 297) | stakeSui(amount: SuiAmountsArg, validatorAddr: SuiAddressArg) {
FILE: src/libs/suiTxBuilder/util.ts
type SuiObjectRef (line 25) | interface SuiObjectRef {
constant SIMPLE_BCS_TYPES (line 32) | const SIMPLE_BCS_TYPES = [
type SimpleBcsType (line 43) | type SimpleBcsType = (typeof SIMPLE_BCS_TYPES)[number];
function toOpenSignatureBody (line 46) | function toOpenSignatureBody(type: string): SuiClientTypes.OpenSignature...
function isAmountArg (line 76) | function isAmountArg(arg: any): arg is bigint | number | string {
function isMoveVecArg (line 90) | function isMoveVecArg(
function isObjectRef (line 111) | function isObjectRef(arg: SuiObjectArg): arg is SuiObjectRef {
function isSharedObjectRef (line 125) | function isSharedObjectRef(
function makeVecParam (line 150) | function makeVecParam(
function convertArgs (line 199) | function convertArgs(
function convertAddressArg (line 230) | function convertAddressArg(
function convertObjArg (line 248) | function convertObjArg(
function convertAmounts (line 290) | function convertAmounts(
FILE: src/suiKit.ts
class SuiKit (line 30) | class SuiKit {
method constructor (line 45) | constructor(params: SuiKitParams) {
method createTxBlock (line 74) | createTxBlock(): SuiTxBlock {
method getKeypair (line 86) | getKeypair(derivePathParams?: DerivePathParams) {
method switchAccount (line 94) | switchAccount(derivePathParams: DerivePathParams) {
method getAddress (line 102) | getAddress(derivePathParams?: DerivePathParams) {
method currentAddress (line 106) | get currentAddress() {
method getBalance (line 110) | async getBalance(coinType?: string, derivePathParams?: DerivePathParam...
method client (line 119) | get client() {
method getObjects (line 123) | async getObjects(
method updateObjects (line 138) | async updateObjects(suiObjects: (SuiSharedObject | SuiOwnedObject)[]) {
method signTxn (line 142) | async signTxn(
method signAndSendTxn (line 158) | async signAndSendTxn(
method dryRunTxn (line 166) | async dryRunTxn(
method transferSui (line 198) | async transferSui<S extends boolean>(
method transferSuiToMany (line 231) | async transferSuiToMany<S extends boolean>(
method transferCoinToMany (line 267) | async transferCoinToMany<S extends boolean>(
method transferCoin (line 315) | async transferCoin<S extends boolean>(
method transferObjects (line 342) | async transferObjects<S extends boolean>(
method moveCall (line 353) | async moveCall(callParams: {
method selectCoinsWithAmount (line 376) | async selectCoinsWithAmount(
method stakeSui (line 404) | async stakeSui<S extends boolean>(
method inspectTxn (line 427) | async inspectTxn(
FILE: src/types/index.ts
type SuiKitParams (line 12) | type SuiKitParams = (AccountManagerParams & {
type SuiInteractorParams (line 18) | type SuiInteractorParams =
type NetworkType (line 27) | type NetworkType = 'testnet' | 'mainnet' | 'devnet' | 'localnet';
type AccountManagerParams (line 29) | type AccountManagerParams = {
type DerivePathParams (line 34) | type DerivePathParams = {
type TransactionBlockType (line 40) | type TransactionBlockType = InstanceType<typeof Transaction>;
type PureCallArg (line 42) | type PureCallArg = {
type SharedObjectRef (line 46) | type SharedObjectRef = {
type SuiObjectRef (line 57) | type SuiObjectRef = {
type ObjectArg (line 69) | type ObjectArg =
type ObjectCallArg (line 74) | type ObjectCallArg = {
type TransactionType (line 77) | type TransactionType = Parameters<TransactionBlockType['add']>;
type TransactionPureArgument (line 79) | type TransactionPureArgument = Extract<
type SuiTxArg (line 87) | type SuiTxArg = TransactionArgument | SerializedBcs<any>;
type SuiAddressArg (line 88) | type SuiAddressArg = Argument | SerializedBcs<any> | string;
type SuiAmountsArg (line 89) | type SuiAmountsArg = SuiTxArg | number | bigint;
type SuiObjectArg (line 91) | type SuiObjectArg =
type SuiVecTxArg (line 98) | type SuiVecTxArg =
type SuiBasicTypes (line 105) | type SuiBasicTypes =
type SuiInputTypes (line 115) | type SuiInputTypes = 'object' | SuiBasicTypes;
type SuiTransactionResult (line 118) | type SuiTransactionResult<
type SuiTransactionBlockResponse (line 123) | type SuiTransactionBlockResponse = SuiClientTypes.TransactionResult<{
type SuiKitReturnType (line 130) | type SuiKitReturnType<T extends boolean> = T extends true
FILE: test/integration/index.spec.ts
constant ENABLE_LOG (line 16) | const ENABLE_LOG = false;
function isTransactionSuccess (line 19) | function isTransactionSuccess(
FILE: test/integration/multiSig.spec.ts
constant ENABLE_LOG (line 8) | const ENABLE_LOG = false;
FILE: test/unit/libs/suiTxBuilder/index.spec.ts
function createTxBlock (line 5) | function createTxBlock() {
FILE: test/unit/libs/suiTxBuilder/utils.spec.ts
function createTx (line 24) | function createTx() {
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (166K chars).
[
{
"path": ".github/workflows/publish-package.yml",
"chars": 1544,
"preview": "name: Publish package to GitHub Packages\n\non:\n release:\n types: [published]\n\njobs:\n release:\n runs-on: ubuntu-22"
},
{
"path": ".gitignore",
"chars": 461,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependency directories\nnode_mod"
},
{
"path": ".husky/pre-commit",
"chars": 69,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
},
{
"path": "CHANGELOG.md",
"chars": 7411,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github."
},
{
"path": "LICENSE",
"chars": 11343,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 7725,
"preview": "# Toolkit for interacting with SUI network\n\n## Features\n\n- [x] Transfer SUI, Custom Coin and objects.\n- [x] Move call\n- "
},
{
"path": "document/README_cn.md",
"chars": 3510,
"preview": "# SUI 网络交互工具箱\n\n## 特点\n\n- [x] 相比于 Mystenlab 的 SDK,更加易于使用\n- [x] 支持转账 SUI 和自定义代币\n- [x] 从开发网络和测试网络请求水龙头\n- [x] 质押 SUI\n- [x] 兼容"
},
{
"path": "document/how-to-achieve-max-performance-on-sui.md",
"chars": 2330,
"preview": "# How to achieve max performance on SUI network?\n\n## 1. Pre-build the transaction\n\nWhen sending transactions, there's a "
},
{
"path": "document/migration-guide-v2.md",
"chars": 3134,
"preview": "# Migration Guide: v1.x to v2.0.0\n\nThis guide helps you migrate your project from sui-kit v1.x to v2.0.0.\n\n## Breaking C"
},
{
"path": "package.json",
"chars": 4504,
"preview": "{\n \"name\": \"@scallop-io/sui-kit\",\n \"version\": \"2.0.1\",\n \"description\": \"Toolkit for interacting with SUI network\",\n "
},
{
"path": "src/index.ts",
"chars": 463,
"preview": "export * from '@mysten/sui/utils';\nexport * from '@mysten/sui/transactions';\nexport { SuiKit } from './suiKit.js';\nexpor"
},
{
"path": "src/libs/multiSig/client.ts",
"chars": 1281,
"preview": "import { MultiSigPublicKey } from '@mysten/sui/multisig';\nimport type { PublicKey } from '@mysten/sui/cryptography';\nimp"
},
{
"path": "src/libs/multiSig/index.ts",
"chars": 46,
"preview": "export { MultiSigClient } from './client.js';\n"
},
{
"path": "src/libs/multiSig/publickey.ts",
"chars": 543,
"preview": "import { PublicKey } from '@mysten/sui/cryptography';\nimport { Ed25519PublicKey } from '@mysten/sui/keypairs/ed25519';\ni"
},
{
"path": "src/libs/suiAccountManager/crypto.ts",
"chars": 289,
"preview": "import { generateMnemonic as genMnemonic } from '@scure/bip39';\nimport { wordlist } from '@scure/bip39/wordlists/english"
},
{
"path": "src/libs/suiAccountManager/index.ts",
"chars": 3290,
"preview": "import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';\nimport { getKeyPair } from './keypair.js';\nimport { hexOr"
},
{
"path": "src/libs/suiAccountManager/keypair.ts",
"chars": 1247,
"preview": "import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';\nimport type { DerivePathParams } from '../../types/index."
},
{
"path": "src/libs/suiAccountManager/util.ts",
"chars": 1951,
"preview": "import { fromBase64, fromHex } from '@mysten/bcs';\n\n/**\n * @description This regular expression matches any string that "
},
{
"path": "src/libs/suiInteractor/index.ts",
"chars": 160,
"preview": "export {\n SuiInteractor,\n getFullnodeUrl,\n type SuiObjectData,\n type SuiObjectDataOptions,\n type SimulateTransactio"
},
{
"path": "src/libs/suiInteractor/suiInteractor.ts",
"chars": 9517,
"preview": "import { SuiInteractorParams, NetworkType } from '../../types/index.js';\nimport { SuiOwnedObject, SuiSharedObject } from"
},
{
"path": "src/libs/suiInteractor/util.ts",
"chars": 288,
"preview": "export const delay = (ms: number) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const batch = <T>(arr:"
},
{
"path": "src/libs/suiModel/index.ts",
"chars": 110,
"preview": "export { SuiOwnedObject } from './suiOwnedObject.js';\nexport { SuiSharedObject } from './suiSharedObject.js';\n"
},
{
"path": "src/libs/suiModel/suiOwnedObject.ts",
"chars": 2031,
"preview": "// TODO: I think we can remove this file or update to NormalizedCallArg\nimport type { SuiClientTypes } from '@mysten/sui"
},
{
"path": "src/libs/suiModel/suiSharedObject.ts",
"chars": 832,
"preview": "// TODO: I think we can remove this file or update to NormalizedCallArg\nimport type { CallArg } from '@mysten/sui/transa"
},
{
"path": "src/libs/suiTxBuilder/index.ts",
"chars": 8856,
"preview": "import { Transaction, TransactionObjectInput } from '@mysten/sui/transactions';\nimport { SUI_SYSTEM_STATE_OBJECT_ID } fr"
},
{
"path": "src/libs/suiTxBuilder/util.ts",
"chars": 7955,
"preview": "import {\n normalizeSuiObjectId,\n normalizeSuiAddress,\n isValidSuiObjectId,\n isValidSuiAddress,\n} from '@mysten/sui/u"
},
{
"path": "src/suiKit.ts",
"chars": 13806,
"preview": "/**\n * @description This file is used to aggregate the tools that used to interact with SUI network.\n */\nimport { Transa"
},
{
"path": "src/types/index.ts",
"chars": 3088,
"preview": "import type {\n Transaction,\n TransactionObjectArgument,\n Argument,\n Inputs,\n TransactionArgument,\n} from '@mysten/s"
},
{
"path": "test/integration/index.spec.ts",
"chars": 23547,
"preview": "import { config as dotenvConfig } from 'dotenv';\nimport { describe, it, expect } from 'vitest';\nimport {\n SUI_TYPE_ARG,"
},
{
"path": "test/integration/multiSig.spec.ts",
"chars": 3698,
"preview": "import { describe, it, expect } from 'vitest';\nimport { Transaction } from '@mysten/sui/transactions';\nimport { SuiGrpcC"
},
{
"path": "test/tsconfig.json",
"chars": 169,
"preview": "{\n \"extends\": \"../tsconfig.json\",\n \"include\": [\"./\", \"../src\"],\n \"compilerOptions\": {\n \"rootDir\": \"..\",\n \"noEmi"
},
{
"path": "test/unit/libs/multiSig/publickey.spec.ts",
"chars": 865,
"preview": "import { describe, it, expect } from 'vitest';\nimport { ed25519PublicKeyFromBase64 } from 'src/libs/multiSig/publickey.j"
},
{
"path": "test/unit/libs/suiAccountManager/crypto.spec.ts",
"chars": 573,
"preview": "import { describe, it, expect } from 'vitest';\nimport { generateMnemonic } from 'src/libs/suiAccountManager/crypto.js';\n"
},
{
"path": "test/unit/libs/suiAccountManager/util.spec.ts",
"chars": 2335,
"preview": "import { describe, it, expect } from 'vitest';\nimport {\n isHex,\n isBase64,\n hexOrBase64ToUint8Array,\n normalizePriva"
},
{
"path": "test/unit/libs/suiInteractor/suiInteractor.spec.ts",
"chars": 8152,
"preview": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { SuiInteractor } from 'src/libs/suiInteractor/sui"
},
{
"path": "test/unit/libs/suiModel/suiOwnedObject.spec.ts",
"chars": 2830,
"preview": "import { SuiOwnedObject } from 'src/libs/suiModel/suiOwnedObject.js';\nimport { describe, it, expect } from 'vitest';\n\nde"
},
{
"path": "test/unit/libs/suiModel/suiSharedObject.spec.ts",
"chars": 1702,
"preview": "import { SuiSharedObject } from 'src/libs/suiModel/suiSharedObject.js';\nimport { describe, it, expect } from 'vitest';\n\n"
},
{
"path": "test/unit/libs/suiTxBuilder/index.spec.ts",
"chars": 6908,
"preview": "import { describe, it, expect } from 'vitest';\nimport { SuiKit } from 'src/suiKit.js';\nimport { SuiTxBlock } from 'src/l"
},
{
"path": "test/unit/libs/suiTxBuilder/utils.spec.ts",
"chars": 6160,
"preview": "import { describe, it, expect } from 'vitest';\nimport {\n getDefaultSuiInputType,\n makeVecParam,\n convertArgs,\n conve"
},
{
"path": "tsconfig.json",
"chars": 793,
"preview": "{\n \"ts-node\": {\n \"require\": [\"tsconfig-paths/register\"]\n },\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib"
},
{
"path": "tsup.config.ts",
"chars": 1348,
"preview": "import { defineConfig } from 'tsup';\n\nexport default defineConfig((options) => {\n const isProduction = options.env?.NOD"
},
{
"path": "vitest.config.ts",
"chars": 396,
"preview": "import { defineConfig } from 'vitest/config';\nimport path from 'path';\n\nexport default defineConfig({\n resolve: {\n a"
}
]
About this extraction
This page contains the full source code of the scallop-io/sui-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (153.6 KB), approximately 44.6k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.