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

safe-global / safe-core-sdk / 13326841204

14 Feb 2025 10:00AM UTC coverage: 77.267% (-0.8%) from 78.049%
13326841204

Pull #1135

github

web-flow
Merge e0caf16ec into 2021fb930
Pull Request #1135: chore: api kit improvements

263 of 413 branches covered (63.68%)

Branch coverage included in aggregate %.

45 of 74 new or added lines in 1 file covered. (60.81%)

896 of 1087 relevant lines covered (82.43%)

4.58 hits per line

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

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

56
export interface SafeApiKitConfig {
57
  /** chainId - The chainId */
58
  chainId: bigint
59
  /** txServiceUrl - Safe Transaction Service URL */
60
  txServiceUrl?: string
61
}
62

63
class SafeApiKit {
64
  #chainId: bigint
2✔
65
  #txServiceBaseUrl: string
2✔
66

67
  constructor({ chainId, txServiceUrl }: SafeApiKitConfig) {
68
    this.#chainId = chainId
2✔
69

70
    if (txServiceUrl) {
2✔
71
      this.#txServiceBaseUrl = txServiceUrl
1✔
72
    } else {
73
      const url = TRANSACTION_SERVICE_URLS[chainId.toString()]
1✔
74
      if (!url) {
1!
75
        throw new TypeError(
×
76
          `There is no transaction service available for chainId ${chainId}. Please set the txServiceUrl property to use a custom transaction service.`
77
        )
78
      }
79

80
      this.#txServiceBaseUrl = url
1✔
81
    }
82
  }
83

84
  #isValidAddress(address: string) {
1✔
85
    try {
3✔
86
      validateEthereumAddress(address)
3✔
87
      return true
3✔
88
    } catch {
89
      return false
×
90
    }
91
  }
92

93
  #getEip3770Address(fullAddress: string): Eip3770Address {
94
    return validateEip3770Address(fullAddress, this.#chainId)
39✔
95
  }
96

97
  /**
98
   * Returns the information and configuration of the service.
99
   *
100
   * @returns The information and configuration of the service
101
   */
102
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
103
    return sendRequest({
2✔
104
      url: `${this.#txServiceBaseUrl}/v1/about`,
105
      method: HttpMethod.Get
106
    })
107
  }
108

109
  /**
110
   * Returns the list of Safe singletons.
111
   *
112
   * @returns The list of Safe singletons
113
   */
114
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
115
    return sendRequest({
1✔
116
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
117
      method: HttpMethod.Get
118
    })
119
  }
120

121
  /**
122
   * Decodes the specified Safe transaction data.
123
   *
124
   * @param data - The Safe transaction data. '0x' prefixed hexadecimal string.
125
   * @param to - The address of the receiving contract. If provided, the decoded data will be more accurate, as in case of an ABI collision the Safe Transaction Service would know which ABI to use
126
   * @returns The transaction data decoded
127
   * @throws "Invalid data"
128
   * @throws "Not Found"
129
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
130
   */
131
  async decodeData(data: string, to?: string): Promise<DataDecoded> {
132
    if (data === '') {
1!
133
      throw new Error('Invalid data')
×
134
    }
135

136
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
137

138
    if (to) {
1!
139
      dataDecoderRequest.to = to
×
140
    }
141

142
    return sendRequest({
1✔
143
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
144
      method: HttpMethod.Post,
145
      body: dataDecoderRequest
146
    })
147
  }
148

149
  /**
150
   * Returns the list of delegates.
151
   *
152
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
153
   * @returns The list of delegates
154
   * @throws "Checksum address validation failed"
155
   */
