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

safe-global / safe-core-sdk / 13327901144

14 Feb 2025 11:04AM UTC coverage: 77.725% (-0.3%) from 78.049%
13327901144

Pull #1135

github

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

256 of 406 branches covered (63.05%)

Branch coverage included in aggregate %.

48 of 67 new or added lines in 1 file covered. (71.64%)

899 of 1080 relevant lines covered (83.24%)

4.63 hits per line

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

55.52
/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
  QueryParamsOptions,
22
  SafeCreationInfoResponse,
23
  SafeDelegateListResponse,
24
  SafeInfoResponse,
25
  SafeMessage,
26
  SafeMessageListResponse,
27
  SafeModuleTransactionListResponse,
28
  SafeMultisigTransactionEstimate,
29
  SafeMultisigTransactionEstimateResponse,
30
  SafeMultisigTransactionListResponse,
31
  SafeServiceInfoResponse,
32
  SafeSingletonResponse,
33
  SignatureResponse,
34
  SignedSafeDelegateResponse,
35
  TokenInfoListOptions,
36
  TokenInfoListResponse,
37
  TokenInfoResponse,
38
  TransferListResponse
39
} from '@safe-global/api-kit/types/safeTransactionServiceTypes'
40
import { HttpMethod, sendRequest } from '@safe-global/api-kit/utils/httpRequests'
1✔
41
import { signDelegate } from '@safe-global/api-kit/utils/signDelegate'
1✔
42
import { validateEip3770Address, validateEthereumAddress } from '@safe-global/protocol-kit'
1✔
43
import {
44
  DataDecoded,
45
  Eip3770Address,
46
  SafeMultisigConfirmationListResponse,
47
  SafeMultisigTransactionResponse,
48
  SafeOperation,
49
  SafeOperationConfirmationListResponse,
50
  SafeOperationResponse,
51
  UserOperationV06
52
} from '@safe-global/types-kit'
53
import { TRANSACTION_SERVICE_URLS } from './utils/config'
1✔
54
import { isEmptyData } from './utils'
1✔
55
import { getAddSafeOperationProps, isSafeOperation } from './utils/safeOperation'
1✔
56

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

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

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

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

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

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

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

98
  #addUrlQueryParams<T extends QueryParamsOptions>(url: URL, options: T): void {
99
    // Handle any additional query parameters
100
    Object.entries(options || {}).forEach(([key, value]) => {
2!
101
      // Skip undefined values
102
      if (value !== undefined) {
3✔
103
        // Add options as query parameters
104
        url.searchParams.set(key, value.toString())
3✔
105
      }
106
    })
107
  }
108

109
  /**
110
   * Returns the information and configuration of the service.
111
   *
112
   * @returns The information and configuration of the service
113
   */
114
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
115
    return sendRequest({
2✔
116
      url: `${this.#txServiceBaseUrl}/v1/about`,
117
      method: HttpMethod.Get
118
    })
119
  }
120

121
  /**
122
   * Returns the list of Safe singletons.
123
   *
124
   * @returns The list of Safe singletons
125
   */
126
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
127
    return sendRequest({
1✔
128
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
129
      method: HttpMethod.Get
130
    })
131
  }
132

133
  /**
134
   * Decodes the specified Safe transaction data.
135
   *
136
   * @param data - The Safe transaction data. '0x' prefixed hexadecimal string.
137
   * @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
138
   * @returns The transaction data decoded
139
   * @throws "Invalid data"
140
   * @throws "Not Found"
141
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
142
   */
143
  async decodeData(data: string, to?: string): Promise<DataDecoded> {
144
    if (data === '') {
1!
145
      throw new Error('Invalid data')
×
146
    }
147

148
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
149

150
    if (to) {
1!
151
      dataDecoderRequest.to = to
×
152
    }
153

154
    return sendRequest({
1✔
155
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
156
      method: HttpMethod.Post,
157
      body: dataDecoderRequest
158
    })
159
  }
160

161
  /**
162
   * Returns the list of delegates.
163
   *
164
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
165
   * @returns The list of delegates
166
   * @throws "Checksum address validation failed"
167
   */
