@eth-optimism/actions-sdk 0.6.0 → 0.7.0

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.
Files changed (88) hide show
  1. package/dist/actions/lend/core/LendProvider.js +2 -2
  2. package/dist/actions/lend/core/LendProvider.js.map +1 -1
  3. package/dist/actions/shared/morpho/contracts.d.ts +1 -1
  4. package/dist/actions/shared/morpho/contracts.js +1 -1
  5. package/dist/actions/swap/core/SwapProvider.d.ts.map +1 -1
  6. package/dist/actions/swap/core/SwapProvider.js +4 -4
  7. package/dist/actions/swap/core/SwapProvider.js.map +1 -1
  8. package/dist/actions/swap/namespaces/WalletSwapNamespace.d.ts.map +1 -1
  9. package/dist/actions/swap/namespaces/WalletSwapNamespace.js +5 -2
  10. package/dist/actions/swap/namespaces/WalletSwapNamespace.js.map +1 -1
  11. package/dist/constants/supportedChains.d.ts +5790 -2
  12. package/dist/constants/supportedChains.d.ts.map +1 -1
  13. package/dist/constants/supportedChains.js +66 -20
  14. package/dist/constants/supportedChains.js.map +1 -1
  15. package/dist/core/error/errors.d.ts +24 -0
  16. package/dist/core/error/errors.d.ts.map +1 -1
  17. package/dist/core/error/errors.js +28 -0
  18. package/dist/core/error/errors.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/services/ChainManager.d.ts.map +1 -1
  24. package/dist/services/ChainManager.js +16 -0
  25. package/dist/services/ChainManager.js.map +1 -1
  26. package/dist/types/actions.d.ts +12 -7
  27. package/dist/types/actions.d.ts.map +1 -1
  28. package/dist/types/actions.js +22 -1
  29. package/dist/types/actions.js.map +1 -1
  30. package/dist/types/providers.d.ts +20 -6
  31. package/dist/types/providers.d.ts.map +1 -1
  32. package/dist/types/providers.js +12 -1
  33. package/dist/types/providers.js.map +1 -1
  34. package/dist/utils/__tests__/lendConfig.test.d.ts +2 -0
  35. package/dist/utils/__tests__/lendConfig.test.d.ts.map +1 -0
  36. package/dist/utils/__tests__/lendConfig.test.js +44 -0
  37. package/dist/utils/__tests__/lendConfig.test.js.map +1 -0
  38. package/dist/utils/lendConfig.d.ts +9 -0
  39. package/dist/utils/lendConfig.d.ts.map +1 -0
  40. package/dist/utils/lendConfig.js +12 -0
  41. package/dist/utils/lendConfig.js.map +1 -0
  42. package/dist/wallet/core/wallets/abstract/Wallet.d.ts +9 -0
  43. package/dist/wallet/core/wallets/abstract/Wallet.d.ts.map +1 -1
  44. package/dist/wallet/core/wallets/abstract/Wallet.js +11 -0
  45. package/dist/wallet/core/wallets/abstract/Wallet.js.map +1 -1
  46. package/dist/wallet/core/wallets/abstract/__tests__/Wallet.spec.js +13 -0
  47. package/dist/wallet/core/wallets/abstract/__tests__/Wallet.spec.js.map +1 -1
  48. package/dist/wallet/core/wallets/eoa/EOAWallet.d.ts +12 -4
  49. package/dist/wallet/core/wallets/eoa/EOAWallet.d.ts.map +1 -1
  50. package/dist/wallet/core/wallets/eoa/EOAWallet.js +17 -12
  51. package/dist/wallet/core/wallets/eoa/EOAWallet.js.map +1 -1
  52. package/dist/wallet/core/wallets/eoa/__tests__/EOAWallet.spec.js +37 -20
  53. package/dist/wallet/core/wallets/eoa/__tests__/EOAWallet.spec.js.map +1 -1
  54. package/dist/wallet/node/wallets/hosted/privy/__tests__/PrivyWallet.spec.js +4 -1
  55. package/dist/wallet/node/wallets/hosted/privy/__tests__/PrivyWallet.spec.js.map +1 -1
  56. package/dist/wallet/node/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.js +2 -1
  57. package/dist/wallet/node/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.js.map +1 -1
  58. package/dist/wallet/node/wallets/local/__tests__/LocalWallet.spec.js +2 -1
  59. package/dist/wallet/node/wallets/local/__tests__/LocalWallet.spec.js.map +1 -1
  60. package/dist/wallet/react/wallets/hosted/dynamic/__tests__/DynamicWallet.spec.js +2 -1
  61. package/dist/wallet/react/wallets/hosted/dynamic/__tests__/DynamicWallet.spec.js.map +1 -1
  62. package/dist/wallet/react/wallets/hosted/privy/__tests__/PrivyWallet.spec.js +2 -1
  63. package/dist/wallet/react/wallets/hosted/privy/__tests__/PrivyWallet.spec.js.map +1 -1
  64. package/dist/wallet/react/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.js +2 -1
  65. package/dist/wallet/react/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/actions/lend/core/LendProvider.ts +2 -2
  68. package/src/actions/shared/morpho/contracts.ts +1 -1
  69. package/src/actions/swap/core/SwapProvider.ts +4 -5
  70. package/src/actions/swap/namespaces/WalletSwapNamespace.ts +5 -4
  71. package/src/constants/supportedChains.ts +81 -21
  72. package/src/core/error/errors.ts +37 -0
  73. package/src/index.ts +13 -1
  74. package/src/services/ChainManager.ts +19 -0
  75. package/src/types/actions.ts +25 -10
  76. package/src/types/providers.ts +24 -6
  77. package/src/utils/__tests__/lendConfig.test.ts +52 -0
  78. package/src/utils/lendConfig.ts +17 -0
  79. package/src/wallet/core/wallets/abstract/Wallet.ts +12 -0
  80. package/src/wallet/core/wallets/abstract/__tests__/Wallet.spec.ts +15 -0
  81. package/src/wallet/core/wallets/eoa/EOAWallet.ts +17 -12
  82. package/src/wallet/core/wallets/eoa/__tests__/EOAWallet.spec.ts +43 -24
  83. package/src/wallet/node/wallets/hosted/privy/__tests__/PrivyWallet.spec.ts +4 -1
  84. package/src/wallet/node/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.ts +2 -1
  85. package/src/wallet/node/wallets/local/__tests__/LocalWallet.spec.ts +2 -1
  86. package/src/wallet/react/wallets/hosted/dynamic/__tests__/DynamicWallet.spec.ts +2 -1
  87. package/src/wallet/react/wallets/hosted/privy/__tests__/PrivyWallet.spec.ts +2 -1
  88. package/src/wallet/react/wallets/hosted/turnkey/__tests__/TurnkeyWallet.spec.ts +2 -1
