@eth-optimism/actions-sdk 0.4.0 → 0.5.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.
- package/dist/actions.d.ts +9 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +12 -1
- package/dist/actions.js.map +1 -1
- package/dist/ens/EnsNamespace.d.ts +57 -0
- package/dist/ens/EnsNamespace.d.ts.map +1 -0
- package/dist/ens/EnsNamespace.js +158 -0
- package/dist/ens/EnsNamespace.js.map +1 -0
- package/dist/ens/EnsNamespace.spec.d.ts +2 -0
- package/dist/ens/EnsNamespace.spec.d.ts.map +1 -0
- package/dist/ens/EnsNamespace.spec.js +144 -0
- package/dist/ens/EnsNamespace.spec.js.map +1 -0
- package/dist/ens/errors.d.ts +24 -0
- package/dist/ens/errors.d.ts.map +1 -0
- package/dist/ens/errors.js +35 -0
- package/dist/ens/errors.js.map +1 -0
- package/dist/ens/index.d.ts +4 -0
- package/dist/ens/index.d.ts.map +1 -0
- package/dist/ens/index.js +4 -0
- package/dist/ens/index.js.map +1 -0
- package/dist/ens/types.d.ts +63 -0
- package/dist/ens/types.d.ts.map +1 -0
- package/dist/ens/types.js +14 -0
- package/dist/ens/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ChainManager.d.ts +7 -0
- package/dist/services/ChainManager.d.ts.map +1 -1
- package/dist/services/ChainManager.js +9 -0
- package/dist/services/ChainManager.js.map +1 -1
- package/dist/services/__mocks__/MockChainManager.d.ts +1 -0
- package/dist/services/__mocks__/MockChainManager.d.ts.map +1 -1
- package/dist/services/__mocks__/MockChainManager.js +5 -0
- package/dist/services/__mocks__/MockChainManager.js.map +1 -1
- package/dist/swap/core/SwapProvider.d.ts +6 -6
- package/dist/swap/core/SwapProvider.d.ts.map +1 -1
- package/dist/swap/core/SwapProvider.js +4 -8
- package/dist/swap/core/SwapProvider.js.map +1 -1
- package/dist/swap/namespaces/BaseSwapNamespace.d.ts +3 -1
- package/dist/swap/namespaces/BaseSwapNamespace.d.ts.map +1 -1
- package/dist/swap/namespaces/BaseSwapNamespace.js +16 -11
- package/dist/swap/namespaces/BaseSwapNamespace.js.map +1 -1
- package/dist/swap/namespaces/WalletSwapNamespace.d.ts +2 -1
- package/dist/swap/namespaces/WalletSwapNamespace.d.ts.map +1 -1
- package/dist/swap/namespaces/WalletSwapNamespace.js +9 -4
- package/dist/swap/namespaces/WalletSwapNamespace.js.map +1 -1
- package/dist/swap/namespaces/__tests__/BaseSwapNamespace.spec.js +4 -4
- package/dist/swap/namespaces/__tests__/BaseSwapNamespace.spec.js.map +1 -1
- package/dist/swap/providers/uniswap/UniswapSwapProvider.d.ts +3 -2
- package/dist/swap/providers/uniswap/UniswapSwapProvider.d.ts.map +1 -1
- package/dist/swap/providers/uniswap/UniswapSwapProvider.js.map +1 -1
- package/dist/swap/providers/velodrome/VelodromeSwapProvider.d.ts +3 -2
- package/dist/swap/providers/velodrome/VelodromeSwapProvider.d.ts.map +1 -1
- package/dist/swap/providers/velodrome/VelodromeSwapProvider.js.map +1 -1
- package/dist/swap/providers/velodrome/__tests__/VelodromeSwapProvider.test.js +1 -0
- package/dist/swap/providers/velodrome/__tests__/VelodromeSwapProvider.test.js.map +1 -1
- package/dist/types/swap/base.d.ts +5 -4
- package/dist/types/swap/base.d.ts.map +1 -1
- package/dist/types/swap/base.js.map +1 -1
- package/dist/utils/ens.d.ts +25 -0
- package/dist/utils/ens.d.ts.map +1 -0
- package/dist/utils/ens.js +53 -0
- package/dist/utils/ens.js.map +1 -0
- package/dist/utils/ens.test.d.ts +2 -0
- package/dist/utils/ens.test.d.ts.map +1 -0
- package/dist/utils/ens.test.js +75 -0
- package/dist/utils/ens.test.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +10 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/wallet/core/wallets/abstract/Wallet.d.ts.map +1 -1
- package/dist/wallet/core/wallets/abstract/Wallet.js +3 -1
- package/dist/wallet/core/wallets/abstract/Wallet.js.map +1 -1
- package/package.json +1 -1
- package/src/actions.ts +15 -0
- package/src/ens/EnsNamespace.spec.ts +171 -0
- package/src/ens/EnsNamespace.ts +210 -0
- package/src/ens/errors.ts +45 -0
- package/src/ens/index.ts +12 -0
- package/src/ens/types.ts +76 -0
- package/src/index.ts +9 -0
- package/src/services/ChainManager.ts +10 -0
- package/src/services/__mocks__/MockChainManager.ts +8 -0
- package/src/swap/core/SwapProvider.ts +16 -15
- package/src/swap/namespaces/BaseSwapNamespace.ts +43 -27
- package/src/swap/namespaces/WalletSwapNamespace.ts +12 -4
- package/src/swap/namespaces/__tests__/BaseSwapNamespace.spec.ts +4 -0
- package/src/swap/providers/uniswap/UniswapSwapProvider.ts +4 -2
- package/src/swap/providers/velodrome/VelodromeSwapProvider.ts +4 -2
- package/src/swap/providers/velodrome/__tests__/VelodromeSwapProvider.test.ts +1 -0
- package/src/types/swap/base.ts +5 -4
- package/src/utils/ens.test.ts +104 -0
- package/src/utils/ens.ts +85 -0
- package/src/utils/validation.ts +11 -0
- package/src/wallet/core/wallets/abstract/Wallet.ts +3 -0
|
@@ -3,6 +3,10 @@ import { formatUnits } from 'viem'
|
|
|
3
3
|
|
|
4
4
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
5
5
|
import { ACTIONS_SUPPORTED_CHAIN_IDS } from '@/constants/supportedChains.js'
|
|
6
|
+
import type {
|
|
7
|
+
SwapExecuteParamsResolved,
|
|
8
|
+
SwapQuoteParamsResolved,
|
|
9
|
+
} from '@/ens/types.js'
|
|
6
10
|
import type { ChainManager } from '@/services/ChainManager.js'
|
|
7
11
|
import { UNIVERSAL_ROUTER_MSG_SENDER } from '@/swap/core/markets.js'
|
|
8
12
|
import type { SwapSettings } from '@/types/actions.js'
|
|
@@ -16,7 +20,6 @@ import type {
|
|
|
16
20
|
SwapMarketConfig,
|
|
17
21
|
SwapProviderConfig,
|
|
18
22
|
SwapQuote,
|
|
19
|
-
SwapQuoteParams,
|
|
20
23
|
SwapTransaction,
|
|
21
24
|
SwapTransactionData,
|
|
22
25
|
} from '@/types/swap/index.js'
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
validateNotBothAmounts,
|
|
41
44
|
validateNotSameAsset,
|
|
42
45
|
validateNotZeroAddress,
|
|
46
|
+
validateRecipient,
|
|
43
47
|
validateSlippage,
|
|
44
48
|
} from '@/utils/validation.js'
|
|
45
49
|
|
|
@@ -129,14 +133,15 @@ export abstract class SwapProvider<
|
|
|
129
133
|
* @returns Transaction data ready for wallet execution
|
|
130
134
|
*/
|
|
131
135
|
async execute(
|
|
132
|
-
params:
|
|
136
|
+
params: SwapExecuteParamsResolved | SwapQuote,
|
|
133
137
|
): Promise<SwapTransaction> {
|
|
134
|
-
this.validateSwapExecute(params)
|
|
135
|
-
|
|
136
138
|
if (QUOTE_DISCRIMINATOR in params) {
|
|
139
|
+
this.validateSwapExecute(params)
|
|
137
140
|
return this.executeFromQuote(params)
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
this.validateSwapExecute(params)
|
|
144
|
+
|
|
140
145
|
// Raw params only
|
|
141
146
|
validateNotBothAmounts(params.amountIn, params.amountOut)
|
|
142
147
|
validateNotZeroAddress(params.walletAddress, 'walletAddress')
|
|
@@ -149,7 +154,7 @@ export abstract class SwapProvider<
|
|
|
149
154
|
* @param params - Quote parameters (assets, amounts, chain, slippage)
|
|
150
155
|
* @returns SwapQuote with pricing, amounts, and pre-encoded calldata
|
|
151
156
|
*/
|
|
152
|
-
async getQuote(params:
|
|
157
|
+
async getQuote(params: SwapQuoteParamsResolved): Promise<SwapQuote> {
|
|
153
158
|
validateChainSupported(params.chainId, this.supportedChainIds())
|
|
154
159
|
return this._getQuote(params)
|
|
155
160
|
}
|
|
@@ -267,7 +272,7 @@ export abstract class SwapProvider<
|
|
|
267
272
|
* @param params - Raw quote params from the user
|
|
268
273
|
* @returns Resolved slippage, deadline, recipient, amountInRaw, and current timestamp
|
|
269
274
|
*/
|
|
270
|
-
protected resolveQuoteDefaults(params:
|
|
275
|
+
protected resolveQuoteDefaults(params: SwapQuoteParamsResolved) {
|
|
271
276
|
const slippage = params.slippage ?? this.defaultSlippage
|
|
272
277
|
const now = Math.floor(Date.now() / 1000)
|
|
273
278
|
const deadline = params.deadline ?? now + this.quoteExpirationSeconds
|
|
@@ -449,13 +454,7 @@ export abstract class SwapProvider<
|
|
|
449
454
|
validateAmountPositiveIfExists(params.amountIn)
|
|
450
455
|
validateAmountPositiveIfExists(params.amountOut)
|
|
451
456
|
validateSlippage(params.slippage ?? this.defaultSlippage, this.maxSlippage)
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private validateRecipient(params: SwapExecuteParams | SwapQuote): void {
|
|
456
|
-
if ('recipient' in params && params.recipient) {
|
|
457
|
-
validateNotZeroAddress(params.recipient, 'recipient')
|
|
458
|
-
}
|
|
457
|
+
validateRecipient('recipient' in params ? params.recipient : undefined)
|
|
459
458
|
}
|
|
460
459
|
|
|
461
460
|
private validateQuoteExpiration(quote: SwapQuote): void {
|
|
@@ -467,7 +466,7 @@ export abstract class SwapProvider<
|
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
private resolveParams(params:
|
|
469
|
+
private resolveParams(params: SwapExecuteParamsResolved): ResolvedSwapParams {
|
|
471
470
|
return {
|
|
472
471
|
amountInRaw: parseAssetAmount(params.assetIn, params.amountIn),
|
|
473
472
|
amountOutRaw: parseAssetAmount(params.assetOut, params.amountOut),
|
|
@@ -555,7 +554,9 @@ export abstract class SwapProvider<
|
|
|
555
554
|
params: ResolvedSwapParams,
|
|
556
555
|
): Promise<SwapTransaction>
|
|
557
556
|
|
|
558
|
-
protected abstract _getQuote(
|
|
557
|
+
protected abstract _getQuote(
|
|
558
|
+
params: SwapQuoteParamsResolved,
|
|
559
|
+
): Promise<SwapQuote>
|
|
559
560
|
|
|
560
561
|
/**
|
|
561
562
|
* Build provider-specific approval transactions for a swap.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
2
|
+
import type { SwapQuoteParamsResolved } from '@/ens/types.js'
|
|
2
3
|
import type { SwapProvider } from '@/swap/core/SwapProvider.js'
|
|
3
4
|
import type { SwapProviderName, SwapSettings } from '@/types/actions.js'
|
|
4
5
|
import type { Asset } from '@/types/asset.js'
|
|
@@ -11,15 +12,21 @@ import type {
|
|
|
11
12
|
SwapQuote,
|
|
12
13
|
SwapQuoteParams,
|
|
13
14
|
} from '@/types/swap/index.js'
|
|
15
|
+
import { passthroughResolver, type RecipientResolver } from '@/utils/ens.js'
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Base swap namespace with shared read-only operations
|
|
17
19
|
*/
|
|
18
20
|
export abstract class BaseSwapNamespace {
|
|
21
|
+
protected readonly resolveRecipient: RecipientResolver
|
|
22
|
+
|
|
19
23
|
constructor(
|
|
20
24
|
protected readonly providers: SwapProviders,
|
|
25
|
+
resolveRecipient?: RecipientResolver,
|
|
21
26
|
protected readonly settings?: SwapSettings,
|
|
22
|
-
) {
|
|
27
|
+
) {
|
|
28
|
+
this.resolveRecipient = resolveRecipient ?? passthroughResolver
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
32
|
* Get a swap quote with pre-built execution data.
|
|
@@ -29,30 +36,31 @@ export abstract class BaseSwapNamespace {
|
|
|
29
36
|
* @returns The best available SwapQuote
|
|
30
37
|
*/
|
|
31
38
|
async getQuote(params: SwapQuoteParams): Promise<SwapQuote> {
|
|
39
|
+
const recipient = await this.resolveRecipient(params.recipient)
|
|
40
|
+
const resolved: SwapQuoteParamsResolved = { ...params, recipient }
|
|
41
|
+
|
|
32
42
|
// Explicit provider — skip routing
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
return provider.getQuote(params)
|
|
43
|
+
if (resolved.provider) {
|
|
44
|
+
return this.resolveProvider(
|
|
45
|
+
resolved.provider,
|
|
46
|
+
resolved.assetIn,
|
|
47
|
+
resolved.assetOut,
|
|
48
|
+
resolved.chainId,
|
|
49
|
+
).getQuote(resolved)
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
// Price routing — quote all eligible providers, return best
|
|
44
53
|
if (this.settings?.routing === 'price') {
|
|
45
|
-
return this.getBestQuote(
|
|
54
|
+
return this.getBestQuote(resolved)
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
// No routing — resolve single provider via fallback logic
|
|
49
|
-
|
|
58
|
+
return this.resolveProvider(
|
|
50
59
|
undefined,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
return provider.getQuote(params)
|
|
60
|
+
resolved.assetIn,
|
|
61
|
+
resolved.assetOut,
|
|
62
|
+
resolved.chainId,
|
|
63
|
+
).getQuote(resolved)
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|
|
@@ -61,7 +69,9 @@ export abstract class BaseSwapNamespace {
|
|
|
61
69
|
* @returns The quote with the highest amountOut
|
|
62
70
|
* @throws If no provider returns a valid quote
|
|
63
71
|
*/
|
|
64
|
-
private async getBestQuote(
|
|
72
|
+
private async getBestQuote(
|
|
73
|
+
params: SwapQuoteParamsResolved,
|
|
74
|
+
): Promise<SwapQuote> {
|
|
65
75
|
const quotes = await this.fetchAllQuotes(params)
|
|
66
76
|
|
|
67
77
|
let best: SwapQuote | null = null
|
|
@@ -86,7 +96,9 @@ export abstract class BaseSwapNamespace {
|
|
|
86
96
|
* @param params - Quote parameters
|
|
87
97
|
* @returns Array of successful quotes (may be empty if all providers fail)
|
|
88
98
|
*/
|
|
89
|
-
private async fetchAllQuotes(
|
|
99
|
+
private async fetchAllQuotes(
|
|
100
|
+
params: SwapQuoteParamsResolved,
|
|
101
|
+
): Promise<SwapQuote[]> {
|
|
90
102
|
const eligible = this.getAllProviders().filter((p) =>
|
|
91
103
|
p.isMarketSupported(params.assetIn, params.assetOut, params.chainId),
|
|
92
104
|
)
|
|
@@ -116,17 +128,21 @@ export abstract class BaseSwapNamespace {
|
|
|
116
128
|
* @returns Array of SwapQuotes sorted by amountOut descending (best first)
|
|
117
129
|
*/
|
|
118
130
|
async getQuotes(params: SwapQuoteParams): Promise<SwapQuote[]> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
const recipient = await this.resolveRecipient(params.recipient)
|
|
132
|
+
const resolved: SwapQuoteParamsResolved = { ...params, recipient }
|
|
133
|
+
|
|
134
|
+
if (resolved.provider) {
|
|
135
|
+
return [
|
|
136
|
+
await this.resolveProvider(
|
|
137
|
+
resolved.provider,
|
|
138
|
+
resolved.assetIn,
|
|
139
|
+
resolved.assetOut,
|
|
140
|
+
resolved.chainId,
|
|
141
|
+
).getQuote(resolved),
|
|
142
|
+
]
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
const quotes = await this.fetchAllQuotes(
|
|
145
|
+
const quotes = await this.fetchAllQuotes(resolved)
|
|
130
146
|
return quotes.sort((a, b) =>
|
|
131
147
|
a.amountOutRaw > b.amountOutRaw
|
|
132
148
|
? -1
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
2
|
+
import type { SwapExecuteParamsResolved } from '@/ens/types.js'
|
|
2
3
|
import { QUOTE_DISCRIMINATOR } from '@/swap/core/SwapProvider.js'
|
|
3
4
|
import { BaseSwapNamespace } from '@/swap/namespaces/BaseSwapNamespace.js'
|
|
4
5
|
import type { SwapSettings } from '@/types/actions.js'
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
SwapTransaction,
|
|
11
12
|
WalletSwapParams,
|
|
12
13
|
} from '@/types/swap/index.js'
|
|
14
|
+
import type { RecipientResolver } from '@/utils/ens.js'
|
|
13
15
|
import type { Wallet } from '@/wallet/core/wallets/abstract/Wallet.js'
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -20,9 +22,10 @@ export class WalletSwapNamespace extends BaseSwapNamespace {
|
|
|
20
22
|
constructor(
|
|
21
23
|
providers: SwapProviders,
|
|
22
24
|
private readonly wallet: Wallet,
|
|
25
|
+
resolveRecipient?: RecipientResolver,
|
|
23
26
|
settings?: SwapSettings,
|
|
24
27
|
) {
|
|
25
|
-
super(providers, settings)
|
|
28
|
+
super(providers, resolveRecipient, settings)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -54,11 +57,16 @@ export class WalletSwapNamespace extends BaseSwapNamespace {
|
|
|
54
57
|
*/
|
|
55
58
|
async execute(params: WalletSwapParams | SwapQuote): Promise<SwapReceipt> {
|
|
56
59
|
// Inject walletAddress — raw params need it for validation,
|
|
57
|
-
// quotes need it for on-chain allowance checks during approval building
|
|
58
|
-
|
|
60
|
+
// quotes need it for on-chain allowance checks during approval building.
|
|
61
|
+
// Resolve ENS recipient here so providers only ever receive an Address.
|
|
62
|
+
const executeParams: SwapExecuteParamsResolved | SwapQuote =
|
|
59
63
|
QUOTE_DISCRIMINATOR in params
|
|
60
64
|
? { ...params, recipient: this.wallet.address }
|
|
61
|
-
: {
|
|
65
|
+
: {
|
|
66
|
+
...params,
|
|
67
|
+
walletAddress: this.wallet.address,
|
|
68
|
+
recipient: await this.resolveRecipient(params.recipient),
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
const provider = this.resolveProvider(
|
|
64
72
|
params.provider,
|
|
@@ -67,6 +67,7 @@ describe('BaseSwapNamespace', () => {
|
|
|
67
67
|
|
|
68
68
|
const namespace = new ActionsSwapNamespace(
|
|
69
69
|
{ uniswap: cheapProvider, velodrome: expensiveProvider },
|
|
70
|
+
undefined,
|
|
70
71
|
{ routing: 'price' },
|
|
71
72
|
)
|
|
72
73
|
|
|
@@ -94,6 +95,7 @@ describe('BaseSwapNamespace', () => {
|
|
|
94
95
|
|
|
95
96
|
const namespace = new ActionsSwapNamespace(
|
|
96
97
|
{ uniswap: failingProvider, velodrome: workingProvider },
|
|
98
|
+
undefined,
|
|
97
99
|
{ routing: 'price' },
|
|
98
100
|
)
|
|
99
101
|
|
|
@@ -115,6 +117,7 @@ describe('BaseSwapNamespace', () => {
|
|
|
115
117
|
|
|
116
118
|
const namespace = new ActionsSwapNamespace(
|
|
117
119
|
{ uniswap: failingProvider },
|
|
120
|
+
undefined,
|
|
118
121
|
{ routing: 'price' },
|
|
119
122
|
)
|
|
120
123
|
|
|
@@ -140,6 +143,7 @@ describe('BaseSwapNamespace', () => {
|
|
|
140
143
|
|
|
141
144
|
const namespace = new ActionsSwapNamespace(
|
|
142
145
|
{ uniswap: cheapProvider, velodrome: expensiveProvider },
|
|
146
|
+
undefined,
|
|
143
147
|
{ routing: 'price' },
|
|
144
148
|
)
|
|
145
149
|
|
|
@@ -2,6 +2,7 @@ import { formatUnits } from 'viem'
|
|
|
2
2
|
|
|
3
3
|
import { UNISWAP } from '@/constants/providers.js'
|
|
4
4
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
5
|
+
import type { SwapQuoteParamsResolved } from '@/ens/types.js'
|
|
5
6
|
import { expandMarkets, findMarket } from '@/swap/core/markets.js'
|
|
6
7
|
import { SwapProvider } from '@/swap/core/SwapProvider.js'
|
|
7
8
|
import {
|
|
@@ -27,7 +28,6 @@ import type {
|
|
|
27
28
|
ResolvedSwapParams,
|
|
28
29
|
SwapMarket,
|
|
29
30
|
SwapQuote,
|
|
30
|
-
SwapQuoteParams,
|
|
31
31
|
SwapTransaction,
|
|
32
32
|
} from '@/types/swap/index.js'
|
|
33
33
|
import { isNativeAsset, parseAssetAmount } from '@/utils/assets.js'
|
|
@@ -84,7 +84,9 @@ export class UniswapSwapProvider extends SwapProvider<UniswapSwapProviderConfig>
|
|
|
84
84
|
)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
protected async _getQuote(
|
|
87
|
+
protected async _getQuote(
|
|
88
|
+
params: SwapQuoteParamsResolved,
|
|
89
|
+
): Promise<SwapQuote> {
|
|
88
90
|
const { chainId, assetIn, assetOut } = params
|
|
89
91
|
const addresses = getUniswapAddresses(chainId)
|
|
90
92
|
const publicClient = this.chainManager.getPublicClient(chainId)
|
|
@@ -2,6 +2,7 @@ import { formatUnits } from 'viem'
|
|
|
2
2
|
|
|
3
3
|
import { VELODROME } from '@/constants/providers.js'
|
|
4
4
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
5
|
+
import type { SwapQuoteParamsResolved } from '@/ens/types.js'
|
|
5
6
|
import { expandMarkets, findMarket } from '@/swap/core/markets.js'
|
|
6
7
|
import { SwapProvider } from '@/swap/core/SwapProvider.js'
|
|
7
8
|
import {
|
|
@@ -29,7 +30,6 @@ import type {
|
|
|
29
30
|
ResolvedSwapParams,
|
|
30
31
|
SwapMarket,
|
|
31
32
|
SwapQuote,
|
|
32
|
-
SwapQuoteParams,
|
|
33
33
|
SwapTransaction,
|
|
34
34
|
} from '@/types/swap/index.js'
|
|
35
35
|
import { getAssetAddress, isNativeAsset } from '@/utils/assets.js'
|
|
@@ -113,7 +113,9 @@ export class VelodromeSwapProvider extends SwapProvider<VelodromeSwapProviderCon
|
|
|
113
113
|
* @returns SwapQuote with amounts, price, route, and encoded calldata
|
|
114
114
|
* @throws If amountOut is provided (Velodrome only supports exact-input)
|
|
115
115
|
*/
|
|
116
|
-
protected async _getQuote(
|
|
116
|
+
protected async _getQuote(
|
|
117
|
+
params: SwapQuoteParamsResolved,
|
|
118
|
+
): Promise<SwapQuote> {
|
|
117
119
|
const { chainId, assetIn, assetOut } = params
|
|
118
120
|
|
|
119
121
|
if (params.amountOut !== undefined) {
|
|
@@ -34,6 +34,7 @@ function createMockChainManager(): ChainManager {
|
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
getPublicClient: vi.fn().mockReturnValue(mockPublicClient),
|
|
37
|
+
tryGetPublicClient: vi.fn().mockReturnValue(undefined),
|
|
37
38
|
getSupportedChains: vi.fn().mockReturnValue([CHAIN_ID]),
|
|
38
39
|
} as unknown as ChainManager
|
|
39
40
|
}
|
package/src/types/swap/base.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
|
|
3
3
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
4
|
+
import type { EnsName } from '@/ens/types.js'
|
|
4
5
|
import type { SwapProviderName } from '@/types/actions.js'
|
|
5
6
|
import type { Asset } from '@/types/asset.js'
|
|
6
7
|
import type { TransactionData } from '@/types/transaction.js'
|
|
@@ -88,8 +89,8 @@ export interface WalletSwapParams {
|
|
|
88
89
|
slippage?: number
|
|
89
90
|
/** Transaction deadline as Unix timestamp. Defaults to now + 1 minute. */
|
|
90
91
|
deadline?: number
|
|
91
|
-
/** Recipient address. Defaults to wallet address. */
|
|
92
|
-
recipient?: Address
|
|
92
|
+
/** Recipient address or ENS name (e.g. "vitalik.eth"). Defaults to wallet address. */
|
|
93
|
+
recipient?: Address | EnsName
|
|
93
94
|
/** Explicitly select a swap provider. Overrides routing config. */
|
|
94
95
|
provider?: SwapProviderName
|
|
95
96
|
}
|
|
@@ -156,8 +157,8 @@ export interface SwapQuoteParams {
|
|
|
156
157
|
slippage?: number
|
|
157
158
|
/** Transaction deadline as Unix timestamp */
|
|
158
159
|
deadline?: number
|
|
159
|
-
/** Recipient address */
|
|
160
|
-
recipient?: Address
|
|
160
|
+
/** Recipient address or ENS name (e.g. "vitalik.eth"). Defaults to wallet address. */
|
|
161
|
+
recipient?: Address | EnsName
|
|
161
162
|
/** Explicitly select a swap provider */
|
|
162
163
|
provider?: SwapProviderName
|
|
163
164
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Address, PublicClient } from 'viem'
|
|
2
|
+
import { zeroAddress } from 'viem'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { resolveAddress } from '@/utils/ens.js'
|
|
6
|
+
|
|
7
|
+
const REAL_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Address
|
|
8
|
+
const ZERO_ADDRESS = zeroAddress
|
|
9
|
+
|
|
10
|
+
function mockClient(
|
|
11
|
+
returnValue: Address | null,
|
|
12
|
+
rejects = false,
|
|
13
|
+
): PublicClient {
|
|
14
|
+
const getEnsAddress = rejects
|
|
15
|
+
? vi.fn().mockRejectedValue(new Error('network error'))
|
|
16
|
+
: vi.fn().mockResolvedValue(returnValue)
|
|
17
|
+
return { getEnsAddress } as unknown as PublicClient
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('resolveAddress', () => {
|
|
21
|
+
describe('hex address input', () => {
|
|
22
|
+
it('returns a valid hex address as-is', async () => {
|
|
23
|
+
expect(await resolveAddress(REAL_ADDRESS)).toBe(REAL_ADDRESS)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('does not require a mainnet client for hex addresses', async () => {
|
|
27
|
+
await expect(resolveAddress(REAL_ADDRESS)).resolves.toBe(REAL_ADDRESS)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('ENS name input', () => {
|
|
32
|
+
it('resolves a valid ENS name', async () => {
|
|
33
|
+
const client = mockClient(REAL_ADDRESS)
|
|
34
|
+
const result = await resolveAddress('vitalik.eth', client)
|
|
35
|
+
expect(result).toBe(REAL_ADDRESS)
|
|
36
|
+
expect(client.getEnsAddress).toHaveBeenCalledWith({ name: 'vitalik.eth' })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('normalises the ENS name before resolving', async () => {
|
|
40
|
+
const client = mockClient(REAL_ADDRESS)
|
|
41
|
+
await resolveAddress('Vitalik.ETH', client)
|
|
42
|
+
expect(client.getEnsAddress).toHaveBeenCalledWith({ name: 'vitalik.eth' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('throws EnsNotConfiguredError when no mainnet client is provided', async () => {
|
|
46
|
+
const { EnsNotConfiguredError } = await import('@/ens/errors.js')
|
|
47
|
+
await expect(resolveAddress('vitalik.eth')).rejects.toThrow(
|
|
48
|
+
EnsNotConfiguredError,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('includes chain ID 1 in the error when no mainnet client provided', async () => {
|
|
53
|
+
await expect(resolveAddress('vitalik.eth')).rejects.toThrow('1')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws when ENS name cannot be resolved (returns null)', async () => {
|
|
57
|
+
const client = mockClient(null)
|
|
58
|
+
await expect(resolveAddress('unresolvable.eth', client)).rejects.toThrow(
|
|
59
|
+
'"unresolvable.eth" could not be resolved',
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('throws when ENS name resolves to zero address', async () => {
|
|
64
|
+
const client = mockClient(ZERO_ADDRESS)
|
|
65
|
+
await expect(resolveAddress('zero.eth', client)).rejects.toThrow(
|
|
66
|
+
'zero address',
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws with RPC error label on network failure', async () => {
|
|
71
|
+
const client = mockClient(null, true)
|
|
72
|
+
await expect(resolveAddress('vitalik.eth', client)).rejects.toThrow(
|
|
73
|
+
'RPC error',
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('preserves the original cause on RPC failure', async () => {
|
|
78
|
+
const client = mockClient(null, true)
|
|
79
|
+
let caught: Error | undefined
|
|
80
|
+
try {
|
|
81
|
+
await resolveAddress('vitalik.eth', client)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
caught = e as Error
|
|
84
|
+
}
|
|
85
|
+
expect(caught?.cause).toBeInstanceOf(Error)
|
|
86
|
+
expect((caught?.cause as Error).message).toBe('network error')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('invalid input', () => {
|
|
91
|
+
it('throws for ENS-shaped strings that fail normalisation', async () => {
|
|
92
|
+
// Has a dot so satisfies EnsName, but contains invalid ENS characters
|
|
93
|
+
await expect(
|
|
94
|
+
resolveAddress('not!valid.eth', {} as PublicClient),
|
|
95
|
+
).rejects.toThrow('Invalid address or ENS name')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('includes the invalid input in the error message', async () => {
|
|
99
|
+
await expect(
|
|
100
|
+
resolveAddress('!!bad!!.eth', {} as PublicClient),
|
|
101
|
+
).rejects.toThrow('"!!bad!!.eth"')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
package/src/utils/ens.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Address, PublicClient } from 'viem'
|
|
2
|
+
import { isAddress, isAddressEqual, zeroAddress } from 'viem'
|
|
3
|
+
import { normalize } from 'viem/ens'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EnsNotConfiguredError,
|
|
7
|
+
EnsResolutionError,
|
|
8
|
+
EnsRpcError,
|
|
9
|
+
} from '@/ens/errors.js'
|
|
10
|
+
import type { EnsName } from '@/ens/types.js'
|
|
11
|
+
|
|
12
|
+
export { type EnsName, isEnsName } from '@/ens/types.js'
|
|
13
|
+
|
|
14
|
+
/** Resolves an ENS name or address to a checksummed hex Address, or returns undefined. */
|
|
15
|
+
export type RecipientResolver = (
|
|
16
|
+
recipient: Address | EnsName | undefined,
|
|
17
|
+
) => Promise<Address | undefined>
|
|
18
|
+
|
|
19
|
+
/** Pass-through resolver used when no ENS resolution is configured. Throws on ENS names. */
|
|
20
|
+
export const passthroughResolver: RecipientResolver = (r) => {
|
|
21
|
+
if (r !== undefined && !isAddress(r, { strict: false })) {
|
|
22
|
+
throw new Error(`ENS resolution is not configured; received "${r}"`)
|
|
23
|
+
}
|
|
24
|
+
return Promise.resolve(r as Address | undefined)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Low-level utility to resolve an ENS name or hex address to a checksummed hex address.
|
|
29
|
+
* Use this when you manage your own viem PublicClient (e.g. inside providers or scripts).
|
|
30
|
+
*
|
|
31
|
+
* If you have an {@link Actions} instance, prefer `actions.ens.getAddress()` instead —
|
|
32
|
+
* it handles client lookup and fallback automatically from your chain configuration.
|
|
33
|
+
*
|
|
34
|
+
* Hex addresses (0x...) are returned as-is after format validation.
|
|
35
|
+
* ENS names require a mainnet public client for on-chain resolution.
|
|
36
|
+
* @param input - Hex address (0x...) or ENS name (e.g. "vitalik.eth")
|
|
37
|
+
* @param mainnetClient - Public client connected to Ethereum mainnet (required for ENS names)
|
|
38
|
+
* @returns Resolved hex address
|
|
39
|
+
* @throws {EnsNotConfiguredError} If mainnet client is not provided
|
|
40
|
+
* @throws {EnsResolutionError} If the name is invalid or cannot be resolved
|
|
41
|
+
* @throws {EnsRpcError} If the RPC call fails
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveAddress(
|
|
44
|
+
input: Address | EnsName,
|
|
45
|
+
mainnetClient?: PublicClient,
|
|
46
|
+
): Promise<Address> {
|
|
47
|
+
if (isAddress(input, { strict: false })) return input
|
|
48
|
+
|
|
49
|
+
if (!mainnetClient) throw new EnsNotConfiguredError()
|
|
50
|
+
|
|
51
|
+
const normalized = (() => {
|
|
52
|
+
try {
|
|
53
|
+
return normalize(input)
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
throw new EnsResolutionError(
|
|
56
|
+
`Invalid address or ENS name: "${input}"`,
|
|
57
|
+
input,
|
|
58
|
+
{ cause },
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
|
|
63
|
+
const resolved = await mainnetClient
|
|
64
|
+
.getEnsAddress({ name: normalized })
|
|
65
|
+
.catch((cause: unknown) => {
|
|
66
|
+
throw new EnsRpcError(
|
|
67
|
+
`ENS resolution failed for "${input}": RPC error`,
|
|
68
|
+
input,
|
|
69
|
+
{ cause },
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!resolved)
|
|
74
|
+
throw new EnsResolutionError(
|
|
75
|
+
`ENS name "${input}" could not be resolved`,
|
|
76
|
+
input,
|
|
77
|
+
)
|
|
78
|
+
if (isAddressEqual(resolved, zeroAddress)) {
|
|
79
|
+
throw new EnsResolutionError(
|
|
80
|
+
`ENS name "${input}" resolved to the zero address`,
|
|
81
|
+
input,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return resolved
|
|
85
|
+
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Address } from 'viem'
|
|
2
|
+
import { isAddress } from 'viem'
|
|
2
3
|
|
|
3
4
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
4
5
|
import type { Asset } from '@/types/asset.js'
|
|
@@ -74,3 +75,13 @@ export function validateAssetOnChain(
|
|
|
74
75
|
)
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate that a resolved recipient address is not the zero address.
|
|
81
|
+
* ENS names are skipped — only resolved `Address` values are checked.
|
|
82
|
+
*/
|
|
83
|
+
export function validateRecipient(recipient: string | undefined): void {
|
|
84
|
+
if (recipient && isAddress(recipient)) {
|
|
85
|
+
validateNotZeroAddress(recipient, 'recipient')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Address, LocalAccount } from 'viem'
|
|
2
2
|
|
|
3
3
|
import type { SupportedChainId } from '@/constants/supportedChains.js'
|
|
4
|
+
import { EnsNamespace } from '@/ens/index.js'
|
|
4
5
|
import { WalletLendNamespace } from '@/lend/namespaces/WalletLendNamespace.js'
|
|
5
6
|
import type { ChainManager } from '@/services/ChainManager.js'
|
|
6
7
|
import { fetchERC20Balance, fetchETHBalance } from '@/services/tokenBalance.js'
|
|
@@ -72,9 +73,11 @@ export abstract class Wallet {
|
|
|
72
73
|
this.lend = new WalletLendNamespace(this.lendProviders, this)
|
|
73
74
|
}
|
|
74
75
|
if (Object.values(this.swapProviders).some(Boolean)) {
|
|
76
|
+
const ens = new EnsNamespace(this.chainManager)
|
|
75
77
|
this.swap = new WalletSwapNamespace(
|
|
76
78
|
this.swapProviders,
|
|
77
79
|
this,
|
|
80
|
+
(r) => (r ? ens.getAddress(r) : Promise.resolve(undefined)),
|
|
78
81
|
swapSettings,
|
|
79
82
|
)
|
|
80
83
|
}
|