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

safe-global / safe-core-sdk / 15116832295

19 May 2025 03:18PM UTC coverage: 78.36% (+0.07%) from 78.287%
15116832295

Pull #1216

github

web-flow
Merge 2ee089535 into 82cfd46b2
Pull Request #1216: feat(api-kit): Api keys integration

256 of 402 branches covered (63.68%)

Branch coverage included in aggregate %.

20 of 29 new or added lines in 4 files covered. (68.97%)

68 existing lines in 2 files now uncovered.

910 of 1086 relevant lines covered (83.79%)

4.72 hits per line

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

59.55
/packages/api-kit/src/SafeApiKit.ts
1
import {
2
  AddMessageOptions,
3
  AddSafeDelegateProps,
4
  AddSafeOperationProps,
5
  AllTransactionsListResponse,
6
  AllTransactionsOptions,
7
  DeleteSafeDelegateProps,
8
  GetIncomingTransactionsOptions,
9
  GetModuleTransactionsOptions,
10
  GetMultisigTransactionsOptions,
11
  GetPendingSafeOperationListOptions,
12
  GetSafeDelegateProps,
13
  GetSafeMessageListOptions,
14
  GetSafeOperationListOptions,
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, HttpRequest, 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 { getTransactionServiceUrl } 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
  /** txServiceApiKey - The API key to access the Safe Transaction Service */
62
  txServiceApiKey: string
63
  /** txServiceUrl - Safe Transaction Service URL */
64
  txServiceUrl?: string
65
}
66

67
class SafeApiKit {
68
  #chainId: bigint
2✔
69
  #txServiceApiKey: string
2✔
70
  #txServiceBaseUrl: string
2✔
71

72
  constructor({ chainId, txServiceUrl, txServiceApiKey }: SafeApiKitConfig) {
73
    this.#chainId = chainId
2✔
74

75
    if (txServiceUrl) {
2✔
76
      this.#txServiceBaseUrl = txServiceUrl
1✔
77
    } else {
78
      const url = getTransactionServiceUrl(chainId)
1✔
79
      if (!url) {
1!
UNCOV
80
        throw new TypeError(
×
81
          `There is no transaction service available for chainId ${chainId}. Please set the txServiceUrl property to use a custom transaction service.`
82
        )
83
      }
84

85
      this.#txServiceBaseUrl = url
1✔
86
    }
87

88
    this.#txServiceApiKey = txServiceApiKey
2✔
89
  }
90

91
  #isValidAddress(address: string) {
1✔
92
    try {
3✔
93
      validateEthereumAddress(address)
3✔
94
      return true
3✔
95
    } catch {
NEW
96
      return false
×
97
    }
98
  }
99

100
  #getEip3770Address(fullAddress: string): Eip3770Address {
101
    return validateEip3770Address(fullAddress, this.#chainId)
42✔
102
  }
103

104
  /**
105
   * Adds query parameters from an options object to a given URL.
106
   * Converts parameter names to snake_case automatically. If a specific mapping exists in QUERY_PARAMS_MAP,
107
   * it will be used instead of the converted name.
108
   *
109
   * @param {URL} url - The URL object to which query parameters will be added.
110
   * @param {T} options - An object containing key-value pairs representing query parameters.
111
   * @returns {void}
112
   */
113
  #addUrlQueryParams<T extends QueryParamsOptions>(url: URL, options?: T): void {
114
    const camelToSnake = (str: string) => str.replace(/([A-Z])/g, '_$1').toLowerCase()
14✔
115

116
    // Handle any additional query parameters
117
    Object.entries(options || {}).forEach(([key, value]) => {
14✔
118
      // Skip undefined values
119
      if (value !== undefined) {
4✔
120
        const name = QUERY_PARAMS_MAP[key] ?? camelToSnake(key)
4✔
121
        // Add options as query parameters
122
        url.searchParams.set(name, value.toString())
4✔
123
      }
124
    })
125
  }
126

127
  async #api<T>(request: HttpRequest): Promise<T> {
128
    return sendRequest(request, this.#txServiceApiKey)
51✔
129
  }
130