168
  async getSafeDelegates({
169
    safeAddress,
170
    delegateAddress,
171
    delegatorAddress,
172
    label,
173
    limit,
174
    offset
175
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
176
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
177

178
    if (safeAddress) {
2✔
179
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
180
      url.searchParams.set('safe', safe)
2✔
181
    }
182
    if (delegateAddress) {
2!
NEW
183
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
NEW
184
      url.searchParams.set('delegate', delegate)
×
185
    }
186
    if (delegatorAddress) {
2!
NEW
187
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
NEW
188
      url.searchParams.set('delegator', delegator)
×
189
    }
190
    if (label) {
2!
NEW
191
      url.searchParams.set('label', label)
×
192
    }
193
    if (limit != null) {
2!
NEW
194
      url.searchParams.set('limit', limit.toString())
×
195
    }
196
    if (offset != null) {
2!
NEW
197
      url.searchParams.set('offset', offset.toString())
×
198
    }
199

200
    return sendRequest({
2✔
201
      url: url.toString(),
202
      method: HttpMethod.Get
203
    })
204
  }
205

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

239
    const body: any = {
2✔
240
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
241
      delegate,
242
      delegator,
243
      label,
244
      signature
245
    }
246
    return sendRequest({
2✔
247
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
248
      method: HttpMethod.Post,
249
      body
250
    })
251
  }
252

253
  /**
254
   * Removes a delegate for a given Safe address.
255
   *
256
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
257
   * @returns
258
   * @throws "Invalid Safe delegate address"
259
   * @throws "Invalid Safe delegator address"
260
   * @throws "Checksum address validation failed"
261
   * @throws "Signing owner is not an owner of the Safe"
262
   * @throws "Not found"
263
   */
264
  async removeSafeDelegate({
265
    delegateAddress,
266
    delegatorAddress,
267
    signer
268
  }: DeleteSafeDelegateProps): Promise<void> {
269
    if (delegateAddress === '') {
2!
NEW
270
      throw new Error('Invalid Safe delegate address')
×
271
    }
272
    if (delegatorAddress === '') {
2!
NEW
273
      throw new Error('Invalid Safe delegator address')
×
274
    }
275
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
276
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
277
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
278

279
    return sendRequest({
2✔
280
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
281
      method: HttpMethod.Delete,
282
      body: {
283
        delegator,
284
        signature
285
      }
286
    })
287
  }
288

289
  /**
290
   * Get a message by its safe message hash
291
   * @param messageHash The Safe message hash
292
   * @returns The message
293
   */
294
  async getMessage(messageHash: string): Promise<SafeMessage> {
295
    if (!messageHash) {
1!
NEW
296
      throw new Error('Invalid messageHash')
×
297
    }
298

299
    return sendRequest({
1✔
300
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
301
      method: HttpMethod.Get
302
    })
303
  }
304

305
  /**
306
   * Get the list of messages associated to a Safe account
307
   * @param safeAddress The safe address
308
   * @param options The options to filter the list of messages
309
   * @returns The paginated list of messages
310
   */
311
  async getMessages(
312
    safeAddress: string,
313
    { ordering, limit, offset }: GetSafeMessageListProps = {}
1✔
314
  ): Promise<SafeMessageListResponse> {
315
    if (!this.#isValidAddress(safeAddress)) {
1!
NEW
316
      throw new Error('Invalid safeAddress')
×
317
    }
318

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

321
    if (ordering) {
1!
NEW
322
      url.searchParams.set('ordering', ordering)
×
323
    }
324

325
    if (limit != null) {
1!
NEW
326
      url.searchParams.set('limit', limit.toString())
×
327
    }
328

329
    if (offset != null) {
1!
NEW
330
      url.searchParams.set('offset', offset.toString())
×
331
    }
332

333
    return sendRequest({
1✔
334
      url: url.toString(),
335
      method: HttpMethod.Get
336
    })
337
  }
338

339
  /**
340
   * Creates a new message with an initial signature
341
   * Add more signatures from other owners using addMessageSignature()
342
   * @param safeAddress The safe address
343
   * @param options The raw message to add, signature and safeAppId if any
344
   */
345
  async addMessage(safeAddress: string, addMessageProps: AddMessageProps): Promise<void> {
346
    if (!this.#isValidAddress(safeAddress)) {
2!
NEW
347
      throw new Error('Invalid safeAddress')
×
348
    }
349

350
    return sendRequest({
2✔
351
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
352
      method: HttpMethod.Post,
353
      body: addMessageProps
354
    })
355
  }