156
  async getSafeDelegates({
157
    safeAddress,
158
    delegateAddress,
159
    delegatorAddress,
160
    label,
161
    limit,
162
    offset
163
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
164
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
165

166
    if (safeAddress) {
2✔
167
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
168
      url.searchParams.set('safe', safe)
2✔
169
    }
170
    if (delegateAddress) {
2!
NEW
171
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
NEW
172
      url.searchParams.set('delegate', delegate)
×
173
    }
174
    if (delegatorAddress) {
2!
NEW
175
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
NEW
176
      url.searchParams.set('delegator', delegator)
×
177
    }
178
    if (label) {
2!
NEW
179
      url.searchParams.set('label', label)
×
180
    }
181
    if (limit != null) {
2!
NEW
182
      url.searchParams.set('limit', limit.toString())
×
183
    }
184
    if (offset != null) {
2!
NEW
185
      url.searchParams.set('offset', offset.toString())
×
186
    }
187

188
    return sendRequest({
2✔
189
      url: url.toString(),
190
      method: HttpMethod.Get
191
    })
192
  }
193

194
  /**
195
   * Adds a new delegate for a given Safe address.
196
   *
197
   * @param addSafeDelegateProps - The configuration of the new delegate
198
   * @returns
199
   * @throws "Invalid Safe delegate address"
200
   * @throws "Invalid Safe delegator address"
201
   * @throws "Invalid label"
202
   * @throws "Checksum address validation failed"
203
   * @throws "Address <delegate_address> is not checksumed"
204
   * @throws "Safe=<safe_address> does not exist or it's still not indexed"
205
   * @throws "Signing owner is not an owner of the Safe"
206
   */
207
  async addSafeDelegate({
208
    safeAddress,
209
    delegateAddress,
210
    delegatorAddress,
211
    label,
212
    signer
213
  }: AddSafeDelegateProps): Promise<SignedSafeDelegateResponse> {
214
    if (delegateAddress === '') {
2!
NEW
215
      throw new Error('Invalid Safe delegate address')
×
216
    }
217
    if (delegatorAddress === '') {
2!
NEW
218
      throw new Error('Invalid Safe delegator address')
×
219
    }
220
    if (label === '') {
2!
NEW
221
      throw new Error('Invalid label')
×
222
    }
223
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
224
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
225
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
226

227
    const body: any = {
2✔
228
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
229
      delegate,
230
      delegator,
231
      label,
232
      signature
233
    }
234
    return sendRequest({
2✔
235
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
236
      method: HttpMethod.Post,
237
      body
238
    })
239
  }
240

241
  /**
242
   * Removes a delegate for a given Safe address.
243
   *
244
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
245
   * @returns
246
   * @throws "Invalid Safe delegate address"
247
   * @throws "Invalid Safe delegator address"
248
   * @throws "Checksum address validation failed"
249
   * @throws "Signing owner is not an owner of the Safe"
250
   * @throws "Not found"
251
   */
252
  async removeSafeDelegate({
253
    delegateAddress,
254
    delegatorAddress,
255
    signer
256
  }: DeleteSafeDelegateProps): Promise<void> {
257
    if (delegateAddress === '') {
2!
NEW
258
      throw new Error('Invalid Safe delegate address')
×
259
    }
260
    if (delegatorAddress === '') {
2!
NEW
261
      throw new Error('Invalid Safe delegator address')
×
262
    }
263
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
264
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
265
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
266

267
    return sendRequest({
2✔
268
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
269
      method: HttpMethod.Delete,
270
      body: {
271
        delegator,
272
        signature
273
      }
274
    })
275
  }
276

277
  /**
278
   * Get a message by its safe message hash
279
   * @param messageHash The Safe message hash
280
   * @returns The message
281
   */
282
  async getMessage(messageHash: string): Promise<SafeMessage> {
283
    if (!messageHash) {
1!
NEW
284
      throw new Error('Invalid messageHash')
×
285
    }
286

287
    return sendRequest({
1✔
288
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
289
      method: HttpMethod.Get
290
    })
291
  }
292

293
  /**
294
   * Get the list of messages associated to a Safe account
295
   * @param safeAddress The safe address
296
   * @param options The options to filter the list of messages
297
   * @returns The paginated list of messages
298
   */
299
  async getMessages(
300
    safeAddress: string,
301
    { ordering, limit, offset }: GetSafeMessageListProps = {}
1✔
302
  ): Promise<SafeMessageListResponse> {
303
    if (!this.#isValidAddress(safeAddress)) {
1!
NEW
304
      throw new Error('Invalid safeAddress')
×
305
    }
306

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

309
    if (ordering) {
1!
NEW
310
      url.searchParams.set('ordering', ordering)
×
311
    }
312

313
    if (limit != null) {
1!
NEW
314
      url.searchParams.set('limit', limit.toString())
×
315
    }
316

317
    if (offset != null) {
1!
NEW
318
      url.searchParams.set('offset', offset.toString())
×
319
    }
320

321
    return sendRequest({
1✔
322
      url: url.toString(),
323
      method: HttpMethod.Get
324
    })
325
  }
326

327
  /**
328
   * Creates a new message with an initial signature
329
   * Add more signatures from other owners using addMessageSignature()
330
   * @param safeAddress The safe address
331
   * @param options The raw message to add, signature and safeAppId if any
332
   */
333
  async addMessage(safeAddress: string, addMessageProps: AddMessageProps): Promise<void> {
334
    if (!this.#isValidAddress(safeAddress)) {
2!
NEW
335
      throw new Error('Invalid safeAddress')
×
336
    }
337

338
    return sendRequest({
2✔
339
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
340
      method: HttpMethod.Post,
341
      body: addMessageProps
342
    })
343
  }
344

345
  /**
346
   * Add a signature to an existing message
347
   * @param messageHash The safe message hash
348
   * @param signature The signature
349
   */
350
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
351
    if (!messageHash || !signature) {
1!
NEW
352
      throw new Error('Invalid messageHash or signature')
×
353
    }
354

355
    return sendRequest({
1✔
356
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
357
      method: HttpMethod.Post,
358
      body: {
359
        signature
360
      }
361
    })
362
  }
