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

safe-global / safe-core-sdk / 12008523214

25 Nov 2024 10:55AM CUT coverage: 75.515%. Remained the same
12008523214

push

github

web-flow
feat(protocol-kit): Add react native compatibility (#1033)

241 of 390 branches covered (61.79%)

Branch coverage included in aggregate %.

786 of 970 relevant lines covered (81.03%)

3.76 hits per line

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

62.15
/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 {
1✔
38
  Eip3770Address,
39
  isSafeOperation,
40
  SafeMultisigConfirmationListResponse,
41
  SafeMultisigTransactionResponse,
42
  SafeOperation,
43
  SafeOperationConfirmationListResponse,
44
  SafeOperationResponse
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
119
   * @returns The transaction data decoded
120
   * @throws "Invalid data"
121
   * @throws "Not Found"
122
   * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)."
123
   */
124
  async decodeData(data: string): Promise<any> {
125
    if (data === '') {
1!
126
      throw new Error('Invalid data')
×
127
    }
128
    return sendRequest({
1✔
129
      url: `${this.#txServiceBaseUrl}/v1/data-decoder/`,
130
      method: HttpMethod.Post,
131
      body: { data }
132
    })
133
  }
134

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

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

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

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

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

237
  /**
238
   * Returns the information and configuration of the provided Safe address.
239
   *
240
   * @param safeAddress - The Safe address
241
   * @returns The information and configuration of the provided Safe address
242
   * @throws "Invalid Safe address"
243
   * @throws "Checksum address validation failed"
244
   */
245
  async getSafeInfo(safeAddress: string): Promise<SafeInfoResponse> {
246
    if (safeAddress === '') {
2!
247
      throw new Error('Invalid Safe address')
×
248
    }
249
    const { address } = this.#getEip3770Address(safeAddress)
2✔
250
    return sendRequest({
2✔
251
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/`,
252
      method: HttpMethod.Get
253
    })
254
  }
255

256
  /**
257
   * Returns the list of delegates.
258
   *
259
   * @param getSafeDelegateProps - Properties to filter the returned list of delegates
260
   * @returns The list of delegates
261
   * @throws "Checksum address validation failed"
262
   */
263
  async getSafeDelegates({
264
    safeAddress,
265
    delegateAddress,
266
    delegatorAddress,
267
    label,
268
    limit,
269
    offset
270
  }: GetSafeDelegateProps): Promise<SafeDelegateListResponse> {
271
    const url = new URL(`${this.#txServiceBaseUrl}/v2/delegates`)
2✔
272

273
    if (safeAddress) {
2!
274
      const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
275
      url.searchParams.set('safe', safe)
2✔
276
    }
277
    if (delegateAddress) {
2!
278
      const { address: delegate } = this.#getEip3770Address(delegateAddress)
×
279
      url.searchParams.set('delegate', delegate)
×
280
    }
281
    if (delegatorAddress) {
2!
282
      const { address: delegator } = this.#getEip3770Address(delegatorAddress)
×
283
      url.searchParams.set('delegator', delegator)
×
284
    }
285
    if (label) {
2!
286
      url.searchParams.set('label', label)
×
287
    }
288
    if (limit != null) {
2!
289
      url.searchParams.set('limit', limit.toString())
×
290
    }
291
    if (offset != null) {
2!
292
      url.searchParams.set('offset', offset.toString())
×
293
    }
294

295
    return sendRequest({
2✔
296
      url: url.toString(),
297
      method: HttpMethod.Get
298
    })
299
  }
300

301
  /**
302
   * Adds a new delegate for a given Safe address.
303
   *
304
   * @param addSafeDelegateProps - The configuration of the new delegate
305
   * @returns
306
   * @throws "Invalid Safe delegate address"
307
   * @throws "Invalid Safe delegator address"
308
   * @throws "Invalid label"
309
   * @throws "Checksum address validation failed"
310
   * @throws "Address <delegate_address> is not checksumed"
311
   * @throws "Safe=<safe_address> does not exist or it's still not indexed"
312
   * @throws "Signing owner is not an owner of the Safe"
313
   */
314
  async addSafeDelegate({
315
    safeAddress,
316
    delegateAddress,
317
    delegatorAddress,
318
    label,
319
    signer
320
  }: AddSafeDelegateProps): Promise<SignedSafeDelegateResponse> {
321
    if (delegateAddress === '') {
2!
322
      throw new Error('Invalid Safe delegate address')
×
323
    }
324
    if (delegatorAddress === '') {
2!
325
      throw new Error('Invalid Safe delegator address')
×
326
    }
327
    if (label === '') {
2!
328
      throw new Error('Invalid label')
×
329
    }
330
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
331
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
332
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
333

334
    const body: any = {
2✔
335
      safe: safeAddress ? this.#getEip3770Address(safeAddress).address : null,
2!
336
      delegate,
337
      delegator,
338
      label,
339
      signature
340
    }
341
    return sendRequest({
2✔
342
      url: `${this.#txServiceBaseUrl}/v2/delegates/`,
343
      method: HttpMethod.Post,
344
      body
345
    })
346
  }
347

348
  /**
349
   * Removes a delegate for a given Safe address.
350
   *
351
   * @param deleteSafeDelegateProps - The configuration for the delegate that will be removed
352
   * @returns
353
   * @throws "Invalid Safe delegate address"
354
   * @throws "Invalid Safe delegator address"
355
   * @throws "Checksum address validation failed"
356
   * @throws "Signing owner is not an owner of the Safe"
357
   * @throws "Not found"
358
   */
359
  async removeSafeDelegate({
360
    delegateAddress,
361
    delegatorAddress,
362
    signer
363
  }: DeleteSafeDelegateProps): Promise<void> {
364
    if (delegateAddress === '') {
2!
365
      throw new Error('Invalid Safe delegate address')
×
366
    }
367
    if (delegatorAddress === '') {
2!
368
      throw new Error('Invalid Safe delegator address')
×
369
    }
370
    const { address: delegate } = this.#getEip3770Address(delegateAddress)
2✔
371
    const { address: delegator } = this.#getEip3770Address(delegatorAddress)
2✔
372
    const signature = await signDelegate(signer, delegate, this.#chainId)
2✔
373

374
    return sendRequest({
2✔
375
      url: `${this.#txServiceBaseUrl}/v2/delegates/${delegate}`,
376
      method: HttpMethod.Delete,
377
      body: {
378
        delegator,
379
        signature
380
      }
381
    })
382
  }
383

384
  /**
385
   * Returns the creation information of a Safe.
386
   *
387
   * @param safeAddress - The Safe address
388
   * @returns The creation information of a Safe
389
   * @throws "Invalid Safe address"
390
   * @throws "Safe creation not found"
391
   * @throws "Checksum address validation failed"
392
   * @throws "Problem connecting to Ethereum network"
393
   */
394
  async getSafeCreationInfo(safeAddress: string): Promise<SafeCreationInfoResponse> {
395
    if (safeAddress === '') {
2!
396
      throw new Error('Invalid Safe address')
×
397
    }
398
    const { address } = this.#getEip3770Address(safeAddress)
2✔
399
    return sendRequest({
2✔
400
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/creation/`,
401
      method: HttpMethod.Get
402
    })
403
  }
404

405
  /**
406
   * Estimates the safeTxGas for a given Safe multi-signature transaction.
407
   *
408
   * @param safeAddress - The Safe address
409
   * @param safeTransaction - The Safe transaction to estimate
410
   * @returns The safeTxGas for the given Safe transaction
411
   * @throws "Invalid Safe address"
412
   * @throws "Data not valid"
413
   * @throws "Safe not found"
414
   * @throws "Tx not valid"
415
   */
416
  async estimateSafeTransaction(
417
    safeAddress: string,
418
    safeTransaction: SafeMultisigTransactionEstimate
419
  ): Promise<SafeMultisigTransactionEstimateResponse> {
420
    if (safeAddress === '') {
2!
421
      throw new Error('Invalid Safe address')
×
422
    }
423
    const { address } = this.#getEip3770Address(safeAddress)
2✔
424
    return sendRequest({
2✔
425
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/estimations/`,
426
      method: HttpMethod.Post,
427
      body: safeTransaction
428
    })
429
  }
430

431
  /**
432
   * Creates a new multi-signature transaction with its confirmations and stores it in the Safe Transaction Service.
433
   *
434
   * @param proposeTransactionConfig - The configuration of the proposed transaction
435
   * @returns The hash of the Safe transaction proposed
436
   * @throws "Invalid Safe address"
437
   * @throws "Invalid safeTxHash"
438
   * @throws "Invalid data"
439
   * @throws "Invalid ethereum address/User is not an owner/Invalid signature/Nonce already executed/Sender is not an owner"
440
   */
441
  async proposeTransaction({
442
    safeAddress,
443
    safeTransactionData,
444
    safeTxHash,
445
    senderAddress,
446
    senderSignature,
447
    origin
448
  }: ProposeTransactionProps): Promise<void> {
449
    if (safeAddress === '') {
2!
450
      throw new Error('Invalid Safe address')
×
451
    }
452
    const { address: safe } = this.#getEip3770Address(safeAddress)
2✔
453
    const { address: sender } = this.#getEip3770Address(senderAddress)
2✔
454
    if (safeTxHash === '') {
2!
455
      throw new Error('Invalid safeTxHash')
×
456
    }
457
    return sendRequest({
2✔
458
      url: `${this.#txServiceBaseUrl}/v1/safes/${safe}/multisig-transactions/`,
459
      method: HttpMethod.Post,
460
      body: {
461
        ...safeTransactionData,
462
        contractTransactionHash: safeTxHash,
463
        sender,
464
        signature: senderSignature,
465
        origin
466
      }
467
    })
468
  }
469

470
  /**
471
   * Returns the history of incoming transactions of a Safe account.
472
   *
473
   * @param safeAddress - The Safe address
474
   * @returns The history of incoming transactions
475
   * @throws "Invalid Safe address"
476
   * @throws "Checksum address validation failed"
477
   */
478
  async getIncomingTransactions(safeAddress: string): Promise<TransferListResponse> {
479
    if (safeAddress === '') {
2!
480
      throw new Error('Invalid Safe address')
×
481
    }
482
    const { address } = this.#getEip3770Address(safeAddress)
2✔
483
    return sendRequest({
2✔
484
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/incoming-transfers?executed=true`,
485
      method: HttpMethod.Get
486
    })
487
  }
488

489
  /**
490
   * Returns the history of module transactions of a Safe account.
491
   *
492
   * @param safeAddress - The Safe address
493
   * @returns The history of module transactions
494
   * @throws "Invalid Safe address"
495
   * @throws "Invalid data"
496
   * @throws "Invalid ethereum address"
497
   */
498
  async getModuleTransactions(safeAddress: string): Promise<SafeModuleTransactionListResponse> {
499
    if (safeAddress === '') {
2!
500
      throw new Error('Invalid Safe address')
×
501
    }
502
    const { address } = this.#getEip3770Address(safeAddress)
2✔
503
    return sendRequest({
2✔
504
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/module-transactions/`,
505
      method: HttpMethod.Get
506
    })
507
  }
508

509
  /**
510
   * Returns the history of multi-signature transactions of a Safe account.
511
   *
512
   * @param safeAddress - The Safe address
513
   * @returns The history of multi-signature transactions
514
   * @throws "Invalid Safe address"
515
   * @throws "Checksum address validation failed"
516
   */
517
  async getMultisigTransactions(safeAddress: string): Promise<SafeMultisigTransactionListResponse> {
518
    if (safeAddress === '') {
2!
519
      throw new Error('Invalid Safe address')
×
520
    }
521
    const { address } = this.#getEip3770Address(safeAddress)
2✔
522
    return sendRequest({
2✔
523
      url: `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/`,
524
      method: HttpMethod.Get
525
    })
526
  }
527

528
  /**
529
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
530
   *
531
   * @param safeAddress - The Safe address
532
   * @param currentNonce - Deprecated, use inside object property: Current nonce of the Safe.
533
   * @returns The list of transactions waiting for the confirmation of the Safe owners
534
   * @throws "Invalid Safe address"
535
   * @throws "Invalid data"
536
   * @throws "Invalid ethereum address"
537
   */
538
  async getPendingTransactions(
539
    safeAddress: string,
540
    currentNonce?: number
541
  ): Promise<SafeMultisigTransactionListResponse>
542
  /**
543
   * Returns the list of multi-signature transactions that are waiting for the confirmation of the Safe owners.
544
   *
545
   * @param safeAddress - The Safe address
546
   * @param {PendingTransactionsOptions} options The options to filter the list of transactions
547
   * @returns The list of transactions waiting for the confirmation of the Safe owners
548
   * @throws "Invalid Safe address"
549
   * @throws "Invalid data"
550
   * @throws "Invalid ethereum address"
551
   */
552
  async getPendingTransactions(
553
    safeAddress: string,
554
    { currentNonce, hasConfirmations, ordering, limit, offset }: PendingTransactionsOptions
555
  ): Promise<SafeMultisigTransactionListResponse>
556
  async getPendingTransactions(
557
    safeAddress: string,
558
    propsOrCurrentNonce: PendingTransactionsOptions | number = {}
×
559
  ): Promise<SafeMultisigTransactionListResponse> {
560
    if (safeAddress === '') {
2!
561
      throw new Error('Invalid Safe address')
×
562
    }
563

564
    // TODO: Remove @deprecated migration code
565
    let currentNonce: number | undefined
566
    let hasConfirmations: boolean | undefined
567
    let ordering: string | undefined
568
    let limit: number | undefined
569
    let offset: number | undefined
570
    if (typeof propsOrCurrentNonce === 'object') {
2!
571
      ;({ currentNonce, hasConfirmations, ordering, limit, offset } = propsOrCurrentNonce)
2✔
572
    } else {
573
      console.warn(
×
574
        'Deprecated: Use `currentNonce` inside an object instead. See `PendingTransactionsOptions`.'
575
      )
576
      currentNonce = propsOrCurrentNonce
×
577
    }
578
    // END of @deprecated migration code
579

580
    const { address } = this.#getEip3770Address(safeAddress)
2✔
581
    const nonce = currentNonce ? currentNonce : (await this.getSafeInfo(address)).nonce
2!
582

583
    const url = new URL(
2✔
584
      `${this.#txServiceBaseUrl}/v1/safes/${address}/multisig-transactions/?executed=false&nonce__gte=${nonce}`
585
    )
586

587
    if (hasConfirmations) {
2!
588
      url.searchParams.set('has_confirmations', hasConfirmations.toString())
×
589
    }
590

591
    if (ordering) {
2!
592
      url.searchParams.set('ordering', ordering)
×
593
    }
594

595
    if (limit != null) {
2!
596
      url.searchParams.set('limit', limit.toString())
×
597
    }
598

599
    if (offset != null) {
2!
600
      url.searchParams.set('offset', offset.toString())
×
601
    }
602

603
    return sendRequest({
2✔
604
      url: url.toString(),
605
      method: HttpMethod.Get
606
    })
607
  }
608

609
  /**
610
   * Returns a list of transactions for a Safe. The list has different structures depending on the transaction type
611
   *
612
   * @param safeAddress - The Safe address
613
   * @returns The list of transactions waiting for the confirmation of the Safe owners
614
   * @throws "Invalid Safe address"
615
   * @throws "Checksum address validation failed"
616
   */
617
  async getAllTransactions(
618
    safeAddress: string,
619
    options?: AllTransactionsOptions
620
  ): Promise<AllTransactionsListResponse> {
621
    if (safeAddress === '') {
2!
622
      throw new Error('Invalid Safe address')
×
623
    }
624
    const { address } = this.#getEip3770Address(safeAddress)
2✔
625
    const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/all-transactions/`)
2✔
626

627
    const trusted = options?.trusted?.toString() || 'true'
2✔
628
    url.searchParams.set('trusted', trusted)
2✔
629

630
    const queued = options?.queued?.toString() || 'true'
2✔
631
    url.searchParams.set('queued', queued)
2✔
632

633
    const executed = options?.executed?.toString() || 'false'
2✔
634
    url.searchParams.set('executed', executed)
2✔
635

636
    return sendRequest({
2✔
637
      url: url.toString(),
638
      method: HttpMethod.Get
639
    })
640
  }
641

642
  /**
643
   * Returns the right nonce to propose a new transaction after the last pending transaction.
644
   *
645
   * @param safeAddress - The Safe address
646
   * @returns The right nonce to propose a new transaction after the last pending transaction
647
   * @throws "Invalid Safe address"
648
   * @throws "Invalid data"
649
   * @throws "Invalid ethereum address"
650
   */
651
  async getNextNonce(safeAddress: string): Promise<number> {
652
    if (safeAddress === '') {
×
653
      throw new Error('Invalid Safe address')
×
654
    }
655
    const { address } = this.#getEip3770Address(safeAddress)
×
656
    const pendingTransactions = await this.getPendingTransactions(address)
×
657
    if (pendingTransactions.results.length > 0) {
×
658
      const nonces = pendingTransactions.results.map((tx) => tx.nonce)
×
659
      const lastNonce = Math.max(...nonces)
×
660
      return lastNonce + 1
×
661
    }
662
    const safeInfo = await this.getSafeInfo(address)
×
663
    return safeInfo.nonce
×
664
  }
665

666
  /**
667
   * Returns the list of all the ERC20 tokens handled by the Safe.
668
   *
669
   * @returns The list of all the ERC20 tokens
670
   */
671
  async getTokenList(): Promise<TokenInfoListResponse> {
672
    return sendRequest({
1✔
673
      url: `${this.#txServiceBaseUrl}/v1/tokens/`,
674
      method: HttpMethod.Get
675
    })
676
  }
677

678
  /**
679
   * Returns the information of a given ERC20 token.
680
   *
681
   * @param tokenAddress - The token address
682
   * @returns The information of the given ERC20 token
683
   * @throws "Invalid token address"
684
   * @throws "Checksum address validation failed"
685
   */
686
  async getToken(tokenAddress: string): Promise<TokenInfoResponse> {
687
    if (tokenAddress === '') {
2!
688
      throw new Error('Invalid token address')
×
689
    }
690
    const { address } = this.#getEip3770Address(tokenAddress)
2✔
691
    return sendRequest({
2✔
692
      url: `${this.#txServiceBaseUrl}/v1/tokens/${address}/`,
693
      method: HttpMethod.Get
694
    })
695
  }
696

697
  /**
698
   * Get a message by its safe message hash
699
   * @param messageHash The Safe message hash
700
   * @returns The message
701
   */
702
  async getMessage(messageHash: string): Promise<SafeMessage> {
703
    if (!messageHash) {
1!
704
      throw new Error('Invalid messageHash')
×
705
    }
706

707
    return sendRequest({
1✔
708
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/`,
709
      method: HttpMethod.Get
710
    })
711
  }
712

713
  /**
714
   * Get the list of messages associated to a Safe account
715
   * @param safeAddress The safe address
716
   * @param options The options to filter the list of messages
717
   * @returns The paginated list of messages
718
   */
719
  async getMessages(
720
    safeAddress: string,
721
    { ordering, limit, offset }: GetSafeMessageListProps = {}
1✔
722
  ): Promise<SafeMessageListResponse> {
723
    if (!this.#isValidAddress(safeAddress)) {
1!
724
      throw new Error('Invalid safeAddress')
×
725
    }
726

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

729
    if (ordering) {
1!
730
      url.searchParams.set('ordering', ordering)
×
731
    }
732

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

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

741
    return sendRequest({
1✔
742
      url: url.toString(),
743
      method: HttpMethod.Get
744
    })
745
  }
746

747
  /**
748
   * Creates a new message with an initial signature
749
   * Add more signatures from other owners using addMessageSignature()
750
   * @param safeAddress The safe address
751
   * @param options The raw message to add, signature and safeAppId if any
752
   */
753
  async addMessage(safeAddress: string, addMessageProps: AddMessageProps): Promise<void> {
754
    if (!this.#isValidAddress(safeAddress)) {
2!
755
      throw new Error('Invalid safeAddress')
×
756
    }
757

758
    return sendRequest({
2✔
759
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/messages/`,
760
      method: HttpMethod.Post,
761
      body: addMessageProps
762
    })
763
  }
764

765
  /**
766
   * Add a signature to an existing message
767
   * @param messageHash The safe message hash
768
   * @param signature The signature
769
   */
770
  async addMessageSignature(messageHash: string, signature: string): Promise<void> {
771
    if (!messageHash || !signature) {
1!
772
      throw new Error('Invalid messageHash or signature')
×
773
    }
774

775
    return sendRequest({
1✔
776
      url: `${this.#txServiceBaseUrl}/v1/messages/${messageHash}/signatures/`,
777
      method: HttpMethod.Post,
778
      body: {
779
        signature
780
      }
781
    })
782
  }
783

784
  /**
785
   * Get the SafeOperations that were sent from a particular address.
786
   * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations
787
   * @throws "Safe address must not be empty"
788
   * @throws "Invalid Ethereum address {safeAddress}"
789
   * @returns The SafeOperations sent from the given Safe's address
790
   */
791
  async getSafeOperationsByAddress({
792
    safeAddress,
793
    ordering,
794
    limit,
795
    offset
796
  }: GetSafeOperationListProps): Promise<GetSafeOperationListResponse> {
797
    if (!safeAddress) {
1!
798
      throw new Error('Safe address must not be empty')
×
799
    }
800

801
    const { address } = this.#getEip3770Address(safeAddress)
1✔
802

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

805
    if (ordering) {
1!
806
      url.searchParams.set('ordering', ordering)
×
807
    }
808

809
    if (limit != null) {
1!
810
      url.searchParams.set('limit', limit.toString())
×
811
    }
812

813
    if (offset != null) {
1!
814
      url.searchParams.set('offset', offset.toString())
×
815
    }
816

817
    return sendRequest({
1✔
818
      url: url.toString(),
819
      method: HttpMethod.Get
820
    })
821
  }
822

823
  /**
824
   * Get a SafeOperation by its hash.
825
   * @param safeOperationHash The SafeOperation hash
826
   * @throws "SafeOperation hash must not be empty"
827
   * @throws "Not found."
828
   * @returns The SafeOperation
829
   */
830
  async getSafeOperation(safeOperationHash: string): Promise<SafeOperationResponse> {
831
    if (!safeOperationHash) {
1!
832
      throw new Error('SafeOperation hash must not be empty')
×
833
    }
834

835
    return sendRequest({
1✔
836
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`,
837
      method: HttpMethod.Get
838
    })
839
  }
840

841
  /**
842
   * Create a new 4337 SafeOperation for a Safe.
843
   * @param addSafeOperationProps - The configuration of the SafeOperation
844
   * @throws "Safe address must not be empty"
845
   * @throws "Invalid Safe address {safeAddress}"
846
   * @throws "Module address must not be empty"
847
   * @throws "Invalid module address {moduleAddress}"
848
   * @throws "Signature must not be empty"
849
   */
850
  async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise<void> {
851
    let safeAddress: string, moduleAddress: string
852
    let addSafeOperationProps: AddSafeOperationProps
853

854
    if (isSafeOperation(safeOperation)) {
1!
855
      addSafeOperationProps = await getAddSafeOperationProps(safeOperation)
×
856
    } else {
857
      addSafeOperationProps = safeOperation
1✔
858
    }
859

860
    const {
861
      entryPoint,
862
      moduleAddress: moduleAddressProp,
863
      options,
864
      safeAddress: safeAddressProp,
865
      userOperation
866
    } = addSafeOperationProps
1✔
867
    if (!safeAddressProp) {
1!
868
      throw new Error('Safe address must not be empty')
×
869
    }
870
    try {
1✔
871
      safeAddress = this.#getEip3770Address(safeAddressProp).address
1✔
872
    } catch (err) {
873
      throw new Error(`Invalid Safe address ${safeAddressProp}`)
×
874
    }
875

876
    if (!moduleAddressProp) {
1!
877
      throw new Error('Module address must not be empty')
×
878
    }
879

880
    try {
1✔
881
      moduleAddress = this.#getEip3770Address(moduleAddressProp).address
1✔
882
    } catch (err) {
883
      throw new Error(`Invalid module address ${moduleAddressProp}`)
×
884
    }
885

886
    if (isEmptyData(userOperation.signature)) {
1!
887
      throw new Error('Signature must not be empty')
×
888
    }
889

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

894
    return sendRequest({
1✔
895
      url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`,
896
      method: HttpMethod.Post,
897
      body: {
898
        nonce: Number(userOperation.nonce),
899
        initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode,
1!
900
        callData: userOperation.callData,
901
        callGasLimit: userOperation.callGasLimit.toString(),
902
        verificationGasLimit: userOperation.verificationGasLimit.toString(),
903
        preVerificationGas: userOperation.preVerificationGas.toString(),
904
        maxFeePerGas: userOperation.maxFeePerGas.toString(),
905
        maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(),
906
        paymasterAndData: isEmptyData(userOperation.paymasterAndData)
1!
907
          ? null
908
          : userOperation.paymasterAndData,
909
        entryPoint,
910
        validAfter: getISOString(options?.validAfter),
911
        validUntil: getISOString(options?.validUntil),
912
        signature: userOperation.signature,
913
        moduleAddress
914
      }
915
    })
916
  }
917

918
  /**
919
   * Returns the list of confirmations for a given a SafeOperation.
920
   *
921
   * @param safeOperationHash - The hash of the SafeOperation to get confirmations for
922
   * @param getSafeOperationConfirmationsOptions - Additional options for fetching the list of confirmations
923
   * @returns The list of confirmations
924
   * @throws "Invalid SafeOperation hash"
925
   * @throws "Invalid data"
926
   */
927
  async getSafeOperationConfirmations(
928
    safeOperationHash: string,
929
    { limit, offset }: ListOptions = {}
1✔
930
  ): Promise<SafeOperationConfirmationListResponse> {
931
    if (!safeOperationHash) {
1!
932
      throw new Error('Invalid SafeOperation hash')
×
933
    }
934

935
    const url = new URL(
1✔
936
      `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`
937
    )
938

939
    if (limit != null) {
1!
940
      url.searchParams.set('limit', limit.toString())
×
941
    }
942

943
    if (offset != null) {
1!
944
      url.searchParams.set('offset', offset.toString())
×
945
    }
946

947
    return sendRequest({
1✔
948
      url: url.toString(),
949
      method: HttpMethod.Get
950
    })
951
  }
952

953
  /**
954
   * Adds a confirmation for a SafeOperation.
955
   *
956
   * @param safeOperationHash The SafeOperation hash
957
   * @param signature - Signature of the SafeOperation
958
   * @returns
959
   * @throws "Invalid SafeOperation hash"
960
   * @throws "Invalid signature"
961
   * @throws "Malformed data"
962
   * @throws "Error processing data"
963
   */
964
  async confirmSafeOperation(safeOperationHash: string, signature: string): Promise<void> {
965
    if (!safeOperationHash) {
1!
966
      throw new Error('Invalid SafeOperation hash')
×
967
    }
968
    if (!signature) {
1!
969
      throw new Error('Invalid signature')
×
970
    }
971
    return sendRequest({
1✔
972
      url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/confirmations/`,
973
      method: HttpMethod.Post,
974
      body: { signature }
975
    })
976
  }
977
}
978

979
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

© 2025 Coveralls, Inc