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

safe-global / safe-core-sdk / 13183898291

06 Feb 2025 04:57PM UTC coverage: 77.99%. First build
13183898291

Pull #1103

github

web-flow
Merge e49b3e87e into 2a9f78ade
Pull Request #1103: feat(relay-kit): Add Entrypoint v0.7 support

256 of 398 branches covered (64.32%)

Branch coverage included in aggregate %.

205 of 212 new or added lines in 15 files covered. (96.7%)

885 of 1065 relevant lines covered (83.1%)

4.46 hits per line

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

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

50
export interface SafeApiKitConfig {
51
  /** chainId - The chainId */
52
  chainId: bigint
53
  /** txServiceUrl - Safe Transaction Service URL */
54
  txServiceUrl?: string
55
}
56

57
class SafeApiKit {
58
  #chainId: bigint
2✔
59
  #txServiceBaseUrl: string
2✔
60

61
  constructor({ chainId, txServiceUrl }: SafeApiKitConfig) {
62
    this.#chainId = chainId
2✔
63

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

74
      this.#txServiceBaseUrl = url
1✔
75
    }
76
  }
77

78
  #isValidAddress(address: string) {
1✔
79
    try {
3✔
80
      validateEthereumAddress(address)
3✔
81
      return true
3✔
82
    } catch {
83
      return false
×
84
    }
85
  }
86

87
  #getEip3770Address(fullAddress: string): Eip3770Address {
88
    return validateEip3770Address(fullAddress, this.#chainId)
39✔
89
  }
90

91
  /**
92
   * Returns the information and configuration of the service.
93
   *
94
   * @returns The information and configuration of the service
95
   */
96
  async getServiceInfo(): Promise<SafeServiceInfoResponse> {
97
    return sendRequest({
2✔
98
      url: `${this.#txServiceBaseUrl}/v1/about`,
99
      method: HttpMethod.Get
100
    })
101
  }
102

103
  /**
104
   * Returns the list of Safe singletons.
105
   *
106
   * @returns The list of Safe singletons
107
   */
108
  async getServiceSingletonsInfo(): Promise<SafeSingletonResponse[]> {
109
    return sendRequest({
1✔
110
      url: `${this.#txServiceBaseUrl}/v1/about/singletons`,
111
      method: HttpMethod.Get
112
    })
113
  }
114

115
  /**
116
   * Decodes the specified Safe transaction data.
117
   *
118
   * @param data - The Safe transaction data. '0x' prefixed hexadecimal string.
119
   * @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
120
   * @returns The transaction data decoded
121
   * @throws "Invalid data"
122
   * @throws "Not Found"
123
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
124
   */
125
  async decodeData(data: string, to?: string): Promise<any> {
126
    if (data === '') {
1!
127
      throw new Error('Invalid data')
×
128
    }
129

130
    const dataDecoderRequest: { data: string; to?: string } = { data }
1✔
131

132
    if (to) {
1!
133
      dataDecoderRequest.to = to
×
134
    }
135

136
    return sendRequest({
1✔
137
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
138
      method: HttpMethod.Post,
139
      body: dataDecoderRequest
140
    })
141
  }
142

143
  /**
144
   * Returns the list of Safes where the address provided is an owner.
145
   *
146
   * @param ownerAddress - The owner address
147
   * @returns The list of Safes where the address provided is an owner
148
   * @throws "Invalid owner address"
149
   * @throws "Checksum address validation failed"
150
   */
151
  async getSafesByOwner(ownerAddress: string): Promise<OwnerResponse> {
152
    if (ownerAddress === '') {
2!
153
      throw new Error('Invalid owner address')
×
154
    }
155
    const { address } = this.#getEip3770Address(ownerAddress)
2✔
156
    return sendRequest({
2✔
157
      url: `${this.#txServiceBaseUrl}/v1/owners/${address}/safes/`,
158
      method: HttpMethod.Get
159
    })
160
  }
161