@@ -78,7 +78,7 @@ export function getMorphoContracts(
78
78
  /**
79
79
  * Get all chain IDs where Morpho contracts are deployed.
80
80
  * Returns chains present in the local contracts registry.
81
- * Filtering against ACTIONS_SUPPORTED_CHAIN_IDS and developer-configured chains
81
+ * Filtering against SUPPORTED_CHAIN_IDS and developer-configured chains
82
82
  * is handled by the LendProvider base class.
83
83
  */
84
84
  export function getSupportedChainIds(): number[] {
@@ -3,11 +3,12 @@ import { formatUnits } from 'viem'
3
3
 
4
4
  import { UNIVERSAL_ROUTER_MSG_SENDER } from '@/actions/swap/core/markets.js'
5
5
  import type { SupportedChainId } from '@/constants/supportedChains.js'
6
- import { ACTIONS_SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js'
6
+ import { SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js'
7
7
  import {
8
8
  MarketNotAllowedError,
9
9
  ProviderNotConfiguredError,
10
10
  QuoteExpiredError,
11
+ QuoteRecipientMissingError,
11
12
  } from '@/core/error/errors.js'
12
13
  import type { ChainManager } from '@/services/ChainManager.js'
13
14
  import type {
@@ -221,7 +222,7 @@ export abstract class SwapProvider<
221
222
  const configuredChains = this.chainManager.getSupportedChains()
222
223
  return this.protocolSupportedChainIds().filter(
223
224
  (id) =>
224
- (ACTIONS_SUPPORTED_CHAIN_IDS as readonly number[]).includes(id) &&
225
+ (SUPPORTED_CHAIN_IDS as readonly number[]).includes(id) &&
225
226
  configuredChains.includes(id),
226
227
  )
227
228
  }
@@ -436,9 +437,7 @@ export abstract class SwapProvider<
436
437
  quote: SwapQuote,
437
438
  ): Promise<SwapTransaction> {
438
439
  if (!quote.recipient) {
439
- throw new Error(
440
- 'SwapQuote.recipient missing — _getQuote must populate it',
441
- )
440
+ throw new QuoteRecipientMissingError()
442
441
  }
443
442
  const approvals = await this._buildApprovals(quote)
444
443
 
@@ -3,6 +3,7 @@ import { isAddressEqual } from 'viem'
3
3
  import { QUOTE_DISCRIMINATOR } from '@/actions/swap/core/SwapProvider.js'
4
4
  import { BaseSwapNamespace } from '@/actions/swap/namespaces/BaseSwapNamespace.js'
5
5
  import type { SupportedChainId } from '@/constants/supportedChains.js'
6
+ import { QuoteRecipientMismatchError } from '@/core/error/errors.js'
6
7
  import type { SwapExecuteParamsResolved } from '@/services/nameservices/ens/types.js'
7
8
  import type { RecipientResolver } from '@/services/nameservices/ens/utils.js'
8
9
  import type { SwapSettings } from '@/types/actions.js'
@@ -91,10 +92,10 @@ export class WalletSwapNamespace extends BaseSwapNamespace {
91
92
  */
92
93
  private requireQuoteForThisWallet(quote: SwapQuote): SwapQuote {
93
94
  if (!isAddressEqual(quote.recipient, this.wallet.address)) {
94
- throw new Error(
95
- `SwapQuote was generated for a different recipient (${quote.recipient}). ` +
96
- `Re-quote via wallet.swap.getQuote(...) so calldata is bound to this wallet (${this.wallet.address}).`,
97
- )
95
+ throw new QuoteRecipientMismatchError({
96
+ quoteRecipient: quote.recipient,
97
+ walletAddress: this.wallet.address,
98
+ })
98
99
  }
99
100
  return quote
100
101
  }
@@ -20,26 +20,86 @@ import {
20
20
  worldchain,
21
21
  } from 'viem/chains'
22
22
 
23
- export const ACTIONS_SUPPORTED_CHAIN_IDS = [
24
- mainnet.id,
25
- sepolia.id,
26
- optimism.id,
27
- optimismSepolia.id,
28
- base.id,
29
- baseSepolia.id,
30
- unichain.id,
31
- unichainSepolia.id,
32
- worldchain.id,
33
- bob.id,
34
- celo.id,
35
- fraxtal.id,
36
- ink.id,
37
- lisk.id,
38
- metalL2.id,
39
- mode.id,
40
- soneium.id,
41
- superseed.id,
42
- swellchain.id,
23
+ const slug = (name: string): string => name.toLowerCase().replace(/\s+/g, '-')
24
+
25
+ /**
26
+ * Single source of truth for the chains the SDK supports. All other
27
+ * chain-related constants in this module are derived from this list.
28
+ */
29
+ const SUPPORTED_CHAINS = [
30
+ mainnet,
31
+ sepolia,
32
+ optimism,
33
+ optimismSepolia,
34
+ base,
35
+ baseSepolia,
36
+ unichain,
37
+ unichainSepolia,
38
+ worldchain,
39
+ bob,
40
+ celo,
41
+ fraxtal,
42
+ ink,
43
+ lisk,
44
+ metalL2,
45
+ mode,
46
+ soneium,
47
+ superseed,
48
+ swellchain,
43
49
  ] as const
44
50
 
45
- export type SupportedChainId = (typeof ACTIONS_SUPPORTED_CHAIN_IDS)[number]
51
+ export type SupportedChainId = (typeof SUPPORTED_CHAINS)[number]['id']
52
+
53
+ export const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map(
54
+ (c) => c.id,
55
+ ) as readonly SupportedChainId[]
56
+
57
+ /**
58
+ * Extra names that resolve to a chain in addition to its canonical
59
+ * `slug(chain.name)`. Add entries here only when there's a shorter or
60
+ * more familiar name worth accepting alongside the viem one.
61
+ */
62
+ const EXTRA_CHAIN_ALIASES: Partial<
63
+ Record<SupportedChainId, readonly string[]>
64
+ > = {
65
+ [mainnet.id]: ['mainnet'], // viem: 'ethereum'
66
+ [optimism.id]: ['optimism'], // viem: 'op-mainnet'
67
+ [worldchain.id]: ['worldchain'], // viem: 'world-chain'
68
+ [metalL2.id]: ['metal'], // viem: 'metal-l2'
69
+ [mode.id]: ['mode'], // viem: 'mode-mainnet'
70
+ [soneium.id]: ['soneium'], // viem: 'soneium-mainnet'
71
+ [swellchain.id]: ['swell'], // viem: 'swellchain'
72
+ }
73
+
74
+ /**
75
+ * Canonical CLI / human-friendly shortname for each supported chain:
76
+ * `slug(chain.name)` (e.g. `base-sepolia`, `ethereum`, `op-mainnet`).
77
+ * Use this for display. For input parsing prefer `chainIdFromShortname`,
78
+ * which also accepts entries from `EXTRA_CHAIN_ALIASES`.
79
+ */
80
+ export const SUPPORTED_CHAIN_SHORTNAMES: Record<SupportedChainId, string> =
81
+ Object.fromEntries(
82
+ SUPPORTED_CHAINS.map((chain) => [chain.id, slug(chain.name)]),
83
+ ) as Record<SupportedChainId, string>
84
+
85
+ const NAME_TO_ID: Record<string, SupportedChainId> = (() => {
86
+ const index: Record<string, SupportedChainId> = {}
87
+ for (const chain of SUPPORTED_CHAINS) {
88
+ const id = chain.id as SupportedChainId
89
+ index[SUPPORTED_CHAIN_SHORTNAMES[id]] = id
90
+ for (const alias of EXTRA_CHAIN_ALIASES[id] ?? []) index[alias] = id
91
+ }
92
+ return index
93
+ })()
94
+
95
+ /**
96
+ * Resolve a user-typed chain name to a `SupportedChainId`. Accepts both
97
+ * the canonical shortname (`mainnet`, `optimism`) and the viem `chain.name`
98
+ * slug (`ethereum`, `op-mainnet`). Case-insensitive. Returns `undefined`
99
+ * for unknown names.
100
+ */
101
+ export function chainIdFromShortname(
102
+ name: string,
103
+ ): SupportedChainId | undefined {
104
+ return NAME_TO_ID[name.toLowerCase()]
105
+ }
@@ -285,3 +285,40 @@ export class InvalidParamsError extends ActionsError {
285
285
  this.received = params.received
286
286
  }
287
287
  }
288
+
289
+ // ─────────────────────────────────────────────────────────────────────────────
290
+ // Swap Quote
291
+ // ─────────────────────────────────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Thrown when a pre-built `SwapQuote` is dispatched against a wallet whose
295
+ * address differs from the quote's `recipient`. Some routers (Velodrome
296
+ * v2/leaf) encode the recipient directly into calldata, so silently swapping
297
+ * recipients would route output tokens to the wrong address.
298
+ */
299
+ export class QuoteRecipientMismatchError extends ActionsError {
300
+ override name = 'QuoteRecipientMismatchError' as const
301
+ quoteRecipient: string
302
+ walletAddress: string
303
+
304
+ constructor(params: { quoteRecipient: string; walletAddress: string }) {
305
+ super(
306
+ `SwapQuote was generated for a different recipient (${params.quoteRecipient}); re-quote via wallet.swap.getQuote(...) so calldata is bound to this wallet (${params.walletAddress})`,
307
+ )
308
+ this.quoteRecipient = params.quoteRecipient
309
+ this.walletAddress = params.walletAddress
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Thrown when a provider's `_getQuote` returns a `SwapQuote` without a
315
+ * `recipient`. The base namespace requires every quote to be wallet-bound
316
+ * before approvals or calldata are built.
317
+ */
318
+ export class QuoteRecipientMissingError extends ActionsError {
319
+ override name = 'QuoteRecipientMissingError' as const
320
+
321
+ constructor() {
322
+ super('SwapQuote.recipient missing — _getQuote must populate it')
323
+ }
324
+ }
package/src/index.ts CHANGED
@@ -58,7 +58,9 @@ export {
58
58
  WSTETH,
59
59
  } from '@/constants/assets.js'
60
60
  export {
61
- ACTIONS_SUPPORTED_CHAIN_IDS,
61
+ chainIdFromShortname,
62
+ SUPPORTED_CHAIN_IDS,
63
+ SUPPORTED_CHAIN_SHORTNAMES,
62
64
  type SupportedChainId,
63
65
  } from '@/constants/supportedChains.js'
64
66
  export * from '@/core/error/errors.js'
@@ -77,9 +79,11 @@ export {
77
79
  } from '@/services/nameservices/ens/utils.js'
78
80
  export type {
79
81
  ActionsConfig,
82
+ ApprovalMode,
80
83
  ApyBreakdown,
81
84
  Asset,
82
85
  EOATransactionReceipt,
86
+ LendAction,
83
87
  LendConfig,
84
88
  LendMarket,
85
89
  LendMarketConfig,
@@ -88,6 +92,7 @@ export type {
88
92
  LendMarketPosition,
89
93
  LendMarketSupply,
90
94
  LendProviderConfig,
95
+ LendProviderName,
91
96
  LendTransaction,
92
97
  LendTransactionReceipt,
93
98
  SwapConfig,
@@ -112,7 +117,14 @@ export type {
112
117
  WalletConfig,
113
118
  WalletSwapParams,
114
119
  } from '@/types/index.js'
120
+ export {
121
+ APPROVAL_MODES,
122
+ LEND_ACTIONS,
123
+ LEND_PROVIDER_NAMES,
124
+ SWAP_PROVIDER_NAMES,
125
+ } from '@/types/index.js'
115
126
  export { getAssetAddress, isAssetSupportedOnChain } from '@/utils/assets.js'
127
+ export { getLendMarketAllowlist } from '@/utils/lendConfig.js'
116
128
  export { serializeBigInt } from '@/utils/serializers.js'
117
129
  export * from '@/wallet/core/error/errors.js'
118
130
  export { Wallet } from '@/wallet/core/wallets/abstract/Wallet.js'
@@ -11,11 +11,28 @@ import {
11
11
  } from 'viem'
12
12
  import type { BundlerClient, SmartAccount } from 'viem/account-abstraction'
13
13
  import { createBundlerClient } from 'viem/account-abstraction'
14
+ import { mainnet, sepolia } from 'viem/chains'
14
15
 
15
16
  import type { SupportedChainId } from '@/constants/supportedChains.js'
16
17
  import { ChainNotSupportedError } from '@/core/error/errors.js'
17
18
  import type { ChainConfig } from '@/types/chain.js'
18
19
 
20
+ /** viem `pollingInterval` (ms) for L2-class chains with ~1-2s blocks. */
21
+ const L2_POLLING_INTERVAL_MS = 1000
22
+ /** viem `pollingInterval` (ms) for L1-class chains with ~12s blocks. */
23
+ const L1_POLLING_INTERVAL_MS = 4000
24
+
25
+ const L1_CHAIN_IDS: ReadonlySet<SupportedChainId> = new Set([
26
+ mainnet.id,
27
+ sepolia.id,
28
+ ])
29
+
30
+ function pollingIntervalForChain(chainId: SupportedChainId): number {
31
+ return L1_CHAIN_IDS.has(chainId)
32
+ ? L1_POLLING_INTERVAL_MS
33
+ : L2_POLLING_INTERVAL_MS
34
+ }
35
+
19
36
  /**
20
37
  * Chain Manager Service
21
38
  * @description Manages public clients and chain infrastructure for the Verbs SDK.
@@ -99,6 +116,7 @@ export class ChainManager {
99
116
  const client = createPublicClient({
100
117
  chain: this.getChain(chainId),
101
118
  transport: http(bundlerUrl),
119
+ pollingInterval: pollingIntervalForChain(chainId),
102
120
  })
103
121
  return createBundlerClient({
104
122
  account,
@@ -191,6 +209,7 @@ export class ChainManager {
191
209
  const client = createPublicClient({
192
210
  chain,
193
211
  transport: this.getTransportForChain(chainConfig.chainId),
212
+ pollingInterval: pollingIntervalForChain(chainConfig.chainId),
194
213
  })
195
214
 
196
215
  clients.set(chainConfig.chainId, client)
@@ -4,14 +4,24 @@ import type { ChainManager } from '@/services/ChainManager.js'
4
4
  import type { Asset } from '@/types/asset.js'
5
5
  import type { ChainConfig } from '@/types/chain.js'
6
6
  import type { LendProviderConfig } from '@/types/lend/index.js'
7
- import type { LendProviders, SwapProviders } from '@/types/providers.js'
7
+ import type {
8
+ LendProviders,
9
+ SwapProviderName,
10
+ SwapProviders,
11
+ } from '@/types/providers.js'
8
12
  import type { SwapProviderConfig } from '@/types/swap/index.js'
9
13
  import type { ProviderSpec } from '@/wallet/core/providers/hosted/types/index.js'
10
14
 
11
15
  // Re-export provider configs for convenience
12
16
  export type { LendProviderConfig, SwapProviderConfig }
13
- // Re-export centralized provider maps
14
- export type { LendProviders, SwapProviders } from '@/types/providers.js'
17
+ // Re-export centralized provider maps and constants
18
+ export type {
19
+ LendProviderName,
20
+ LendProviders,
21
+ SwapProviderName,
22
+ SwapProviders,
23
+ } from '@/types/providers.js'
24
+ export { LEND_PROVIDER_NAMES, SWAP_PROVIDER_NAMES } from '@/types/providers.js'
15
25
 
16
26
  /** Require at least one property to be defined */
17
27
  type RequireAtLeastOne<T> = {
@@ -42,12 +52,6 @@ export type LendConfig = RequireAtLeastOne<{
42
52
  settings?: LendSettings
43
53
  }
44
54
 
45
- /** Names of available swap providers — derived from SwapProviders registry */
46
- export type SwapProviderName = keyof SwapProviders
47
-
48
- /** Names of available lend providers — derived from LendProviders registry */
49
- export type LendProviderName = keyof LendProviders
50
-
51
55
  /** Routing strategy for selecting a provider when multiple are configured. */
52
56
  export type SwapRoutingStrategy = 'price'
53
57
 
@@ -151,7 +155,18 @@ export interface ActionsContext {
151
155
  * Default is `"exact"` for safety. Demo / dogfood configs typically opt into
152
156
  * `"max"` to avoid an extra approval tx per swap or supply.
153
157
  */
154
- export type ApprovalMode = 'exact' | 'max'
158
+ export const APPROVAL_MODES = ['exact', 'max'] as const
159
+
160
+ export type ApprovalMode = (typeof APPROVAL_MODES)[number]
161
+
162
+ /**
163
+ * The lend write actions exposed by the SDK's wallet namespace
164
+ * (`openPosition` / `closePosition`). Useful for callers that emit
165
+ * action-tagged output envelopes or branch on the action being performed.
166
+ */
167
+ export const LEND_ACTIONS = ['open', 'close'] as const
168
+
169
+ export type LendAction = (typeof LEND_ACTIONS)[number]
155
170
 
156
171
  /**
157
172
  * Actions SDK configuration
@@ -3,20 +3,38 @@ import type { SwapProvider } from '@/actions/swap/core/SwapProvider.js'
3
3
  import type { LendProviderConfig } from '@/types/lend/index.js'
4
4
  import type { SwapProviderConfig } from '@/types/swap/index.js'
5
5
 
6
+ /**
7
+ * Runtime list of lend provider names. Source of truth for both the
8
+ * `LendProviderName` type union and any consumer (CLI, validators) that
9
+ * needs to enumerate provider names at runtime.
10
+ */
11
+ export const LEND_PROVIDER_NAMES = ['morpho', 'aave'] as const
12
+
13
+ /** Names of available lend providers. */
14
+ export type LendProviderName = (typeof LEND_PROVIDER_NAMES)[number]
15
+
6
16
  /**
7
17
  * Map of available lend providers keyed by provider name.
8
- * Add new lend providers here this is the single source of truth.
18
+ * Add new providers by extending `LEND_PROVIDER_NAMES`.
9
19
  */
10
20
  export type LendProviders = {
11
- morpho?: LendProvider<LendProviderConfig>
12
- aave?: LendProvider<LendProviderConfig>
21
+ [K in LendProviderName]?: LendProvider<LendProviderConfig>
13
22
  }
14
23
 
24
+ /**
25
+ * Runtime list of swap provider names. Source of truth for both the
26
+ * `SwapProviderName` type union and any consumer (CLI, validators) that
27
+ * needs to enumerate provider names at runtime.
28
+ */
29
+ export const SWAP_PROVIDER_NAMES = ['uniswap', 'velodrome'] as const
30
+
31
+ /** Names of available swap providers. */
32
+ export type SwapProviderName = (typeof SWAP_PROVIDER_NAMES)[number]
33
+
15
34
  /**
16
35
  * Map of available swap providers keyed by provider name.
17
- * Add new swap providers here this is the single source of truth.
36
+ * Add new providers by extending `SWAP_PROVIDER_NAMES`.
18
37
  */
19
38
  export type SwapProviders = {
20
- uniswap?: SwapProvider<SwapProviderConfig>
21
- velodrome?: SwapProvider<SwapProviderConfig>
39
+ [K in SwapProviderName]?: SwapProvider<SwapProviderConfig>
22
40
  }
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { MockUSDCAsset } from '@/__mocks__/MockAssets.js'
4
+ import type { LendConfig } from '@/types/actions.js'
5
+ import type { LendMarketConfig } from '@/types/lend/base.js'
6
+ import { getLendMarketAllowlist } from '@/utils/lendConfig.js'
7
+
8
+ const morphoMarket: LendMarketConfig = {
9
+ address: '0x0000000000000000000000000000000000000001',
10
+ chainId: 130,
11
+ name: 'Morpho USDC',
12
+ asset: MockUSDCAsset,
13
+ lendProvider: 'morpho',
14
+ }
15
+
16
+ const aaveMarket: LendMarketConfig = {
17
+ address: '0x0000000000000000000000000000000000000002',
18
+ chainId: 130,
19
+ name: 'Aave USDC',
20
+ asset: MockUSDCAsset,
21
+ lendProvider: 'aave',
22
+ }
23
+
24
+ describe('getLendMarketAllowlist', () => {
25
+ it('returns empty list when lend config is undefined', () => {
26
+ expect(getLendMarketAllowlist(undefined)).toEqual([])
27
+ })
28
+
29
+ it('flattens allowlists across all providers', () => {
30
+ const lend: LendConfig = {
31
+ morpho: { marketAllowlist: [morphoMarket] },
32
+ aave: { marketAllowlist: [aaveMarket] },
33
+ }
34
+ expect(getLendMarketAllowlist(lend)).toEqual([morphoMarket, aaveMarket])
35
+ })
36
+
37
+ it('skips the settings sibling key', () => {
38
+ const lend: LendConfig = {
39
+ morpho: { marketAllowlist: [morphoMarket] },
40
+ settings: { approvalMode: 'max' },
41
+ }
42
+ expect(getLendMarketAllowlist(lend)).toEqual([morphoMarket])
43
+ })
44
+
45
+ it('returns empty list when no provider declares an allowlist', () => {
46
+ const lend: LendConfig = {
47
+ morpho: {},
48
+ settings: { approvalMode: 'exact' },
49
+ }
50
+ expect(getLendMarketAllowlist(lend)).toEqual([])
51
+ })
52
+ })
@@ -0,0 +1,17 @@
1
+ import type { LendConfig } from '@/types/actions.js'
2
+ import type { LendMarketConfig } from '@/types/lend/base.js'
3
+ import { LEND_PROVIDER_NAMES } from '@/types/providers.js'
4
+
5
+ /**
6
+ * Flatten every provider's `marketAllowlist` from a `LendConfig` into a single
7
+ * list. Returns an empty list when `lend` is undefined or no provider declares
8
+ * an allowlist.
9
+ */
10
+ export function getLendMarketAllowlist(
11
+ lend: LendConfig | undefined,
12
+ ): readonly LendMarketConfig[] {
13
+ if (!lend) return []
14
+ return LEND_PROVIDER_NAMES.flatMap(
15
+ (name) => lend[name]?.marketAllowlist ?? [],
16
+ )
17
+ }
@@ -84,6 +84,18 @@ export abstract class Wallet {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * Check whether a wallet namespace (`lend`, `swap`) is configured on this
89
+ * wallet. Useful for callers that branch on capability instead of catching
90
+ * a `TypeError` from `wallet.lend!.openPosition(...)` later. Returns `false`
91
+ * when the namespace is undefined (no providers were registered for it).
92
+ * @param namespace - Wallet namespace name to probe.
93
+ * @returns `true` when the namespace is configured.
94
+ */
95
+ has(namespace: 'lend' | 'swap'): boolean {
96
+ return this[namespace] !== undefined
97
+ }
98
+
87
99
  /**
88
100
  * Get asset balances across the requested chains (or all supported chains).
89
101
  * @description Fetches ETH and ERC20 token balances for this wallet. By default queries every chain returned by the SDK's `ChainManager`. Pass `options.chainIds` to restrict the query to a subset of those chains; each id is validated against the configured chains and an `InvalidParamsError` / `ChainNotSupportedError` is thrown for unusable input. Uses the configured supported assets from `ActionsConfig.assets` if provided.
@@ -125,4 +125,19 @@ describe('Wallet (base)', () => {
125
125
  expect(wallet.lend).toBeDefined()
126
126
  expect(wallet.lend).toEqual({})
127
127
  })
128
+
129
+ describe('has', () => {
130
+ it("returns false for a namespace that wasn't configured", () => {
131
+ const wallet = new TestWallet(chainManager, address, signer)
132
+ expect(wallet.has('lend')).toBe(false)
133
+ expect(wallet.has('swap')).toBe(false)
134
+ })
135
+
136
+ it('returns true once a namespace has been attached', () => {
137
+ const wallet = new TestWallet(chainManager, address, signer)
138
+ wallet.lend = {} as WalletLendNamespace
139
+ expect(wallet.has('lend')).toBe(true)
140
+ expect(wallet.has('swap')).toBe(false)
141
+ })
142
+ })
128
143
  })
@@ -5,7 +5,7 @@ import type {
5
5
  LocalAccount,
6
6
  WalletClient,
7
7
  } from 'viem'
8
- import { createWalletClient } from 'viem'
8
+ import { createWalletClient, nonceManager } from 'viem'
9
9
 
10
10
  import type { SupportedChainId } from '@/constants/supportedChains.js'
11
11
  import type { TransactionData } from '@/types/lend/index.js'
@@ -23,8 +23,10 @@ export abstract class EOAWallet extends Wallet {
23
23
  /**
24
24
  * Create a WalletClient for this EOA wallet.
25
25
  *
26
- * Creates a viem-compatible WalletClient configured with this wallet's account
27
- * and the specified chain. Supports fallback transport for multiple RPC URLs.
26
+ * Attaches viem's default `nonceManager` to the signer so back-to-back
27
+ * `sendTransaction` calls receive sequential nonces without re-fetching
28
+ * `eth_getTransactionCount('pending')` per tx. This avoids races on
29
+ * load-balanced RPCs where pending state lags by one block.
28
30
  * @param chainId - The chain ID to create the wallet client for
29
31
  * @returns Promise resolving to a WalletClient configured for the specified chain
30
32
  */
@@ -39,8 +41,11 @@ export abstract class EOAWallet extends Wallet {
39
41
  []
40
42
  >
41
43
  > {
44
+ const account: LocalAccount = this.signer.nonceManager
45
+ ? this.signer
46
+ : { ...this.signer, nonceManager }
42
47
  return createWalletClient({
43
- account: this.signer,
48
+ account,
44
49
  chain: this.chainManager.getChain(chainId),
45
50
  transport: this.chainManager.getTransportForChain(chainId),
46
51
  })
@@ -70,8 +75,14 @@ export abstract class EOAWallet extends Wallet {
70
75
  /**
71
76
  * Send multiple transactions sequentially from this EOA wallet.
72
77
  *
73
- * Executes transactions one at a time in order, waiting for 2 confirmations
74
- * between each to ensure nonce updates. Returns an array of receipts.
78
+ * Each transaction is awaited to inclusion (one confirmation) via `send()`
79
+ * before the next is signed. The `nonceManager` attached in `walletClient()`
80
+ * keeps nonces in sequence locally, so the wait does not need extra
81
+ * confirmations to guarantee nonce monotonicity.
82
+ *
83
+ * Note: this method assumes a sequencer-ordered chain (e.g. OP-stack L2s).
84
+ * On chains with deeper reorg risk, consider an additional confirmations
85
+ * pass at the call site.
75
86
  * @param transactionData - Array of transactions to send
76
87
  * @param chainId - Chain to send the transactions on
77
88
  * @returns Promise resolving to array of transaction receipts (one per transaction)
@@ -83,12 +94,6 @@ export abstract class EOAWallet extends Wallet {
83
94
  const receipts: EOATransactionReceipt[] = []
84
95
  for (const tx of transactionData) {
85
96
  const receipt = await this.send(tx, chainId)
86
- const publicClient = this.chainManager.getPublicClient(chainId)
87
- // wait an extra confirmation so give time for nonce to be updated
88
- await publicClient.waitForTransactionReceipt({
89
- hash: receipt.transactionHash,
90
- confirmations: 2,
91
- })
92
97
  receipts.push(receipt)
93
98
  }
94
99
  return receipts