@renegade-fi/node
Version:
Node.js library for Renegade
118 lines (106 loc) • 4.19 kB
text/typescript
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 };
}