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

safe-global / safe-core-sdk / 13328467783

14 Feb 2025 11:40AM UTC coverage: 78.287% (+0.2%) from 78.049%
13328467783

Pull #1135

github

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

260 of 405 branches covered (64.2%)

Branch coverage included in aggregate %.

54 of 73 new or added lines in 2 files covered. (73.97%)

901 of 1078 relevant lines covered (83.58%)

4.65 hits per line

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

57.7
/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
import { QUERY_PARAMS_MAP } from './utils/queryParamsMap'
1✔
57

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

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

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

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

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

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

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

99
  #addUrlQueryParams<T extends QueryParamsOptions>(url: URL, options: T): void {
100
    const camelToSnake = (str: string) => str.replace(/([A-Z])/g, '_$1').toLowerCase()
4✔
101

102
    // Handle any additional query parameters
103
    Object.entries(options || {}).forEach(([key, value]) => {
4!
104
      // Skip undefined values
105
      if (value !== undefined) {
4✔
106
        const name = QUERY_PARAMS_MAP[key] ?? camelToSnake(key)
4✔
107
        // Add options as query parameters
108
        url.searchParams.set(name, value.toString())
4✔
109
      }
110
    })
111
  }
112

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

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

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

152
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
153

154
    if (to) {
1!
155
      dataDecoderRequest.to = to
×
156
    }
157

158
    return sendRequest({
1✔
159
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
160
      method: HttpMethod.Post,
161
      body: dataDecoderRequest
162
    })
163
  }
164

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

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

204
    return sendRequest({
2✔
205
      url: url.toString(),
206
      method: HttpMethod.Get
207
    })
208
  }
209

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

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

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

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

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

303
    return sendRequest({
1✔
304
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
305
      method: HttpMethod.Get
306
    })
307
  }
308

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

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

325
    if (ordering) {
1!
NEW
326
      url.searchParams.set('ordering', ordering)
×
327
    }
328

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

333
    if (offset != null) {
1!
NEW
334
      url.searchParams.set('offset', offset.toString())
×
335
    }
336

337
    return sendRequest({
1✔
338
      url: url.toString(),
339
      method: HttpMethod.Get
340
    })
341
  }
342

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

354
    return sendRequest({
2✔
355
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
356
      method: HttpMethod.Post,
357
      body: addMessageProps
358
    })
359
  }
360

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

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

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

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

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

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

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

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

505
      return response as SafeInfoResponse
×
506
    })
507
  }
508

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

534
      return response as SafeCreationInfoResponse
×
535
    })
536
  }
537

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

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

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

622
    // Handle additional query parameters
623
    if (options !== undefined) this.#addUrlQueryParams<GetIncomingTransactionsOptions>(url, options)
3✔
624

625
    return sendRequest({
3✔
626
      url: url.toString(),
627
      method: HttpMethod.Get
628
    })
629
  }
630

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

651
    // Handle additional query parameters
652
    if (options !== undefined) this.#addUrlQueryParams<GetModuleTransactionsOptions>(url, options)
2!
653

654
    return sendRequest({
2✔
655
      url: url.toString(),
656
      method: HttpMethod.Get
657
    })
658
  }
659

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

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

680
    // Handle additional query parameters
681
    if (options !== undefined) this.#addUrlQueryParams<GetMultisigTransactionsOptions>(url, options)
4✔
682

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

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

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

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

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

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

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

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

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

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

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

790
    // Handle additional query parameters
791
    if (options !== undefined) this.#addUrlQueryParams<AllTransactionsOptions>(url, options)
2!
792

793
    return sendRequest({
2✔
794
      url: url.toString(),
795
      method: HttpMethod.Get
796
    })
797
  }
798

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

820
      return (maxNonce + 1n).toString()
×
821
    }
822
    const safeInfo = await this.getSafeInfo(address)
×
823
    return safeInfo.nonce
×
824
  }
825

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

835
    // Handle additional query parameters
836
    if (options !== undefined) this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1!
837

838
    return sendRequest({
1✔
839
      url: url.toString(),
840
      method: HttpMethod.Get
841
    })
842
  }
843

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

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

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

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

882
    // Handle additional query parameters
883
    if (options !== undefined) this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1✔
884

885
    return sendRequest({
1✔
886
      url: url.toString(),
887
      method: HttpMethod.Get
888
    })
889
  }
890

891
  /**
892
   * Get the SafeOperations that are pending to send to the bundler
893
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
894
   * @throws "Safe address must not be empty"
895
   * @throws "Invalid Ethereum address {safeAddress}"
896
   * @returns The pending SafeOperations
897
   */
898
  async getPendingSafeOperations(
899
    props: GetPendingSafeOperationListProps
900
  ): Promise<GetSafeOperationListResponse> {
901
    return this.getSafeOperationsByAddress({
×
902
      ...props,
903
      executed: false
904
    })
905
  }