131
  /**
132
   * Returns the information and configuration of the service.
133
   *
134
   * @returns The information and configuration of the service
135
   */
136
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
137
    return this.#api({
2✔
138
      url: `${this.#txServiceBaseUrl}/v1/about`,
139
      method: HttpMethod.Get
140
    })
141
  }
142

143
  /**
144
   * Returns the list of Safe singletons.
145
   *
146
   * @returns The list of Safe singletons
147
   */
148
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
149
    return this.#api({
1✔
150
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
151
      method: HttpMethod.Get
152
    })
153
  }
154

155
  /**
156
   * Decodes the specified Safe transaction data.
157
   *
158
   * @param data - The Safe transaction data. '0x' prefixed hexadecimal string.
159
   * @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
160
   * @returns The transaction data decoded
161
   * @throws "Invalid data"
162
   * @throws "Not Found"
163
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
164
   */
165
  async decodeData(data: string, to?: string): Promise<DataDecoded> {
166
    if (data === '') {
1!
UNCOV
167
      throw new Error('Invalid data')
×
168
    }
169

170
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
171

172
    if (to) {
1!
UNCOV
173
      dataDecoderRequest.to = to
×
174
    }
175

176
    return this.#api({
1✔
177
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
178
      method: HttpMethod.Post,
179
      body: dataDecoderRequest
180
    })
181
  }
182

183
  /**
184
   * Returns the list of delegates.
185
   *
186
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
187
   * @returns The list of delegates
188
   * @throws "Checksum address validation failed"
189
   */
190
  async getSafeDelegates({
191
    safeAddress,
192
    delegateAddress,
193
    delegatorAddress,
194
    label,
195
    limit,
196
    offset
197
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
198
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
199

200
    if (safeAddress) {
2✔
201
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
202
      url.searchParams.set('safe', safe)
2✔
203
    }
204
    if (delegateAddress) {
2!
UNCOV
205
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
UNCOV
206
      url.searchParams.set('delegate', delegate)
×
207
    }
208
    if (delegatorAddress) {
2!
UNCOV
209
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
UNCOV
210
      url.searchParams.set('delegator', delegator)
×
211
    }
212
    if (label) {
2!
UNCOV
213
      url.searchParams.set('label', label)
×
214
    }
215
    if (limit != null) {
2!
UNCOV
216
      url.searchParams.set('limit', limit.toString())
×
217
    }
218
    if (offset != null) {
2!
UNCOV
219
      url.searchParams.set('offset', offset.toString())
×
220
    }
221

222
    return this.#api({
2✔
223
      url: url.toString(),
224
      method: HttpMethod.Get
225
    })
226
  }
227

228
  /**
229
   * Adds a new delegate for a given Safe address.
230
   *
231
   * @param addSafeDelegateProps - The configuration of the new delegate
232
   * @returns
233
   * @throws "Invalid Safe delegate address"
234
   * @throws "Invalid Safe delegator address"
235
   * @throws "Invalid label"
236
   * @throws "Checksum address validation failed"
237
   * @throws "Address <delegate_address> is not checksumed"
238
   * @throws "Safe=<safe_address> does not exist or it's still not indexed"
239
   * @throws "Signing owner is not an owner of the Safe"
240
   */
241
  async addSafeDelegate({
242
    safeAddress,
243
    delegateAddress,
244
    delegatorAddress,
245
    label,
246
    signer
247
  }: AddSafeDelegateProps): Promise<SignedSafeDelegateResponse> {
248
    if (delegateAddress === '') {
2!
UNCOV
249
      throw new Error('Invalid Safe delegate address')
×
250
    }
251
    if (delegatorAddress === '') {
2!
UNCOV
252
      throw new Error('Invalid Safe delegator address')
×
253
    }
254
    if (label === '') {
2!
UNCOV
255
      throw new Error('Invalid label')
×
256
    }
257
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
258
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
259
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
260

261
    const body = {
2✔
262
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
263
      delegate,
264
      delegator,
265
      label,
266
      signature
267
    }
268
    return this.#api({
2✔
269
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
270
      method: HttpMethod.Post,
271
      body
272
    })
273
  }
