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

safe-global / safe-core-sdk / 13410251357

19 Feb 2025 10:12AM UTC coverage: 78.879% (+4.1%) from 74.773%
13410251357

push

github

dasanra
Merge branch 'development' into prepare-release

257 of 394 branches covered (65.23%)

Branch coverage included in aggregate %.

278 of 303 new or added lines in 23 files covered. (91.75%)

2 existing lines in 2 files now uncovered.

897 of 1069 relevant lines covered (83.91%)

4.7 hits per line

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

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

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

122
  /**
123
   * Returns the information and configuration of the service.
124
   *
125
   * @returns The information and configuration of the service
126
   */
127
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
128
    return sendRequest({
2✔
129
      url: `${this.#txServiceBaseUrl}/v1/about`,
130
      method: HttpMethod.Get
131
    })
132
  }
133

134
  /**
135
   * Returns the list of Safe singletons.
136
   *
137
   * @returns The list of Safe singletons
138
   */
139
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
140
    return sendRequest({
1✔
141
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
142
      method: HttpMethod.Get
143
    })
144
  }
145

146
  /**
147
   * Decodes the specified Safe transaction data.
148
   *
149
   * @param data - The Safe transaction data. '0x' prefixed hexadecimal string.
150
   * @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
151
   * @returns The transaction data decoded
152
   * @throws "Invalid data"
153
   * @throws "Not Found"
154
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
155
   */
156
  async decodeData(data: string, to?: string): Promise<DataDecoded> {
157
    if (data === '') {
1!
158
      throw new Error('Invalid data')
×
159
    }
160

161
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
162

163
    if (to) {
1!
164
      dataDecoderRequest.to = to
×
165
    }
166

167
    return sendRequest({
1✔
168
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
169
      method: HttpMethod.Post,
170
      body: dataDecoderRequest
171
    })
172
  }
173

174
  /**
175
   * Returns the list of delegates.
176
   *
177
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
178
   * @returns The list of delegates
179
   * @throws "Checksum address validation failed"
180
   */
181
  async getSafeDelegates({
182
    safeAddress,
183
    delegateAddress,
184
    delegatorAddress,
185
    label,
186
    limit,
187
    offset
188
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
189
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
190

191
    if (safeAddress) {
2✔
192
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
193
      url.searchParams.set('safe', safe)
2✔
194
    }
195
    if (delegateAddress) {
2!
NEW
196
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
NEW
197
      url.searchParams.set('delegate', delegate)
×
198
    }
199
    if (delegatorAddress) {
2!
NEW
200
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
NEW
201
      url.searchParams.set('delegator', delegator)
×
202
    }
203
    if (label) {
2!
NEW
204
      url.searchParams.set('label', label)
×
205
    }
206
    if (limit != null) {
2!
NEW
207
      url.searchParams.set('limit', limit.toString())
×
208
    }
209
    if (offset != null) {
2!
NEW
210
      url.searchParams.set('offset', offset.toString())
×
211
    }
212

213
    return sendRequest({
2✔
214
      url: url.toString(),
215
      method: HttpMethod.Get
216
    })
217
  }
218

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

252
    const body = {
2✔
253
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
254
      delegate,
255
      delegator,
256
      label,
257
      signature
258
    }
259
    return sendRequest({
2✔
260
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
261
      method: HttpMethod.Post,
262
      body
263
    })
264
  }
265

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

292
    return sendRequest({
2✔
293
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
294
      method: HttpMethod.Delete,
295
      body: {
296
        delegator,
297
        signature
298
      }
299
    })
300
  }
301

302
  /**
303
   * Get a message by its safe message hash
304
   * @param messageHash The Safe message hash
305
   * @returns The message
306
   */
307
  async getMessage(messageHash: string): Promise<SafeMessage> {
308
    if (!messageHash) {
1!
NEW
309
      throw new Error('Invalid messageHash')
×
310
    }
311

312
    return sendRequest({
1✔
313
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
314
      method: HttpMethod.Get
315
    })
316
  }
317

318
  /**
319
   * Get the list of messages associated to a Safe account
320
   * @param safeAddress The safe address
321
   * @param options The options to filter the list of messages
322
   * @returns The paginated list of messages
323
   */
324
  async getMessages(
325
    safeAddress: string,
326
    options: GetSafeMessageListOptions = {}
1✔
327
  ): Promise<SafeMessageListResponse> {
328
    if (!this.#isValidAddress(safeAddress)) {
1!
NEW
329
      throw new Error('Invalid safeAddress')
×
330
    }
331

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

334
    // Check if options are given and add query parameters
335
    this.#addUrlQueryParams<GetSafeMessageListOptions>(url, options)
1✔
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, addMessageOptions: AddMessageOptions): 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: addMessageOptions
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
    // Check if options are given and add query parameters
623
    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
    // Check if options are given and add query parameters
652
    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
    // Check if options are given and add query parameters