162
  /**
163
   * Returns the list of Safes where the module address provided is enabled.
164
   *
165
   * @param moduleAddress - The Safe module address
166
   * @returns The list of Safe addresses where the module provided is enabled
167
   * @throws "Invalid module address"
168
   * @throws "Module address checksum not valid"
169
   */
170
  async getSafesByModule(moduleAddress: string): Promise<ModulesResponse> {
171
    if (moduleAddress === '') {
2!
172
      throw new Error('Invalid module address')
×
173
    }
174
    const { address } = this.#getEip3770Address(moduleAddress)
2✔
175
    return sendRequest({
2✔
176
      url: `${this.#txServiceBaseUrl}/v1/modules/${address}/safes/`,
177
      method: HttpMethod.Get
178
    })
179
  }
180

181
  /**
182
   * Returns all the information of a Safe transaction.
183
   *
184
   * @param safeTxHash - Hash of the Safe transaction
185
   * @returns The information of a Safe transaction
186
   * @throws "Invalid safeTxHash"
187
   * @throws "Not found."
188
   */
189
  async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
190
    if (safeTxHash === '') {
1!
191
      throw new Error('Invalid safeTxHash')
×
192
    }
193
    return sendRequest({
1✔
194
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/`,
195
      method: HttpMethod.Get
196
    })
197
  }
198

199
  /**
200
   * Returns the list of confirmations for a given a Safe transaction.
201
   *
202
   * @param safeTxHash - The hash of the Safe transaction
203
   * @returns The list of confirmations
204
   * @throws "Invalid safeTxHash"
205
   */
206
  async getTransactionConfirmations(
207
    safeTxHash: string
208
  ): Promise<SafeMultisigConfirmationListResponse> {
209
    if (safeTxHash === '') {
1!
210
      throw new Error('Invalid safeTxHash')
×
211
    }
212
    return sendRequest({
1✔
213
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
214
      method: HttpMethod.Get
215
    })
216
  }
217

218
  /**
219
   * Adds a confirmation for a Safe transaction.
220
   *
221
   * @param safeTxHash - Hash of the Safe transaction that will be confirmed
222
   * @param signature - Signature of the transaction
223
   * @returns
224
   * @throws "Invalid safeTxHash"
225
   * @throws "Invalid signature"
226
   * @throws "Malformed data"
227
   * @throws "Error processing data"
228
   */
229
  async confirmTransaction(safeTxHash: string, signature: string): Promise<SignatureResponse> {
230
    if (safeTxHash === '') {
1!
231
      throw new Error('Invalid safeTxHash')
×
232
    }
233
    if (signature === '') {
1!
234
      throw new Error('Invalid signature')
×
235
    }
236
    return sendRequest({
1✔
237
      url: `${this.#txServiceBaseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
238
      method: HttpMethod.Post,
239
      body: {
240
        signature
241
      }
242
    })
243
  }
244

245
  /**
246
   * Returns the information and configuration of the provided Safe address.
247
   *
248
   * @param safeAddress - The Safe address
249
   * @returns The information and configuration of the provided Safe address
250
   * @throws "Invalid Safe address"
251
   * @throws "Checksum address validation failed"
252
   */
253
  async getSafeInfo(safeAddress: string): Promise<SafeInfoResponse> {
254
    if (safeAddress === '') {
2!
255
      throw new Error('Invalid Safe address')
×
256
    }
257
    const { address } = this.#getEip3770Address(safeAddress)
2✔
258
    return sendRequest({
2✔
259
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
260
      method: HttpMethod.Get
261
    }).then((response: any) => {
262
      // FIXME remove when the transaction service returns the singleton property instead of masterCopy
263
      if (!response?.singleton) {
2✔
264
        const { masterCopy, ...rest } = response
2✔
265
        return { ...rest, singleton: masterCopy } as SafeInfoResponse
2✔
266
      }
267

268
      return response as SafeInfoResponse
×
269
    })
270
  }
271

272
  /**
273
   * Returns the list of delegates.
274
   *
275
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
276
   * @returns The list of delegates
277
   * @throws "Checksum address validation failed"
278
   */
