• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-core-sdk / 11029255652

25 Sep 2024 08:18AM CUT coverage: 77.534%. Remained the same
11029255652

push

github

dasanra
chore: set release versions

261 of 410 branches covered (63.66%)

Branch coverage included in aggregate %.

978 of 1188 relevant lines covered (82.32%)

3.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.02
/packages/api-kit/src/SafeApiKit.ts
1
import {
2
  AddMessageProps,
3
  AddSafeDelegateProps,
4
  AddSafeOperationProps,
5
  AllTransactionsListResponse,
6
  AllTransactionsOptions,
7
  DeleteSafeDelegateProps,
8
  GetSafeDelegateProps,
9
  GetSafeMessageListProps,
10
  GetSafeOperationListProps,
11
  GetSafeOperationListResponse,
12
  ListOptions,
13
  ModulesResponse,
14
  OwnerResponse,
15
  ProposeTransactionProps,
16
  SafeCreationInfoResponse,
17
  SafeDelegateListResponse,
18
  SafeInfoResponse,
19
  SafeMessage,
20
  SafeMessageListResponse,
21
  SafeModuleTransactionListResponse,
22
  SafeMultisigTransactionEstimate,
23
  SafeMultisigTransactionEstimateResponse,
24
  SafeMultisigTransactionListResponse,
25
  SafeServiceInfoResponse,
26
  SafeSingletonResponse,
27
  SignatureResponse,
28
  SignedSafeDelegateResponse,
29
  TokenInfoListResponse,
30
  TokenInfoResponse,
31
  TransferListResponse
32
} from '@safe-global/api-kit/types/safeTransactionServiceTypes'
33
import { HttpMethod, sendRequest } from '@safe-global/api-kit/utils/httpRequests'
1✔
34
import { signDelegate } from '@safe-global/api-kit/utils/signDelegate'
1✔
35
import { validateEip3770Address, validateEthereumAddress } from '@safe-global/protocol-kit'
1✔
36
import {
1✔
37
  Eip3770Address,
38
  isSafeOperation,
39
  SafeMultisigConfirmationListResponse,
40
  SafeMultisigTransactionResponse,
41
  SafeOperation,
42
  SafeOperationConfirmationListResponse,
43
  SafeOperationResponse
44
} from '@safe-global/safe-core-sdk-types'
45
import { TRANSACTION_SERVICE_URLS } from './utils/config'
1✔
46
import { isEmptyData } from './utils'
1✔
47
import { getAddSafeOperationProps } from './utils/safeOperation'
1✔
48

49
export interface SafeApiKitConfig {
50
  /** chainId - The chainId */
51
  chainId: bigint
52
  /** txServiceUrl - Safe Transaction Service URL */
53
  txServiceUrl?: string
54
}
55

56
class SafeApiKit {
57
  #chainId: bigint
2✔
58
  #txServiceBaseUrl: string
2✔
59

60
  constructor({ chainId, txServiceUrl }: SafeApiKitConfig) {
61
    this.#chainId = chainId
2✔
62

63
    if (txServiceUrl) {
2✔
64
      this.#txServiceBaseUrl = txServiceUrl
1✔
65
    } else {
66
      const url = TRANSACTION_SERVICE_URLS[chainId.toString()]
1✔
67
      if (!url) {
1!
68
        throw new TypeError(
×
69
          `There is no transaction service available for chainId ${chainId}. Please set the txServiceUrl property to use a custom transaction service.`
70
        )
71
      }
72

73
      this.#txServiceBaseUrl = `${url}/api`
1✔
74
    }
75
  }
76

77
  #isValidAddress(address: string) {
1✔
78
    try {
3✔
79
      validateEthereumAddress(address)
3✔
80
      return true
3✔
81
    } catch {
82
      return false
×
83
    }
84
  }
85

86
  #getEip3770Address(fullAddress: string): Eip3770Address {