681
    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 {PendingTransactionsOptions} options The options to filter the list of transactions
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
    options: PendingTransactionsOptions = {}
×
702
  ): Promise<SafeMultisigTransactionListResponse> {
703
    if (safeAddress === '') {
2!
704
      throw new Error('Invalid Safe address')
×
705
    }
706
    const { currentNonce, hasConfirmations, ordering, limit, offset } = options
2✔
707

708
    const { address } = this.#getEip3770Address(safeAddress)
2✔
709
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
710

711
    const url = new URL(
2✔
712
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
713
    )
714

715
    if (hasConfirmations) {
2!
716
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
717
    }
718

719
    if (ordering) {
2!
720
      url.searchParams.set('ordering', ordering)
×
721
    }
722

723
    if (limit != null) {
2!
724
      url.searchParams.set('limit', limit.toString())
×
725
    }
726

727
    if (offset != null) {
2!
728
      url.searchParams.set('offset', offset.toString())
×
729
    }
730

731
    return sendRequest({
2✔
732
      url: url.toString(),
733
      method: HttpMethod.Get
734
    })
735
  }
736

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

757
    // Check if options are given and add query parameters
758
    this.#addUrlQueryParams<AllTransactionsOptions>(url, options)
2✔
759

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

766
  /**
767
   * Returns the right nonce to propose a new transaction after the last pending transaction.
768
   *
769
   * @param safeAddress - The Safe address
770
   * @returns The right nonce to propose a new transaction after the last pending transaction
771
   * @throws "Invalid Safe address"
772
   * @throws "Invalid data"
773
   * @throws "Invalid ethereum address"
774
   */
775
  async getNextNonce(safeAddress: string): Promise<string> {
776
    if (safeAddress === '') {
×
777
      throw new Error('Invalid Safe address')
×
778
    }
779
    const { address } = this.#getEip3770Address(safeAddress)
×
780
    const pendingTransactions = await this.getPendingTransactions(address)
×
781
    if (pendingTransactions.results.length > 0) {
×
782
      const maxNonce = pendingTransactions.results.reduce((acc, tx) => {
×
783
        const curr = BigInt(tx.nonce)
×
784
        return curr > acc ? curr : acc
×
785
      }, 0n)
786

787
      return (maxNonce + 1n).toString()
×
788
    }
789
    const safeInfo = await this.getSafeInfo(address)
×
790
    return safeInfo.nonce
×
791
  }
792

793
  /**
794
   * Returns the list of all the ERC20 tokens handled by the Safe.
795
   *
796
   * @param options - Optional parameters to filter or modify the response
797
   * @returns The list of all the ERC20 tokens
798
   */
799
  async getTokenList(options?: TokenInfoListOptions): Promise<TokenInfoListResponse> {
800
    const url = new URL(`${this.#txServiceBaseUrl}/v1/tokens/`)
1✔
801

802
    // Check if options are given and add query parameters
803
    this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1✔
804

805
    return sendRequest({
1✔
806
      url: url.toString(),
807
      method: HttpMethod.Get
808
    })
809
  }
810

811
  /**
812
   * Returns the information of a given ERC20 token.
813
   *
814
   * @param tokenAddress - The token address
815
   * @returns The information of the given ERC20 token
816
   * @throws "Invalid token address"
817
   * @throws "Checksum address validation failed"
818
   */
819
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
820
    if (tokenAddress === '') {
2!
821
      throw new Error('Invalid token address')
×
822
    }
823
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
824
    return sendRequest({
2✔
825
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
826
      method: HttpMethod.Get
827
    })
828
  }
829

830
  /**
831
   * Get the SafeOperations that were sent from a particular address.
832
   * @param safeAddress - The Safe address
833
   * @param options - Optional parameters to filter or modify the response
834
   * @throws "Safe address must not be empty"
835
   * @throws "Invalid Ethereum address {safeAddress}"
836
   * @returns The SafeOperations sent from the given Safe's address
837
   */
838
  async getSafeOperationsByAddress(
839
    safeAddress: string,
840
    options?: GetSafeOperationListOptions
841
  ): Promise<GetSafeOperationListResponse> {
842
    if (!safeAddress) {
1!
843
      throw new Error('Safe address must not be empty')
×
844
    }
845

846
    const { address } = this.#getEip3770Address(safeAddress)
1✔
847

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

850
    // Check if options are given and add query parameters
851
    this.#addUrlQueryParams<TokenInfoListOptions>(url, options)
1✔
852

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

859
  /**
860
   * Get the SafeOperations that are pending to send to the bundler
861
   * @param safeAddress - The Safe address
862
   * @param options - Optional parameters to filter or modify the response
863
   * @throws "Safe address must not be empty"
864
   * @throws "Invalid Ethereum address {safeAddress}"
865
   * @returns The pending SafeOperations
866
   */
867
  async getPendingSafeOperations(
868
    safeAddress: string,
869
    options?: GetPendingSafeOperationListOptions
870
  ): Promise<GetSafeOperationListResponse> {
NEW
871
    return this.getSafeOperationsByAddress(safeAddress, {
×
872
      ...options,
873
      executed: false
874
    })
875
  }
876

877
  /**
878
   * Get a SafeOperation by its hash.
879
   * @param safeOperationHash The SafeOperation hash
880
   * @throws "SafeOperation hash must not be empty"
881
   * @throws "Not found."
882
   * @returns The SafeOperation
883
   */
884
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
885
    if (!safeOperationHash) {
1!
886
      throw new Error('SafeOperation hash must not be empty')
×
887
    }
888

889
    return sendRequest({
1✔
890
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
891
      method: HttpMethod.Get
892
    })
893
  }