363

364
  /**
365
   * Returns the list of Safes where the address provided is an owner.
366
   *
367
   * @param ownerAddress - The owner address
368
   * @returns The list of Safes where the address provided is an owner
369
   * @throws "Invalid owner address"
370
   * @throws "Checksum address validation failed"
371
   */
372
  async getSafesByOwner(ownerAddress: string): Promise<OwnerResponse> {
373
    if (ownerAddress === '') {
2!
374
      throw new Error('Invalid owner address')
×
375
    }
376
    const { address } = this.#getEip3770Address(ownerAddress)
2✔
377
    return sendRequest({
2✔
378
      url: `${this.#txServiceBaseUrl}/v1/owners/${address}/safes/`,
379
      method: HttpMethod.Get
380
    })
381
  }
382

383
  /**
384
   * Returns the list of Safes where the module address provided is enabled.
385
   *
386
   * @param moduleAddress - The Safe module address
387
   * @returns The list of Safe addresses where the module provided is enabled
388
   * @throws "Invalid module address"
389
   * @throws "Module address checksum not valid"
390
   */
391
  async getSafesByModule(moduleAddress: string): Promise<ModulesResponse> {
392
    if (moduleAddress === '') {
2!
393
      throw new Error('Invalid module address')
×
394
    }
395
    const { address } = this.#getEip3770Address(moduleAddress)
2✔
396
    return sendRequest({
2✔
397
      url: `${this.#txServiceBaseUrl}/v1/modules/${address}/safes/`,
398
      method: HttpMethod.Get
399
    })
400
  }
401

402
  /**
403
   * Returns all the information of a Safe transaction.
404
   *
405
   * @param safeTxHash - Hash of the Safe transaction
406
   * @returns The information of a Safe transaction
407
   * @throws "Invalid safeTxHash"
408
   * @throws "Not found."
409
   */
410
  async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
411
    if (safeTxHash === '') {
1!
412
      throw new Error('Invalid safeTxHash')
×
413
    }
