UNPKG

@renegade-fi/node

Version:
118 lines (106 loc) 4.19 kB
import { type TypedDataDomain, type WalletClient, hashTypedData, verifyTypedData } from "viem"; import { publicKeyToAddress, recoverPublicKey } from "viem/utils"; export function millisecondsToSeconds(milliseconds: number): number { return Math.floor(milliseconds / 1000); } const TOKEN_PERMISSIONS = [ { name: "token", type: "address" }, { name: "amount", type: "uint256" }, ]; const DEPOSIT_WITNESS = [{ name: "pkRoot", type: "uint256[4]" }]; const PERMIT_WITNESS_TRANSFER_FROM_TYPES = { PermitWitnessTransferFrom: [ { name: "permitted", type: "TokenPermissions" }, { name: "spender", type: "address" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, { name: "witness", type: "DepositWitness" }, ], TokenPermissions: TOKEN_PERMISSIONS, DepositWitness: DEPOSIT_WITNESS, }; /** * Signs a permit allowing a specified spender to transfer a specified amount of tokens from the signer's account. * This function constructs a domain and message for the permit, signs it using the wallet client, and verifies the signature. * It also ensures the public key recovered from the signature matches the wallet's public key. * @param {bigint} amount - The decimal-adjusted amount of tokens to permit the spender to transfer. * @param {number} chainId - The chain ID of the network. * @param {string} spender - The address of the spender who is permitted to transfer the tokens. * @param {string} permit2Address - The address of the deployed Permit2 contract. * @param {string} tokenAddress - The address of the token to be transferred. * @param {WalletClient} walletClient - The wallet client used to sign the permit. * * @returns {Promise<{signature: string, nonce: bigint, deadline: bigint}>} An object containing the signature, nonce, and deadline of the permit. * * @throws {Error} Throws an error if the wallet client's account address is not found, the signature is invalid, or the recovered public key does not match the wallet's public key. */ export async function signPermit2({ amount, chainId, spender, permit2Address, tokenAddress, walletClient, pkRoot, }: { amount: bigint; chainId: number; spender: `0x${string}`; permit2Address: `0x${string}`; tokenAddress: `0x${string}`; walletClient: WalletClient; pkRoot: bigint[]; }) { if (!walletClient.account) throw new Error("`0x${string}` not found on wallet client"); // Construct Domain const domain: TypedDataDomain = { name: "Permit2", chainId, verifyingContract: permit2Address as `0x${string}`, }; // Construct Message const message = { permitted: { token: tokenAddress, amount, }, spender, nonce: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)), deadline: BigInt(millisecondsToSeconds(Date.now() + 1000 * 60 * 30)), witness: { pkRoot }, } as const; // Generate signature const signature = await walletClient.signTypedData({ account: walletClient.account, domain, types: PERMIT_WITNESS_TRANSFER_FROM_TYPES, primaryType: "PermitWitnessTransferFrom", message, }); // Verify signature const valid = await verifyTypedData({ address: walletClient.account.address, domain, types: PERMIT_WITNESS_TRANSFER_FROM_TYPES, primaryType: "PermitWitnessTransferFrom", message, signature, }); if (!valid) throw new Error("Invalid signature"); // Ensure correct public key is recovered const hash = hashTypedData({ domain, types: PERMIT_WITNESS_TRANSFER_FROM_TYPES, primaryType: "PermitWitnessTransferFrom", message, }); const recoveredPubKey = publicKeyToAddress( await recoverPublicKey({ hash, signature, }), ); if (recoveredPubKey !== walletClient.account.address) throw new Error("Recovered public key does not match wallet public key"); return { signature, nonce: message.nonce, deadline: message.deadline }; }