279
  async getSafeDelegates({
280
    safeAddress,
281
    delegateAddress,
282
    delegatorAddress,
283
    label,
284
    limit,
285
    offset
286
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
287
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
288

289
    if (safeAddress) {
2✔
290
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
291
      url.searchParams.set('safe', safe)
2✔
292
    }
293
    if (delegateAddress) {
2!
294
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
295
      url.searchParams.set('delegate', delegate)
×
296
    }
297
    if (delegatorAddress) {
2!
298
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
299
      url.searchParams.set('delegator', delegator)
×
300
    }
301
    if (label) {
2!
302
      url.searchParams.set('label', label)
×
303
    }
304
    if (limit != null) {
2!
305
      url.searchParams.set('limit', limit.toString())
×
306
    }
307
    if (offset != null) {
2!
308
      url.searchParams.set('offset', offset.toString())
×
309
    }
310

311
    return sendRequest({
2✔
312
      url: url.toString(),
313
      method: HttpMethod.Get
314
    })
315
  }
316

317
  /**
318
   * Adds a new delegate for a given Safe address.
319
   *
320
   * @param addSafeDelegateProps - The configuration of the new delegate
321
   * @returns
322
   * @throws "Invalid Safe delegate address"
323
   * @throws "Invalid Safe delegator address"
324
   * @throws "Invalid label"
325
   * @throws "Checksum address validation failed"
326
   * @throws "Address <delegate_address> is not checksumed"
327
   * @throws "Safe=<safe_address> does not exist or it's still not indexed"
328
   * @throws "Signing owner is not an owner of the Safe"
329
   */
330
  async addSafeDelegate({
331
    safeAddress,
332
    delegateAddress,
333
    delegatorAddress,
334
    label,
335
    signer
336
  }: AddSafeDelegateProps): Promise<SignedSafeDelegateResponse> {
337
    if (delegateAddress === '') {
2!
338
      throw new Error('Invalid Safe delegate address')
×
339
    }
340
    if (delegatorAddress === '') {
2!
341
      throw new Error('Invalid Safe delegator address')
×
342
    }
343
    if (label === '') {
2!
344
      throw new Error('Invalid label')
×
345
    }
346
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
347
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
348
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
349

350
    const body: any = {
2✔
351
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
352
      delegate,
353
      delegator,
354
      label,
355
      signature
356
    }
357
    return sendRequest({
2✔
358
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
359
      method: HttpMethod.Post,
360
      body
361
    })
362
  }
363

364
  /**
365
   * Removes a delegate for a given Safe address.
366
   *
367
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
368
   * @returns
369
   * @throws "Invalid Safe delegate address"
370
   * @throws "Invalid Safe delegator address"
371
   * @throws "Checksum address validation failed"
372
   * @throws "Signing owner is not an owner of the Safe"
373
   * @throws "Not found"
374
   */
375
  async removeSafeDelegate({
376
    delegateAddress,
377
    delegatorAddress,
378
    signer
379
  }: DeleteSafeDelegateProps): Promise<void> {
380
    if (delegateAddress === '') {
2!
381
      throw new Error('Invalid Safe delegate address')
×
382
    }
383
    if (delegatorAddress === '') {
2!
384
      throw new Error('Invalid Safe delegator address')
×
385
    }
386
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
387
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
388
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
389

390
    return sendRequest({
2✔
391
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
392
      method: HttpMethod.Delete,
393
      body: {
394
        delegator,
395
        signature
396
      }
397
    })
398
  }
399

400
  /**
401
   * Returns the creation information of a Safe.
402
   *
403
   * @param safeAddress - The Safe address
404
   * @returns The creation information of a Safe
405
   * @throws "Invalid Safe address"
406
   * @throws "Safe creation not found"
407
   * @throws "Checksum address validation failed"
408
   * @throws "Problem connecting to Ethereum network"
409
   */
410
  async getSafeCreationInfo(safeAddress: string): Promise<SafeCreationInfoResponse> {
411
    if (safeAddress === '') {
2!
412
      throw new Error('Invalid Safe address')
×
413
    }
414
    const { address } = this.#getEip3770Address(safeAddress)
2✔
415
    return sendRequest({
2✔
416
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/creation/`,
417
      method: HttpMethod.Get
418
    }).then((response: any) => {
419
      // FIXME remove when the transaction service returns the singleton property instead of masterCopy
420
      if (!response?.singleton) {
2✔
421
        const { masterCopy, ...rest } = response
2✔
422
        return { ...rest, singleton: masterCopy } as SafeCreationInfoResponse
2✔
423
      }
424

425
      return response as SafeCreationInfoResponse
×
426
    })
427
  }
428

429
  /**
430
   * Estimates the safeTxGas for a given Safe multi-signature transaction.
431
   *
432
   * @param safeAddress - The Safe address
433
   * @param safeTransaction - The Safe transaction to estimate
434
   * @returns The safeTxGas for the given Safe transaction
435
   * @throws "Invalid Safe address"
436
   * @throws "Data not valid"
437
   * @throws "Safe not found"
438
   * @throws "Tx not valid"
439
   */
440
  async estimateSafeTransaction(
441
    safeAddress: string,
442
    safeTransaction: SafeMultisigTransactionEstimate
443
  ): Promise<SafeMultisigTransactionEstimateResponse> {
444
    if (safeAddress === '') {
2!
445
      throw new Error('Invalid Safe address')
×
446
    }
447
    const { address } = this.#getEip3770Address(safeAddress)
2✔
448
    return sendRequest({
2✔
449
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/estimations/`,
450
      method: HttpMethod.Post,
451
      body: safeTransaction
452
    })
453
  }
454

455
  /**
456
   * Creates a new multi-signature transaction with its confirmations and stores it in the Safe Transaction Service.
457
   *
458
   * @param proposeTransactionConfig - The configuration of the proposed transaction
459
   * @returns The hash of the Safe transaction proposed
460
   * @throws "Invalid Safe address"
461
   * @throws "Invalid safeTxHash"
462
   * @throws "Invalid data"
463
   * @throws "Invalid ethereum address/User is not an owner/Invalid signature/Nonce already executed/Sender is not an owner"
464
   */
465
  async proposeTransaction({
466
    safeAddress,
467
    safeTransactionData,
468
    safeTxHash,
469
    senderAddress,
470
    senderSignature,
471
    origin
472
  }: ProposeTransactionProps): Promise<void> {
473
    if (safeAddress === '') {
2!
474
      throw new Error('Invalid Safe address')
×
475
    }
476
    const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
477
    const { address: sender } = this.#getEip3770Address(senderAddress)
2✔
478
    if (safeTxHash === '') {
2!
479
      throw new Error('Invalid safeTxHash')
×
480
    }
481
    return sendRequest({
2✔
482
      url: `${this.#txServiceBaseUrl}/v1/safes/${safe}/multisig-transactions/`,
483
      method: HttpMethod.Post,
484
      body: {
485
        ...safeTransactionData,
486
        contractTransactionHash: safeTxHash,
487
        sender,
488
        signature: senderSignature,
489
        origin
490
      }
491
    })
492
  }
493

494
  /**
495
   * Returns the history of incoming transactions of a Safe account.
496
   *
497
   * @param safeAddress - The Safe address
498
   * @returns The history of incoming transactions
499
   * @throws "Invalid Safe address"
500
   * @throws "Checksum address validation failed"
501
   */
502
  async getIncomingTransactions(safeAddress: string): Promise<TransferListResponse> {
503
    if (safeAddress === '') {
2!
504
      throw new Error('Invalid Safe address')
×
505
    }
506
    const { address } = this.#getEip3770Address(safeAddress)
2✔
507
    return sendRequest({
2✔
508
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/incoming-transfers?executed=true`,
509
      method: HttpMethod.Get
510
    })
511
  }
512

513
  /**
514
   * Returns the history of module transactions of a Safe account.
515
   *
516
   * @param safeAddress - The Safe address
517
   * @returns The history of module transactions
518
   * @throws "Invalid Safe address"
519
   * @throws "Invalid data"
520
   * @throws "Invalid ethereum address"
521
   */
522
  async getModuleTransactions(safeAddress: string): Promise<SafeModuleTransactionListResponse> {
523
    if (safeAddress === '') {
2!
524
      throw new Error('Invalid Safe address')
×
525
    }
526
    const { address } = this.#getEip3770Address(safeAddress)
2✔
527
    return sendRequest({
2✔
528
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/module-transactions/`,
529
      method: HttpMethod.Get
530
    })
531
  }
532

533
  /**
534
   * Returns the history of multi-signature transactions of a Safe account.
535
   *
536
   * @param safeAddress - The Safe address
537
   * @returns The history of multi-signature transactions
538
   * @throws "Invalid Safe address"
539
   * @throws "Checksum address validation failed"
540
   */
541
  async getMultisigTransactions(safeAddress: string): Promise<SafeMultisigTransactionListResponse> {
542
    if (safeAddress === '') {
2!
543
      throw new Error('Invalid Safe address')
×
544
    }
545
    const { address } = this.#getEip3770Address(safeAddress)
2✔
546
    return sendRequest({
2✔
547
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/`,
548
      method: HttpMethod.Get
549
    })
550
  }
551

552
  /**
553
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
554
   *
555
   * @param safeAddress - The Safe address
556
   * @param currentNonce - Deprecated, use inside object property: Current nonce of the Safe.
557
   * @returns The list of transactions waiting for the confirmation of the Safe owners
558
   * @throws "Invalid Safe address"
559
   * @throws "Invalid data"
560
   * @throws "Invalid ethereum address"
561
   */
562
  async getPendingTransactions(
563
    safeAddress: string,
564
    currentNonce?: number
565
  ): Promise<SafeMultisigTransactionListResponse>
566
  /**
567
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
568
   *
569
   * @param safeAddress - The Safe address
570
   * @param {PendingTransactionsOptions} options The options to filter the list of transactions
571
   * @returns The list of transactions waiting for the confirmation of the Safe owners
572
   * @throws "Invalid Safe address"
573
   * @throws "Invalid data"
574
   * @throws "Invalid ethereum address"
575
   */
576
  async getPendingTransactions(
577
    safeAddress: string,
578
    { currentNonce, hasConfirmations, ordering, limit, offset }: PendingTransactionsOptions
579
  ): Promise<SafeMultisigTransactionListResponse>
580
  async getPendingTransactions(
581
    safeAddress: string,
582
    propsOrCurrentNonce: PendingTransactionsOptions | number = {}
×
583
  ): Promise<SafeMultisigTransactionListResponse> {
584
    if (safeAddress === '') {
2!
585
      throw new Error('Invalid Safe address')
×
586
    }
587

588
    // TODO: Remove @deprecated migration code
589
    let currentNonce: number | undefined
590
    let hasConfirmations: boolean | undefined
591
    let ordering: string | undefined
592
    let limit: number | undefined
593
    let offset: number | undefined
594
    if (typeof propsOrCurrentNonce === 'object') {
2!
595
      ;({ currentNonce, hasConfirmations, ordering, limit, offset } = propsOrCurrentNonce)
2✔
596
    } else {
597
      console.warn(
×
598
        'Deprecated: Use `currentNonce` inside an object instead. See `PendingTransactionsOptions`.'
599
      )
600
      currentNonce = propsOrCurrentNonce
×
601
    }
602
    // END of @deprecated migration code
603

604
    const { address } = this.#getEip3770Address(safeAddress)
2✔
605
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
606

607
    const url = new URL(
2✔
608
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
609
    )
610

611
    if (hasConfirmations) {
2!
612
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
613
    }
614

615
    if (ordering) {
2!
616
      url.searchParams.set('ordering', ordering)
×
617
    }
618

619
    if (limit != null) {
2!
620
      url.searchParams.set('limit', limit.toString())
×
621
    }
622

623
    if (offset != null) {
2!
624
      url.searchParams.set('offset', offset.toString())
×
625
    }
626

627
    return sendRequest({
2✔
628
      url: url.toString(),
629
      method: HttpMethod.Get
630
    })
631
  }
632

633
  /**
634
   * Returns a list of transactions for a Safe. The list has different structures depending on the transaction type
635
   *
636
   * @param safeAddress - The Safe address
637
   * @returns The list of transactions waiting for the confirmation of the Safe owners
638
   * @throws "Invalid Safe address"
639
   * @throws "Checksum address validation failed"
640
   */
641
  async getAllTransactions(
642
    safeAddress: string,
643
    options?: AllTransactionsOptions
644
  ): Promise<AllTransactionsListResponse> {
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}/all-transactions/`)
2✔
650

651
    const trusted = options?.trusted?.toString() || 'true'
2✔
652
    url.searchParams.set('trusted', trusted)
2✔
653

654
    const queued = options?.queued?.toString() || 'true'
2✔
655
    url.searchParams.set('queued', queued)
2✔
656

657
    const executed = options?.executed?.toString() || 'false'
2✔
658
    url.searchParams.set('executed', executed)
2✔
659

660
    return sendRequest({
2✔
661
      url: url.toString(),
662
      method: HttpMethod.Get
663
    })
664
  }
665

666
  /**
667
   * Returns the right nonce to propose a new transaction after the last pending transaction.
668
   *
669
   * @param safeAddress - The Safe address
670
   * @returns The right nonce to propose a new transaction after the last pending transaction
671
   * @throws "Invalid Safe address"
672
   * @throws "Invalid data"
673
   * @throws "Invalid ethereum address"
674
   */
675
  async getNextNonce(safeAddress: string): Promise<string> {
676
    if (safeAddress === '') {
×
677
      throw new Error('Invalid Safe address')
×
678
    }
679
    const { address } = this.#getEip3770Address(safeAddress)
×
680
    const pendingTransactions = await this.getPendingTransactions(address)
×
681
    if (pendingTransactions.results.length > 0) {
×
682
      const maxNonce = pendingTransactions.results.reduce((acc, tx) => {
×
683
        const curr = BigInt(tx.nonce)
×
684
        return curr > acc ? curr : acc
×
685
      }, 0n)
686

687
      return (maxNonce + 1n).toString()
×
688
    }
689
    const safeInfo = await this.getSafeInfo(address)
×
690
    return safeInfo.nonce
×
691
  }
692

693
  /**
694
   * Returns the list of all the ERC20 tokens handled by the Safe.
695
   *
696
   * @returns The list of all the ERC20 tokens
697
   */
698
  async getTokenList(): Promise<TokenInfoListResponse> {
699
    return sendRequest({
1✔
700
      url: `${this.#txServiceBaseUrl}/v1/tokens/`,
701
      method: HttpMethod.Get
702
    })
703
  }
704

705
  /**
706
   * Returns the information of a given ERC20 token.
707
   *
708
   * @param tokenAddress - The token address
709
   * @returns The information of the given ERC20 token
710
   * @throws "Invalid token address"
711
   * @throws "Checksum address validation failed"
712
   */
713
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
714
    if (tokenAddress === '') {
2!
715
      throw new Error('Invalid token address')
×
716
    }
717
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
718
    return sendRequest({
2✔
719
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
720
      method: HttpMethod.Get
721
    })
722
  }
723

724
  /**
725
   * Get a message by its safe message hash
726
   * @param messageHash The Safe message hash
727
   * @returns The message
728
   */
729
  async getMessage(messageHash: string): Promise<SafeMessage> {
730
    if (!messageHash) {
1!
731
      throw new Error('Invalid messageHash')
×
732
    }
733

734
    return sendRequest({
1✔
735
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
736
      method: HttpMethod.Get
737
    })
738
  }
739

740
  /**
741
   * Get the list of messages associated to a Safe account
742
   * @param safeAddress The safe address
743
   * @param options The options to filter the list of messages
744
   * @returns The paginated list of messages
745
   */
746
  async getMessages(
747
    safeAddress: string,
748
    { ordering, limit, offset }: GetSafeMessageListProps = {}
1✔
749
  ): Promise<SafeMessageListResponse> {
750
    if (!this.#isValidAddress(safeAddress)) {
1!
751
      throw new Error('Invalid safeAddress')
×
752
    }
753

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

756
    if (ordering) {
1!
757
      url.searchParams.set('ordering', ordering)
×
758
    }
759

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

764
    if (offset != null) {
1!
765
      url.searchParams.set('offset', offset.toString())
×
766
    }
767

768
    return sendRequest({
1✔
769
      url: url.toString(),
770
      method: HttpMethod.Get
771
    })
772
  }
773

774
  /**
775
   * Creates a new message with an initial signature
776
   * Add more signatures from other owners using addMessageSignature()
777
   * @param safeAddress The safe address
778
   * @param options The raw message to add, signature and safeAppId if any
779
   */
780
  async addMessage(safeAddress: string, addMessageProps: AddMessageProps): Promise<void> {
781
    if (!this.#isValidAddress(safeAddress)) {
2!
782
      throw new Error('Invalid safeAddress')
×
783
    }
784

785
    return sendRequest({
2✔
786
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
787
      method: HttpMethod.Post,
788
      body: addMessageProps
789
    })
790
  }
791

792
  /**
793
   * Add a signature to an existing message
794
   * @param messageHash The safe message hash
795
   * @param signature The signature
796
   */
797
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
798
    if (!messageHash || !signature) {
1!
799
      throw new Error('Invalid messageHash or signature')
×
800
    }
801

802
    return sendRequest({
1✔
803
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
804
      method: HttpMethod.Post,
805
      body: {
806
        signature
807
      }
808
    })
809
  }