274

275
  /**
276
   * Removes a delegate for a given Safe address.
277
   *
278
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
279
   * @returns
280
   * @throws "Invalid Safe delegate address"
281
   * @throws "Invalid Safe delegator address"
282
   * @throws "Checksum address validation failed"
283
   * @throws "Signing owner is not an owner of the Safe"
284
   * @throws "Not found"
285
   */
286
  async removeSafeDelegate({
287
    delegateAddress,
288
    delegatorAddress,
289
    signer
290
  }: DeleteSafeDelegateProps): Promise<void> {
291
    if (delegateAddress === '') {
2!
UNCOV
292
      throw new Error('Invalid Safe delegate address')
×
293
    }
294
    if (delegatorAddress === '') {
2!
UNCOV
295
      throw new Error('Invalid Safe delegator address')
×
296
    }
297
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
298
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
299
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
300

301
    return this.#api({
2✔
302
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
303
      method: HttpMethod.Delete,
304
      body: {
305
        delegator,
306
        signature
307
      }
308
    })
309
  }
310

311
  /**
312
   * Get a message by its safe message hash
313
   * @param messageHash The Safe message hash
314
   * @returns The message
315
   */
316
  async getMessage(messageHash: string): Promise<SafeMessage> {
317
    if (!messageHash) {
1!
UNCOV
318
      throw new Error('Invalid messageHash')
×
319
    }
320

321
    return this.#api({
1✔
322
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
323
      method: HttpMethod.Get
324
    })
325
  }
326

327
  /**
328
   * Get the list of messages associated to a Safe account
329
   * @param safeAddress The safe address
330
   * @param options The options to filter the list of messages
331
   * @returns The paginated list of messages
332
   */
333
  async getMessages(
334
    safeAddress: string,
335
    options: GetSafeMessageListOptions = {}
1✔
336
  ): Promise<SafeMessageListResponse> {
337
    if (!this.#isValidAddress(safeAddress)) {
1!
UNCOV
338
      throw new Error('Invalid safeAddress')
×
339
    }
340

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

343
    // Check if options are given and add query parameters
344
    this.#addUrlQueryParams<GetSafeMessageListOptions>(url, options)
1✔
345

346
    return this.#api({
1✔
347
      url: url.toString(),
348
      method: HttpMethod.Get
349
    })
350
  }
351

352
  /**
353
   * Creates a new message with an initial signature
354
   * Add more signatures from other owners using addMessageSignature()
355
   * @param safeAddress The safe address
356
   * @param options The raw message to add, signature and safeAppId if any
357
   */
358
  async addMessage(safeAddress: string, addMessageOptions: AddMessageOptions): Promise<void> {
359
    if (!this.#isValidAddress(safeAddress)) {
2!
UNCOV
360
      throw new Error('Invalid safeAddress')
×
361
    }
362

363
    return this.#api({
2✔
364
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
365
      method: HttpMethod.Post,
366
      body: addMessageOptions
367
    })
368
  }
369

370
  /**
371
   * Add a signature to an existing message
372
   * @param messageHash The safe message hash
373
   * @param signature The signature
374
   */
375
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
376
    if (!messageHash || !signature) {
1!
UNCOV
377
      throw new Error('Invalid messageHash or signature')
×
378
    }
379

380
    return this.#api({
1✔
381
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
382
      method: HttpMethod.Post,
383
      body: {
384
        signature
385
      }
386
    })
387
  }
388

389
  /**
390
   * Returns the list of Safes where the address provided is an owner.
391
   *
392
   * @param ownerAddress - The owner address
393
   * @returns The list of Safes where the address provided is an owner
394
   * @throws "Invalid owner address"
395
   * @throws "Checksum address validation failed"
396
   */
397
  async getSafesByOwner(ownerAddress: string): Promise<OwnerResponse> {
398
    if (ownerAddress === '') {
2!
NEW
399
      throw new Error('Invalid owner address')
×
400
    }
401
    const { address } = this.#getEip3770Address(ownerAddress)
2✔
402
    return this.#api({
2✔
403
      url: `${this.#txServiceBaseUrl}/v1/owners/${address}/safes/`,
404
      method: HttpMethod.Get
405
    })