906

907
  /**
908
   * Get a SafeOperation by its hash.
909
   * @param safeOperationHash The SafeOperation hash
910
   * @throws "SafeOperation hash must not be empty"
911
   * @throws "Not found."
912
   * @returns The SafeOperation
913
   */
914
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
915
    if (!safeOperationHash) {
1!
916
      throw new Error('SafeOperation hash must not be empty')
×
917
    }
918

919
    return sendRequest({
1✔
920
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
921
      method: HttpMethod.Get
922
    })
923
  }
924

925
  /**
926
   * Create a new 4337 SafeOperation for a Safe.
927
   * @param addSafeOperationProps - The configuration of the SafeOperation
928
   * @throws "Safe address must not be empty"
929
   * @throws "Invalid Safe address {safeAddress}"
930
   * @throws "Module address must not be empty"
931
   * @throws "Invalid module address {moduleAddress}"
932
   * @throws "Signature must not be empty"
933
   */
934
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
935
    let safeAddress: string, moduleAddress: string
936
    let addSafeOperationProps: AddSafeOperationProps
937

938
    if (isSafeOperation(safeOperation)) {
1!
939
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
940
    } else {
941
      addSafeOperationProps = safeOperation
1✔
942
    }
943

944
    const {
945
      entryPoint,
946
      moduleAddress: moduleAddressProp,
947
      options,
948
      safeAddress: safeAddressProp,
949
      userOperation
950
    } = addSafeOperationProps
1✔
951
    if (!safeAddressProp) {
1!
952
      throw new Error('Safe address must not be empty')
×
953
    }
954
    try {
1✔
955
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
956
    } catch (err) {
957
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
958
    }
959

960
    if (!moduleAddressProp) {
1!
961
      throw new Error('Module address must not be empty')
×
962
    }
963

964
    try {
1✔
965
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
966
    } catch (err) {
967
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
968
    }
969

970
    if (isEmptyData(userOperation.signature)) {
1!
971
      throw new Error('Signature must not be empty')
×
972
    }
973

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

978
    const userOperationV06 = userOperation as UserOperationV06
1✔
979

980
    return sendRequest({
1✔
981
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
982
      method: HttpMethod.Post,
983
      body: {
984
        initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
1!
985
        nonce: userOperation.nonce,
986
        callData: userOperation.callData,
987
        callGasLimit: userOperation.callGasLimit.toString(),
988
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
989
        preVerificationGas: userOperation.preVerificationGas.toString(),
990
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
991
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
992
        paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
1!
993
          ? null
994
          : userOperationV06.paymasterAndData,
995
        entryPoint,
996
        validAfter: getISOString(options?.validAfter),
997
        validUntil: getISOString(options?.validUntil),
998
        signature: userOperation.signature,
999
        moduleAddress
1000
      }
1001
    })
1002
  }
1003

1004
  /**
1005
   * Returns the list of confirmations for a given a SafeOperation.
1006
   *
1007
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
1008
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
1009
   * @returns The list of confirmations
1010
   * @throws "Invalid SafeOperation hash"
1011
   * @throws "Invalid data"
1012
   */
1013
  async getSafeOperationConfirmations(
1014
    safeOperationHash: string,
1015
    { limit, offset }: ListOptions = {}
1✔
1016
  ): Promise<SafeOperationConfirmationListResponse> {
1017
    if (!safeOperationHash) {
1!
1018
      throw new Error('Invalid SafeOperation hash')
×
1019
    }
1020

1021
    const url = new URL(
1✔
1022
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
1023
    )
1024

1025
    if (limit != null) {
1!
1026
      url.searchParams.set('limit', limit.toString())
×
1027
    }
1028

1029
    if (offset != null) {
1!
1030
      url.searchParams.set('offset', offset.toString())
×
1031
    }
1032

1033
    return sendRequest({
1✔
1034
      url: url.toString(),
1035
      method: HttpMethod.Get
1036
    })
1037
  }
1038

1039
  /**
1040
   * Adds a confirmation for a SafeOperation.
1041
   *
1042
   * @param safeOperationHash The SafeOperation hash
1043
   * @param signature - Signature of the SafeOperation
1044
   * @returns
1045
   * @throws "Invalid SafeOperation hash"
1046
   * @throws "Invalid signature"
1047
   * @throws "Malformed data"
1048
   * @throws "Error processing data"
1049
   */
1050
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
1051
    if (!safeOperationHash) {
1!
1052
      throw new Error('Invalid SafeOperation hash')
×
1053
    }
1054
    if (!signature) {
1!
1055
      throw new Error('Invalid signature')
×
1056
    }
1057
    return sendRequest({
1✔
1058
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
1059
      method: HttpMethod.Post,
1060
      body: { signature }
1061
    })
1062
  }
1063
}
1064

1065
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