Before signing, it’s recommended to get another quote to avoid stale pricing.
Preparing the payload
First, convert the intent operation you received from the API into a typed message to sign.Copy
Ask AI
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).Copy
Ask AI
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