406
  }
407

408
  /**
409
   * Returns the list of Safes where the module address provided is enabled.
410
   *
411
   * @param moduleAddress - The Safe module address
412
   * @returns The list of Safe addresses where the module provided is enabled
413
   * @throws "Invalid module address"
414
   * @throws "Module address checksum not valid"
415
   */
416
  async getSafesByModule(moduleAddress: string): Promise<ModulesResponse> {
417
    if (moduleAddress === '') {
2!
418
      throw new Error('Invalid module address')
×
419
    }
420
    const { address } = this.#getEip3770Address(moduleAddress)
2✔
421
    return this.#api({
2✔
422
      url: `${this.#txServiceBaseUrl}/v1/modules/${address}/safes/`,
423
      method: HttpMethod.Get
424
    })
425
  }
426

427
  /**
428
   * Returns all the information of a Safe transaction.
429
   *
430
   * @param safeTxHash - Hash of the Safe transaction
431
   * @returns The information of a Safe transaction
432
   * @throws "Invalid safeTxHash"
433
   * @throws "Not found."
434
   */
435
  async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
436
    if (safeTxHash === '') {
1!
437
      throw new Error('Invalid safeTxHash')
×
438
    }
439
    return this.#api({
1✔
440
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/`,
441
      method: HttpMethod.Get
442
    })
443
  }
444

445
  /**
446
   * Returns the list of confirmations for a given a Safe transaction.
447
   *
448
   * @param safeTxHash - The hash of the Safe transaction
449
   * @returns The list of confirmations
450
   * @throws "Invalid safeTxHash"
451
   */
452
  async getTransactionConfirmations(
453
    safeTxHash: string
454
  ): Promise<SafeMultisigConfirmationListResponse> {
455
    if (safeTxHash === '') {
1!
456
      throw new Error('Invalid safeTxHash')
×
457
    }
458
    return this.#api({
1✔
459
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
460
      method: HttpMethod.Get
461
    })
462
  }
463

464
  /**
465
   * Adds a confirmation for a Safe transaction.
466
   *
467
   * @param safeTxHash - Hash of the Safe transaction that will be confirmed
468
   * @param signature - Signature of the transaction
469
   * @returns
470
   * @throws "Invalid safeTxHash"
471
   * @throws "Invalid signature"
472
   * @throws "Malformed data"
473
   * @throws "Error processing data"
474
   */
475
  async confirmTransaction(safeTxHash: string, signature: string): Promise<SignatureResponse> {
476
    if (safeTxHash === '') {
1!
NEW
477
      throw new Error('Invalid safeTxHash')
×
478
    }
479
    if (signature === '') {
1!
UNCOV
480
      throw new Error('Invalid signature')
×
481
    }
482
    return this.#api({
1✔
483
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
484
      method: HttpMethod.Post,
485
      body: {
486
        signature
487
      }
488
    })
489
  }
490

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

UNCOV
514
      return response as SafeInfoResponse
×
515
    })
516
  }
517

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

UNCOV
543
      return response as SafeCreationInfoResponse
×
544
    })
545
  }
546

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

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

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

631
    // Check if options are given and add query parameters
632
    this.#addUrlQueryParams<GetIncomingTransactionsOptions>(url, options)
3✔
633

634
    return this.#api({
3✔
635
      url: url.toString(),
636
      method: HttpMethod.Get
637
    })
638
  }
639

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

660
    // Check if options are given and add query parameters
661
    this.#addUrlQueryParams<GetModuleTransactionsOptions>(url, options)
2✔
662

663
    return this.#api({
2✔
664
      url: url.toString(),
665
      method: HttpMethod.Get
666
    })
667
  }
668

669
  /**
670
   * Returns the history of multi-signature transactions of a Safe account.
671
   *
672
   * @param safeAddress - The Safe address
673
   * @param options - Optional parameters to filter or modify the response
674
   * @returns The history of multi-signature transactions
675
   * @throws "Invalid Safe address"
676
   * @throws "Checksum address validation failed"
677
   */
678
  async getMultisigTransactions(
679
    safeAddress: string,
680
    options?: GetMultisigTransactionsOptions
681
  ): Promise<SafeMultisigTransactionListResponse> {
682
    if (safeAddress === '') {
4!
UNCOV
683
      throw new Error('Invalid Safe address')
×
684
    }
685

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

689
    // Check if options are given and add query parameters
690
    this.#addUrlQueryParams<GetMultisigTransactionsOptions>(url, options)
4✔
691

692
    return this.#api({
4✔
693
      url: url.toString(),
694
      method: HttpMethod.Get
695
    })
696
  }
697

698
  /**
699
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
700
   *
701
   * @param safeAddress - The Safe address
702
   * @param {PendingTransactionsOptions} options The options to filter the list of transactions
703
   * @returns The list of transactions waiting for the confirmation of the Safe owners
704
   * @throws "Invalid Safe address"
705
   * @throws "Invalid data"
706
   * @throws "Invalid ethereum address"
707
   */
708
  async getPendingTransactions(
709
    safeAddress: string,
710
    options: PendingTransactionsOptions = {}
×
711
  ): Promise<SafeMultisigTransactionListResponse> {
712
    if (safeAddress === '') {
2!
UNCOV
713
      throw new Error('Invalid Safe address')
×
714
    }
715
    const { currentNonce, hasConfirmations, ordering, limit, offset } = options
2✔
716

717
    const { address } = this.#getEip3770Address(safeAddress)
2✔
718
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
719

720
    const url = new URL(
2✔
721
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
722
    )
723

724
    if (hasConfirmations) {
2!
UNCOV
725
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
726
    }
727

728
    if (ordering) {
2!
UNCOV
729
      url.searchParams.set('ordering', ordering)
×
730
    }
731

732
    if (limit != null) {
2!
UNCOV
733
      url.searchParams.set('limit', limit.toString())
×
734
    }
735

736
    if (offset != null) {
2!
UNCOV
737
      url.searchParams.set('offset', offset.toString())
×
738
    }
739

740
    return this.#api({
2✔
741
      url: url.toString(),
742
      method: HttpMethod.Get
743
    })
744
  }
745

746
  /**
747
   * Returns a list of transactions for a Safe. The list has different structures depending on the transaction type
748
   *
749
   * @param safeAddress - The Safe address
750
   * @param options - Optional parameters to filter or modify the response
751
   * @returns The list of transactions waiting for the confirmation of the Safe owners
752
   * @throws "Invalid Safe address"
753
   * @throws "Checksum address validation failed"
754
   * @throws "Ordering field is not valid"
755
   */
756
  async getAllTransactions(
757
    safeAddress: string,
758
    options?: AllTransactionsOptions
759
  ): Promise<AllTransactionsListResponse> {
760
    if (safeAddress === '') {
2!
UNCOV
761
      throw new Error('Invalid Safe address')
×
762
    }
763
    const { address } = this.#getEip3770Address(safeAddress)
2✔
764
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/all-transactions/`)
2✔
765

