@msafe/sui3-sdk 0.0.2-pre-9f39791.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc +87 -0
  3. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  4. package/.idea/jsLinters/eslint.xml +6 -0
  5. package/.idea/misc.xml +6 -0
  6. package/.idea/modules.xml +8 -0
  7. package/.idea/msafe-sui3-sdk.iml +9 -0
  8. package/.idea/vcs.xml +6 -0
  9. package/.prettierrc +22 -0
  10. package/README.md +3 -0
  11. package/jest.config.ts +63 -0
  12. package/package.json +54 -0
  13. package/scripts/prerelease.sh +5 -0
  14. package/src/backend/CoreDatabase.ts +55 -0
  15. package/src/backend/PseudoBackend.ts +851 -0
  16. package/src/backend/interface.ts +57 -0
  17. package/src/backend/types.ts +22 -0
  18. package/src/core/CreateHelper.ts +76 -0
  19. package/src/core/MSafeAccount.ts +167 -0
  20. package/src/core/MSafeClient.ts +91 -0
  21. package/src/core/MessageHelper.ts +63 -0
  22. package/src/core/PublicKeyHelper.ts +88 -0
  23. package/src/core/index.ts +4 -0
  24. package/src/globals/MSafeGlobals.ts +48 -0
  25. package/src/globals/const.ts +95 -0
  26. package/src/globals/index.ts +2 -0
  27. package/src/index.ts +5 -0
  28. package/src/transactions/coin-transfer.ts +64 -0
  29. package/src/transactions/index.ts +1 -0
  30. package/src/transactions/intention.ts +63 -0
  31. package/src/transactions/object-transfer.ts +66 -0
  32. package/src/transactions/reject.ts +17 -0
  33. package/src/transactions/stream.ts +1 -0
  34. package/src/types/creation.ts +19 -0
  35. package/src/types/index.ts +3 -0
  36. package/src/types/msafe.ts +79 -0
  37. package/src/types/wallet.ts +23 -0
  38. package/src/utils/buffer.ts +11 -0
  39. package/src/utils/coin.ts +64 -0
  40. package/src/utils/crypto.ts +95 -0
  41. package/src/utils/format.ts +25 -0
  42. package/src/utils/index.ts +6 -0
  43. package/src/utils/multi-sig.ts +113 -0
  44. package/src/utils/sui.ts +90 -0
  45. package/temp_package.json +54 -0
  46. package/test/lib/TestHelper.ts +94 -0
  47. package/test/lib/account.ts +87 -0
  48. package/test/lib/config.ts +9 -0
  49. package/test/lib/faucet.ts +49 -0
  50. package/test/unit/backend/backend.test.ts +367 -0
  51. package/test/unit/core/CreateHelper.test.ts +121 -0
  52. package/test/unit/core/MessageHelper.test.ts +32 -0
  53. package/test/unit/core/PublicKeyHelper.test.ts +80 -0
  54. package/test/unit/core/msafe.test.ts +298 -0
  55. package/test/unit/utils/buffer.test.ts +17 -0
  56. package/test/unit/utils/crypto.test.ts +32 -0
  57. package/test/unit/utils/multi-sig.test.ts +47 -0
  58. package/test/unit/utils/sui.test.ts +25 -0
  59. package/tsconfig.json +37 -0
  60. package/tsup.config.ts +9 -0