414
    return sendRequest({
1✔
415
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/`,
416
      method: HttpMethod.Get
417
    })
418
  }
419

420
  /**
421
   * Returns the list of confirmations for a given a Safe transaction.
422
   *
423
   * @param safeTxHash - The hash of the Safe transaction
424
   * @returns The list of confirmations
425
   * @throws "Invalid safeTxHash"
426
   */
427
  async getTransactionConfirmations(
428
    safeTxHash: string
429
  ): Promise<SafeMultisigConfirmationListResponse> {
430
    if (safeTxHash === '') {
1!
431
      throw new Error('Invalid safeTxHash')
×
432
    }
433
    return sendRequest({
1✔
434
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
435
      method: HttpMethod.Get
436
    })
437
  }
438

439
  /**
440
   * Adds a confirmation for a Safe transaction.
441
   *
442
   * @param safeTxHash - Hash of the Safe transaction that will be confirmed
443
   * @param signature - Signature of the transaction
444
   * @returns
445
   * @throws "Invalid safeTxHash"
446
   * @throws "Invalid signature"
447
   * @throws "Malformed data"
448
   * @throws "Error processing data"
449
   */
450
  async confirmTransaction(safeTxHash: string, signature: string): Promise<SignatureResponse> {
451
    if (safeTxHash === '') {
1!
452
      throw new Error('Invalid safeTxHash')
×
453
    }
454
    if (signature === '') {
1!
455
      throw new Error('Invalid signature')
×
456
    }
457
    return sendRequest({
1✔
458
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
459
      method: HttpMethod.Post,
460
      body: {
461
        signature
462
      }
463
    })
464
  }
465

466
  /**
467
   * Returns the information and configuration of the provided Safe address.
468
   *
469
   * @param safeAddress - The Safe address
470
   * @returns The information and configuration of the provided Safe address
471
   * @throws "Invalid Safe address"
472
   * @throws "Checksum address validation failed"
473
   */
474
  async getSafeInfo(safeAddress: string): Promise<SafeInfoResponse> {
475
    if (safeAddress === '') {
2!
476
      throw new Error('Invalid Safe address')
×
477
    }
478
    const { address } = this.#getEip3770Address(safeAddress)
2✔
479
    return sendRequest({
2✔
480
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
481
      method: HttpMethod.Get
482
    }).then((response: any) => {
483
      // FIXME remove when the transaction service returns the singleton property instead of masterCopy
484
      if (!response?.singleton) {
2✔
485
        const { masterCopy, ...rest } = response
2✔
486
        return { ...rest, singleton: masterCopy } as SafeInfoResponse
2✔
487
      }
488

489
      return response as SafeInfoResponse
×
490
    })
491
  }
492

493
  /**
494
   * Returns the creation information of a Safe.
495
   *
496
   * @param safeAddress - The Safe address
497
   * @returns The creation information of a Safe
498
   * @throws "Invalid Safe address"
499
   * @throws "Safe creation not found"
500
   * @throws "Checksum address validation failed"
501
   * @throws "Problem connecting to Ethereum network"
502
   */
503
  async getSafeCreationInfo(safeAddress: string): Promise<SafeCreationInfoResponse> {
504
    if (safeAddress === '') {
2!
505
      throw new Error('Invalid Safe address')
×
506
    }
507
    const { address } = this.#getEip3770Address(safeAddress)
2✔
508
    return sendRequest({
2✔
509
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/creation/`,
510
      method: HttpMethod.Get
511
    }).then((response: any) => {
512
      // FIXME remove when the transaction service returns the singleton property instead of masterCopy
513
      if (!response?.singleton) {
2✔
514
        const { masterCopy, ...rest } = response
2✔
515
        return { ...rest, singleton: masterCopy } as SafeCreationInfoResponse
2✔
516
      }
517

518
      return response as SafeCreationInfoResponse
×
519
    })
520
  }
521

522
  /**
523
   * Estimates the safeTxGas for a given Safe multi-signature transaction.
524
   *
525
   * @param safeAddress - The Safe address
526
   * @param safeTransaction - The Safe transaction to estimate
527
   * @returns The safeTxGas for the given Safe transaction
528
   * @throws "Invalid Safe address"
529
   * @throws "Data not valid"
530
   * @throws "Safe not found"
531
   * @throws "Tx not valid"
532
   */