356

357
  /**
358
   * Add a signature to an existing message
359
   * @param messageHash The safe message hash
360
   * @param signature The signature
361
   */
362
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
363
    if (!messageHash || !signature) {
1!
NEW
364
      throw new Error('Invalid messageHash or signature')
×
365
    }
366

367
    return sendRequest({
1✔
368
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
369
      method: HttpMethod.Post,
370
      body: {
371
        signature
372
      }
373
    })
374
  }
375

376
  /**
377
   * Returns the list of Safes where the address provided is an owner.
378
   *
379
   * @param ownerAddress - The owner address
380
   * @returns The list of Safes where the address provided is an owner
381
   * @throws "Invalid owner address"
382
   * @throws "Checksum address validation failed"
383
   */
384
  async getSafesByOwner(ownerAddress: string): Promise<OwnerResponse> {
385
    if (ownerAddress === '') {
2!
386
      throw new Error('Invalid owner address')
×
387
    }
388
    const { address } = this.#getEip3770Address(ownerAddress)
2✔
389
    return sendRequest({
2✔
390
      url: `${this.#txServiceBaseUrl}/v1/owners/${address}/safes/`,
391
      method: HttpMethod.Get
392
    })
393
  }
394

395
  /**
396
   * Returns the list of Safes where the module address provided is enabled.
397
   *
398
   * @param moduleAddress - The Safe module address
399
   * @returns The list of Safe addresses where the module provided is enabled
400
   * @throws "Invalid module address"
401
   * @throws "Module address checksum not valid"
402
   */
403
  async getSafesByModule(moduleAddress: string): Promise<ModulesResponse> {
404
    if (moduleAddress === '') {
2!
405
      throw new Error('Invalid module address')
×
406
    }
407
    const { address } = this.#getEip3770Address(moduleAddress)
2✔
408
    return sendRequest({
2✔
409
      url: `${this.#txServiceBaseUrl}/v1/modules/${address}/safes/`,
410
      method: HttpMethod.Get
411
    })
412
  }
413

414
  /**
415
   * Returns all the information of a Safe transaction.
416
   *
417
   * @param safeTxHash - Hash of the Safe transaction
418
   * @returns The information of a Safe transaction
419
   * @throws "Invalid safeTxHash"
420
   * @throws "Not found."
421
   */
422
  async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
423
    if (safeTxHash === '') {
1!
424
      throw new Error('Invalid safeTxHash')
×
425
    }
