@msafe/sui3-sdk 0.0.21 → 0.0.23

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.
@@ -31,9 +31,10 @@ import {
31
31
  } from '@msafe/sui3-utils';
32
32
  import { SerializedSignature } from '@mysten/sui.js/cryptography';
33
33
  import { PublicKey } from '@mysten/sui.js/src/cryptography';
34
- import axios from 'axios';
34
+ import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
35
35
 
36
36
  import { IBackend } from '@/backend/interface';
37
+ import { addPrefix } from '@/utils';
37
38
 
38
39
  export class BackendImpl implements IBackend {
39
40
  private _token: JWTToken;
@@ -41,18 +42,15 @@ export class BackendImpl implements IBackend {
41
42
  constructor(private readonly apiURL: string) {}
42
43
 
43
44
  async authSign(input: IAuthLoginReq): Promise<JWTToken> {
44
- const res = await axios.post<IAuthLoginResp>(`${this.apiURL}/auth/login`, input);
45
- // TODO unify response struct
46
- if (res.status !== 200 && res.status !== 201) {
47
- throw new Error(`invalid authSign return: ${res}`);
48
- }
45
+ const res = await this.post<IAuthLoginResp>(`/auth/login`, input);
46
+
49
47
  this._token = res.data.accessToken;
50
48
  return this._token;
51
49
  }
52
50
 
53
51
  async verifyToken(jwt: JWTToken): Promise<boolean> {
54
52
  try {
55
- const res = await axios.get(`${this.apiURL}/auth`, { headers: this.headers(jwt) });
53
+ const res = await this.get(`/auth`, { headers: this.headers(jwt) });
56
54
  return res.status === 200;
57
55
  } catch (_) {
58
56
  return false;
@@ -71,15 +69,10 @@ export class BackendImpl implements IBackend {
71
69
  const query: IGetPublicKeyBatchQuery = {
72
70
  userAddressList: addresses,
73
71
  };
74
- const res = await axios.get<IGetPublicKeyBatchResp>(`${this.apiURL}/user/public-keys`, {
72
+ const res = await this.get<IGetPublicKeyBatchResp>(`/user/public-keys`, {
75
73
  params: query,
76
74
  headers: this.headers(),
77
75
  });
78
-
79
- // TODO unify response struct
80
- if (res.status !== 200 && res.status !== 201) {
81
- throw new Error(`invalid getPublicKeyBatch return: ${res}`);
82
- }
83
76
  return res.data?.map((publicKeyWithSchema) =>
84
77
  publicKeyWithSchema ? PublicKeySerde.de(publicKeyWithSchema) : undefined,
85
78
  );
@@ -89,25 +82,17 @@ export class BackendImpl implements IBackend {
89
82
  const q: IGetMSafeQuery = {
90
83
  msafeAddress,
91
84
  };
92
- const res = await axios.get<IMSafeInfoResp>(`${this.apiURL}/msafe`, {
85
+ const res = await this.get<IMSafeInfoResp>(`/msafe`, {
93
86
  params: q,
94
87
  headers: this.headers(),
95
88
  });
96
- // TODO unify response struct
97
- if (res.status !== 200 && res.status !== 201) {
98
- throw new Error(`invalid getPublicKeyBatch return: ${res}`);
99
- }
100
89
  return BackendImpl.toMSafeConfig(res.data);
101
90
  }
102
91
 
103
92
  async getUserInfo(): Promise<IUserInfoResp> {
104
- const userRes = await axios.get<IUserInfoResp>(`${this.apiURL}/user`, {
93
+ const userRes = await this.get<IUserInfoResp>(`/user`, {
105
94
  headers: this.headers(),
106
95
  });
107
- // TODO unify response struct
108
- if (userRes.status !== 200 && userRes.status !== 201) {
109
- throw new Error(`invalid getPublicKeyBatch return: ${userRes}`);
110
- }
111
96
  return userRes.data;
112
97
  }
113
98
 
@@ -124,13 +109,10 @@ export class BackendImpl implements IBackend {
124
109
  }
125
110
  : {}),
126
111
  };
127
- const res = await axios.get<IPagedResult<IMSafeInfoResp>>(`${this.apiURL}/msafe/owned`, {
112
+ const res = await this.get<IPagedResult<IMSafeInfoResp>>(`/msafe/owned`, {
128
113
  params: q,
129
114
  headers: this.headers(),
130
115
  });
131
- if (res.status !== 200 && res.status !== 201) {
132
- throw new Error(`invalid getOwnedMSafeByStatus return: ${res}`);
133
- }
134
116
  return {
135
117
  data: res.data.data.map(BackendImpl.toMSafeConfig),
136
118
  meta: res.data.meta,
@@ -139,85 +121,61 @@ export class BackendImpl implements IBackend {
139
121
 
140
122
  async updateMSafeStatus(input: { msafeAddress: string; status: UserMSafeStatus }): Promise<void> {
141
123
  const p: IUpdateMSafeStatusReq = input;
142
- const res = await axios.post(`${this.apiURL}/msafe/status`, p, { headers: this.headers() });
143
- if (res.status !== 200 && res.status !== 201) {
144
- throw new Error(`Invalid updateMSafeStatus return: ${res}`);
145
- }
124
+ await this.post(`/msafe/status`, p, { headers: this.headers() });
146
125
  }
147
126
 
148
127
  async getPendingTransactions(input: IGetPendingTransactionRequest): Promise<GetPendingTransactionResponse> {
149
- const res = await axios.get<GetPendingTransactionResponse>(`${this.apiURL}/transaction/pending`, {
128
+ const res = await this.get<GetPendingTransactionResponse>(`/transaction/pending`, {
150
129
  params: input,
151
130
  headers: this.headers(),
152
131
  });
153
- if (res.status !== 200 && res.status !== 201) {
154
- throw new Error(`invalid getPublicKeyBatch return: ${res}`);
155
- }
156
132
  return res.data;
157
133
  }
158
134
 
159
135
  async getHistoryTransactions(input: IGetHistoryTransactionsRequest): Promise<GetHistoryTransactionsResponse> {
160
- const res = await axios.get<GetHistoryTransactionsResponse>(`${this.apiURL}/transaction/history`, {
136
+ const res = await this.get<GetHistoryTransactionsResponse>(`/transaction/history`, {
161
137
  params: input,
162
138
  headers: this.headers(),
163
139
  });
164
- if (res.status !== 200 && res.status !== 201) {
165
- throw new Error(`invalid getCurrentSequenceNumber return: ${res}`);
166
- }
167
140
  return res.data;
168
141
  }
169
142
 
170
143
  async getFutureIntentions(input: IGetIntentionsRequest): Promise<GetTransactionIntentionsResponse> {
171
- const res = await axios.get<GetTransactionIntentionsResponse>(`${this.apiURL}/transaction/intention`, {
144
+ const res = await this.get<GetTransactionIntentionsResponse>(`/transaction/intention`, {
172
145
  params: input,
173
146
  headers: this.headers(),
174
147
  });
175
- if (res.status !== 200 && res.status !== 201) {
176
- throw new Error(`invalid getCurrentSequenceNumber return: ${res}`);
177
- }
178
148
  return res.data;
179
149
  }
180
150
 
181
151
  async getCurrentSequenceNumber(address: string): Promise<number> {
182
- const res = await axios.get<number>(`${this.apiURL}/transaction/sn/current`, {
152
+ const res = await this.get<number>(`/transaction/sn/current`, {
183
153
  params: {
184
154
  msafeAddress: address,
185
155
  },
186
156
  headers: this.headers(),
187
157
  });
188
- if (res.status !== 200 && res.status !== 201) {
189
- throw new Error(`invalid getCurrentSequenceNumber return: ${res}`);
190
- }
191
158
  return res.data;
192
159
  }
193
160
 
194
161
  async getNextSequenceNumber(address: string): Promise<number> {
195
- const res = await axios.get<number>(`${this.apiURL}/transaction/sn/next`, {
162
+ const res = await this.get<number>(`/transaction/sn/next`, {
196
163
  params: {
197
164
  msafeAddress: address,
198
165
  },
199
166
  headers: this.headers(),
200
167
  });
201
- if (res.status !== 200 && res.status !== 201) {
202
- throw new Error(`invalid getNextSequenceNumber return: ${res}`);
203
- }
204
168
  return res.data;
205
169
  }
206
170
 
207
171
  async createMSafeAccount(input: ICreateMSafeReq): Promise<void> {
208
- const res = await axios.post(`${this.apiURL}/msafe/create`, input, {
172
+ await this.post(`/msafe/create`, input, {
209
173
  headers: this.headers(),
210
174
  });
211
- if (res.status !== 200 && res.status !== 201) {
212
- throw new Error(`invalid createMSafeAccount return: ${res}`);
213
- }
214
175
  }
215
176
 
216
177
  async proposeIntention(input: IProposeIntentionRequest): Promise<void> {
217
- const res = await axios.post(`${this.apiURL}/transaction/intention`, input, { headers: this.headers() });
218
- if (res.status !== 200 && res.status !== 201) {
219
- throw new Error(`invalid proposeIntention return: ${res}`);
220
- }
178
+ await this.post(`/transaction/intention`, input, { headers: this.headers() });
221
179
  }
222
180
 
223
181
  // TODO later
@@ -233,58 +191,35 @@ export class BackendImpl implements IBackend {
233
191
  }
234
192
 
235
193
  async rejectCurrentTx(input: IVoteTransactionRequest) {
236
- const res = await axios.post(`${this.apiURL}/transaction/pending/reject`, input, {
194
+ await this.post(`/transaction/pending/reject`, input, {
237
195
  headers: this.headers(),
238
196
  });
239
- if (res.status !== 200 && res.status !== 201) {
240
- throw new Error(`invalid voteForTransaction return: ${res}`);
241
- }
242
197
  }
243
198
 
244
199
  async voteForTransaction(input: IVoteTransactionRequest) {
245
- const res = await axios.post(`${this.apiURL}/transaction/pending/vote`, input, {
200
+ await this.post(`/transaction/pending/vote`, input, {
246
201
  headers: this.headers(),
247
202
  });
248
- if (res.status !== 200 && res.status !== 201) {
249
- throw new Error(`invalid voteForTransaction return: ${res}`);
250
- }
251
203
  }
252
204
 
253
205
  async buildNextIntentionAndAddToPending(input: IBuildTransactionRequest) {
254
- const res = await axios.post(`${this.apiURL}/transaction/pending/build`, input, { headers: this.headers() });
255
- if (res.status !== 200 && res.status !== 201) {
256
- throw new Error(`invalid buildNextIntentionAndAddToPending return: ${res}`);
257
- }
206
+ await this.post(`/transaction/pending/build`, input, { headers: this.headers() });
258
207
  }
259
208
 
260
209
  async skipNextFailedIntention(input: ISkipIntentionRequest) {
261
- const res = await axios.post(`${this.apiURL}/transaction/pending/skip`, input, { headers: this.headers() });
262
- if (res.status !== 200 && res.status !== 201) {
263
- throw new Error(`invalid skipNextFailedIntention return: ${res}`);
264
- }
210
+ await this.post(`/transaction/pending/skip`, input, { headers: this.headers() });
265
211
  }
266
212
 
267
213
  async getAddressBookEntries(pagination?: IPageOptions): Promise<IGetAddressBookResult> {
268
- const res = await axios.get<IGetAddressBookResult>(`${this.apiURL}/address-book`, {
214
+ const res = await this.get<IGetAddressBookResult>(`/address-book`, {
269
215
  headers: this.headers(),
270
216
  params: pagination,
271
217
  });
272
- if (res.status !== 200) {
273
- throw new Error(`Invalid address-book return: ${res}`);
274
- }
275
218
  return res.data;
276
219
  }
277
220
 
278
221
  async updateAddressBook(input: { updates: UpdateAddressBookEntry[]; signature: SerializedSignature }) {
279
- const res = await axios.post(`${this.apiURL}/address-book`, input, { headers: this.headers() });
280
- if (res.status !== 200 && res.status !== 201) {
281
- throw new Error(`invalid updateAddressBook return: ${res}`);
282
- }
283
- }
284
-
285
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
286
- async processExecutedTransaction(_digest: string) {
287
- return undefined;
222
+ await this.post(`/address-book`, input, { headers: this.headers() });
288
223
  }
289
224
 
290
225
  private headers(token?: string) {
@@ -304,4 +239,69 @@ export class BackendImpl implements IBackend {
304
239
  })),
305
240
  };
306
241
  }
242
+
243
+ private async get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R> {
244
+ const fullUrl = this.getFullUrl(url);
245
+ try {
246
+ return await axios.get<T, R, D>(fullUrl, config);
247
+ } catch (e: any) {
248
+ throw BackendError.fromError(e) ?? e;
249
+ }
250
+ }
251
+
252
+ private async post<T = any, R = AxiosResponse<T>, D = any>(
253
+ url: string,
254
+ data?: D,
255
+ config?: AxiosRequestConfig<D>,
256
+ ): Promise<R> {
257
+ const fullUrl = this.getFullUrl(url);
258
+ try {
259
+ return await axios.post<T, R, D>(fullUrl, data, config);
260
+ } catch (e: unknown) {
261
+ throw BackendError.fromError(e) ?? e;
262
+ }
263
+ }
264
+
265
+ private getFullUrl(url: string) {
266
+ return url.startsWith(this.apiURL) ? url : `${this.apiURL}${addPrefix(url, '/')}`;
267
+ }
268
+ }
269
+
270
+ export class BackendError extends Error {
271
+ constructor(public readonly e: AxiosError) {
272
+ super();
273
+ Error.captureStackTrace(this, this.constructor);
274
+ }
275
+
276
+ public readonly name: 'Backend';
277
+
278
+ static fromError(e: unknown) {
279
+ if (axios.isAxiosError(e) && e?.response?.data && 'message' in e.response.data) {
280
+ return new BackendError(e as AxiosError);
281
+ }
282
+ return undefined;
283
+ }
284
+
285
+ get status() {
286
+ return this.e.response?.status ?? undefined;
287
+ }
288
+
289
+ get message(): string {
290
+ return `Request to ${this.endpoint} failed: ${this.status} ${this.respMessage() ?? 'Unknown resp'}`;
291
+ }
292
+
293
+ private respMessage() {
294
+ if (!this.e.response?.data) {
295
+ return undefined;
296
+ }
297
+ return (this.e.response?.data as any).message ?? undefined;
298
+ }
299
+
300
+ get endpoint() {
301
+ return this.e.config?.url ?? '';
302
+ }
303
+
304
+ toString() {
305
+ return this.message;
306
+ }
307
307
  }
@@ -57,7 +57,6 @@ export interface IBackend {
57
57
  voteForTransaction(input: IVoteTransactionRequest): Promise<void>;
58
58
  buildNextIntentionAndAddToPending(input: IBuildTransactionRequest): Promise<void>;
59
59
  skipNextFailedIntention(input: ISkipIntentionRequest): Promise<void>;
60
- processExecutedTransaction(digest: string): Promise<void>;
61
60
 
62
61
  getAddressBookEntries(pagination?: IPageOptions): Promise<IGetAddressBookResult>;
63
62
  updateAddressBook(input: { updates: UpdateAddressBookEntry[]; signature: SerializedSignature }): Promise<void>;
@@ -11,6 +11,7 @@ import {
11
11
  TxIntention,
12
12
  buildObjectTransferTxb,
13
13
  buildRejectTxb,
14
+ isSameAddress,
14
15
  } from '@msafe/sui3-utils';
15
16
  import { SuiObjectData } from '@mysten/sui.js/client';
16
17
  import { SerializedSignature } from '@mysten/sui.js/cryptography';
@@ -76,6 +77,7 @@ export class MSafeAccount {
76
77
  return getAllOwnedObjects(this.suiClient, this.address, filterCoinObjectOptions);
77
78
  }
78
79
 
80
+ // TODO: Calculate the votes
79
81
  async pendingTransaction(): Promise<PendingTx | undefined> {
80
82
  const res = await this.backend.getPendingTransactions({ msafeAddress: this.address });
81
83
  if (!res.pending) {
@@ -98,13 +100,16 @@ export class MSafeAccount {
98
100
  sequenceNumber: pendingTx.sequenceNumber,
99
101
  payload: pendingTx.payload,
100
102
  votes: pendingTx.votes,
103
+ approvalWeight: this.calculateWeightFromVotes(pendingTx.votes),
101
104
  msafeAddress: pendingTx.msafeAddress,
102
105
  rejectDigest: rejectPending?.digest ?? '',
103
106
  rejectPayload: rejectPending?.payload ?? '',
104
107
  rejectVotes: rejectPending?.votes ?? [],
108
+ rejectWeight: this.calculateWeightFromVotes(rejectPending?.votes ?? []),
105
109
  };
106
110
  }
107
111
 
112
+ // TODO: Calculate the votes.
108
113
  async historyTransaction(pagination?: Pagination) {
109
114
  return this.backend.getHistoryTransactions({
110
115
  msafeAddress: this.address,
@@ -241,28 +246,24 @@ export class MSafeAccount {
241
246
  };
242
247
  }
243
248
 
244
- async executePendingTx(pending: PendingTx) {
245
- let gotSigs: Map<string, string>;
246
- let payload: string;
247
- if (pending.votes.length >= this.info.threshold) {
248
- gotSigs = new Map(pending.votes.map((vote) => [vote.userAddress, vote.signature]));
249
- payload = pending.payload;
250
- } else if (pending.rejectVotes && pending.rejectPayload && pending.rejectVotes?.length >= this.info.threshold) {
251
- gotSigs = new Map(pending.rejectVotes.map((vote) => [vote.userAddress, vote.signature]));
252
- payload = pending.rejectPayload;
253
- } else {
254
- throw new Error('Not enough signatures');
249
+ async executePendingTx(pending: PendingTx, isRejectTx: boolean = false) {
250
+ const votes = isRejectTx ? pending.rejectVotes : pending.votes;
251
+ const payload = isRejectTx ? pending.rejectPayload : pending.payload;
252
+
253
+ if (this.calculateWeightFromVotes(votes) < this.info.threshold) {
254
+ throw new Error('Not enough signature');
255
255
  }
256
256
 
257
- const sigs: SerializedSignature[] = [];
257
+ const sortedSigs: SerializedSignature[] = [];
258
+ const gotSigs = new Map(votes.map((vote) => [vote.userAddress, vote.signature]));
258
259
  for (let i = 0; i < this.info.owners.length; i++) {
259
260
  const owner = this.info.owners[i];
260
261
  const signature = gotSigs.get(owner.address);
261
262
  if (signature) {
262
- sigs.push(signature);
263
+ sortedSigs.push(signature);
263
264
  }
264
265
  }
265
- const multiSignature = this.multiSig.combinePartialSignatures(sigs);
266
+ const multiSignature = this.multiSig.combinePartialSignatures(sortedSigs);
266
267
  return this.suiClient.executeTransactionBlock({
267
268
  transactionBlock: HexToUint8Array(payload),
268
269
  signature: multiSignature,
@@ -270,6 +271,22 @@ export class MSafeAccount {
270
271
  });
271
272
  }
272
273
 
274
+ private calculateWeightFromVotes(votes: { userAddress: string }[]) {
275
+ return this.calculateWeight(votes.map((vote) => vote.userAddress));
276
+ }
277
+
278
+ private calculateWeight(addressList: string[]) {
279
+ let gotWeight = 0;
280
+
281
+ for (let i = 0; i < addressList.length; i++) {
282
+ const found = this.info.owners.find((owner) => isSameAddress(owner.address, addressList[i]));
283
+ if (found) {
284
+ gotWeight += found.weight;
285
+ }
286
+ }
287
+ return gotWeight;
288
+ }
289
+
273
290
  get address() {
274
291
  return this.multiSig.address;
275
292
  }
@@ -12,9 +12,11 @@ export interface PendingTx {
12
12
  creator: string;
13
13
  createdAt: Date;
14
14
  sequenceNumber: number;
15
+ approvalWeight: number;
15
16
  votes: PendingVote[];
16
17
  rejectDigest: string;
17
18
  rejectPayload: string;
19
+ rejectWeight: number;
18
20
  rejectVotes: PendingVote[];
19
21
  }
20
22
 
@@ -23,3 +23,10 @@ export class Formatter {
23
23
  return Coin.isCoin(struct);
24
24
  }
25
25
  }
26
+
27
+ export function addPrefix(s: string, prefix: string) {
28
+ if (s.startsWith(prefix)) {
29
+ return s;
30
+ }
31
+ return prefix + s;
32
+ }