894

895
  /**
896
   * Create a new 4337 SafeOperation for a Safe.
897
   * @param addSafeOperationProps - The configuration of the SafeOperation
898
   * @throws "Safe address must not be empty"
899
   * @throws "Invalid Safe address {safeAddress}"
900
   * @throws "Module address must not be empty"
901
   * @throws "Invalid module address {moduleAddress}"
902
   * @throws "Signature must not be empty"
903
   */
904
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
905
    let safeAddress: string, moduleAddress: string
906
    let addSafeOperationProps: AddSafeOperationProps
907

908
    if (isSafeOperation(safeOperation)) {
1!
909
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
910
    } else {
911
      addSafeOperationProps = safeOperation
1✔
912
    }
913

914
    const {
915
      entryPoint,
916
      moduleAddress: moduleAddressProp,
917
      options,
918
      safeAddress: safeAddressProp,
919
      userOperation
920
    } = addSafeOperationProps
1✔
921
    if (!safeAddressProp) {
1!
922
      throw new Error('Safe address must not be empty')
×
923
    }
924
    try {
1✔
925
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
926
    } catch (err) {
927
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
928
    }
929

930
    if (!moduleAddressProp) {
1!
931
      throw new Error('Module address must not be empty')
×
932
    }
933

934
    try {
1✔
935
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
936
    } catch (err) {
937
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
938
    }
939

940
    if (isEmptyData(userOperation.signature)) {
1!
941
      throw new Error('Signature must not be empty')
×
942
    }
943

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

948
    const userOperationV06 = userOperation as UserOperationV06
1✔
949

950
    return sendRequest({
1✔
951
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
952
      method: HttpMethod.Post,
953
      body: {
954
        initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode,
1!
955
        nonce: userOperation.nonce,
956
        callData: userOperation.callData,
957
        callGasLimit: userOperation.callGasLimit.toString(),
958
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
959
        preVerificationGas: userOperation.preVerificationGas.toString(),
960
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
961
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
962
        paymasterAndData: isEmptyData(userOperationV06.paymasterAndData)
1!
963
          ? null
964
          : userOperationV06.paymasterAndData,
965
        entryPoint,
966
        validAfter: getISOString(options?.validAfter),
967
        validUntil: getISOString(options?.validUntil),
968
        signature: userOperation.signature,
969
        moduleAddress
970
      }
971
    })
972
  }
973

974
  /**
975
   * Returns the list of confirmations for a given a SafeOperation.
976
   *
977
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
978
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
979
   * @returns The list of confirmations
980
   * @throws "Invalid SafeOperation hash"
981
   * @throws "Invalid data"
982
   */
983
  async getSafeOperationConfirmations(
984
    safeOperationHash: string,
985
    { limit, offset }: ListOptions = {}
1✔
986
  ): Promise<SafeOperationConfirmationListResponse> {
987
    if (!safeOperationHash) {
1!
988
      throw new Error('Invalid SafeOperation hash')
×
989
    }
990

991
    const url = new URL(
1✔
992
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
993
    )
994

995
    if (limit != null) {
1!
996
      url.searchParams.set('limit', limit.toString())
×
997
    }
998

999
    if (offset != null) {
1!
1000
      url.searchParams.set('offset', offset.toString())
×
1001
    }
1002

1003
    return sendRequest({
1✔
1004
      url: url.toString(),
1005
      method: HttpMethod.Get
1006
    })
1007
  }
1008

1009
  /**
1010
   * Adds a confirmation for a SafeOperation.
1011
   *
1012
   * @param safeOperationHash The SafeOperation hash
1013
   * @param signature - Signature of the SafeOperation
1014
   * @returns
1015
   * @throws "Invalid SafeOperation hash"
1016
   * @throws "Invalid signature"
1017
   * @throws "Malformed data"
1018
   * @throws "Error processing data"
1019
   */
1020
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
1021
    if (!safeOperationHash) {
1!
1022
      throw new Error('Invalid SafeOperation hash')
×
1023
    }
1024
    if (!signature) {
1!
1025
      throw new Error('Invalid signature')
×
1026
    }
1027
    return sendRequest({
1✔
1028
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
1029
      method: HttpMethod.Post,
1030
      body: { signature }
1031
    })
1032
  }
1033
}
1034

1035
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