533
  async estimateSafeTransaction(
534
    safeAddress: string,
535
    safeTransaction: SafeMultisigTransactionEstimate
536
  ): Promise<SafeMultisigTransactionEstimateResponse> {
537
    if (safeAddress === '') {
2!
538
      throw new Error('Invalid Safe address')
×
539
    }
540
    const { address } = this.#getEip3770Address(safeAddress)
2✔
541
    return sendRequest({
2✔
542
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/estimations/`,
543
      method: HttpMethod.Post,
544
      body: safeTransaction
545
    })
546
  }
547

548
  /**
549
   * Creates a new multi-signature transaction with its confirmations and stores it in the Safe Transaction Service.
550
   *
551
   * @param proposeTransactionConfig - The configuration of the proposed transaction
552
   * @returns The hash of the Safe transaction proposed
553
   * @throws "Invalid Safe address"
554
   * @throws "Invalid safeTxHash"
555
   * @throws "Invalid data"
556
   * @throws "Invalid ethereum address/User is not an owner/Invalid signature/Nonce already executed/Sender is not an owner"
557
   */
558
  async proposeTransaction({
559
    safeAddress,
560
    safeTransactionData,
561
    safeTxHash,
562
    senderAddress,
563
    senderSignature,
564
    origin
565
  }: ProposeTransactionProps): Promise<void> {
566
    if (safeAddress === '') {
2!
567
      throw new Error('Invalid Safe address')
×
568
    }
569
    const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
570
    const { address: sender } = this.#getEip3770Address(senderAddress)
2✔
571
    if (safeTxHash === '') {
2!
572
      throw new Error('Invalid safeTxHash')
×
573
    }
574
    return sendRequest({
2✔
575
      url: `${this.#txServiceBaseUrl}/v1/safes/${safe}/multisig-transactions/`,
576
      method: HttpMethod.Post,
577
      body: {
578
        ...safeTransactionData,
579
        contractTransactionHash: safeTxHash,
580
        sender,
581
        signature: senderSignature,
582
        origin
583
      }
584
    })
585
  }
586

587
  /**
588
   * Returns the history of incoming transactions of a Safe account.
589
   *
590
   * @param safeAddress - The Safe address
591
   * @param options - Optional parameters to filter or modify the response
592
   * @returns The history of incoming transactions
593
   * @throws "Invalid Safe address"
594
   * @throws "Checksum address validation failed"
595
   */
596
  async getIncomingTransactions(
597
    safeAddress: string,
598
    options?: GetIncomingTransactionsOptions
599
  ): Promise<TransferListResponse> {
600
    if (safeAddress === '') {
2!
601
      throw new Error('Invalid Safe address')
×
602
    }
603
    const { address } = this.#getEip3770Address(safeAddress)
2✔
604
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/incoming-transfers/`)
2✔
605

606
    // Handle any additional query parameters
607
    Object.entries(options || {}).forEach(([key, value]) => {
2✔
608
      // Skip undefined values
NEW
609
      if (value !== undefined) {
×
610
        // Add options as query parameters
NEW
611
        url.searchParams.set(key, value.toString())
×
612
      }
613
    })
614

615
    return sendRequest({
2✔
616
      url: url.toString(),
617
      method: HttpMethod.Get
618
    })
619
  }
620

621
  /**
622
   * Returns the history of module transactions of a Safe account.
623
   *
624
   * @param safeAddress - The Safe address
625
   * @param options - Optional parameters to filter or modify the response
626
   * @returns The history of module transactions
627
   * @throws "Invalid Safe address"
628
   * @throws "Invalid data"
629
   * @throws "Invalid ethereum address"
630
   */
631
  async getModuleTransactions(
632
    safeAddress: string,
633
    options?: GetModuleTransactionsOptions
634
  ): Promise<SafeModuleTransactionListResponse> {
635
    if (safeAddress === '') {
2!
636
      throw new Error('Invalid Safe address')
×
637
    }
638
    const { address } = this.#getEip3770Address(safeAddress)
2✔
639
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/module-transactions/`)
2✔
640

641
    // Handle any additional query parameters
642
    Object.entries(options || {}).forEach(([key, value]) => {
2✔
643
      // Skip undefined values
NEW
644
      if (value !== undefined) {
×
645
        // Add options as query parameters
NEW
646
        url.searchParams.set(key, value.toString())
×
647
      }
648
    })
649
    return sendRequest({
2✔
650
      url: url.toString(),
651
      method: HttpMethod.Get
652
    })
653
  }
654

655
  /**
656
   * Returns the history of multi-signature transactions of a Safe account.
657
   *
658
   * @param safeAddress - The Safe address
659
   * @param options - Optional parameters to filter or modify the response
660
   * @returns The history of multi-signature transactions
661
   * @throws "Invalid Safe address"
662
   * @throws "Checksum address validation failed"
663
   */
