Overview
After receiving a quote, sign all payloads using thesignQuoteSignablePayload function.
signer.ts - Complete signing utility
signer.ts - Complete signing utility
Copy
import { Address, Hex, SignTypedDataParameters } from "viem";
/**
* Types for different signable payloads.
*/
// Payload for EIP-712 typed data signature (smart-account v2.2.1, permits)
type SignableTypedDataPayload = Omit<SignTypedDataParameters, "account">;
// Payload for personal message signing (legacy v2.1.0, eoa-7702)
type SignablePersonalMessagePayload = {
message: {
raw: Hex;
};
};
// Union type for "simple" quote type payloads (can be either format)
type SignableSimplePayload =
| SignableTypedDataPayload
| SignablePersonalMessagePayload;
// Payload for on-chain transaction signing
type SignableOnChainPayload =
| {
data: Hex;
to: Address;
chainId: number;
value: string;
}
| {
data: Hex;
to: Address;
chainId: number;
value?: string | undefined;
};
/**
* Type guard to detect EIP-712 typed data payload.
* v2.2.1 smart accounts use typed data, v2.1.0 uses personal message.
*/
function isTypedDataPayload(
payload: SignableSimplePayload,
): payload is SignableTypedDataPayload {
return "domain" in payload && "primaryType" in payload;
}
/**
* Signs a "simple" quote payload. Handles both formats:
* - EIP-712 typed data (v2.2.1 smart accounts)
* - Personal message (v2.1.0 legacy accounts, eoa-7702)
*
* @param walletClient - The viem wallet client to use for signing.
* @param signablePayload - The payload to sign (auto-detected format).
* @returns The signature as a Hex string.
*
* @example
* // EIP-712 typed data (v2.2.1 smart account)
* const signature = await signSimpleQuoteSignablePayload(walletClient, {
* domain: { name: "Nexus" },
* types: { MeeUserOp: [...], SuperTx: [{ name: "meeUserOps", type: "MeeUserOp[]" }] },
* primaryType: "SuperTx",
* message: { meeUserOps: [{ userOpHash: "0x...", lowerBoundTimestamp: 0, upperBoundTimestamp: 1765465749 }] }
* });
*
* @example
* // Personal message (v2.1.0 or eoa-7702)
* const signature = await signSimpleQuoteSignablePayload(walletClient, {
* message: { raw: "0x68656c6c6f" }
* });
*/
const signSimpleQuoteSignablePayload = async (
walletClient: any,
signablePayload: SignableSimplePayload,
): Promise<Hex> => {
if (isTypedDataPayload(signablePayload)) {
return await walletClient.signTypedData(signablePayload);
} else {
return await walletClient.signMessage(signablePayload);
}
};
/**
* Signs a permit quote payload (EIP-712 typed data for ERC20 permits).
*
* @param walletClient - The viem wallet client to use for signing.
* @param signablePayload - The EIP-712 payload to sign.
* @returns The signature as a Hex string.
*
* @example
* const signature = await signPermitQuoteSignablePayload(walletClient, {
* domain: { ... },
* types: { ... },
* primaryType: "Permit",
* message: { ... }
* });
*/
const signPermitQuoteSignablePayload = async (
walletClient: any,
signablePayload: SignableTypedDataPayload,
): Promise<Hex> => {
return await walletClient.signTypedData(signablePayload);
};
/**
* Signs an on-chain quote payload (transaction).
*
* @param walletClient - The viem wallet client to use for signing.
* @param signablePayload - The transaction payload to send.
* @returns The transaction hash as a Hex string.
*
* @example
* const txHash = await signOnChainQuoteSignablePayload(walletClient, {
* to: "0x...",
* data: "0x...",
* value: "0",
* chainId: 1
* });
*/
const signOnChainQuoteSignablePayload = async (
walletClient: any,
signablePayload: SignableOnChainPayload,
): Promise<Hex> => {
const hash = await walletClient.sendTransaction({
account: walletClient.account,
to: signablePayload.to,
data: signablePayload.data,
value: BigInt(signablePayload.value || "0"),
});
await walletClient.waitForTransactionReceipt({ hash, confirmations: 5 });
return hash;
};
/**
* Union type for all supported payloads to sign.
*/
type PayloadToSign =
| {
signablePayload: SignableSimplePayload;
metadata?: any;
}
| {
signablePayload: SignableTypedDataPayload;
metadata: {
nonce: string;
name: string;
version: string;
domainSeparator: string;
owner: string;
spender: string;
amount: string;
};
}
| SignableOnChainPayload;
/**
* Signs a quote payload based on its type.
* Automatically handles both EIP-712 and personal message formats for "simple" type.
*
* @param walletClient - The viem wallet client to use for signing.
* @param quoteType - The type of quote from the API response.
* @param payloadToSign - The payload to sign.
* @returns The signature or transaction hash as a Hex string.
*
* @example
* // Simple - EIP-712 (v2.2.1 smart account)
* const sig = await signQuoteSignablePayload(walletClient, "simple", {
* signablePayload: { domain: { name: "Nexus" }, types: {...}, primaryType: "SuperTx", message: { meeUserOps: [...] } }
* });
*
* @example
* // Simple - Personal message (v2.1.0 or eoa-7702)
* const sig = await signQuoteSignablePayload(walletClient, "simple", {
* signablePayload: { message: { raw: "0x68656c6c6f" } }
* });
*
* @example
* // Permit (EIP-712 for ERC20)
* const sig = await signQuoteSignablePayload(walletClient, "permit", {
* signablePayload: { domain: {...}, types: {...}, primaryType: "Permit", message: {...} },
* metadata: { ... }
* });
*
* @example
* // On-chain transaction
* const txHash = await signQuoteSignablePayload(walletClient, "onchain", {
* to: "0x...",
* data: "0x...",
* value: "0",
* chainId: 1
* });
*/
export const signQuoteSignablePayload = async (
walletClient: any,
quoteType: string,
payloadToSign: any,
): Promise<Hex> => {
switch (quoteType) {
case "simple":
if ("signablePayload" in payloadToSign) {
return await signSimpleQuoteSignablePayload(
walletClient,
payloadToSign.signablePayload,
);
}
return await signSimpleQuoteSignablePayload(walletClient, payloadToSign);
case "permit":
if (!("signablePayload" in payloadToSign)) {
throw new Error("Permit requires signablePayload");
}
return await signPermitQuoteSignablePayload(
walletClient,
payloadToSign.signablePayload,
);
case "onchain":
return await signOnChainQuoteSignablePayload(walletClient, payloadToSign);
default:
throw new Error("Unsupported quote type, can't sign the payload");
}
};
export {
signSimpleQuoteSignablePayload,
signPermitQuoteSignablePayload,
signOnChainQuoteSignablePayload,
};
export type {
SignableTypedDataPayload,
SignablePersonalMessagePayload,
SignableSimplePayload,
SignableOnChainPayload,
PayloadToSign,
};
Usage
Copy
import "dotenv/config";
import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { signQuoteSignablePayload } from "./signer";
const account = privateKeyToAccount(process.env.PK! as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
}).extend(publicActions);
interface QuoteResponse {
payloadToSign: Array<{
signablePayload?: unknown;
metadata?: unknown;
[key: string]: unknown;
}>;
quote: unknown;
fee: unknown;
quoteType: "simple" | "permit" | "onchain";
ownerAddress: string;
[key: string]: unknown;
}
async function main() {
const res = await fetch("http://127.0.0.1:8000/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fromToken: {
chainId: 10, // Optimism
address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // USDC
},
toToken: { slug: "wrapped-ethereum" },
feeToken: {
chainId: 10, // Optimism
address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // USDC
},
allowChains: [10, 8453],
amount: "100000", // 0.10 USDC
fromAddress: account.address,
toAddress: account.address,
}),
});
if (!res.ok) throw new Error(`Quote failed: ${res.statusText}`);
const quoteResponse = (await res.json()) as QuoteResponse;
const { payloadToSign, quoteType } = quoteResponse;
for (let i = 0; i < payloadToSign.length; i++) {
const signature = await signQuoteSignablePayload(
walletClient,
quoteType,
payloadToSign[i] as any,
);
payloadToSign[i] = { ...payloadToSign[i], signature };
}
}
main().catch(console.error);