accounts 0.19.0 → 0.19.1

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.
@@ -26,6 +26,7 @@ import { type Handler, from } from '../../Handler.js'
26
26
  import * as Kv from '../../Kv.js'
27
27
  import { cached } from '../kv.js'
28
28
  import * as Tokenlist from '../tokenlist.js'
29
+ import * as FeeLiquidity from './feeLiquidity.js'
29
30
  import * as Sponsorship from './sponsorship.js'
30
31
  import * as Utils from './utils.js'
31
32
 
@@ -210,6 +211,72 @@ export function relay(options: relay.Options = {}): Handler {
210
211
  ),
211
212
  ]
212
213
 
214
+ async function fillUnsponsored() {
215
+ const excluded = new Set<string>()
216
+ const balances = new Map<string, Promise<bigint>>()
217
+ const liquidity = new Map<string, ReturnType<typeof FeeLiquidity.prepare>>()
218
+ function prepareFeeToken(token: Address) {
219
+ const key = token.toLowerCase()
220
+ let pending = liquidity.get(key)
221
+ if (!pending) {
222
+ pending = FeeLiquidity.prepare(client, { token, kv })
223
+ liquidity.set(key, pending)
224
+ void pending.catch(() => {})
225
+ }
226
+ return pending
227
+ }
228
+ let liquidityError: Error | undefined
229
+ for (;;) {
230
+ const token = features.feeTokenResolution
231
+ ? await resolveFeeToken(client, {
232
+ account: from,
233
+ feeToken: requestFeeToken,
234
+ kv,
235
+ tokens: unsponsoredTokens,
236
+ excluded,
237
+ balances,
238
+ prepareFeeToken,
239
+ })
240
+ : requestFeeToken
241
+ if (!token && liquidityError) throw liquidityError
242
+ try {
243
+ const result = await fill(client, {
244
+ autoSwap,
245
+ feeToken: token,
246
+ kv,
247
+ resolveFeeToken: resolveFeeTokenForSwap,
248
+ transaction: { ...normalized, chainId, ...(token ? { feeToken: token } : {}) },
249
+ })
250
+ // Estimation can succeed even when the fee AMM cannot settle
251
+ // the fee. Check the filled maximum fee before asking for a signature.
252
+ if (token && !requestFeeToken && features.feeTokenResolution) {
253
+ const { gas, maxFeePerGas } = result.transaction
254
+ if (
255
+ gas &&
256
+ maxFeePerGas &&
257
+ !(await (
258
+ await prepareFeeToken(token)
259
+ )((gas * maxFeePerGas + 10n ** 12n - 1n) / 10n ** 12n))
260
+ )
261
+ throw new Error('Insufficient liquidity in FeeAMM pool for transaction fee.')
262
+ }
263
+ return { result, token }
264
+ } catch (error) {
265
+ // Never change an explicit fee-token choice or retry unrelated failures.
266
+ if (
267
+ !token ||
268
+ requestFeeToken ||
269
+ !features.feeTokenResolution ||
270
+ !(error instanceof Error) ||
271
+ !/insufficient liquidity in FeeAMM pool/i.test(error.message)
272
+ )
273
+ throw error
274
+ excluded.add(token.toLowerCase())
275
+ liquidityError = error
276
+ }
277
+ }
278
+ }
279
+
213
280
  // When the app provides its own fee payer URL, route the fill
214
281
  // through that service so it can sign the transaction.
215
282
  const fillClient = externalFeePayerUrl
@@ -299,44 +366,16 @@ export function relay(options: relay.Options = {}): Handler {
299
366
  filled = fill_sponsored
300
367
  } else {
301
368
  // Sponsor rejected — resolve fee token and fill unsponsored.
302
- const feeToken_unsponsored = features.feeTokenResolution
303
- ? await resolveFeeToken(client, {
304
- account: from,
305
- feeToken: requestFeeToken,
306
- kv,
307
- tokens: unsponsoredTokens,
308
- })
309
- : requestFeeToken
310
- const tx_unsponsored = {
311
- ...baseTx,
312
- ...(feeToken_unsponsored ? { feeToken: feeToken_unsponsored } : {}),
313
- }
314
- filled = await fill(client, {
315
- ...options,
316
- feeToken: feeToken_unsponsored,
317
- transaction: tx_unsponsored,
318
- })
319
- feeToken = feeToken_unsponsored
369
+ const { result, token } = await fillUnsponsored()
370
+ filled = result
371
+ feeToken = token
320
372
  }