87
    return validateEip3770Address(fullAddress, this.#chainId)
39✔
88
  }
89

90
  /**
91
   * Returns the information and configuration of the service.
92
   *
93
   * @returns The information and configuration of the service
94
   */
95
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
96
    return sendRequest({
2✔
97
      url: `${this.#txServiceBaseUrl}/v1/about`,
98
      method: HttpMethod.Get
99
    })
100
  }
101

102
  /**
103
   * Returns the list of Safe singletons.
104
   *
105
   * @returns The list of Safe singletons
106
   */
107
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
108
    return sendRequest({
1✔
109
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
110
      method: HttpMethod.Get
111
    })
112
  }
113

114
  /**
115
   * Decodes the specified Safe transaction data.
116
   *
117
   * @param data - The Safe transaction data
118
   * @returns The transaction data decoded
119
   * @throws "Invalid data"
120
   * @throws "Not Found"
121
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
122
   */
123
  async decodeData(data: string): Promise<any> {
124
    if (data === '') {
1!
125
      throw new Error('Invalid data')
×
126
    }
127
    return sendRequest({
1✔
128
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
129
      method: HttpMethod.Post,
130
      body: { data }
131
    })
132
  }
133

134
  /**
135
   * Returns the list of Safes where the address provided is an owner.
136
   *
137
   * @param ownerAddress - The owner address
138
   * @returns The list of Safes where the address provided is an owner
139
   * @throws "Invalid owner address"
140
   * @throws "Checksum address validation failed"
141
   */
142
  async getSafesByOwner(ownerAddress: string): Promise<OwnerResponse> {
143
    if (ownerAddress === '') {
2!
144
      throw new Error('Invalid owner address')
×
145
    }
146
    const { address } = this.#getEip3770Address(ownerAddress)
2✔
147
    return sendRequest({
2✔
148
      url: `${this.#txServiceBaseUrl}/v1/owners/${address}/safes/`,
149
      method: HttpMethod.Get
150
    })
151
  }
152

153
  /**
154
   * Returns the list of Safes where the module address provided is enabled.
155
   *
156
   * @param moduleAddress - The Safe module address
157
   * @returns The list of Safe addresses where the module provided is enabled
158
   * @throws "Invalid module address"
159
   * @throws "Module address checksum not valid"
160
   */
161
  async getSafesByModule(moduleAddress: string): Promise<ModulesResponse> {
162
    if (moduleAddress === '') {
2!
163
      throw new Error('Invalid module address')
×
164
    }
165
    const { address } = this.#getEip3770Address(moduleAddress)
2✔
166
    return sendRequest({
2✔
167
      url: `${this.#txServiceBaseUrl}/v1/modules/${address}/safes/`,
168
      method: HttpMethod.Get
169
    })
170
  }
171

172
  /**
173
   * Returns all the information of a Safe transaction.
174
   *
175
   * @param safeTxHash - Hash of the Safe transaction
176
   * @returns The information of a Safe transaction
177
   * @throws "Invalid safeTxHash"
178
   * @throws "Not found."
179
   */
180
  async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
181
    if (safeTxHash === '') {
1!
182
      throw new Error('Invalid safeTxHash')
×
183
    }
