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

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.
@@ -1,94 +0,0 @@
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
- }
@@ -1,87 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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 });
@@ -1,49 +0,0 @@
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
- }
@@ -1,367 +0,0 @@
1
- // eslint-disable-next-line import/no-extraneous-dependencies
2
- import 'reflect-metadata';
3
-
4
- import { CoreModel, PendingTransaction } from '@msafe/sui3-model/core';
5
-
6
- import { WALLET_TYPE_KEY } from '@/backend/CoreDatabase';
7
- import { PseudoBackend } from '@/backend/PseudoBackend';
8
- import { MessageHelper } from '@/core/MessageHelper';
9
- import { MSafeEnv } from '@/globals/const';
10
- import { MSafeGlobals } from '@/globals/MSafeGlobals';
11
- import { IntentionCoinTransfer } from '@/transactions/coin-transfer';
12
- import { IntentionHelper, TxIntention } from '@/transactions/intention';
13
- import { HexToUint8Array } from '@/utils/buffer';
14
- import { SUI_COIN } from '@/utils/sui';
15
-
16
- import { LocalWallet } from '../../lib/account';
17
- import { TESTNET_SUI_CLIENT } from '../../lib/config';
18
- import { TestHelper } from '../../lib/TestHelper';
19
-
20
- // TODO: Move these tests cases to backend code.
21
- describe('PseudoBackend', () => {
22
- let backend: PseudoBackend;
23
- let globals: MSafeGlobals;
24
- let testHelper: TestHelper;
25
- const testWallet = new LocalWallet(TESTNET_SUI_CLIENT);
26
-
27
- beforeAll(async () => {
28
- globals = await MSafeGlobals.New(MSafeEnv.unit);
29
- testHelper = new TestHelper(globals);
30
- backend = globals.backend as PseudoBackend;
31
- await testHelper.init();
32
- });
33
-
34
- afterAll(async () => {
35
- await testHelper.close();
36
- });
37
-
38
- it('authSign success', async () => {
39
- const message = MessageHelper.welcomeMessage(new Date().toUTCString());
40
- const sig = await testWallet.signPersonalMessage({
41
- messageStr: message,
42
- });
43
- await backend.authSign({
44
- address: await testWallet.address(),
45
- message,
46
- signature: sig.signature,
47
- walletType: 'Unknown',
48
- });
49
-
50
- const userSetting = await backend.db.coreModel.userSetting.findOneBy({
51
- userAddress: await testWallet.address(),
52
- key: WALLET_TYPE_KEY,
53
- });
54
- expect(userSetting).not.toBeNull();
55
- expect(userSetting?.value).toBe('Unknown');
56
- });
57
-
58
- it('Invalid welcome message', async () => {
59
- const message = 'User Sign';
60
- const sig = await testWallet.signPersonalMessage({
61
- messageStr: message,
62
- });
63
- await expect(
64
- backend.authSign({
65
- address: await testWallet.address(),
66
- message: 'User sign',
67
- signature: sig.signature,
68
- walletType: 'Unknown',
69
- }),
70
- ).rejects.toThrow('Invalid welcome message');
71
- });
72
-
73
- it('user info no wallet', async () => {
74
- const user = await backend.getUserInfo(await testWallet.address());
75
- expect(user).toBeDefined();
76
- expect(user.address).toBe(await testWallet.address());
77
- });
78
-
79
- it('public key', async () => {
80
- const got = await backend.getPublicKey(await testWallet.address());
81
- expect(got!.toSuiAddress()).toEqual(await testWallet.address());
82
- });
83
-
84
- it('public key not found', async () => {
85
- expect(await backend.getPublicKey('0x1234')).toBeUndefined();
86
- });
87
-
88
- it('create MSafe', async () => {
89
- const wallets = [new LocalWallet(globals.suiClient), new LocalWallet(globals.suiClient)];
90
- const msafeAddr = await testHelper.createMSafe(wallets);
91
- const msafe = await backend.db.coreModel.msafe.findOneBy({ address: msafeAddr });
92
- expect(msafe !== null).toBeTruthy();
93
-
94
- const userInfo = await backend.getUserInfo(await wallets[0].address());
95
- expect(userInfo.ownedMSafe.length).toBe(1);
96
- expect(userInfo.creationNonce).toBe(1);
97
- });
98
- });
99
-
100
- describe('PseudoBackend transaction related', () => {
101
- let globals: MSafeGlobals;
102
- let backend: PseudoBackend;
103
- let testHelper: TestHelper;
104
- let testWallets: LocalWallet[];
105
- let msafeAddress: string;
106
- let coreModel: CoreModel;
107
- let userAddress: string;
108
- let userWallet: LocalWallet;
109
-
110
- beforeAll(async () => {
111
- globals = await MSafeGlobals.New(MSafeEnv.unit);
112
- testHelper = new TestHelper(globals);
113
- backend = globals.backend as PseudoBackend;
114
-
115
- testWallets = Array.from({ length: 3 }, () => new LocalWallet(globals.suiClient));
116
- await testHelper.init(testWallets[0]);
117
- msafeAddress = await testHelper.createMSafe(testWallets);
118
-
119
- await testHelper.faucetWallet(msafeAddress, 100000n);
120
-
121
- coreModel = backend.db.coreModel;
122
- userAddress = await testWallets[0].address();
123
- [userWallet] = testWallets;
124
- });
125
-
126
- afterAll(async () => {
127
- await testHelper.close();
128
- });
129
-
130
- it('Initialization status check', async () => {
131
- const histories = await coreModel.historyTransaction.findBy({ msafeAddress });
132
- expect(histories.length).toBe(0);
133
-
134
- const pendings = await coreModel.pendingTransaction.findBy({ msafeAddress });
135
- expect(pendings.length).toBe(0);
136
-
137
- const intentions = await coreModel.transactionIntention.findBy({ msafeAddress });
138
- expect(intentions.length).toBe(0);
139
-
140
- const votes = await coreModel.userVote.findBy({ msafeAddress });
141
- expect(votes.length).toBe(0);
142
- });
143
-
144
- it('Propose an intention', async () => {
145
- const testIntention: TxIntention = {
146
- txType: 'CoinTransfer',
147
- recipient: await testWallets[0].address(),
148
- amount: '100',
149
- coinType: SUI_COIN,
150
- };
151
- const message = MessageHelper.proposeIntentionMessage({ intention: testIntention, sn: 0 });
152
- const signature = await testWallets[0].signPersonalMessage({ messageStr: message });
153
- await backend.proposeIntention({
154
- intention: testIntention,
155
- sequenceNumber: 0,
156
- userAddress,
157
- msafeAddress,
158
- signature: signature.signature,
159
- });
160
-
161
- await backend.buildNextIntentionAndAddToPending({ msafeAddress });
162
-
163
- const pendings = await coreModel.pendingTransaction.findBy({ msafeAddress });
164
- expect(pendings.length).toBe(1);
165
- const intentions = await coreModel.transactionIntention.findBy({ msafeAddress });
166
- expect(intentions.length).toBe(1);
167
- });
168
-
169
- it('Propose more intention will add to intention not pendings', async () => {
170
- const testIntention: IntentionCoinTransfer = {
171
- txType: 'CoinTransfer',
172
- recipient: await testWallets[0].address(),
173
- amount: '200',
174
- coinType: SUI_COIN,
175
- };
176
- const message2 = MessageHelper.proposeIntentionMessage({ intention: testIntention, sn: 1 });
177
- const signature2 = await testWallets[0].signPersonalMessage({ messageStr: message2 });
178
- await backend.proposeIntention({
179
- intention: testIntention,
180
- sequenceNumber: 1,
181
- userAddress,
182
- msafeAddress,
183
- signature: signature2.signature,
184
- });
185
-
186
- const intentions = await coreModel.transactionIntention.findBy({ msafeAddress });
187
- expect(intentions.length).toBe(2);
188
- const pendings = await coreModel.pendingTransaction.findBy({ msafeAddress });
189
- expect(pendings.length).toBe(1);
190
- });
191
-
192
- it('Vote for transaction', async () => {
193
- const pending = await coreModel.pendingTransaction.findOneBy({ msafeAddress });
194
- if (pending === null) {
195
- throw new Error('Null pending');
196
- }
197
- const payload = HexToUint8Array(pending.payload);
198
- const signature = await testWallets[0].signTransactionBlock({ transactionBlock: payload });
199
- await backend.voteForTransaction({
200
- msafeAddress,
201
- userAddress,
202
- txDigest: pending.digest,
203
- signature: signature.signature,
204
- });
205
-
206
- // Check votes, user shall have 1 valid vote.
207
- const userVotes = await coreModel.userVote.findBy({
208
- userAddress,
209
- msafeAddress,
210
- isValid: true,
211
- });
212
- expect(userVotes.length).toBe(1);
213
- expect(userVotes[0].msafeAddress).toBe(msafeAddress);
214
- });
215
-
216
- it('rejectCurrentTx', async () => {
217
- const pendingsTxs = await backend.getPendingTransactions(msafeAddress);
218
- const payloadToReject = pendingsTxs[0].payload;
219
- const txb = await IntentionHelper.buildRejectTransaction({ msafeAddress, payloadToReject });
220
- const digest = await txb.getDigest({ client: globals.suiClient });
221
- const signature = await testWallets[0].signTransactionBlock({ transactionBlock: txb });
222
- await backend.rejectCurrentTx({
223
- msafeAddress,
224
- userAddress,
225
- digest,
226
- signature: signature.signature,
227
- });
228
- const pendings = await coreModel.pendingTransaction.findBy({ msafeAddress });
229
- expect(pendings.length).toBe(2);
230
- expect(pendings.find((pending) => pending.isRejectTx)).toBeDefined();
231
- expect(pendings.find((pending) => !pending.isRejectTx)).toBeDefined();
232
-
233
- const rejectTx = pendings.find((pending) => pending.isRejectTx) as PendingTransaction;
234
- const rejectVote = await coreModel.userVote.findOneBy({
235
- userAddress,
236
- msafeAddress,
237
- txDigest: rejectTx.digest,
238
- isValid: true,
239
- });
240
- expect(rejectVote === null).toBeFalsy();
241
-
242
- // Approve vote need to be invalid
243
- const approveTx = pendings.find((pending) => !pending.isRejectTx) as PendingTransaction;
244
- const invalidApprove = await coreModel.userVote.findOneBy({
245
- txDigest: approveTx.digest,
246
- userAddress,
247
- msafeAddress,
248
- });
249
- expect(invalidApprove === null).toBeFalsy();
250
- });
251
-
252
- it('vote overwrite', async () => {
253
- const pending = await coreModel.pendingTransaction.findOneBy({
254
- msafeAddress,
255
- isRejectTx: false,
256
- });
257
- if (pending === null) {
258
- throw new Error('pending tx not exist');
259
- }
260
- const { signature } = await userWallet.signTransactionBlock({ transactionBlock: HexToUint8Array(pending.payload) });
261
- await backend.voteForTransaction({
262
- txDigest: pending.digest,
263
- msafeAddress,
264
- userAddress,
265
- signature,
266
- });
267
-
268
- const vote = await coreModel.userVote.findOneBy({
269
- msafeAddress,
270
- txDigest: pending.digest,
271
- });
272
- expect(vote).not.toBeNull();
273
- expect(vote!.isValid).toBeTruthy();
274
-
275
- // Another pending transaction with isRejectTx = true, shall have
276
- // user vote with isValid = false.
277
- const rejectPending = await coreModel.pendingTransaction.findOneBy({
278
- msafeAddress,
279
- isRejectTx: true,
280
- });
281
- if (rejectPending === null) {
282
- throw new Error('Test cases must execute in sequence');
283
- }
284
- const userVote = await coreModel.userVote.findOneBy({
285
- msafeAddress,
286
- userAddress,
287
- txDigest: rejectPending.digest,
288
- });
289
- expect(userVote).not.toBeNull();
290
- expect(userVote!.isValid).toBeFalsy();
291
- });
292
-
293
- it('processExecutedTransaction', async () => {
294
- const oldPendings = await coreModel.pendingTransaction.findBy({
295
- msafeAddress,
296
- });
297
- expect(oldPendings.length).toBe(2);
298
-
299
- const executedPending = oldPendings.find((pending) => pending.isRejectTx);
300
- const rejectedPending = oldPendings.find((pending) => !pending.isRejectTx);
301
- expect(executedPending).toBeDefined();
302
- expect(rejectedPending).toBeDefined();
303
- if (!executedPending || !rejectedPending) {
304
- throw new Error('pendings not found');
305
- }
306
-
307
- // Process one transaction.
308
- await backend.processExecutedTransaction(executedPending.digest);
309
- await backend.buildNextIntentionAndAddToPending({ msafeAddress });
310
-
311
- // Transaction has been added to history
312
- const histories = await coreModel.historyTransaction.findBy({ msafeAddress });
313
- expect(histories.length).toBe(2);
314
- expect(
315
- histories.find((history) => history.status === 'success' && history.digest === executedPending.digest),
316
- ).toBeDefined();
317
- expect(
318
- histories.find((history) => history.status === 'rejected' && history.digest === rejectedPending.digest),
319
- ).toBeDefined();
320
-
321
- // Intentions has been updated
322
- const intentions = await coreModel.transactionIntention.findBy({
323
- msafeAddress,
324
- sequenceNumber: executedPending.sequenceNumber,
325
- });
326
- expect(intentions.length).toBe(1);
327
- expect(intentions[0].status).toBe('processed');
328
- });
329
-
330
- it('proposePendingTransaction', async () => {
331
- const testIntention: IntentionCoinTransfer = {
332
- txType: 'CoinTransfer',
333
- recipient: await testWallets[0].address(),
334
- amount: '300',
335
- coinType: SUI_COIN,
336
- };
337
- const pendings = await coreModel.pendingTransaction.findBy({ msafeAddress });
338
- expect(pendings.length).toBe(1);
339
- await backend.processExecutedTransaction(pendings[0].digest);
340
- const txb = await IntentionHelper.buildTxb({
341
- suiClient: globals.suiClient,
342
- intention: testIntention,
343
- sender: msafeAddress,
344
- });
345
- const digest = await txb.getDigest({ client: globals.suiClient });
346
- const signature = await userWallet.signTransactionBlock({ transactionBlock: txb });
347
- await backend.proposePendingTransaction({
348
- intention: testIntention,
349
- userAddress,
350
- msafeAddress,
351
- digest,
352
- signature: signature.signature,
353
- });
354
-
355
- const intentions = await coreModel.transactionIntention.findBy({
356
- msafeAddress,
357
- sequenceNumber: pendings[0].sequenceNumber + 1,
358
- });
359
- expect(intentions.length).toBe(1);
360
-
361
- const newPendings = await coreModel.pendingTransaction.findBy({
362
- msafeAddress,
363
- });
364
- expect(newPendings.length).toBe(1);
365
- expect(newPendings[0].sequenceNumber).toBe(pendings[0].sequenceNumber + 1);
366
- });
367
- });