321
373
  }
322
374
  } else {
323
375
  // Path C: no sponsorship configured — resolve fee token, fill once.
324
- feeToken = features.feeTokenResolution
325
- ? await resolveFeeToken(client, {
326
- account: from,
327
- feeToken: requestFeeToken,
328
- kv,
329
- tokens: unsponsoredTokens,
330
- })
331
- : requestFeeToken
332
- const transaction = { ...baseTx, ...(feeToken ? { feeToken } : {}) }
333
- filled = await fill(client, {
334
- autoSwap,
335
- feeToken,
336
- kv,
337
- resolveFeeToken: resolveFeeTokenForSwap,
338
- transaction,
339
- })
376
+ const { result, token } = await fillUnsponsored()
377
+ filled = result
378
+ feeToken = token
340
379
  }
341
380
 
342
381
  const transaction_filled = filled.transaction
@@ -859,6 +898,20 @@ async function resolveFeeToken(
859
898
  const { feeToken, account, kv, tokens } = options
860
899
  if (feeToken) return feeToken
861
900
  if (!account) return undefined
901
+ const sender = account
902
+
903
+ function getBalance(token: Address) {
904
+ options.prepareFeeToken?.(token)
905
+ const key = token.toLowerCase()
906
+ const pending = options.balances?.get(key)
907
+ if (pending) return pending
908
+ const balance = Actions.token
909
+ .getBalance(client, { account: sender, token })
910
+ .then(({ amount }) => amount)
911
+ .catch(() => 0n)
912
+ options.balances?.set(key, balance)
913
+ return balance
914
+ }
862
915
 
863
916
  // Cache the user's preferred fee token for `userTokenCacheTtl` seconds.
864
917
  // The on-chain preference rarely changes; a short TTL avoids the
@@ -885,17 +938,14 @@ async function resolveFeeToken(
885
938
  ? Promise.all(
886
939
  tokens.map(async (token) => ({
887
940
  address: token,
888
- balance: await Actions.token
889
- .getBalance(client, { account, token })
890
- .then(({ amount }) => amount)
891
- .catch(() => 0n),
941
+ balance: await getBalance(token),
892
942
  })),
893
943
  )
894
944
  : [],
895
945
  ])
896
946
 
897
947
  // If on-chain preference is set and user has balance, use it.
898
- if (userToken) {
948
+ if (userToken && !options.excluded?.has(userToken.address.toLowerCase())) {
899
949
  const match = balances.find(
900
950
  (b) => b.address.toLowerCase() === userToken.address.toLowerCase() && b.balance > 0n,
901
951
  )
@@ -904,11 +954,7 @@ async function resolveFeeToken(
904
954
  // Token list may not include the preference — check on-chain directly.
905
955
  if (!match) {
906
956
  try {
907
- const balance = await Actions.token.getBalance(client, {
908
- account,
909
- token: userToken.address,
910
- })
911
- if (balance.amount > 0n) return userToken.address
957
+ if ((await getBalance(userToken.address)) > 0n) return userToken.address
912
958
  } catch {}
913
959
  }
914
960
  }
@@ -917,6 +963,7 @@ async function resolveFeeToken(
917
963
  let best: { address: Address; balance: bigint } | undefined
918
964
  for (const asset of balances) {
919
965
  if (asset.balance <= 0n) continue
966
+ if (options.excluded?.has(asset.address.toLowerCase())) continue
920
967
  if (!best || asset.balance > best.balance) best = asset
921
968
  }
922
969
  if (best) return best.address
@@ -928,6 +975,9 @@ declare namespace resolveFeeToken {
928
975
  account?: Address | undefined
929
976
  kv?: Kv.Kv | undefined
930
977
  tokens?: readonly Address[] | undefined
978
+ excluded?: Set<string> | undefined
979
+ balances?: Map<string, Promise<bigint>> | undefined
980
+ prepareFeeToken?: ((token: Address) => void) | undefined
931
981
  /** TTL in seconds for the cached `userTokens` lookup. @default 60 */
932
982
  userTokenCacheTtl?: number | undefined
933
983
  }