@@ -0,0 +1,113 @@
1
+ import { PublicKey, SerializedSignature } from '@mysten/sui.js/cryptography';
2
+ import { Ed25519PublicKey } from '@mysten/sui.js/keypairs/ed25519';
3
+ import { MultiSigPublicKey } from '@mysten/sui.js/multisig';
4
+
5
+ import { MSafeAccountInfo } from '@/types/msafe';
6
+ import { stringToBuffer } from '@/utils/buffer';
7
+
8
+ export interface PublicKeyWithWeight {
9
+ publicKey: PublicKey;
10
+ weight: number;
11
+ }
12
+
13
+ export interface MultiSigConfig {
14
+ threshold: number;
15
+ ownerWithWeight: PublicKeyWithWeight[];
16
+ creationNonce: number | undefined;
17
+ }
18
+
19
+ export const NONCE_PK_PREFIX = 'maven';
20
+ export const NONCE_PREFIX_MAX_SIZE = 16;
21
+ export const NONCE_PK_WEIGHT = 1;
22
+
23
+ export const MAX_WEIGHT = 255;
24
+ export const MIN_WEIGHT = 1;
25
+ export const MAX_OWNER_WITH_NONCE = 9;
26
+ export const MAX_OWNER_WITHOUT_NONCE = 10;
27
+ export const MIN_THRESHOLD = 1;
28
+
29
+ export class RawMultiSig {
30
+ rawMsPK: MultiSigPublicKey;
31
+
32
+ constructor(public readonly config: MultiSigConfig) {
33
+ this.rawMsPK = getMultiSigPublicKey(config);
34
+ }
35
+
36
+ static fromMSafeAccountInfo(info: MSafeAccountInfo) {
37
+ const parsed: MultiSigConfig = {
38
+ threshold: info.threshold,
39
+ ownerWithWeight: info.ownersWithWeightPK,
40
+ creationNonce: info.creationNonce,
41
+ };
42
+ return new RawMultiSig(parsed);
43
+ }
44
+
45
+ get suiAddress(): string {
46
+ return this.rawMsPK.toSuiAddress();
47
+ }
48
+
49
+ get publicKeys(): PublicKeyWithWeight[] {
50
+ return this.rawMsPK.getPublicKeys();
51
+ }
52
+
53
+ get threshold(): number {
54
+ return this.config.threshold;
55
+ }
56
+
57
+ combinePartialSignatures(signatures: SerializedSignature[]): SerializedSignature {
58
+ return this.rawMsPK.combinePartialSignatures(signatures);
59
+ }
60
+
61
+ async verifyPersonalMessage(messageStr: string, multiSigSignature: SerializedSignature): Promise<boolean> {
62
+ const message = stringToBuffer(messageStr);
63
+ return this.rawMsPK.verifyPersonalMessage(message, multiSigSignature);
64
+ }
65
+ }
66
+
67
+ export function getMultiSigPublicKey(config: MultiSigConfig) {
68
+ const { ownerWithWeight, threshold, creationNonce } = config;
69
+ const pks = ownerWithWeight.map((pk) => ({
70
+ publicKey: pk.publicKey,
71
+ weight: pk.weight,
72
+ }));
73
+ if (creationNonce !== undefined) {
74
+ pks.push({
75
+ publicKey: makeNoncePublicKey(creationNonce),
76
+ weight: NONCE_PK_WEIGHT,
77
+ });
78
+ }
79
+ return MultiSigPublicKey.fromPublicKeys({ threshold, publicKeys: pks });
80
+ }
81
+
82
+ export function makeNoncePublicKey(nonce: number) {
83
+ const buffer = new ArrayBuffer(Ed25519PublicKey.SIZE);
84
+ const textEncoder = new TextEncoder();
85
+ textEncoder.encodeInto(NONCE_PK_PREFIX, new Uint8Array(buffer, 0, NONCE_PREFIX_MAX_SIZE));
86
+ const nonceView = new DataView(buffer, NONCE_PREFIX_MAX_SIZE, 4);
87
+ nonceView.setUint32(0, nonce, true);
88
+ return new Ed25519PublicKey(new Uint8Array(buffer));
89
+ }
90
+
91
+ export function validateMultiSigConfig(config: MultiSigConfig) {
92
+ config.ownerWithWeight.forEach((pk) => {
93
+ const { weight } = pk;
94
+ if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) {
95
+ throw new Error(`Invalid multi-sig weight: ${weight} (1-${MAX_WEIGHT})`);
96
+ }
97
+ });
98
+ const totalWeight = config.ownerWithWeight.reduce((s, pk) => s + pk.weight, 0);
99
+ if (config.threshold > totalWeight) {
100
+ throw new Error('Threshold is larger than total weight');
101
+ }
102
+ if (config.threshold < MIN_THRESHOLD) {
103
+ throw new Error('Threshold is smaller than 1');
104
+ }
105
+ const maxOwner = config.creationNonce === undefined ? MAX_OWNER_WITHOUT_NONCE : MAX_OWNER_WITH_NONCE;
106
+ if (config.ownerWithWeight.length > maxOwner) {
107
+ throw new Error('Owner number bigger than upper cap');
108
+ }
109
+ const addressSet = new Set(config.ownerWithWeight.map((pk) => pk.publicKey.toSuiAddress()));
110
+ if (addressSet.size !== config.ownerWithWeight.length) {
111
+ throw new Error('Duplicate address detected');
112
+ }
113
+ }
@@ -0,0 +1,90 @@
1
+ import { SuiClient, PaginatedTransactionResponse, PaginatedCoins, CoinStruct } from '@mysten/sui.js/client';
2
+ import { parseSerializedSignature, PublicKey, SerializedSignature } from '@mysten/sui.js/cryptography';
3
+ import { MultiSigPublicKey } from '@mysten/sui.js/multisig';
4
+
5
+ import { PublicKeySerde } from '@/utils/crypto';
6
+ import { Formatter } from '@/utils/format';
7
+
8
+ export const SUI_COIN = '0x2::sui::SUI';
9
+
10
+ export async function getPublicKeyFromChain(suiClient: SuiClient, address: string): Promise<PublicKey | undefined> {
11
+ let txs: PaginatedTransactionResponse;
12
+ try {
13
+ txs = await suiClient.queryTransactionBlocks({
14
+ // Disable naming rule since the variable is defined by Mysten
15
+ filter: { FromAddress: address },
16
+ options: { showInput: true },
17
+ limit: 2,
18
+ });
19
+ } catch (e) {
20
+ // Currently when there is no history transaction, will report an error:
21
+ // Error: byte deserialization failed, cause by: Odd number of digits
22
+ // TODO: remove the catch logic when sui part is fixed;
23
+ return undefined;
24
+ }
25
+
26
+ if (txs.data.length === 0 || !txs.data[0].transaction?.txSignatures) {
27
+ return undefined;
28
+ }
29
+ const tx = txs.data[0];
30
+ const signatures = tx.transaction?.txSignatures as string[];
31
+
32
+ for (let i = 0; i !== signatures.length; i++) {
33
+ const serializedSig = signatures[i];
34
+ const pk = getAddressFromSignatures(serializedSig, address);
35
+ if (!pk) {
36
+ continue;
37
+ }
38
+ return pk;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ function getAddressFromSignatures(serializedSig: SerializedSignature, targetAddress: string) {
44
+ const decoded = parseSerializedSignature(serializedSig);
45
+ switch (decoded.signatureScheme) {
46
+ case 'MultiSig': {
47
+ const multiSigAddress = new MultiSigPublicKey(decoded.multisig.multisig_pk).toSuiAddress();
48
+ if (Formatter.isSuiAddressEqual(multiSigAddress, targetAddress)) {
49
+ throw new Error('multi-sig wallet cannot be owner');
50
+ }
51
+ return undefined;
52
+ }
53
+ case 'ZkLogin': {
54
+ if (Formatter.isSuiAddressEqual(decoded.zkLogin.address, targetAddress)) {
55
+ throw new Error('ZkLogin wallet cannot be owner');
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ case 'ED25519':
61
+ case 'Secp256k1':
62
+ case 'Secp256r1': {
63
+ const pk = PublicKeySerde.de({ publicKey: decoded.publicKey, scheme: decoded.signatureScheme });
64
+ if (Formatter.isSuiAddressEqual(pk.toSuiAddress(), targetAddress)) {
65
+ return pk;
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ default:
71
+ throw new Error('Unknown schema');
72
+ }
73
+ }
74
+
75
+ export async function getAllCoins(input: { suiClient: SuiClient; owner: string; coinType: string | undefined }) {
76
+ let hasNext = true;
77
+ let cursor: string | undefined | null;
78
+ const res: CoinStruct[] = [];
79
+ while (hasNext) {
80
+ const currentPage: PaginatedCoins = await input.suiClient.getCoins({
81
+ owner: input.owner,
82
+ coinType: input.coinType,
83
+ cursor,
84
+ });
85
+ res.push(...currentPage.data);
86
+ hasNext = currentPage.hasNextPage;
87
+ cursor = currentPage.nextCursor;
88
+ }
89
+ return res;
90
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@msafe/sui3-sdk",
3
+ "version": "0.0.1",
4
+ "description": "SDK for MSafe SUI V3",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "source": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ }
12
+ },
13
+ "repository": "git@github.com:Momentum-Safe/msafe-sui3-sdk.git",
14
+ "author": "MSafe <admin@m-safe.io>",
15
+ "license": "MIT",
16
+ "devDependencies": {
17
+ "@types/jest": "^29.5.8",
18
+ "@types/node": "^20.9.2",
19
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
20
+ "@typescript-eslint/parser": "^6.5.0",
21
+ "eslint": "^8.48.0",
22
+ "eslint-config-airbnb-base": "^15.0.0",
23
+ "eslint-config-airbnb-typescript": "^17.1.0",
24
+ "eslint-config-prettier": "^9.0.0",
25
+ "eslint-import-resolver-alias": "^1.1.2",
26
+ "eslint-import-resolver-typescript": "^3.6.0",
27
+ "eslint-plugin-import": "^2.28.1",
28
+ "eslint-plugin-prettier": "^5.0.0",
29
+ "eslint-plugin-unused-imports": "^3.0.0",
30
+ "exponential-backoff": "^3.1.1",
31
+ "jest": "^29.7.0",
32
+ "prettier": "^3.0.3",
33
+ "ts-jest": "^29.1.1",
34
+ "ts-node": "^10.9.1",
35
+ "tsconfig-paths": "^4.2.0",
36
+ "tsup": "^8.0.1",
37
+ "typescript": "^5.2.2"
38
+ },
39
+ "scripts": {
40
+ "test": "jest",
41
+ "unit-ci": "TEST_ENV=UNIT jest",
42
+ "clean": "rm -rf ./dist",
43
+ "lint": "eslint . --ext .ts,.tsx",
44
+ "build": "yarn clean && yarn _build:node",
45
+ "_build:node": "tsup --format cjs,esm --dts",
46
+ "prerelease": "yarn build && chmod +x ./scripts/prerelease.sh && ./scripts/prerelease.sh"
47
+ },
48
+ "dependencies": {
49
+ "@msafe/sui3-model": "^1.0.9-pre-5a4e334.0",
50
+ "@mysten/sui.js": "0.46.1",
51
+ "reflect-metadata": "^0.1.13",
52
+ "typeorm": "0.3.17"
53
+ }
54
+ }
@@ -0,0 +1,94 @@
1
+ import { PseudoBackend } from '@/backend/PseudoBackend';
2
+ import { CreateHelper } from '@/core/CreateHelper';
3
+ import { MessageHelper } from '@/core/MessageHelper';
4
+ import { PublicKeyHelper } from '@/core/PublicKeyHelper';
5
+ import { MSafeGlobals } from '@/globals/MSafeGlobals';
6
+ import { buildCoinTransferTxb } from '@/transactions/coin-transfer';
7
+ import { SUI_COIN } from '@/utils/sui';
8
+
9
+ import { getWalletWithBalance, LocalWallet } from './account';
10
+
11
+ export class TestHelper {
12
+ // Note the connected wallet in globals is written by TestHelper as needed
13
+ constructor(public readonly globals: MSafeGlobals) {}
14
+
15
+ async registerWallet(localWallet: LocalWallet) {
16
+ const message = MessageHelper.welcomeMessage(new Date().toUTCString());
17
+ const signature = await localWallet.signPersonalMessage({ messageStr: message });
18
+ await this.backend.authSign({
19
+ address: await localWallet.address(),
20
+ message,
21
+ signature: signature.signature,
22
+ walletType: 'LocalWallet',
23
+ });
24
+ }
25
+
26
+ async init(wallet?: LocalWallet) {
27
+ await this.backend.db.coreModel.synchronize(true);
28
+ if (wallet) {
29
+ this.globals.connectWallet(wallet);
30
+ await this.registerWallet(wallet);
31
+ }
32
+ }
33
+
34
+ async close() {
35
+ this.backend.db.coreModel.close();
36
+ }
37
+
38
+ connectWallet(wallet: LocalWallet) {
39
+ this.globals.connectWallet(wallet);
40
+ }
41
+
42
+ async createMSafe(owners: LocalWallet[]) {
43
+ let creationNonce: number | undefined;
44
+ if (owners.length === 0) {
45
+ throw new Error('empty owners');
46
+ }
47
+ for (let i = 0; i < owners.length; i++) {
48
+ const owner = owners[i];
49
+ try {
50
+ const userInfo = await this.backend.getUserInfo(await owner.address());
51
+ if (i === 0) {
52
+ creationNonce = userInfo.creationNonce;
53
+ }
54
+ } catch (_) {
55
+ await this.registerWallet(owner);
56
+ if (i === 0) {
57
+ creationNonce = 0;
58
+ }
59
+ }
60
+ }
61
+ this.globals.connectWallet(owners[0]);
62
+ const ownerAddresses = await Promise.all(owners.map((owner) => owner.address()));
63
+
64
+ const createHelper = new CreateHelper(this.globals, new PublicKeyHelper(this.globals));
65
+ return createHelper.submitMSafeCreation({
66
+ ownerWithWeight: owners.map((owner, i) => ({
67
+ address: ownerAddresses[i],
68
+ weight: 1,
69
+ })),
70
+ threshold: owners.length,
71
+ name: 'test msafe',
72
+ creationNonce: creationNonce || 0,
73
+ });
74
+ }
75
+
76
+ async faucetWallet(recipient: string, amount: bigint) {
77
+ const fromWallet = await getWalletWithBalance();
78
+ const txb = await buildCoinTransferTxb({
79
+ suiClient: this.globals.suiClient,
80
+ sender: await fromWallet.address(),
81
+ intention: {
82
+ txType: 'CoinTransfer',
83
+ recipient,
84
+ coinType: SUI_COIN,
85
+ amount: amount.toString(),
86
+ },
87
+ });
88
+ await fromWallet.signAndExecuteTransactionBlock({ transactionBlock: txb });
89
+ }
90
+
91
+ private get backend() {
92
+ return this.globals.backend as PseudoBackend;
93
+ }
94
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ ExecuteTransactionRequestType,
3
+ SuiClient,
4
+ SuiTransactionBlockResponse,
5
+ SuiTransactionBlockResponseOptions,
6
+ } from '@mysten/sui.js/client';
7
+ import { Keypair, PublicKey } from '@mysten/sui.js/cryptography';
8
+ import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';
9
+ import { SignatureWithBytes } from '@mysten/sui.js/src/cryptography/keypair';
10
+ import { TransactionBlock } from '@mysten/sui.js/transactions';
11
+
12
+ import { IWallet } from '@/types/wallet';
13
+ import { stringToBuffer } from '@/utils/buffer';
14
+
15
+ import { TESTNET_SUI_CLIENT } from './config';
16
+ import { requestFaucetForTestnet } from './faucet';
17
+
18
+ // The implementation of LocalWallet is referenced from RawSigner
19
+ // By mysten. The interface is not exported thus we need another
20
+ // implementation here.
21
+ export class LocalWallet implements IWallet {
22
+ private readonly account: Keypair;
23
+
24
+ public readonly walletType = 'LocalWallet';
25
+
26
+ constructor(
27
+ public readonly suiClient: SuiClient,
28
+ privateKey?: string,
29
+ ) {
30
+ this.account = privateKey ? Ed25519Keypair.fromSecretKey(Buffer.from(privateKey, 'hex')) : new Ed25519Keypair();
31
+ }
32
+
33
+ async address(): Promise<string> {
34
+ return this.account.getPublicKey().toSuiAddress();
35
+ }
36
+
37
+ async publicKey(): Promise<PublicKey> {
38
+ return this.account.getPublicKey();
39
+ }
40
+
41
+ async signTransactionBlock(input: { transactionBlock: TransactionBlock | Uint8Array }): Promise<SignatureWithBytes> {
42
+ if (input.transactionBlock instanceof Uint8Array) {
43
+ return this.account.signTransactionBlock(input.transactionBlock);
44
+ }
45
+ const tbData = await input.transactionBlock.build({ client: this.suiClient });
46
+ return this.account.signTransactionBlock(tbData);
47
+ }
48
+
49
+ async signAndExecuteTransactionBlock(input: {
50
+ transactionBlock: Uint8Array | TransactionBlock;
51
+ requestType?: ExecuteTransactionRequestType;
52
+ options?: SuiTransactionBlockResponseOptions;
53
+ }): Promise<SuiTransactionBlockResponse> {
54
+ return this.suiClient.signAndExecuteTransactionBlock({
55
+ ...input,
56
+ signer: this.account,
57
+ });
58
+ }
59
+
60
+ async signPersonalMessage(input: { messageStr: string }): Promise<SignatureWithBytes> {
61
+ const message = stringToBuffer(input.messageStr);
62
+ return this.account.signPersonalMessage(message);
63
+ }
64
+
65
+ exportPrivateKey(): string {
66
+ return Buffer.from(this.account.export().privateKey, 'base64').toString('hex');
67
+ }
68
+
69
+ static createBatch(suiClient: SuiClient, numWallets: number) {
70
+ const wallets: LocalWallet[] = [];
71
+ for (let i = 0; i !== numWallets; i++) {
72
+ wallets.push(new LocalWallet(suiClient));
73
+ }
74
+ return wallets;
75
+ }
76
+ }
77
+
78
+ const FAUCET_WALLET = new LocalWallet(TESTNET_SUI_CLIENT);
79
+ let HAS_FAUCET: boolean = false;
80
+
81
+ export async function getWalletWithBalance() {
82
+ if (!HAS_FAUCET) {
83
+ await requestFaucetForTestnet(await FAUCET_WALLET.address());
84
+ }
85
+ HAS_FAUCET = true;
86
+ return FAUCET_WALLET;
87
+ }
@@ -0,0 +1,9 @@
1
+ import { SuiClient } from '@mysten/sui.js/client';
2
+
3
+ export const TESTNET_SUI_CLIENT_URL = 'https://sui-testnet.blockvision.org/v1/2Sgk89ivT64MnKdcGzjmyjY2ndD';
4
+
5
+ export const TESTNET_SUI_CLIENT = new SuiClient({ url: TESTNET_SUI_CLIENT_URL });
6
+
7
+ export const MAINNET_SUI_CLIENT_URL = 'https://sui-mainnet.blockvision.org/v1/2Sgk89ivT64MnKdcGzjmyjY2ndD';
8
+
9
+ export const MAINNET_SUI_CLIENT = new SuiClient({ url: MAINNET_SUI_CLIENT_URL });
@@ -0,0 +1,49 @@
1
+ import { exec } from 'child_process';
2
+
3
+ import { SuiClient } from '@mysten/sui.js/client';
4
+ import { backOff } from 'exponential-backoff';
5
+
6
+ import { TESTNET_SUI_CLIENT } from './config';
7
+
8
+ export async function requestFaucetForTestnet(recipient: string) {
9
+ await executeFaucet(recipient);
10
+ await waitForFaucet(TESTNET_SUI_CLIENT, recipient);
11
+ }
12
+
13
+ async function executeFaucet(recipient: string) {
14
+ return new Promise((resolve, reject) => {
15
+ const curlCommand = getFaucetCurlCommand(recipient);
16
+ exec(curlCommand, (error, stdout, stderr) => {
17
+ if (error) {
18
+ reject(error);
19
+ }
20
+ const res = JSON.parse(stdout);
21
+ if (res.error !== null) {
22
+ reject(new Error(`Unexpected response: ${res}`));
23
+ }
24
+ resolve(res);
25
+ });
26
+ });
27
+ }
28
+
29
+ async function waitForFaucet(client: SuiClient, recipient: string) {
30
+ return backOff(async () => {
31
+ const coins = await client.getCoins({
32
+ owner: recipient,
33
+ });
34
+ if (coins.data.length === 0) {
35
+ throw new Error('Address not faucet yet');
36
+ }
37
+ return coins;
38
+ });
39
+ }
40
+
41
+ function getFaucetCurlCommand(recipient: string) {
42
+ return `curl --location --request POST 'https://faucet.testnet.sui.io/v1/gas' \
43
+ --header 'Content-Type: application/json' \
44
+ --data-raw '{
45
+ "FixedAmountRequest": {
46
+ "recipient": "${recipient}"
47
+ }
48
+ }'`;
49
+ }