664
  async getMultisigTransactions(
665
    safeAddress: string,
666
    options?: GetMultisigTransactionsOptions
667
  ): Promise<SafeMultisigTransactionListResponse> {
668
    if (safeAddress === '') {
2!
669
      throw new Error('Invalid Safe address')
×
670
    }
671

672
    const { address } = this.#getEip3770Address(safeAddress)
2✔
673
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/`)
2✔
674

675
    // Handle any additional query parameters
676
    Object.entries(options || {}).forEach(([key, value]) => {
2✔
677
      // Skip undefined values
NEW
678
      if (value !== undefined) {
×
679
        // Add options as query parameters
NEW
680
        url.searchParams.set(key, value.toString())
×
681
      }
682
    })
683

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

690
  /**
691
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
692
   *
693
   * @param safeAddress - The Safe address
694
   * @param currentNonce - Deprecated, use inside object property: Current nonce of the Safe.
695
   * @returns The list of transactions waiting for the confirmation of the Safe owners
696
   * @throws "Invalid Safe address"
697
   * @throws "Invalid data"
698
   * @throws "Invalid ethereum address"
699
   */
700
  async getPendingTransactions(
701
    safeAddress: string,
702
    currentNonce?: number
703
  ): Promise<SafeMultisigTransactionListResponse>
704
  /**
705
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
706
   *
707
   * @param safeAddress - The Safe address
708
   * @param {PendingTransactionsOptions} options The options to filter the list of transactions
709
   * @returns The list of transactions waiting for the confirmation of the Safe owners
710
   * @throws "Invalid Safe address"
711
   * @throws "Invalid data"
712
   * @throws "Invalid ethereum address"
713
   */
714
  async getPendingTransactions(
715
    safeAddress: string,
716
    { currentNonce, hasConfirmations, ordering, limit, offset }: PendingTransactionsOptions
717
  ): Promise<SafeMultisigTransactionListResponse>
718
  async getPendingTransactions(
719
    safeAddress: string,
720
    propsOrCurrentNonce: PendingTransactionsOptions | number = {}
×
721
  ): Promise<SafeMultisigTransactionListResponse> {
722
    if (safeAddress === '') {
2!
723
      throw new Error('Invalid Safe address')
×
724
    }
725

726
    // TODO: Remove @deprecated migration code
727
    let currentNonce: number | undefined
728
    let hasConfirmations: boolean | undefined
729
    let ordering: string | undefined
730
    let limit: number | undefined
731
    let offset: number | undefined
732
    if (typeof propsOrCurrentNonce === 'object') {
2!
733
      ;({ currentNonce, hasConfirmations, ordering, limit, offset } = propsOrCurrentNonce)
2✔
734
    } else {
735
      console.warn(
×
736
        'Deprecated: Use `currentNonce` inside an object instead. See `PendingTransactionsOptions`.'
737
      )
738
      currentNonce = propsOrCurrentNonce
×
739
    }
740
    // END of @deprecated migration code
741

742
    const { address } = this.#getEip3770Address(safeAddress)
2✔
743
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
744

745
    const url = new URL(
2✔
746
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
747
    )
748

749
    if (hasConfirmations) {
2!
750
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
751
    }
752

753
    if (ordering) {
2!
754
      url.searchParams.set('ordering', ordering)
×
755
    }
756

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

761
    if (offset != null) {
2!
762
      url.searchParams.set('offset', offset.toString())
×
763
    }
764

765
    return sendRequest({
2✔
766
      url: url.toString(),
767
      method: HttpMethod.Get
768
    })
769
  }
770

771
  /**
772
   * Returns a list of transactions for a Safe. The list has different structures depending on the transaction type
773
   *
774
   * @param safeAddress - The Safe address
775
   * @param options - Optional parameters to filter or modify the response
776
   * @returns The list of transactions waiting for the confirmation of the Safe owners
777
   * @throws "Invalid Safe address"
778
   * @throws "Checksum address validation failed"
779
   * @throws "Ordering field is not valid"
780
   */
781
  async getAllTransactions(
782
    safeAddress: string,
783
    options?: AllTransactionsOptions
784
  ): Promise<AllTransactionsListResponse> {
785
    if (safeAddress === '') {
2!
786
      throw new Error('Invalid Safe address')
×
787
    }
788
    const { address } = this.#getEip3770Address(safeAddress)
2✔
789
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/all-transactions/`)
2✔
790

791
    // Handle any additional query parameters
792
    Object.entries(options || {}).forEach(([key, value]) => {
2✔
793
      // Skip undefined values
NEW
794
      if (value !== undefined) {
×
795
        // Add options as query parameters
NEW
796
        url.searchParams.set(key, value.toString())
×
797
      }
798
    })
799

800
    return sendRequest({
2✔
801
      url: url.toString(),
802
      method: HttpMethod.Get
803
    })
804
  }
805

806
  /**
807
   * Returns the right nonce to propose a new transaction after the last pending transaction.
808
   *
809
   * @param safeAddress - The Safe address
810
   * @returns The right nonce to propose a new transaction after the last pending transaction
811
   * @throws "Invalid Safe address"
812
   * @throws "Invalid data"
813
   * @throws "Invalid ethereum address"
814
   */