810

811
  /**
812
   * Get the SafeOperations that were sent from a particular address.
813
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
814
   * @throws "Safe address must not be empty"
815
   * @throws "Invalid Ethereum address {safeAddress}"
816
   * @returns The SafeOperations sent from the given Safe's address
817
   */
818
  async getSafeOperationsByAddress({
819
    safeAddress,
820
    executed,
821
    hasConfirmations,
822
    ordering,
823
    limit,
824
    offset
825
  }: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
826
    if (!safeAddress) {
1!
827
      throw new Error('Safe address must not be empty')
×
828
    }
829

830
    const { address } = this.#getEip3770Address(safeAddress)
1✔
831

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

834
    if (ordering) {
1!
835
      url.searchParams.set('ordering', ordering)
×
836
    }
837

838
    if (limit != null) {
1!
839
      url.searchParams.set('limit', limit.toString())
×
840
    }
841

842
    if (offset != null) {
1!
843
      url.searchParams.set('offset', offset.toString())
×
844
    }
845

846
    if (hasConfirmations != null) {
1!
847
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
848
    }
849

850
    if (executed != null) {
1!
851
      url.searchParams.set('executed', executed.toString())
×
852
    }
853

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

860
  /**
861
   * Get the SafeOperations that are pending to send to the bundler
862
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
863
   * @throws "Safe address must not be empty"
864
   * @throws "Invalid Ethereum address {safeAddress}"
865
   * @returns The pending SafeOperations
866
   */
867
  async getPendingSafeOperations(
868
    props: Omit<GetSafeOperationListProps, 'executed'>
869
  ): Promise<GetSafeOperationListResponse> {
870
    return this.getSafeOperationsByAddress({
×
871
      ...props,
872
      executed: false
873
    })
874
  }
875

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

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

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

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

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

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

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

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

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

947
    const userOperationV06 = userOperation as UserOperationV06
1✔
948

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

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

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

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

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

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

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

1034
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