184
    return sendRequest({
1✔
185
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/`,
186
      method: HttpMethod.Get
187
    })
188
  }
189

190
  /**
191
   * Returns the list of confirmations for a given a Safe transaction.
192
   *
193
   * @param safeTxHash - The hash of the Safe transaction
194
   * @returns The list of confirmations
195
   * @throws "Invalid safeTxHash"
196
   */
197
  async getTransactionConfirmations(
198
    safeTxHash: string
199
  ): Promise<SafeMultisigConfirmationListResponse> {
200
    if (safeTxHash === '') {
1!
201
      throw new Error('Invalid safeTxHash')
×
202
    }
203
    return sendRequest({
1✔
204
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
205
      method: HttpMethod.Get
206
    })
207
  }
208

209
  /**
210
   * Adds a confirmation for a Safe transaction.
211
   *
212
   * @param safeTxHash - Hash of the Safe transaction that will be confirmed
213
   * @param signature - Signature of the transaction
214
   * @returns
215
   * @throws "Invalid safeTxHash"
216
   * @throws "Invalid signature"
217
   * @throws "Malformed data"
218
   * @throws "Error processing data"
219
   */
220
  async confirmTransaction(safeTxHash: string, signature: string): Promise<SignatureResponse> {
221
    if (safeTxHash === '') {
1!
222
      throw new Error('Invalid safeTxHash')
×
223
    }
224
    if (signature === '') {
1!
225
      throw new Error('Invalid signature')
×
226
    }
227
    return sendRequest({
1✔
228
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
229
      method: HttpMethod.Post,
230
      body: {
231
        signature
232
      }
233
    })
234
  }
235

236
  /**
237
   * Returns the information and configuration of the provided Safe address.
238
   *
239
   * @param safeAddress - The Safe address
240
   * @returns The information and configuration of the provided Safe address
241
   * @throws "Invalid Safe address"
242
   * @throws "Checksum address validation failed"
243
   */
244
  async getSafeInfo(safeAddress: string): Promise<SafeInfoResponse> {
245
    if (safeAddress === '') {
2!
246
      throw new Error('Invalid Safe address')
×
247
    }
248
    const { address } = this.#getEip3770Address(safeAddress)
2✔
249
    return sendRequest({
2✔
250
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
251
      method: HttpMethod.Get
252
    })
253
  }
254

255
  /**
256
   * Returns the list of delegates.
257
   *
258
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
259
   * @returns The list of delegates
260
   * @throws "Checksum address validation failed"
261
   */
262
  async getSafeDelegates({
263
    safeAddress,
264
    delegateAddress,
265
    delegatorAddress,
266
    label,
267
    limit,
268
    offset
269
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
270
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
271

272
    if (safeAddress) {
2!
273
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
274
      url.searchParams.set('safe', safe)
2✔
275
    }
276
    if (delegateAddress) {
2!
277
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
278
      url.searchParams.set('delegate', delegate)
×
279
    }
280
    if (delegatorAddress) {
2!
281
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
282
      url.searchParams.set('delegator', delegator)
×
283
    }
284
    if (label) {
2!
285
      url.searchParams.set('label', label)
×
286
    }
287
    if (limit != null) {
2!
288
      url.searchParams.set('limit', limit.toString())
×
289
    }
290
    if (offset != null) {
2!
291
      url.searchParams.set('offset', offset.toString())
×
292
    }
293

294
    return sendRequest({
2✔
295
      url: url.toString(),
296
      method: HttpMethod.Get
297
    })
298
  }
299

300
  /**
301
   * Adds a new delegate for a given Safe address.
302
   *
303
   * @param addSafeDelegateProps - The configuration of the new delegate
304
   * @returns
305
   * @throws "Invalid Safe delegate address"
306
   * @throws "Invalid Safe delegator address"
307
   * @throws "Invalid label"
308
   * @throws "Checksum address validation failed"
309
   * @throws "Address <delegate_address> is not checksumed"
310
   * @throws "Safe=<safe_address> does not exist or it's still not indexed"
311
   * @throws "Signing owner is not an owner of the Safe"
312
   */
313
  async addSafeDelegate({
314
    safeAddress,
315
    delegateAddress,
316
    delegatorAddress,
317
    label,
318
    signer
319
  }: AddSafeDelegateProps): Promise<SignedSafeDelegateResponse> {
320
    if (delegateAddress === '') {
2!
321
      throw new Error('Invalid Safe delegate address')
×
322
    }
323
    if (delegatorAddress === '') {
2!
324
      throw new Error('Invalid Safe delegator address')
×
325
    }
326
    if (label === '') {
2!
327
      throw new Error('Invalid label')
×
328
    }
329
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
330
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
331
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
332

333
    const body: any = {
2✔
334
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
335
      delegate,
336
      delegator,
337
      label,
338
      signature
339
    }
340
    return sendRequest({
2✔
341
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
342
      method: HttpMethod.Post,
343
      body
344
    })
345
  }
346

347
  /**
348
   * Removes a delegate for a given Safe address.
349
   *
350
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
351
   * @returns
352
   * @throws "Invalid Safe delegate address"
353
   * @throws "Invalid Safe delegator address"
354
   * @throws "Checksum address validation failed"
355
   * @throws "Signing owner is not an owner of the Safe"
356
   * @throws "Not found"
357
   */
358
  async removeSafeDelegate({
359
    delegateAddress,
360
    delegatorAddress,
361
    signer
362
  }: DeleteSafeDelegateProps): Promise<void> {
363
    if (delegateAddress === '') {
2!
364
      throw new Error('Invalid Safe delegate address')
×
365
    }
366
    if (delegatorAddress === '') {
2!
367
      throw new Error('Invalid Safe delegator address')
×
368
    }
369
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
370
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
371
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
372

373
    return sendRequest({
2✔
374
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
375
      method: HttpMethod.Delete,
376
      body: {
377
        delegator,
378
        signature
379
      }
380
    })
381
  }
382

383
  /**
384
   * Returns the creation information of a Safe.
385
   *
386
   * @param safeAddress - The Safe address
387
   * @returns The creation information of a Safe
388
   * @throws "Invalid Safe address"
389
   * @throws "Safe creation not found"
390
   * @throws "Checksum address validation failed"
391
   * @throws "Problem connecting to Ethereum network"
392
   */
393
  async getSafeCreationInfo(safeAddress: string): Promise<SafeCreationInfoResponse> {
394
    if (safeAddress === '') {
2!
395
      throw new Error('Invalid Safe address')
×
396
    }
397
    const { address } = this.#getEip3770Address(safeAddress)
2✔
398
    return sendRequest({
2✔
399
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/creation/`,
400
      method: HttpMethod.Get
401
    })