815
  async getNextNonce(safeAddress: string): Promise<string> {
816
    if (safeAddress === '') {
×
817
      throw new Error('Invalid Safe address')
×
818
    }
819
    const { address } = this.#getEip3770Address(safeAddress)
×
820
    const pendingTransactions = await this.getPendingTransactions(address)
×
821
    if (pendingTransactions.results.length > 0) {
×
822
      const maxNonce = pendingTransactions.results.reduce((acc, tx) => {
×
823
        const curr = BigInt(tx.nonce)
×
824
        return curr > acc ? curr : acc
×
825
      }, 0n)
826

827
      return (maxNonce + 1n).toString()
×
828
    }
829
    const safeInfo = await this.getSafeInfo(address)
×
830
    return safeInfo.nonce
×
831
  }
832

833
  /**
834
   * Returns the list of all the ERC20 tokens handled by the Safe.
835
   *
836
   * @param options - Optional parameters to filter or modify the response
837
   * @returns The list of all the ERC20 tokens
838
   */
839
  async getTokenList(options?: TokenInfoListOptions): Promise<TokenInfoListResponse> {
840
    const url = new URL(`${this.#txServiceBaseUrl}/v1/tokens/`)
1✔
841

842
    // Handle any additional query parameters
843
    Object.entries(options || {}).forEach(([key, value]) => {
1✔
844
      // Skip undefined values
NEW
845
      if (value !== undefined) {
×
846
        // Add options as query parameters
NEW
847
        url.searchParams.set(key, value.toString())
×
848
      }
849
    })
850

851
    return sendRequest({
1✔
852
      url: url.toString(),
853
      method: HttpMethod.Get
854
    })
855
  }
856

857
  /**
858
   * Returns the information of a given ERC20 token.
859
   *
860
   * @param tokenAddress - The token address
861
   * @returns The information of the given ERC20 token
862
   * @throws "Invalid token address"
863
   * @throws "Checksum address validation failed"
864
   */
865
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
866
    if (tokenAddress === '') {
2!
867
      throw new Error('Invalid token address')
×
868
    }
869
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
870
    return sendRequest({
2✔
871
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
872
      method: HttpMethod.Get
873
    })
874
  }
875

876
  /**
877
   * Get the SafeOperations that were sent from a particular address.
878
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
879
   * @throws "Safe address must not be empty"
880
   * @throws "Invalid Ethereum address {safeAddress}"
881
   * @returns The SafeOperations sent from the given Safe's address
882
   */
883
  async getSafeOperationsByAddress({
884
    safeAddress,
885
    executed,
886
    ordering,
887
    limit,
888
    offset
889
  }: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
890
    if (!safeAddress) {
1!
891
      throw new Error('Safe address must not be empty')
×
892
    }
893

894
    const { address } = this.#getEip3770Address(safeAddress)
1✔
895

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

898
    if (ordering) {
1!
899
      url.searchParams.set('ordering', ordering)
×
900
    }
901

902
    if (limit != null) {
1!
903
      url.searchParams.set('limit', limit.toString())
×
904
    }
905

906
    if (offset != null) {
1!
907
      url.searchParams.set('offset', offset.toString())
×
908
    }
909

910
    if (executed != null) {
1!
911
      url.searchParams.set('executed', executed.toString())
×
912
    }
913

914
    return sendRequest({
1✔
915
      url: url.toString(),
916
      method: HttpMethod.Get
917
    })
918
  }
919

920
  /**
921
   * Get the SafeOperations that are pending to send to the bundler
922
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
923
   * @throws "Safe address must not be empty"
924
   * @throws "Invalid Ethereum address {safeAddress}"
925
   * @returns The pending SafeOperations
926
   */
927
  async getPendingSafeOperations(
928
    props: GetPendingSafeOperationListProps
929
  ): Promise<GetSafeOperationListResponse> {
930
    return this.getSafeOperationsByAddress({
×
931
      ...props,
932
      executed: false
933
    })
934
  }
935

936
  /**
937
   * Get a SafeOperation by its hash.
938
   * @param safeOperationHash The SafeOperation hash
939
   * @throws "SafeOperation hash must not be empty"
940
   * @throws "Not found."
941
   * @returns The SafeOperation
942
   */
943
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
944
    if (!safeOperationHash) {
1!
945
      throw new Error('SafeOperation hash must not be empty')
×
946
    }
947

948
    return sendRequest({
1✔
949
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
950
      method: HttpMethod.Get
951
    })
952
  }