426
    return sendRequest({
1✔
427
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/`,
428
      method: HttpMethod.Get
429
    })
430
  }
431

432
  /**
433
   * Returns the list of confirmations for a given a Safe transaction.
434
   *
435
   * @param safeTxHash - The hash of the Safe transaction
436
   * @returns The list of confirmations
437
   * @throws "Invalid safeTxHash"
438
   */
439
  async getTransactionConfirmations(
440
    safeTxHash: string
441
  ): Promise<SafeMultisigConfirmationListResponse> {
442
    if (safeTxHash === '') {
1!
443
      throw new Error('Invalid safeTxHash')
×
444
    }
445
    return sendRequest({
1✔
446
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
447
      method: HttpMethod.Get
448
    })
449
  }
450

451
  /**
452
   * Adds a confirmation for a Safe transaction.
453
   *
454
   * @param safeTxHash - Hash of the Safe transaction that will be confirmed
455
   * @param signature - Signature of the transaction
456
   * @returns
457
   * @throws "Invalid safeTxHash"
458
   * @throws "Invalid signature"
459
   * @throws "Malformed data"
460
   * @throws "Error processing data"
461
   */
462
  async confirmTransaction(safeTxHash: string, signature: string): Promise<SignatureResponse> {
463
    if (safeTxHash === '') {
1!
464
      throw new Error('Invalid safeTxHash')
×
465
    }
466
    if (signature === '') {
1!
467
      throw new Error('Invalid signature')
×
468
    }
469
    return sendRequest({
1✔
470
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
471
      method: HttpMethod.Post,
472
      body: {
473
        signature
474
      }
475
    })
476
  }
477

478
  /**
479
   * Returns the information and configuration of the provided Safe address.
480
   *
481
   * @param safeAddress - The Safe address
482
   * @returns The information and configuration of the provided Safe address
483
   * @throws "Invalid Safe address"
484
   * @throws "Checksum address validation failed"
485
   */
486
  async getSafeInfo(safeAddress: string): Promise<SafeInfoResponse> {
487
    if (safeAddress === '') {
2!
488
      throw new Error('Invalid Safe address')
×
489
    }
490
    const { address } = this.#getEip3770Address(safeAddress)
2✔
491
    return sendRequest({
2✔
492
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
493
      method: HttpMethod.Get
494
    }).then((response: any) => {
495
      // FIXME remove when the transaction service returns the singleton property instead of masterCopy
496
      if (!response?.singleton) {
2✔
497
        const { masterCopy, ...rest } = response
2✔
498
        return { ...rest, singleton: masterCopy } as SafeInfoResponse
2✔
499
      }
500

501
      return response as SafeInfoResponse
×
502
    })
503
  }
504

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

530
      return response as SafeCreationInfoResponse
×
531
    })
532
  }
533

534
  /**
535
   * Estimates the safeTxGas for a given Safe multi-signature transaction.
536
   *
537
   * @param safeAddress - The Safe address
538
   * @param safeTransaction - The Safe transaction to estimate
539
   * @returns The safeTxGas for the given Safe transaction
540
   * @throws "Invalid Safe address"
541
   * @throws "Data not valid"
542
   * @throws "Safe not found"
543
   * @throws "Tx not valid"
544
   */
545
  async estimateSafeTransaction(
546
    safeAddress: string,
547
    safeTransaction: SafeMultisigTransactionEstimate
548
  ): Promise<SafeMultisigTransactionEstimateResponse> {
549
    if (safeAddress === '') {
2!
550
      throw new Error('Invalid Safe address')
×
551
    }
552
    const { address } = this.#getEip3770Address(safeAddress)
2✔
553
    return sendRequest({
2✔
554
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/estimations/`,
555
      method: HttpMethod.Post,
556
      body: safeTransaction
557
    })
558
  }
559

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

599
  /**
600
   * Returns the history of incoming transactions of a Safe account.
601
   *
602
   * @param safeAddress - The Safe address
603
   * @param options - Optional parameters to filter or modify the response
604
   * @returns The history of incoming transactions
605
   * @throws "Invalid Safe address"
606
   * @throws "Checksum address validation failed"
607
   */