402
  }
403

404
  /**
405
   * Estimates the safeTxGas for a given Safe multi-signature transaction.
406
   *
407
   * @param safeAddress - The Safe address
408
   * @param safeTransaction - The Safe transaction to estimate
409
   * @returns The safeTxGas for the given Safe transaction
410
   * @throws "Invalid Safe address"
411
   * @throws "Data not valid"
412
   * @throws "Safe not found"
413
   * @throws "Tx not valid"
414
   */
415
  async estimateSafeTransaction(
416
    safeAddress: string,
417
    safeTransaction: SafeMultisigTransactionEstimate
418
  ): Promise<SafeMultisigTransactionEstimateResponse> {
419
    if (safeAddress === '') {
2!
420
      throw new Error('Invalid Safe address')
×
421
    }
422
    const { address } = this.#getEip3770Address(safeAddress)
2✔
423
    return sendRequest({
2✔
424
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/estimations/`,
425
      method: HttpMethod.Post,
426
      body: safeTransaction
427
    })
428
  }
429

430
  /**
431
   * Creates a new multi-signature transaction with its confirmations and stores it in the Safe Transaction Service.
432
   *
433
   * @param proposeTransactionConfig - The configuration of the proposed transaction
434
   * @returns The hash of the Safe transaction proposed
435
   * @throws "Invalid Safe address"
436
   * @throws "Invalid safeTxHash"
437
   * @throws "Invalid data"
438
   * @throws "Invalid ethereum address/User is not an owner/Invalid signature/Nonce already executed/Sender is not an owner"
439
   */
440
  async proposeTransaction({
441
    safeAddress,
442
    safeTransactionData,
443
    safeTxHash,
444
    senderAddress,
445
    senderSignature,
446
    origin
447
  }: ProposeTransactionProps): Promise<void> {
448
    if (safeAddress === '') {
2!
449
      throw new Error('Invalid Safe address')
×
450
    }
451
    const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
452
    const { address: sender } = this.#getEip3770Address(senderAddress)
2✔
453
    if (safeTxHash === '') {
2!
454
      throw new Error('Invalid safeTxHash')
×
455
    }
456
    return sendRequest({
2✔
457
      url: `${this.#txServiceBaseUrl}/v1/safes/${safe}/multisig-transactions/`,
458
      method: HttpMethod.Post,
459
      body: {
460
        ...safeTransactionData,
461
        contractTransactionHash: safeTxHash,
462
        sender,
463
        signature: senderSignature,
464
        origin
465
      }
466
    })
467
  }
468

469
  /**
470
   * Returns the history of incoming transactions of a Safe account.
471
   *
472
   * @param safeAddress - The Safe address
473
   * @returns The history of incoming transactions
474
   * @throws "Invalid Safe address"
475
   * @throws "Checksum address validation failed"
476
   */
477
  async getIncomingTransactions(safeAddress: string): Promise<TransferListResponse> {
478
    if (safeAddress === '') {
2!
479
      throw new Error('Invalid Safe address')
×
480
    }
481
    const { address } = this.#getEip3770Address(safeAddress)
2✔
482
    return sendRequest({
2✔
483
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/incoming-transfers?executed=true`,
484
      method: HttpMethod.Get
485
    })
486
  }
487

488
  /**
489
   * Returns the history of module transactions of a Safe account.
490
   *
491
   * @param safeAddress - The Safe address
492
   * @returns The history of module transactions
493
   * @throws "Invalid Safe address"
494
   * @throws "Invalid data"
495
   * @throws "Invalid ethereum address"
496
   */
497
  async getModuleTransactions(safeAddress: string): Promise<SafeModuleTransactionListResponse> {
498
    if (safeAddress === '') {
2!
499
      throw new Error('Invalid Safe address')
×
500
    }
501
    const { address } = this.#getEip3770Address(safeAddress)
2✔
502
    return sendRequest({
2✔
503
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/module-transactions/`,
504
      method: HttpMethod.Get
505
    })
506
  }
507

508
  /**
509
   * Returns the history of multi-signature transactions of a Safe account.
510
   *
511
   * @param safeAddress - The Safe address
512
   * @returns The history of multi-signature transactions
513
   * @throws "Invalid Safe address"
514
   * @throws "Checksum address validation failed"
515
   */
516
  async getMultisigTransactions(safeAddress: string): Promise<SafeMultisigTransactionListResponse> {
517
    if (safeAddress === '') {
2!
518
      throw new Error('Invalid Safe address')
×
519
    }
520
    const { address } = this.#getEip3770Address(safeAddress)
2✔
521
    return sendRequest({
2✔
522
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/`,
523
      method: HttpMethod.Get
524
    })
525
  }
526

527
  /**
528
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
529
   *
530
   * @param safeAddress - The Safe address
531
   * @param currentNonce - Current nonce of the Safe
532
   * @returns The list of transactions waiting for the confirmation of the Safe owners
533
   * @throws "Invalid Safe address"
534
   * @throws "Invalid data"
535
   * @throws "Invalid ethereum address"
536
   */
537
  async getPendingTransactions(
538
    safeAddress: string,
539
    currentNonce?: number
540
  ): Promise<SafeMultisigTransactionListResponse> {
541
    if (safeAddress === '') {
2!
542
      throw new Error('Invalid Safe address')
×
543
    }
544
    const { address } = this.#getEip3770Address(safeAddress)
2✔
545
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
546

547
    return sendRequest({
2✔
548
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`,
549
      method: HttpMethod.Get
550
    })
551
  }
552

553
  /**
554
   * Returns a list of transactions for a Safe. The list has different structures depending on the transaction type
555
   *
556
   * @param safeAddress - The Safe address
557
   * @returns The list of transactions waiting for the confirmation of the Safe owners
558
   * @throws "Invalid Safe address"
559
   * @throws "Checksum address validation failed"
560
   */
561
  async getAllTransactions(
562
    safeAddress: string,
563
    options?: AllTransactionsOptions
564
  ): Promise<AllTransactionsListResponse> {
565
    if (safeAddress === '') {
2!
566
      throw new Error('Invalid Safe address')
×
567
    }
568
    const { address } = this.#getEip3770Address(safeAddress)
2✔
569
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/all-transactions/`)
2✔
570

571
    const trusted = options?.trusted?.toString() || 'true'
2✔
572
    url.searchParams.set('trusted', trusted)
2✔
573

574
    const queued = options?.queued?.toString() || 'true'
2✔
575
    url.searchParams.set('queued', queued)
2✔
576

577
    const executed = options?.executed?.toString() || 'false'
2✔
578
    url.searchParams.set('executed', executed)
2✔
579

580
    return sendRequest({
2✔
581
      url: url.toString(),
582
      method: HttpMethod.Get
583
    })
584
  }
585

586
  /**
587
   * Returns the right nonce to propose a new transaction after the last pending transaction.
588
   *
589
   * @param safeAddress - The Safe address
590
   * @returns The right nonce to propose a new transaction after the last pending transaction
591
   * @throws "Invalid Safe address"
592
   * @throws "Invalid data"
593
   * @throws "Invalid ethereum address"
594
   */
595
  async getNextNonce(safeAddress: string): Promise<number> {
596
    if (safeAddress === '') {
×
597
      throw new Error('Invalid Safe address')
×
598
    }
599
    const { address } = this.#getEip3770Address(safeAddress)
×
600
    const pendingTransactions = await this.getPendingTransactions(address)
×
601
    if (pendingTransactions.results.length > 0) {
×
602
      const nonces = pendingTransactions.results.map((tx) => tx.nonce)
×
603
      const lastNonce = Math.max(...nonces)
×
604
      return lastNonce + 1
×
605
    }
606
    const safeInfo = await this.getSafeInfo(address)
×
607
    return safeInfo.nonce
×
608
  }
609

610
  /**
611
   * Returns the list of all the ERC20 tokens handled by the Safe.
612
   *
613
   * @returns The list of all the ERC20 tokens
614
   */
615
  async getTokenList(): Promise<TokenInfoListResponse> {
616
    return sendRequest({
1✔
617
      url: `${this.#txServiceBaseUrl}/v1/tokens/`,
618
      method: HttpMethod.Get
619
    })
620
  }
