Skip to main content
Before signing, it’s recommended to get another quote to avoid stale pricing.
Warp uses EIP-712 for human-readable signing.

Preparing the payload

First, convert the intent operation you received from the API into a typed message to sign.
interface TokenPermissions {
  token: Address;
  amount: bigint;
}

interface Ops {
  to: Address;
  value: string;
  data: Hex;
}

interface Op {
  vt: Hex;
  ops: Ops[];
}

interface IntentOpElementMandate {
  recipient: Address;
  tokenOut: [[string, string]];
  destinationChainId: string;
  fillDeadline: string;
  destinationOps: Op;
  preClaimOps: Op;
  qualifier: {
    settlementContext: {
      settlementLayer: string;
      usingJIT: boolean;
      using7579: boolean;
    };
    encodedVal: Hex;
  };
  minGas: string;
}

export interface IntentOpElement {
  arbiter: Address;
  chainId: string;
  idsAndAmounts: [[string, string]];
  spendTokens: [[string, string]];
  beforeFill: boolean;
  smartAccountStatus: {
    accountType: string;
    isDeployed: boolean;
    isERC7579: boolean;
    erc7579AccountType: string;
    erc7579AccountVersion: string;
  };
  mandate: IntentOpElementMandate;
}

function toToken(id: bigint): Address {
  return `0x${(id & ((1n << 160n) - 1n)).toString(16).padStart(40, "0")}`;
}

function getTypedData(
  element: IntentOpElement,
  nonce: bigint,
  expires: bigint,
) {
  const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";

  const tokens = element.idsAndAmounts.map(([id, amount]) => [
    BigInt(id),
    BigInt(amount),
  ]);
  const tokenPermissions = tokens.reduce<TokenPermissions[]>(
    (permissions, [id, amountIn]) => {
      const token = toToken(BigInt(id));
      const amount = BigInt(amountIn);
      const permission: TokenPermissions = { token, amount };
      permissions.push(permission);
      return permissions;
    },
    [],
  );
  const spender = element.arbiter;
  const mandate = element.mandate;

  const typedData = {
    domain: {
      name: "Permit2",
      chainId: Number(element.chainId),
      verifyingContract: PERMIT2_ADDRESS,
    },
    types: {
      TokenPermissions: [
        { name: "token", type: "address" },
        { name: "amount", type: "uint256" },
      ],
      Token: [
        { name: "token", type: "address" },
        { name: "amount", type: "uint256" },
      ],
      Target: [
        { name: "recipient", type: "address" },
        { name: "tokenOut", type: "Token[]" },
        { name: "targetChain", type: "uint256" },
        { name: "fillExpiry", type: "uint256" },
      ],
      Ops: [
        { name: "to", type: "address" },
        { name: "value", type: "uint256" },
        { name: "data", type: "bytes" },
      ],
      Op: [
        { name: "vt", type: "bytes32" },
        { name: "ops", type: "Ops[]" },
      ],
      Mandate: [
        { name: "target", type: "Target" },
        { name: "minGas", type: "uint128" },
        { name: "originOps", type: "Op" },
        { name: "destOps", type: "Op" },
        { name: "q", type: "bytes32" },
      ],
      PermitBatchWitnessTransferFrom: [
        { name: "permitted", type: "TokenPermissions[]" },
        { name: "spender", type: "address" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" },
        { name: "mandate", type: "Mandate" },
      ],
    },
    primaryType: "PermitBatchWitnessTransferFrom",
    message: {
      permitted: tokenPermissions,
      spender: spender,
      nonce: nonce,
      deadline: expires,
      mandate: {
        target: {
          recipient: mandate.recipient,
          tokenOut: mandate.tokenOut.map((token) => ({
            token: toToken(BigInt(token[0])),
            amount: BigInt(token[1]),
          })),
          targetChain: BigInt(mandate.destinationChainId),
          fillExpiry: BigInt(mandate.fillDeadline),
        },
        minGas: BigInt(mandate.minGas),
        originOps: mandate.preClaimOps,
        destOps: mandate.destinationOps,
        q: keccak256(mandate.qualifier.encodedVal),
      },
    },
  } as const;

  return typedData;
}

Signing the message

Once you have the typed data, ask the user to sign it with their wallet. Note that you’ll need to sign every element of the intent (one signature per input chain).
const signatures: Hex[] = [];
for (const element of intentOp.elements) {
  const typedData = getTypedData(
    element,
    BigInt(intentOp.nonce),
    BigInt(intentOp.expires)
  );
  const signature = await signTypedData(wagmiConfig, {
    ...typedData,
  });
  signatures.push(signature);
}
const originSignatures = signatures; // all signatures, including the last
const destinationSignature = signatures.at(-1); // last one reused

Next Steps

Submission

Learn how to submit and track the signed intent