608
  async getIncomingTransactions(
609
    safeAddress: string,
610
    options?: GetIncomingTransactionsOptions
611
  ): Promise<TransferListResponse> {
612
    if (safeAddress === '') {
2!
613
      throw new Error('Invalid Safe address')
×
614
    }
615
    const { address } = this.#getEip3770Address(safeAddress)
2✔
616
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/incoming-transfers/`)
2✔
617

618
    // Handle additional query parameters
619
    if (options !== undefined) this.#addUrlQueryParams<GetIncomingTransactionsOptions>(url, options)
2!
620

621
    return sendRequest({
2✔
622
      url: url.toString(),
623
      method: HttpMethod.Get
624
    })
625
  }
626

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

647
    // Handle additional query parameters
648
    if (options !== undefined) this.#addUrlQueryParams<GetModuleTransactionsOptions>(url, options)
2!
649

650
    return sendRequest({
2✔
651
      url: url.toString(),
652
      method: HttpMethod.Get
653
    })
654
  }
655

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

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

676
    // Handle additional query parameters
677
    if (options !== undefined) this.#addUrlQueryParams<GetMultisigTransactionsOptions>(url, options)
4✔
678

679
    return sendRequest({
4✔
680
      url: url.toString(),
681
      method: HttpMethod.Get
682
    })
683
  }
684

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

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

737
    const { address } = this.#getEip3770Address(safeAddress)
2✔
738
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
739

740
    const url = new URL(
2✔
741
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
742
    )
743

744
    if (hasConfirmations) {
2!
745
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
746
    }
747

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

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

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

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

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

786
    // Handle additional query parameters
787
    if (options !== undefined) this.#addUrlQueryParams<AllTransactionsOptions>(url, options)
2!
788

789
    return sendRequest({
2✔
790
      url: url.toString(),
791
      method: HttpMethod.Get
792
    })
793
  }
794

795
  /**
796
   * Returns the right nonce to propose a new transaction after the last pending transaction.
797
   *
798
   * @param safeAddress - The Safe address
799
   * @returns The right nonce to propose a new transaction after the last pending transaction
800
   * @throws "Invalid Safe address"
801
   * @throws "Invalid data"
802
   * @throws "Invalid ethereum address"
803
   */
804
  async getNextNonce(safeAddress: string): Promise<string> {
805
    if (safeAddress === '') {
×
806
      throw new Error('Invalid Safe address')
×
807
    }
808
    const { address } = this.#getEip3770Address(safeAddress)
×
809
    const pendingTransactions = await this.getPendingTransactions(address)
×
810
    if (pendingTransactions.results.length > 0) {
×
811
      const maxNonce = pendingTransactions.results.reduce((acc, tx) => {
×
812
        const curr = BigInt(tx.nonce)
×
813
        return curr > acc ? curr : acc
×
814
      }, 0n)
815

816
      return (maxNonce + 1n).toString()
×
817
    }
818
    const safeInfo = await this.getSafeInfo(address)
×
819
    return safeInfo.nonce
×
820
  }
821

822
  /**
823
   * Returns the list of all the ERC20 tokens handled by the Safe.
824
   *
825
   * @param options - Optional parameters to filter or modify the response
826
   * @returns The list of all the ERC20 tokens
827
   */
828
  async getTokenList(options?: TokenInfoListOptions): Promise<TokenInfoListResponse> {
829
    const url = new URL(`${this.#txServiceBaseUrl}/v1/tokens/`)
1✔
830

831
    // Handle additional query parameters
832
    if (options !== undefined) this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1!
833

834
    return sendRequest({
1✔
835
      url: url.toString(),
836
      method: HttpMethod.Get
837
    })
838
  }
839

840
  /**
841
   * Returns the information of a given ERC20 token.
842
   *
843
   * @param tokenAddress - The token address
844
   * @returns The information of the given ERC20 token
845
   * @throws "Invalid token address"
846
   * @throws "Checksum address validation failed"
847
   */
848
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
849
    if (tokenAddress === '') {
2!
850
      throw new Error('Invalid token address')
×
851
    }
852
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
853
    return sendRequest({
2✔
854
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
855
      method: HttpMethod.Get
856
    })
857
  }
858

859
  /**
860
   * Get the SafeOperations that were sent from a particular address.
861
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
862
   * @throws "Safe address must not be empty"
863
   * @throws "Invalid Ethereum address {safeAddress}"
864
   * @returns The SafeOperations sent from the given Safe's address
865
   */
866
  async getSafeOperationsByAddress({
867
    safeAddress,
868
    executed,
869
    ordering,
870
    limit,
871
    offset
872
  }: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
873
    if (!safeAddress) {
1!
874
      throw new Error('Safe address must not be empty')
×
875
    }
876

877
    const { address } = this.#getEip3770Address(safeAddress)
1✔
878

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

881
    if (ordering) {
1!
882
      url.searchParams.set('ordering', ordering)
×
883
    }
884

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

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

893
    if (executed != null) {
1!
894
      url.searchParams.set('executed', executed.toString())
×
895
    }
896

897
    return sendRequest({
1✔
898
      url: url.toString(),
899
      method: HttpMethod.Get
900
    })
901
  }
902

903
  /**
904
   * Get the SafeOperations that are pending to send to the bundler
905
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
906
   * @throws "Safe address must not be empty"
907
   * @throws "Invalid Ethereum address {safeAddress}"
908
   * @returns The pending SafeOperations
909
   */
910
  async getPendingSafeOperations(
911
    props: GetPendingSafeOperationListProps
912
  ): Promise<GetSafeOperationListResponse> {
913
    return this.getSafeOperationsByAddress({
×
914
      ...props,
915
      executed: false
916
    })
917
  }
918

919
  /**
920
   * Get a SafeOperation by its hash.
921
   * @param safeOperationHash The SafeOperation hash
922
   * @throws "SafeOperation hash must not be empty"
923
   * @throws "Not found."
924
   * @returns The SafeOperation
925
   */
926
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
927
    if (!safeOperationHash) {
1!
928
      throw new Error('SafeOperation hash must not be empty')
×
929
    }
930

931
    return sendRequest({
1✔
932
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
933
      method: HttpMethod.Get
934
    })
935
  }
936