621

622
  /**
623
   * Returns the information of a given ERC20 token.
624
   *
625
   * @param tokenAddress - The token address
626
   * @returns The information of the given ERC20 token
627
   * @throws "Invalid token address"
628
   * @throws "Checksum address validation failed"
629
   */
630
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
631
    if (tokenAddress === '') {
2!
632
      throw new Error('Invalid token address')
×
633
    }
634
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
635
    return sendRequest({
2✔
636
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
637
      method: HttpMethod.Get
638
    })
639
  }
640

641
  /**
642
   * Get a message by its safe message hash
643
   * @param messageHash The Safe message hash
644
   * @returns The message
645
   */
646
  async getMessage(messageHash: string): Promise<SafeMessage> {
647
    if (!messageHash) {
1!
648
      throw new Error('Invalid messageHash')
×
649
    }
650

651
    return sendRequest({
1✔
652
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
653
      method: HttpMethod.Get
654
    })
655
  }
656

657
  /**
658
   * Get the list of messages associated to a Safe account
659
   * @param safeAddress The safe address
660
   * @param options The options to filter the list of messages
661
   * @returns The paginated list of messages
662
   */
663
  async getMessages(
664
    safeAddress: string,
665
    { ordering, limit, offset }: GetSafeMessageListProps = {}
1✔
666
  ): Promise<SafeMessageListResponse> {
667
    if (!this.#isValidAddress(safeAddress)) {
1!
668
      throw new Error('Invalid safeAddress')
×
669
    }
670

671
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`)
1✔
672

673
    if (ordering) {
1!
674
      url.searchParams.set('ordering', ordering)
×
675
    }
676

677
    if (limit != null) {
1!
678
      url.searchParams.set('limit', limit.toString())
×
679
    }
680

681
    if (offset != null) {
1!
682
      url.searchParams.set('offset', offset.toString())
×
683
    }
684

685
    return sendRequest({
1✔
686
      url: url.toString(),
687
      method: HttpMethod.Get
688
    })
689
  }
690

691
  /**
692
   * Creates a new message with an initial signature
693
   * Add more signatures from other owners using addMessageSignature()
694
   * @param safeAddress The safe address
695
   * @param options The raw message to add, signature and safeAppId if any
696
   */
697
  async addMessage(safeAddress: string, addMessageProps: AddMessageProps): Promise<void> {
698
    if (!this.#isValidAddress(safeAddress)) {
2!
699
      throw new Error('Invalid safeAddress')
×
700
    }
701

702
    return sendRequest({
2✔
703
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
704
      method: HttpMethod.Post,
705
      body: addMessageProps
706
    })
707
  }
708

709
  /**
710
   * Add a signature to an existing message
711
   * @param messageHash The safe message hash
712
   * @param signature The signature
713
   */
714
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
715
    if (!messageHash || !signature) {
1!
716
      throw new Error('Invalid messageHash or signature')
×
717
    }
718

719
    return sendRequest({
1✔
720
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
721
      method: HttpMethod.Post,
722
      body: {
723
        signature
724
      }
725
    })
726
  }
727

728
  /**
729
   * Get the SafeOperations that were sent from a particular address.
730
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
731
   * @throws "Safe address must not be empty"
732
   * @throws "Invalid Ethereum address {safeAddress}"
733
   * @returns The SafeOperations sent from the given Safe's address
734
   */
735
  async getSafeOperationsByAddress({
736
    safeAddress,
737
    ordering,
738
    limit,
739
    offset
740
  }: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
741
    if (!safeAddress) {
1!
742
      throw new Error('Safe address must not be empty')
×
743
    }
744

745
    const { address } = this.#getEip3770Address(safeAddress)
1✔
746

747
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/safe-operations/`)
1✔
748

