Skip to main content

Overview

After receiving a quote, sign all payloads using the signQuoteSignablePayload function.
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

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);

Next Steps

Execute the signed quote