@exodus/solana-api 3.30.7 → 3.30.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +4 -4
- package/src/connection.js +10 -10
- package/src/create-unsigned-tx-for-send.js +32 -5
- package/src/fee-payer.js +3 -3
- package/src/tx-log/token-balance-patch.js +49 -0
- package/src/tx-log/ws-monitor.js +70 -24
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [3.30.9](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.8...@exodus/solana-api@3.30.9) (2026-04-04)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: sponsored Solana token sends when CU simulation fails (#7707)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.30.8](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.7...@exodus/solana-api@3.30.8) (2026-04-02)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @exodus/solana-api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [3.30.7](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.6...@exodus/solana-api@3.30.7) (2026-04-01)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.30.
|
|
3
|
+
"version": "3.30.9",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Solana",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@exodus/fetch": "^1.7.3",
|
|
34
34
|
"@exodus/models": "^13.0.0",
|
|
35
35
|
"@exodus/simple-retry": "^0.0.6",
|
|
36
|
-
"@exodus/solana-lib": "^3.22.
|
|
37
|
-
"@exodus/solana-meta": "^2.0
|
|
36
|
+
"@exodus/solana-lib": "^3.22.5",
|
|
37
|
+
"@exodus/solana-meta": "^2.9.0",
|
|
38
38
|
"@exodus/timer": "^1.1.1",
|
|
39
39
|
"debug": "^4.1.1",
|
|
40
40
|
"delay": "^4.0.1",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@exodus/assets-testing": "^1.0.0",
|
|
50
50
|
"@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "a2cd3fbc02d07f1c3dc127be4d7896292d2dc967",
|
|
53
53
|
"bugs": {
|
|
54
54
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
55
55
|
},
|
package/src/connection.js
CHANGED
|
@@ -74,17 +74,17 @@ export class Connection {
|
|
|
74
74
|
if (this.isOpen) {
|
|
75
75
|
if (typeof this.ws.ping === 'function') {
|
|
76
76
|
this.ws.ping()
|
|
77
|
-
} else {
|
|
78
|
-
// Some WebSocket implementations (like in browser or Electron renderer) don't have a ping method
|
|
79
|
-
this.ws.send(
|
|
80
|
-
JSON.stringify({
|
|
81
|
-
jsonrpc: '2.0',
|
|
82
|
-
id: Date.now(),
|
|
83
|
-
method: 'getVersion',
|
|
84
|
-
params: [],
|
|
85
|
-
})
|
|
86
|
-
)
|
|
87
77
|
}
|
|
78
|
+
|
|
79
|
+
// Application-level keepalive (always); native ping is optional per WebSocket impl
|
|
80
|
+
this.ws.send(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
jsonrpc: '2.0',
|
|
83
|
+
id: Date.now(),
|
|
84
|
+
method: 'getVersion',
|
|
85
|
+
params: [],
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
88
|
}
|
|
89
89
|
}, PING_INTERVAL)
|
|
90
90
|
}
|
|
@@ -10,7 +10,9 @@ import assert from 'minimalistic-assert'
|
|
|
10
10
|
|
|
11
11
|
import { maybeAddFeePayerWithAuth } from './fee-payer.js'
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const COMPUTE_BUDGET_INSTRUCTIONS_CU = 300
|
|
14
|
+
const SOL_TRANSFER_CU = 150 + COMPUTE_BUDGET_INSTRUCTIONS_CU
|
|
15
|
+
const DEFAULT_COMPUTE_UNIT_LIMIT = 200_000
|
|
14
16
|
const TOKEN_ACCOUNT_CREATION_SIZE = 165 // size of the token account
|
|
15
17
|
|
|
16
18
|
export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) => {
|
|
@@ -65,6 +67,7 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
|
|
|
65
67
|
|
|
66
68
|
const feeData =
|
|
67
69
|
providedFeeData ?? (await assetClientInterface.getFeeConfig({ assetName: baseAssetName }))
|
|
70
|
+
const shouldTryFeePayer = feeData.enableFeePayer && useFeePayer
|
|
68
71
|
|
|
69
72
|
const fromAddress =
|
|
70
73
|
providedFromAddress ??
|
|
@@ -211,18 +214,22 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
|
|
|
211
214
|
reference,
|
|
212
215
|
memo,
|
|
213
216
|
// Effective: platform enable AND per-tx intent
|
|
214
|
-
useFeePayer,
|
|
217
|
+
useFeePayer: shouldTryFeePayer,
|
|
215
218
|
...tokenParams,
|
|
216
219
|
...stakingParams,
|
|
217
220
|
...magicEdenParams,
|
|
218
221
|
})
|
|
219
222
|
|
|
220
223
|
unsignedTx.txMeta.stakingParams = stakingParams
|
|
224
|
+
const isFeeSponsoredTokenTransfer = isToken && unsignedTx.txMeta.useFeePayer && !nft && !method
|
|
225
|
+
const shouldUseSafeComputeUnitLimit =
|
|
226
|
+
isFeeSponsoredTokenTransfer &&
|
|
227
|
+
(!unsignedTx.txData.isAssociatedTokenAccountActive || isExchange)
|
|
221
228
|
|
|
222
229
|
const resolveUnitConsumed = async () => {
|
|
223
230
|
// this avoids unnecessary simulations. Also the simulation fails with InsufficientFundsForRent when sending all.
|
|
224
231
|
if (asset.name === asset.baseAsset.name && amount && !nft && !method) {
|
|
225
|
-
return
|
|
232
|
+
return SOL_TRANSFER_CU
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
// Simulate with unsigned transaction. The fee payer service is deterministic -
|
|
@@ -237,10 +244,31 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
|
|
|
237
244
|
// we use this method to compute unitsConsumed
|
|
238
245
|
// we can throw error here and fallback to ~0.025 SOL or estimate fee based on the method
|
|
239
246
|
console.log('error getting units consumed:', err)
|
|
247
|
+
const serializedError = typeof err === 'string' ? err : JSON.stringify(err)
|
|
248
|
+
const isRentOrBalanceError = /insufficientfunds|rent/i.test(serializedError)
|
|
249
|
+
const isAccountNotFoundError = /accountnotfound/i.test(serializedError)
|
|
250
|
+
|
|
251
|
+
// Fee sponsorship is injected after this simulation step. Overestimate CU to
|
|
252
|
+
// avoid on-chain failures; the fee payer absorbs the cost.
|
|
253
|
+
if (!unitsConsumed && isFeeSponsoredTokenTransfer && isRentOrBalanceError) {
|
|
254
|
+
return DEFAULT_COMPUTE_UNIT_LIMIT
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!unitsConsumed && isFeeSponsoredTokenTransfer && isAccountNotFoundError) {
|
|
258
|
+
const senderBaseBalanceLamports = BigInt(await api.getBalance(fromAddress))
|
|
259
|
+
if (senderBaseBalanceLamports === BigInt(0)) return DEFAULT_COMPUTE_UNIT_LIMIT
|
|
260
|
+
}
|
|
261
|
+
|
|
240
262
|
if (!unitsConsumed) throw new Error(err)
|
|
241
263
|
}
|
|
242
264
|
|
|
243
|
-
|
|
265
|
+
const estimatedComputeUnitLimit = unitsConsumed + COMPUTE_BUDGET_INSTRUCTIONS_CU
|
|
266
|
+
|
|
267
|
+
if (shouldUseSafeComputeUnitLimit && estimatedComputeUnitLimit < DEFAULT_COMPUTE_UNIT_LIMIT) {
|
|
268
|
+
return DEFAULT_COMPUTE_UNIT_LIMIT
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return estimatedComputeUnitLimit
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
const priorityFee = feeData.priorityFee
|
|
@@ -272,7 +300,6 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
|
|
|
272
300
|
const tx = await maybeAddFeePayerWithAuth({
|
|
273
301
|
unsignedTx,
|
|
274
302
|
feePayerClient,
|
|
275
|
-
enableFeePayer: feeData.enableFeePayer,
|
|
276
303
|
})
|
|
277
304
|
|
|
278
305
|
const fee = tx.txMeta.usedFeePayer ? asset.feeAsset.currency.ZERO : calculatedFee
|
package/src/fee-payer.js
CHANGED
|
@@ -145,11 +145,11 @@ export const feePayerClientFactory = ({
|
|
|
145
145
|
* @param {Object} params.unsignedTx - The unsigned transaction
|
|
146
146
|
* @param {Object} params.feePayerClient - The fee payer client instance
|
|
147
147
|
*/
|
|
148
|
-
export const maybeAddFeePayerWithAuth = async ({ unsignedTx, feePayerClient
|
|
148
|
+
export const maybeAddFeePayerWithAuth = async ({ unsignedTx, feePayerClient }) => {
|
|
149
149
|
let unsignedTxWithFeePayer = unsignedTx
|
|
150
150
|
|
|
151
|
-
//
|
|
152
|
-
if (!feePayerClient || !
|
|
151
|
+
// `txMeta.useFeePayer` already combines platform-level and per-tx intent.
|
|
152
|
+
if (!feePayerClient || !unsignedTx.txMeta.useFeePayer) {
|
|
153
153
|
unsignedTxWithFeePayer.txMeta.usedFeePayer = false
|
|
154
154
|
return unsignedTxWithFeePayer
|
|
155
155
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds `newData` for a token-balance patch applied after `executeOperationsBatch`.
|
|
3
|
+
* `AccountState.merge` is shallow at the top level: keys omitted from `newData` are left unchanged
|
|
4
|
+
* on the existing state, so an empty `extraNewData` (e.g. account/program WS) does not clear
|
|
5
|
+
* `cursor`, `historyCursor`, etc. set by an earlier patch in the same flush queue.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ tokenBalances?: Record<string, unknown> } & Record<string, unknown>} current - Latest account state from `getAccountState`
|
|
8
|
+
* @param {{ tokenPatch: Record<string, unknown>, extraNewData: Record<string, unknown> }} patch
|
|
9
|
+
*/
|
|
10
|
+
export function buildTokenBalancePatchNewData(current, { tokenPatch, extraNewData }) {
|
|
11
|
+
return {
|
|
12
|
+
...extraNewData,
|
|
13
|
+
tokenBalances: { ...(current.tokenBalances ?? Object.create(null)), ...tokenPatch },
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Applies a queue of token balance patches sequentially. After each `updateAccountState`, the next
|
|
19
|
+
* patch reads fresh state via `getAccountState` so multiple SPL mints in one tx (and mixed WS events)
|
|
20
|
+
* merge correctly.
|
|
21
|
+
*
|
|
22
|
+
* On failure, throws an `Error` with `cause` set to the underlying error, `remainingPatches` set to
|
|
23
|
+
* patches not yet applied (including the one that failed), and `appliedPatchCount` set to how many
|
|
24
|
+
* completed successfully. Callers can re-queue `remainingPatches` for retry.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} aci - Asset client interface with `getAccountState` and `updateAccountState`
|
|
27
|
+
* @param {{ assetName: string, walletAccount: string, patches: Array<{ tokenPatch: Record<string, unknown>, extraNewData: Record<string, unknown> }> }} params
|
|
28
|
+
*/
|
|
29
|
+
export async function flushTokenBalancePatchesQueue(aci, { assetName, walletAccount, patches }) {
|
|
30
|
+
if (!patches?.length) return
|
|
31
|
+
|
|
32
|
+
let i = 0
|
|
33
|
+
try {
|
|
34
|
+
for (; i < patches.length; i++) {
|
|
35
|
+
const { tokenPatch, extraNewData } = patches[i]
|
|
36
|
+
const current = await aci.getAccountState({ assetName, walletAccount })
|
|
37
|
+
await aci.updateAccountState({
|
|
38
|
+
assetName,
|
|
39
|
+
walletAccount,
|
|
40
|
+
newData: buildTokenBalancePatchNewData(current, { tokenPatch, extraNewData }),
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
const err = new Error('Token balance patch flush failed', { cause })
|
|
45
|
+
err.remainingPatches = patches.slice(i)
|
|
46
|
+
err.appliedPatchCount = i
|
|
47
|
+
throw err
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/tx-log/ws-monitor.js
CHANGED
|
@@ -4,6 +4,7 @@ import assert from 'minimalistic-assert'
|
|
|
4
4
|
import { DEFAULT_POOL_ADDRESS } from '../account-state.js'
|
|
5
5
|
import { normalizeTransactionNotificationResult } from '../ws-api.js'
|
|
6
6
|
import { SolanaClarityMonitor } from './clarity-monitor.js'
|
|
7
|
+
import { flushTokenBalancePatchesQueue } from './token-balance-patch.js'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_REMOTE_CONFIG = {
|
|
9
10
|
clarityUrl: [],
|
|
@@ -14,6 +15,8 @@ const DEFAULT_REMOTE_CONFIG = {
|
|
|
14
15
|
|
|
15
16
|
export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
16
17
|
#clarityInitialSyncDone = Object.create(null)
|
|
18
|
+
/** Queued after each `executeOperationsBatch` so token deltas merge onto latest state (fluent batch has no `push`). */
|
|
19
|
+
#pendingTokenBalancePatches = Object.create(null)
|
|
17
20
|
|
|
18
21
|
async #hasLoadedStakingInfo(walletAccount) {
|
|
19
22
|
const accountState = await this.aci.getAccountState({
|
|
@@ -171,13 +174,13 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
171
174
|
tokenAccounts: this.tokenAccountsByOwner[walletAccount],
|
|
172
175
|
})
|
|
173
176
|
if (!unknownTokensList.includes(parsed.mintAddress) && tokenName) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
...accountState.tokenBalances,
|
|
177
|
+
this.#queueTokenBalancePatchAfterBatch({
|
|
178
|
+
tokenPatch: {
|
|
177
179
|
[tokenName]: this.assets[tokenName].currency.baseUnit(parsed.amount),
|
|
178
180
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
extraNewData: {},
|
|
182
|
+
walletAccount,
|
|
183
|
+
})
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
return
|
|
@@ -202,13 +205,13 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
202
205
|
return
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
...accountState.tokenBalances,
|
|
208
|
+
this.#queueTokenBalancePatchAfterBatch({
|
|
209
|
+
tokenPatch: {
|
|
208
210
|
[tokenName]: this.assets[tokenName].currency.baseUnit(amount),
|
|
209
211
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
extraNewData: {},
|
|
213
|
+
walletAccount,
|
|
214
|
+
})
|
|
212
215
|
} else {
|
|
213
216
|
// SOL balance changed
|
|
214
217
|
|
|
@@ -332,16 +335,7 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
332
335
|
tokenBalancesFromTx[tokenName] = this.assets[tokenName].currency.baseUnit(amount)
|
|
333
336
|
}
|
|
334
337
|
|
|
335
|
-
const
|
|
336
|
-
Object.keys(tokenBalancesFromTx).length > 0
|
|
337
|
-
? {
|
|
338
|
-
...cursorState,
|
|
339
|
-
tokenBalances: {
|
|
340
|
-
...accountState.tokenBalances,
|
|
341
|
-
...tokenBalancesFromTx,
|
|
342
|
-
},
|
|
343
|
-
}
|
|
344
|
-
: { ...cursorState }
|
|
338
|
+
const hasTokenBalancePatch = Object.keys(tokenBalancesFromTx).length > 0
|
|
345
339
|
|
|
346
340
|
if (stakingTx) {
|
|
347
341
|
// for staking the balance is not updated by the balance handler
|
|
@@ -369,10 +363,18 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
369
363
|
break
|
|
370
364
|
}
|
|
371
365
|
|
|
372
|
-
|
|
366
|
+
const stakingStateUpdate = { ...cursorState, stakingInfo: temporaryStakingInfo }
|
|
373
367
|
|
|
374
368
|
// we update only stakingInfo, so that in this 12sec window, the spendable balance is valid. In the meanwhile we can fetch the complete one from RPC and update it in background (this one has the "staking accounts" references).
|
|
375
|
-
|
|
369
|
+
if (hasTokenBalancePatch) {
|
|
370
|
+
this.#queueTokenBalancePatchAfterBatch({
|
|
371
|
+
tokenPatch: tokenBalancesFromTx,
|
|
372
|
+
extraNewData: stakingStateUpdate,
|
|
373
|
+
walletAccount,
|
|
374
|
+
})
|
|
375
|
+
} else {
|
|
376
|
+
await this.#updateStateBatch({ newData: stakingStateUpdate, walletAccount })
|
|
377
|
+
}
|
|
376
378
|
|
|
377
379
|
await delay(12_000) // we introduce a delay to make sure the getStakingInfo RPC returns updated data
|
|
378
380
|
// because we NEED at a certain point to have an updated stakingInfo in the state to know what's the new solana stake account address.
|
|
@@ -395,8 +397,16 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
395
397
|
return
|
|
396
398
|
}
|
|
397
399
|
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
+
// Token deltas are applied after `executeOperationsBatch` so we merge onto latest state (swap race).
|
|
401
|
+
if (hasTokenBalancePatch) {
|
|
402
|
+
this.#queueTokenBalancePatchAfterBatch({
|
|
403
|
+
tokenPatch: tokenBalancesFromTx,
|
|
404
|
+
extraNewData: cursorState,
|
|
405
|
+
walletAccount,
|
|
406
|
+
})
|
|
407
|
+
} else if (Object.keys(cursorState).length > 0) {
|
|
408
|
+
await this.#updateStateBatch({ newData: cursorState, walletAccount })
|
|
409
|
+
}
|
|
400
410
|
|
|
401
411
|
return
|
|
402
412
|
}
|
|
@@ -425,10 +435,46 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
425
435
|
const batch = this.batch[walletAccount]
|
|
426
436
|
this.batch[walletAccount] = null
|
|
427
437
|
await this.aci.executeOperationsBatch(batch)
|
|
438
|
+
await this.#flushPendingTokenBalancePatches(walletAccount)
|
|
428
439
|
}, 2000)
|
|
429
440
|
return this.batch[walletAccount]
|
|
430
441
|
}
|
|
431
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Production `createOperationsBatch()` returns a fluent batch (`updateAccountState` / `commit`), not
|
|
445
|
+
* `{ push, exec }` from tests. Token balance deltas are queued here and applied after `executeOperationsBatch`
|
|
446
|
+
* so each patch merges `{ ...current.tokenBalances, ...tokenPatch }` against up-to-date state.
|
|
447
|
+
*/
|
|
448
|
+
#queueTokenBalancePatchAfterBatch({ tokenPatch, extraNewData, walletAccount }) {
|
|
449
|
+
if (!this.#pendingTokenBalancePatches[walletAccount]) {
|
|
450
|
+
this.#pendingTokenBalancePatches[walletAccount] = []
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.#pendingTokenBalancePatches[walletAccount].push({ tokenPatch, extraNewData })
|
|
454
|
+
this.#ensureBatch(walletAccount)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async #flushPendingTokenBalancePatches(walletAccount) {
|
|
458
|
+
const queue = this.#pendingTokenBalancePatches[walletAccount]
|
|
459
|
+
if (!queue?.length) return
|
|
460
|
+
|
|
461
|
+
const snapshot = [...queue]
|
|
462
|
+
try {
|
|
463
|
+
await flushTokenBalancePatchesQueue(this.aci, {
|
|
464
|
+
assetName: this.asset.name,
|
|
465
|
+
walletAccount,
|
|
466
|
+
patches: snapshot,
|
|
467
|
+
})
|
|
468
|
+
this.#pendingTokenBalancePatches[walletAccount] = []
|
|
469
|
+
} catch (e) {
|
|
470
|
+
const remaining = e.remainingPatches
|
|
471
|
+
this.#pendingTokenBalancePatches[walletAccount] = Array.isArray(remaining)
|
|
472
|
+
? remaining
|
|
473
|
+
: snapshot
|
|
474
|
+
throw e
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
432
478
|
async #updateStateBatch({ newData, walletAccount }) {
|
|
433
479
|
const assetName = this.asset.name
|
|
434
480
|
const batch = this.#ensureBatch(walletAccount)
|