749
    if (ordering) {
1!
750
      url.searchParams.set('ordering', ordering)
×
751
    }
752

753
    if (limit != null) {
1!
754
      url.searchParams.set('limit', limit.toString())
×
755
    }
756

757
    if (offset != null) {
1!
758
      url.searchParams.set('offset', offset.toString())
×
759
    }
760

761
    return sendRequest({
1✔
762
      url: url.toString(),
763
      method: HttpMethod.Get
764
    })
765
  }
766

767
  /**
768
   * Get a SafeOperation by its hash.
769
   * @param safeOperationHash The SafeOperation hash
770
   * @throws "SafeOperation hash must not be empty"
771
   * @throws "Not found."
772
   * @returns The SafeOperation
773
   */
774
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
775
    if (!safeOperationHash) {
1!
776
      throw new Error('SafeOperation hash must not be empty')
×
777
    }
778

779
    return sendRequest({
1✔
780
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
781
      method: HttpMethod.Get
782
    })
783
  }
784

785
  /**
786
   * Create a new 4337 SafeOperation for a Safe.
787
   * @param addSafeOperationProps - The configuration of the SafeOperation
788
   * @throws "Safe address must not be empty"
789
   * @throws "Invalid Safe address {safeAddress}"
790
   * @throws "Module address must not be empty"
791
   * @throws "Invalid module address {moduleAddress}"
792
   * @throws "Signature must not be empty"
793
   */