953

954
  /**
955
   * Create a new 4337 SafeOperation for a Safe.
956
   * @param addSafeOperationProps - The configuration of the SafeOperation
957
   * @throws "Safe address must not be empty"
958
   * @throws "Invalid Safe address {safeAddress}"
959
   * @throws "Module address must not be empty"
960
   * @throws "Invalid module address {moduleAddress}"
961
   * @throws "Signature must not be empty"
962
   */
963
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
964
    let safeAddress: string, moduleAddress: string
965
    let addSafeOperationProps: AddSafeOperationProps
966

967
    if (isSafeOperation(safeOperation)) {
1!
968
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
969
    } else {
970
      addSafeOperationProps = safeOperation
1✔
971
    }
972

973
    const {
974
      entryPoint,
975
      moduleAddress: moduleAddressProp,
976
      options,
977
      safeAddress: safeAddressProp,
978
      userOperation
979
    } = addSafeOperationProps
1✔
980
    if (!safeAddressProp) {
1!
981
      throw new Error('Safe address must not be empty')
×
982
    }
983
    try {
1✔
984
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
985
    } catch (err) {
986
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
987
    }
988

989
    if (!moduleAddressProp) {
1!
990
      throw new Error('Module address must not be empty')
×
991
    }
992

993
    try {
1✔
994
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
995
    } catch (err) {
996
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
997
    }
998

999
    if (isEmptyData(userOperation.signature)) {
1!
1000
      throw new Error('Signature must not be empty')
×
1001
    }
1002

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

1007
    const userOperationV06 = userOperation as UserOperationV06
1✔
1008

1009
    return sendRequest({
1✔
1010
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
1011
      method: HttpMethod.Post,
1012
      body: {
1013
        initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
1!
1014
        nonce: userOperation.nonce,
1015
        callData: userOperation.callData,
1016
        callGasLimit: userOperation.callGasLimit.toString(),
1017
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
1018
        preVerificationGas: userOperation.preVerificationGas.toString(),
1019
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
1020
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
1021
        paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
1!
1022
          ? null
1023
          : userOperationV06.paymasterAndData,
1024
        entryPoint,
1025
        validAfter: getISOString(options?.validAfter),
1026
        validUntil: getISOString(options?.validUntil),
1027
        signature: userOperation.signature,
1028
        moduleAddress
1029
      }
1030
    })
1031
  }
1032

1033
  /**
1034
   * Returns the list of confirmations for a given a SafeOperation.
1035
   *
1036
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
1037
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
1038
   * @returns The list of confirmations
1039
   * @throws "Invalid SafeOperation hash"
1040
   * @throws "Invalid data"
1041
   */
1042
  async getSafeOperationConfirmations(
1043
    safeOperationHash: string,
1044
    { limit, offset }: ListOptions = {}
1✔
1045
  ): Promise<SafeOperationConfirmationListResponse> {
1046
    if (!safeOperationHash) {
1!
1047
      throw new Error('Invalid SafeOperation hash')
×
1048
    }
1049

1050
    const url = new URL(
1✔
1051
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
1052
    )
1053

1054
    if (limit != null) {
1!
1055
      url.searchParams.set('limit', limit.toString())
×
1056
    }
1057

1058
    if (offset != null) {
1!
1059
      url.searchParams.set('offset', offset.toString())
×
1060
    }
1061

1062
    return sendRequest({
1✔
1063
      url: url.toString(),
1064
      method: HttpMethod.Get
1065
    })
1066
  }
1067

1068
  /**
1069
   * Adds a confirmation for a SafeOperation.
1070
   *
1071
   * @param safeOperationHash The SafeOperation hash
1072
   * @param signature - Signature of the SafeOperation
1073
   * @returns
1074
   * @throws "Invalid SafeOperation hash"
1075
   * @throws "Invalid signature"
1076
   * @throws "Malformed data"
1077
   * @throws "Error processing data"
1078
   */
1079
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
1080
    if (!safeOperationHash) {
1!
1081
      throw new Error('Invalid SafeOperation hash')
×
1082
    }
1083
    if (!signature) {
1!
1084
      throw new Error('Invalid signature')
×
1085
    }
1086
    return sendRequest({
1✔
1087
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
1088
      method: HttpMethod.Post,
1089
      body: { signature }
1090
    })
1091
  }
1092
}
1093

1094
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

© 2026 Coveralls, Inc