766
    // Check if options are given and add query parameters
767
    this.#addUrlQueryParams<AllTransactionsOptions>(url, options)
2✔
768

769
    return this.#api({
2✔
770
      url: url.toString(),
771
      method: HttpMethod.Get
772
    })
773
  }
774

775
  /**
776
   * Returns the right nonce to propose a new transaction after the last pending transaction.
777
   *
778
   * @param safeAddress - The Safe address
779
   * @returns The right nonce to propose a new transaction after the last pending transaction
780
   * @throws "Invalid Safe address"
781
   * @throws "Invalid data"
782
   * @throws "Invalid ethereum address"
783
   */
784
  async getNextNonce(safeAddress: string): Promise<string> {
UNCOV
785
    if (safeAddress === '') {
×
UNCOV
786
      throw new Error('Invalid Safe address')
×
787
    }
NEW
788
    const { address } = this.#getEip3770Address(safeAddress)
×
UNCOV
789
    const pendingTransactions = await this.getPendingTransactions(address)
×
UNCOV
790
    if (pendingTransactions.results.length > 0) {
×
UNCOV
791
      const maxNonce = pendingTransactions.results.reduce((acc, tx) => {
×
UNCOV
792
        const curr = BigInt(tx.nonce)
×
UNCOV
793
        return curr > acc ? curr : acc
×
794
      }, 0n)
795

UNCOV
796
      return (maxNonce + 1n).toString()
×
797
    }
UNCOV
798
    const safeInfo = await this.getSafeInfo(address)
×
UNCOV
799
    return safeInfo.nonce
×
800
  }