794
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
795
    let safeAddress: string, moduleAddress: string
796
    let addSafeOperationProps: AddSafeOperationProps
797

798
    if (isSafeOperation(safeOperation)) {
1!
799
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
800
    } else {
801
      addSafeOperationProps = safeOperation
1✔
802
    }
803

804
    const {
805
      entryPoint,
806
      moduleAddress: moduleAddressProp,
807
      options,
808
      safeAddress: safeAddressProp,
809
      userOperation
810
    } = addSafeOperationProps
1✔
811
    if (!safeAddressProp) {
1!
812
      throw new Error('Safe address must not be empty')
×
813
    }
814
    try {
1✔
815
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
816
    } catch (err) {
817
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
818
    }
819

820
    if (!moduleAddressProp) {
1!
821
      throw new Error('Module address must not be empty')
×
822
    }
823

824
    try {
1✔
825
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
826
    } catch (err) {
827
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
828
    }
829

830
    if (isEmptyData(userOperation.signature)) {
1!
831
      throw new Error('Signature must not be empty')
×
832
    }
833

834
    // We are receiving the timestamp in seconds (block timestamp), but the API expects it in milliseconds
835
    const getISOString = (date: number | undefined) =>
1✔
836
      !date ? null : new Date(date * 1000).toISOString()