937
  /**
938
   * Create a new 4337 SafeOperation for a Safe.
939
   * @param addSafeOperationProps - The configuration of the SafeOperation
940
   * @throws "Safe address must not be empty"
941
   * @throws "Invalid Safe address {safeAddress}"
942
   * @throws "Module address must not be empty"
943
   * @throws "Invalid module address {moduleAddress}"
944
   * @throws "Signature must not be empty"
945
   */
946
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
947
    let safeAddress: string, moduleAddress: string
948
    let addSafeOperationProps: AddSafeOperationProps
949

950
    if (isSafeOperation(safeOperation)) {
1!
951
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
952
    } else {
953
      addSafeOperationProps = safeOperation
1✔
954
    }
955

956
    const {
957
      entryPoint,
958
      moduleAddress: moduleAddressProp,
959
      options,
960
      safeAddress: safeAddressProp,
961
      userOperation
962
    } = addSafeOperationProps
1✔
963
    if (!safeAddressProp) {
1!
964
      throw new Error('Safe address must not be empty')
×
965
    }
966
    try {
1✔
967
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
968
    } catch (err) {
969
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
970
    }
971

972
    if (!moduleAddressProp) {
1!
973
      throw new Error('Module address must not be empty')
×
974
    }
975

976
    try {
1✔
977
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
978
    } catch (err) {
979
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
980
    }
981

982
    if (isEmptyData(userOperation.signature)) {
1!
983
      throw new Error('Signature must not be empty')
×
984
    }
985

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

990
    const userOperationV06 = userOperation as UserOperationV06
1✔
991

992
    return sendRequest({
1✔
993
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
994
      method: HttpMethod.Post,
995
      body: {
996
        initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
1!
997
        nonce: userOperation.nonce,
998
        callData: userOperation.callData,
999
        callGasLimit: userOperation.callGasLimit.toString(),
1000
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
1001
        preVerificationGas: userOperation.preVerificationGas.toString(),
1002
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
1003
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
1004
        paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
1!
1005
          ? null
1006
          : userOperationV06.paymasterAndData,
1007
        entryPoint,
1008
        validAfter: getISOString(options?.validAfter),
1009
        validUntil: getISOString(options?.validUntil),
1010
        signature: userOperation.signature,
1011
        moduleAddress
1012
      }
1013
    })
1014
  }
1015

1016
  /**
1017
   * Returns the list of confirmations for a given a SafeOperation.
1018
   *
1019
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
1020
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
1021
   * @returns The list of confirmations
1022
   * @throws "Invalid SafeOperation hash"
1023
   * @throws "Invalid data"
1024
   */
1025
  async getSafeOperationConfirmations(
1026
    safeOperationHash: string,
1027
    { limit, offset }: ListOptions = {}
1✔
1028
  ): Promise<SafeOperationConfirmationListResponse> {
1029
    if (!safeOperationHash) {
1!
1030
      throw new Error('Invalid SafeOperation hash')
×
1031
    }
1032

1033
    const url = new URL(
1✔
1034
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
1035
    )
1036

1037
    if (limit != null) {
1!
1038
      url.searchParams.set('limit', limit.toString())
×
1039
    }
1040

1041
    if (offset != null) {
1!
1042
      url.searchParams.set('offset', offset.toString())
×
1043
    }
1044

1045
    return sendRequest({
1✔
1046
      url: url.toString(),
1047
      method: HttpMethod.Get
1048
    })
1049
  }
1050

1051
  /**
1052
   * Adds a confirmation for a SafeOperation.
1053
   *
1054
   * @param safeOperationHash The SafeOperation hash
1055
   * @param signature - Signature of the SafeOperation
1056
   * @returns
1057
   * @throws "Invalid SafeOperation hash"
1058
   * @throws "Invalid signature"
1059
   * @throws "Malformed data"
1060
   * @throws "Error processing data"
1061
   */
1062
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
1063
    if (!safeOperationHash) {
1!
1064
      throw new Error('Invalid SafeOperation hash')
×
1065
    }
1066
    if (!signature) {
1!
1067
      throw new Error('Invalid signature')
×
1068
    }
1069
    return sendRequest({
1✔
1070
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
1071
      method: HttpMethod.Post,
1072
      body: { signature }
1073
    })
1074
  }
1075
}
1076

1077
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