801

802
  /**
803
   * Returns the list of all the ERC20 tokens handled by the Safe.
804
   *
805
   * @param options - Optional parameters to filter or modify the response
806
   * @returns The list of all the ERC20 tokens
807
   */
808
  async getTokenList(options?: TokenInfoListOptions): Promise<TokenInfoListResponse> {
809
    const url = new URL(`${this.#txServiceBaseUrl}/v1/tokens/`)
1✔
810

811
    // Check if options are given and add query parameters
812
    this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1✔
813

814
    return this.#api({
1✔
815
      url: url.toString(),
816
      method: HttpMethod.Get
817
    })
818
  }
819

820
  /**
821
   * Returns the information of a given ERC20 token.
822
   *
823
   * @param tokenAddress - The token address
824
   * @returns The information of the given ERC20 token
825
   * @throws "Invalid token address"
826
   * @throws "Checksum address validation failed"
827
   */
828
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
829
    if (tokenAddress === '') {
2!
UNCOV
830
      throw new Error('Invalid token address')
×
831
    }
832
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
833
    return this.#api({
2✔
834
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
835
      method: HttpMethod.Get
836
    })
837
  }
838

839
  /**
840
   * Get the SafeOperations that were sent from a particular address.
841
   * @param safeAddress - The Safe address
842
   * @param options - Optional parameters to filter or modify the response
843
   * @throws "Safe address must not be empty"
844
   * @throws "Invalid Ethereum address {safeAddress}"
845
   * @returns The SafeOperations sent from the given Safe's address
846
   */
847
  async getSafeOperationsByAddress(
848
    safeAddress: string,
849
    options?: GetSafeOperationListOptions
850
  ): Promise<GetSafeOperationListResponse> {
851
    if (!safeAddress) {
1!
NEW
852
      throw new Error('Safe address must not be empty')
×
853
    }
854

855
    const { address } = this.#getEip3770Address(safeAddress)
1✔
856

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

859
    // Check if options are given and add query parameters
860
    this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1✔
861

862
    return this.#api({
1✔
863
      url: url.toString(),
864
      method: HttpMethod.Get
865
    })
866
  }
867

868
  /**
869
   * Get the SafeOperations that are pending to send to the bundler
870
   * @param safeAddress - The Safe address
871
   * @param options - Optional parameters to filter or modify the response
872
   * @throws "Safe address must not be empty"
873
   * @throws "Invalid Ethereum address {safeAddress}"
874
   * @returns The pending SafeOperations
875
   */
876
  async getPendingSafeOperations(
877
    safeAddress: string,
878
    options?: GetPendingSafeOperationListOptions
879
  ): Promise<GetSafeOperationListResponse> {
UNCOV
880
    return this.getSafeOperationsByAddress(safeAddress, {
×
881
      ...options,
882
      executed: false
883
    })
884
  }
885

886
  /**
887
   * Get a SafeOperation by its hash.
888
   * @param safeOperationHash The SafeOperation hash
889
   * @throws "SafeOperation hash must not be empty"
890
   * @throws "Not found."
891
   * @returns The SafeOperation
892
   */
893
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
894
    if (!safeOperationHash) {
1!
UNCOV
895
      throw new Error('SafeOperation hash must not be empty')
×
896
    }
897

898
    return this.#api({
1✔
899
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
900
      method: HttpMethod.Get
901
    })
902
  }
903

904
  /**
905
   * Create a new 4337 SafeOperation for a Safe.
906
   * @param addSafeOperationProps - The configuration of the SafeOperation
907
   * @throws "Safe address must not be empty"
908
   * @throws "Invalid Safe address {safeAddress}"
909
   * @throws "Module address must not be empty"
910
   * @throws "Invalid module address {moduleAddress}"
911
   * @throws "Signature must not be empty"
912
   */
913
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
914
    let safeAddress: string, moduleAddress: string
915
    let addSafeOperationProps: AddSafeOperationProps
916

917
    if (isSafeOperation(safeOperation)) {
1!
UNCOV
918
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
919
    } else {
920
      addSafeOperationProps = safeOperation
1✔
921
    }
922

923
    const {
924
      entryPoint,
925
      moduleAddress: moduleAddressProp,
926
      options,
927
      safeAddress: safeAddressProp,
928
      userOperation
929
    } = addSafeOperationProps
1✔
930
    if (!safeAddressProp) {
1!
UNCOV
931
      throw new Error('Safe address must not be empty')
×
932
    }
933
    try {
1✔
934
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
935
    } catch (err) {
UNCOV
936
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
937
    }
938

939
    if (!moduleAddressProp) {
1!
UNCOV
940
      throw new Error('Module address must not be empty')
×
941
    }
942

943
    try {
1✔
944
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
945
    } catch (err) {
UNCOV
946
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
947
    }
948

949
    if (isEmptyData(userOperation.signature)) {
1!
950
      throw new Error('Signature must not be empty')
×
951
    }
952

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

957
    const userOperationV06 = userOperation as UserOperationV06
1✔
958

959
    return this.#api({
1✔
960
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
961
      method: HttpMethod.Post,
962
      body: {
963
        initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
1!
964
        nonce: userOperation.nonce,
965
        callData: userOperation.callData,
966
        callGasLimit: userOperation.callGasLimit.toString(),
967
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
968
        preVerificationGas: userOperation.preVerificationGas.toString(),
969
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
970
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
971
        paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
1!
972
          ? null
973
          : userOperationV06.paymasterAndData,
974
        entryPoint,
975
        validAfter: getISOString(options?.validAfter),
976
        validUntil: getISOString(options?.validUntil),
977
        signature: userOperation.signature,
978
        moduleAddress
979
      }
980
    })
981
  }
982

983
  /**
984
   * Returns the list of confirmations for a given a SafeOperation.
985
   *
986
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
987
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
988
   * @returns The list of confirmations
989
   * @throws "Invalid SafeOperation hash"
990
   * @throws "Invalid data"
991
   */
992
  async getSafeOperationConfirmations(
993
    safeOperationHash: string,
994
    { limit, offset }: ListOptions = {}
1✔
995
  ): Promise<SafeOperationConfirmationListResponse> {
996
    if (!safeOperationHash) {
1!
UNCOV
997
      throw new Error('Invalid SafeOperation hash')
×
998
    }
999

1000
    const url = new URL(
1✔
1001
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
1002
    )
1003

1004
    if (limit != null) {
1!
UNCOV
1005
      url.searchParams.set('limit', limit.toString())
×
1006
    }
1007

1008
    if (offset != null) {
1!
UNCOV
1009
      url.searchParams.set('offset', offset.toString())
×
1010
    }
1011

1012
    return this.#api({
1✔
1013
      url: url.toString(),
1014
      method: HttpMethod.Get
1015
    })
1016
  }
1017

1018
  /**
1019
   * Adds a confirmation for a SafeOperation.
1020
   *
1021
   * @param safeOperationHash The SafeOperation hash
1022
   * @param signature - Signature of the SafeOperation
1023
   * @returns
1024
   * @throws "Invalid SafeOperation hash"
1025
   * @throws "Invalid signature"
1026
   * @throws "Malformed data"
1027
   * @throws "Error processing data"
1028
   */
1029
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
1030
    if (!safeOperationHash) {
1!
NEW
1031
      throw new Error('Invalid SafeOperation hash')
×
1032
    }
1033
    if (!signature) {
1!
UNCOV
1034
      throw new Error('Invalid signature')
×
1035
    }
1036
    return this.#api({
1✔
1037
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
1038
      method: HttpMethod.Post,
1039
      body: { signature }
1040
    })
1041
  }
1042
}
1043

1044
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