2!
837

838
    return sendRequest({
1✔
839
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
840
      method: HttpMethod.Post,
841
      body: {
842
        nonce: Number(userOperation.nonce),
843
        initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode,
1!
844
        callData: userOperation.callData,
845
        callGasLimit: userOperation.callGasLimit.toString(),
846
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
847
        preVerificationGas: userOperation.preVerificationGas.toString(),
848
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
849
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
850
        paymasterAndData: isEmptyData(userOperation.paymasterAndData)
1!
851
          ? null
852
          : userOperation.paymasterAndData,
853
        entryPoint,
854
        validAfter: getISOString(options?.validAfter),
855
        validUntil: getISOString(options?.validUntil),
856
        signature: userOperation.signature,
857
        moduleAddress
858
      }
859
    })
860
  }
861

862
  /**
863
   * Returns the list of confirmations for a given a SafeOperation.
864
   *
865
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
866
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
867
   * @returns The list of confirmations
868
   * @throws "Invalid SafeOperation hash"
869
   * @throws "Invalid data"
870
   */
871
  async getSafeOperationConfirmations(
872
    safeOperationHash: string,
873
    { limit, offset }: ListOptions = {}
1✔
874
  ): Promise<SafeOperationConfirmationListResponse> {
875
    if (!safeOperationHash) {
1!
876
      throw new Error('Invalid SafeOperation hash')
×
877
    }
878

879
    const url = new URL(
1✔
880
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
881
    )
882

883
    if (limit != null) {
1!
884
      url.searchParams.set('limit', limit.toString())
×
885
    }
886

887
    if (offset != null) {
1!
888
      url.searchParams.set('offset', offset.toString())
×
889
    }
890

891
    return sendRequest({
1✔
892
      url: url.toString(),
893
      method: HttpMethod.Get
894
    })
895
  }
896

897
  /**
898
   * Adds a confirmation for a SafeOperation.
899
   *
900
   * @param safeOperationHash The SafeOperation hash
901
   * @param signature - Signature of the SafeOperation
902
   * @returns
903
   * @throws "Invalid SafeOperation hash"
904
   * @throws "Invalid signature"
905
   * @throws "Malformed data"
906
   * @throws "Error processing data"
907
   */
908
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
909
    if (!safeOperationHash) {
1!
910
      throw new Error('Invalid SafeOperation hash')
×
911
    }
912
    if (!signature) {
1!
913
      throw new Error('Invalid signature')
×
914
    }
915
    return sendRequest({
1✔
916
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
917
      method: HttpMethod.Post,
918
      body: { signature }
919
    })
920
  }
921
}
922

923
export default SafeApiKit
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc