@@ -2,13 +2,14 @@ import { Command } from "commander";
22import type { Address } from "viem" ;
33import {
44 swapActions ,
5+ type RequestQuoteV0Params ,
56 type RequestQuoteV0Result ,
67} from "@alchemy/wallet-apis/experimental" ;
78import { buildWalletClient } from "../lib/smart-wallet.js" ;
89import type { PaymasterConfig } from "../lib/smart-wallet.js" ;
910import { validateAddress } from "../lib/validators.js" ;
1011import { isJSONMode , printJSON } from "../lib/output.js" ;
11- import { exitWithError , errInvalidArgs } from "../lib/errors.js" ;
12+ import { CLIError , exitWithError , errInvalidArgs } from "../lib/errors.js" ;
1213import { withSpinner , printKeyValueBox , green } from "../lib/ui.js" ;
1314import { nativeTokenSymbol } from "../lib/networks.js" ;
1415import { parseAmount , fetchTokenDecimals } from "./send/shared.js" ;
@@ -32,7 +33,19 @@ async function resolveTokenInfo(
3233 if ( isNativeToken ( tokenAddress ) ) {
3334 return { decimals : NATIVE_DECIMALS , symbol : nativeTokenSymbol ( network ) } ;
3435 }
35- return fetchTokenDecimals ( program , tokenAddress ) ;
36+
37+ try {
38+ return await fetchTokenDecimals ( program , tokenAddress ) ;
39+ } catch ( err ) {
40+ if ( err instanceof CLIError && err . code === "INVALID_ARGS" ) {
41+ throw err ;
42+ }
43+
44+ const detail = err instanceof Error && err . message
45+ ? ` ${ err . message } `
46+ : "" ;
47+ throw errInvalidArgs ( `Failed to resolve token info for ${ tokenAddress } .${ detail } ` ) ;
48+ }
3649}
3750
3851function formatTokenAmount ( rawAmount : bigint , decimals : number ) : string {
@@ -49,16 +62,21 @@ interface SwapOpts {
4962 slippage ?: string ;
5063}
5164
65+ type WalletClient = ReturnType < typeof buildWalletClient > [ "client" ] ;
5266type PaymasterPermitQuote = Extract < RequestQuoteV0Result , { type : "paymaster-permit" } > ;
5367type RawCallsQuote = Extract < RequestQuoteV0Result , { rawCalls : true } > ;
68+ type ExecutablePreparedQuote = Parameters < WalletClient [ "signPreparedCalls" ] > [ 0 ] ;
69+ type PreparedCallsRequest = Parameters < WalletClient [ "prepareCalls" ] > [ 0 ] ;
70+ type SignatureRequest = Parameters < WalletClient [ "signSignatureRequest" ] > [ 0 ] ;
71+ type ExecutableQuote = ExecutablePreparedQuote | RawCallsQuote ;
5472
5573function createQuoteRequest (
5674 fromToken : string ,
5775 toToken : string ,
5876 fromAmount : bigint ,
5977 slippagePercent : number | undefined ,
6078 paymaster ?: PaymasterConfig ,
61- ) {
79+ ) : RequestQuoteV0Params {
6280 const request = {
6381 fromToken : fromToken as Address ,
6482 toToken : toToken as Address ,
@@ -67,9 +85,43 @@ function createQuoteRequest(
6785 ? { slippage : slippagePercentToBasisPoints ( slippagePercent ) }
6886 : { } ) ,
6987 ...( paymaster ? { capabilities : { paymaster } } : { } ) ,
88+ } satisfies RequestQuoteV0Params ;
89+
90+ return request ;
91+ }
92+
93+ async function prepareQuoteForExecution (
94+ client : WalletClient ,
95+ quote : RequestQuoteV0Result ,
96+ ) : Promise < ExecutableQuote > {
97+ if ( ! ( "type" in quote ) || quote . type !== "paymaster-permit" || ! ( "modifiedRequest" in quote ) || ! ( "signatureRequest" in quote ) ) {
98+ return quote as ExecutableQuote ;
99+ }
100+
101+ const permitQuote = quote as PaymasterPermitQuote & {
102+ modifiedRequest : PreparedCallsRequest ;
103+ signatureRequest : SignatureRequest ;
70104 } ;
105+ const permitSignature = await withSpinner (
106+ "Signing permit…" ,
107+ "Permit signed" ,
108+ ( ) => client . signSignatureRequest ( permitQuote . signatureRequest ) ,
109+ ) ;
110+
111+ const preparedQuote = await withSpinner (
112+ "Preparing swap…" ,
113+ "Swap prepared" ,
114+ ( ) => client . prepareCalls ( {
115+ ...permitQuote . modifiedRequest ,
116+ paymasterPermitSignature : permitSignature ,
117+ } ) ,
118+ ) ;
119+
120+ if ( "type" in preparedQuote && preparedQuote . type === "paymaster-permit" ) {
121+ throw errInvalidArgs ( "Swap quote still requires a paymaster permit after signing. The quote response format may be unsupported." ) ;
122+ }
71123
72- return request as Parameters < ReturnType < ReturnType < typeof buildWalletClient > [ "client" ] [ "extend" ] > [ "requestQuoteV0" ] > [ 0 ] ;
124+ return preparedQuote as ExecutableQuote ;
73125}
74126
75127export function registerSwap ( program : Command ) {
@@ -80,9 +132,9 @@ export function registerSwap(program: Command) {
80132 cmd
81133 . command ( "quote" )
82134 . description ( "Get a swap quote without executing" )
83- . requiredOption ( "--from <address >" , "Token address to swap from (use 0xEeee...EEeE for the native token)" )
84- . requiredOption ( "--to <address >" , "Token address to swap to" )
85- . requiredOption ( "--amount <number>" , "Amount to swap (human-readable )" )
135+ . requiredOption ( "--from <token_address >" , "Token address to swap from (use 0xEeee...EEeE for the native token)" )
136+ . requiredOption ( "--to <token_address >" , "Token address to swap to (use 0xEeee...EEeE for the native token) " )
137+ . requiredOption ( "--amount <number>" , "Amount to swap in decimal token units (for example, 1.5 )" )
86138 . option ( "--slippage <percent>" , "Max slippage percentage (omit to use the API default)" )
87139 . addHelpText (
88140 "after" ,
@@ -104,9 +156,9 @@ Examples:
104156 cmd
105157 . command ( "execute" )
106158 . description ( "Execute a token swap" )
107- . requiredOption ( "--from <address >" , "Token address to swap from (use 0xEeee...EEeE for the native token)" )
108- . requiredOption ( "--to <address >" , "Token address to swap to" )
109- . requiredOption ( "--amount <number>" , "Amount to swap (human-readable )" )
159+ . requiredOption ( "--from <token_address >" , "Token address to swap from (use 0xEeee...EEeE for the native token)" )
160+ . requiredOption ( "--to <token_address >" , "Token address to swap to (use 0xEeee...EEeE for the native token) " )
161+ . requiredOption ( "--amount <number>" , "Amount to swap in decimal token units (for example, 1.5 )" )
110162 . option ( "--slippage <percent>" , "Max slippage percentage (omit to use the API default)" )
111163 . addHelpText (
112164 "after" ,
@@ -205,45 +257,13 @@ async function performSwapExecute(program: Command, opts: SwapOpts) {
205257 }
206258
207259 // Get quote with prepared calls
208- let quote = await withSpinner (
260+ const quote = await withSpinner (
209261 "Fetching quote…" ,
210262 "Quote received" ,
211263 ( ) => swapClient . requestQuoteV0 ( createQuoteRequest ( opts . from , opts . to , rawAmount , slippage , paymaster ) ) ,
212264 ) ;
213265
214- // If the quote requires an ERC-7597 permit, sign it and refresh the quote
215- // with the attached permit signature before preparing the final calls.
216- let preparedQuote :
217- | Parameters < typeof client . signPreparedCalls > [ 0 ]
218- | RawCallsQuote
219- | undefined ;
220-
221- if ( "type" in quote && quote . type === "paymaster-permit" && "modifiedRequest" in quote && "signatureRequest" in quote ) {
222- const permitQuote = quote as PaymasterPermitQuote & {
223- modifiedRequest : Parameters < typeof client . prepareCalls > [ 0 ] ;
224- signatureRequest : Parameters < typeof client . signSignatureRequest > [ 0 ] ;
225- } ;
226- const permitSignature = await withSpinner (
227- "Signing permit…" ,
228- "Permit signed" ,
229- ( ) => client . signSignatureRequest ( permitQuote . signatureRequest ) ,
230- ) ;
231-
232- preparedQuote = await withSpinner (
233- "Preparing swap…" ,
234- "Swap prepared" ,
235- ( ) => client . prepareCalls ( {
236- ...permitQuote . modifiedRequest ,
237- paymasterPermitSignature : permitSignature ,
238- } ) ,
239- ) ;
240- } else {
241- preparedQuote = quote ;
242- }
243-
244- if ( "type" in preparedQuote && preparedQuote . type === "paymaster-permit" ) {
245- throw errInvalidArgs ( "Swap quote still requires a paymaster permit after signing. The quote response format may be unsupported." ) ;
246- }
266+ const preparedQuote = await prepareQuoteForExecution ( client , quote ) ;
247267
248268 // Send the quoted swap using the appropriate execution path.
249269 const { id } = await withSpinner (
@@ -258,7 +278,7 @@ async function performSwapExecute(program: Command, opts: SwapOpts) {
258278 } ) ;
259279 }
260280
261- const executablePreparedQuote = preparedQuote as Parameters < typeof client . signPreparedCalls > [ 0 ] ;
281+ const executablePreparedQuote = preparedQuote as ExecutablePreparedQuote ;
262282 const signedQuote = await client . signPreparedCalls ( executablePreparedQuote ) ;
263283 return client . sendPreparedCalls ( signedQuote ) ;
264284 } ,
@@ -313,8 +333,8 @@ async function performSwapExecute(program: Command, opts: SwapOpts) {
313333
314334// ── Helpers ─────────────────────────────────────────────────────────
315335
316- function extractQuoteData ( quote : any ) : { type : string ; minimumOutput ?: bigint } {
317- const type = quote . type ?? "unknown" ;
336+ function extractQuoteData ( quote : RequestQuoteV0Result ) : { type : string ; minimumOutput ?: bigint } {
337+ const type = "type" in quote ? quote . type : "unknown" ;
318338
319339 if ( quote . quote ?. minimumToAmount !== undefined ) {
320340 return { type, minimumOutput : BigInt ( quote . quote . minimumToAmount ) } ;
0 commit comments