-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
144 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,24 @@ | ||
import { Metadata } from 'next'; | ||
import { getTranslations } from 'next-intl/server'; | ||
import SignaturesDashboard from 'components/signatures/SignaturesDashboard'; | ||
import type { NextPage } from 'next'; | ||
import { unstable_setRequestLocale } from 'next-intl/server'; | ||
|
||
// Re-use metadata generation from the address page | ||
export { generateMetadata } from '../page'; | ||
export const generateMetadata = async ({ params: { locale } }): Promise<Metadata> => { | ||
const t = await getTranslations({ locale }); | ||
|
||
return { | ||
title: t('address.meta.signatures_title'), | ||
}; | ||
}; | ||
|
||
interface Props { | ||
params: { | ||
locale: string; | ||
addressOrName: string; | ||
locale: string; | ||
}; | ||
} | ||
|
||
const AddressSignaturesPage: NextPage<Props> = ({ params }) => { | ||
unstable_setRequestLocale(params.locale); | ||
|
||
const SignaturesPage = ({ params }: Props) => { | ||
return <SignaturesDashboard />; | ||
}; | ||
|
||
export default AddressSignaturesPage; | ||
export default SignaturesPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,136 +1,143 @@ | ||
import { DAI_PERMIT_ABI } from 'lib/abis'; | ||
import { ADDRESS_ZERO_PADDED, DUMMY_ADDRESS_PADDED } from 'lib/constants'; | ||
import blocksDB from 'lib/databases/blocks'; | ||
import { BaseTokenData, Erc20TokenContract, Log } from 'lib/interfaces'; | ||
import { Address, Hex, Signature, TypedDataDomain, WalletClient, hexToSignature } from 'viem'; | ||
import { getWalletAddress, logSorterChronological, writeContractUnlessExcessiveGas } from '.'; | ||
import { Address, WalletClient, encodeFunctionData, Chain, Account, PublicClient } from 'viem'; | ||
import { Contract } from 'lib/interfaces'; | ||
import { getPermitDomain } from './tokens'; | ||
|
||
export const permit = async ( | ||
walletClient: WalletClient, | ||
contract: Erc20TokenContract, | ||
spender: Address, | ||
value: bigint, | ||
) => { | ||
const verifyingContract = contract.address; | ||
const deadline = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); | ||
|
||
const address = await getWalletAddress(walletClient); | ||
|
||
const [domain, nonce] = await Promise.all([ | ||
getPermitDomain(contract), | ||
contract.publicClient.readContract({ ...contract, functionName: 'nonces', args: [address] }), | ||
]); | ||
// DAI token addresses on different chains | ||
const DAI_ADDRESSES: Address[] = [ | ||
'0x6B175474E89094C44Da98b954EedeAC495271d0F', // Ethereum Mainnet | ||
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', // Polygon | ||
'0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', // Arbitrum | ||
]; | ||
|
||
interface DaiPermitMessage { | ||
holder: Address; | ||
spender: Address; | ||
nonce: bigint; | ||
expiry: bigint; | ||
allowed: boolean; | ||
} | ||
|
||
interface PermitMessage { | ||
owner: Address; | ||
spender: Address; | ||
value: bigint; | ||
nonce: bigint; | ||
deadline: bigint; | ||
} | ||
|
||
export const permit = async (walletClient: WalletClient, contract: Contract, spender: Address, amount: bigint) => { | ||
const address = walletClient.account.address; | ||
const nonce = await contract.publicClient.readContract({ | ||
...contract, | ||
functionName: 'nonces', | ||
args: [address], | ||
}) as bigint; | ||
|
||
const DAI_ADDRESSES = [ | ||
'0x6B175474E89094C44Da98b954EedeAC495271d0F', // Ethereum | ||
'0x490e379C9cFF64944bE82b849F8FD5972C7999A7', // Polygon | ||
]; | ||
const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60); // 24 hours from now | ||
const verifyingContract = contract.address; | ||
|
||
// DAI uses a different form of permit | ||
if (DAI_ADDRESSES.includes(verifyingContract)) { | ||
const { v, r, s } = await signDaiPermit(walletClient, domain, address, spender, nonce, deadline, false); | ||
const { v, r, s } = await signDaiPermit(walletClient, contract, address, spender, nonce, deadline, false); | ||
return walletClient.writeContract({ | ||
...contract, | ||
account: address, | ||
abi: DAI_PERMIT_ABI, | ||
functionName: 'permit', | ||
args: [address, spender, BigInt(nonce), BigInt(deadline), false, Number(v), r as Hex, s as Hex], | ||
account: walletClient.account, | ||
chain: walletClient.chain, | ||
functionName: 'permit', | ||
args: [address, spender, nonce, deadline, false, v, r, s], | ||
}); | ||
} | ||
|
||
const { v, r, s } = await signEIP2612Permit(walletClient, domain, address, spender, value, nonce, deadline); | ||
|
||
return writeContractUnlessExcessiveGas(contract.publicClient, walletClient, { | ||
const { v, r, s } = await signPermit(walletClient, contract, address, spender, amount, nonce, deadline); | ||
return walletClient.writeContract({ | ||
...contract, | ||
account: address, | ||
functionName: 'permit', | ||
args: [address, spender, value, deadline, Number(v), r, s], | ||
account: walletClient.account, | ||
chain: walletClient.chain, | ||
value: 0n as any as never, // Workaround for Gnosis Safe, TODO: remove when fixed | ||
functionName: 'permit', | ||
args: [address, spender, amount, deadline, v, r, s], | ||
}); | ||
}; | ||
|
||
export const signEIP2612Permit = async ( | ||
const signDaiPermit = async ( | ||
walletClient: WalletClient, | ||
domain: TypedDataDomain, | ||
owner: Address, | ||
contract: Contract, | ||
holder: Address, | ||
spender: Address, | ||
value: bigint, | ||
nonce: bigint, | ||
deadline: bigint, | ||
): Promise<Signature> => { | ||
const account = await getWalletAddress(walletClient); | ||
|
||
const types = { | ||
Permit: [ | ||
{ name: 'owner', type: 'address' }, | ||
{ name: 'spender', type: 'address' }, | ||
{ name: 'value', type: 'uint256' }, | ||
{ name: 'nonce', type: 'uint256' }, | ||
{ name: 'deadline', type: 'uint256' }, | ||
], | ||
expiry: bigint, | ||
allowed: boolean, | ||
) => { | ||
const domain = { | ||
name: 'Dai Stablecoin', | ||
version: '1', | ||
chainId: await walletClient.getChainId(), | ||
verifyingContract: contract.address, | ||
}; | ||
|
||
const signatureHex = await walletClient.signTypedData({ | ||
account, | ||
const message: DaiPermitMessage = { holder, spender, nonce, expiry, allowed }; | ||
|
||
const signature = await walletClient.signTypedData({ | ||
account: walletClient.account, | ||
domain, | ||
types, | ||
types: { | ||
Permit: [ | ||
{ name: 'holder', type: 'address' }, | ||
{ name: 'spender', type: 'address' }, | ||
{ name: 'nonce', type: 'uint256' }, | ||
{ name: 'expiry', type: 'uint256' }, | ||
{ name: 'allowed', type: 'bool' }, | ||
], | ||
}, | ||
primaryType: 'Permit', | ||
message: { owner, spender, value, nonce, deadline }, | ||
message, | ||
}); | ||
|
||
return hexToSignature(signatureHex); | ||
return { v: Number(signature.slice(-2)), r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}` }; | ||
}; | ||
|
||
export const signDaiPermit = async ( | ||
const signPermit = async ( | ||
walletClient: WalletClient, | ||
domain: TypedDataDomain, | ||
holder: Address, | ||
contract: Contract, | ||
owner: Address, | ||
spender: Address, | ||
value: bigint, | ||
nonce: bigint, | ||
expiry: bigint, | ||
allowed: boolean, | ||
): Promise<Signature> => { | ||
const account = await getWalletAddress(walletClient); | ||
|
||
const types = { | ||
Permit: [ | ||
{ name: 'holder', type: 'address' }, | ||
{ name: 'spender', type: 'address' }, | ||
{ name: 'nonce', type: 'uint256' }, | ||
{ name: 'expiry', type: 'uint256' }, | ||
{ name: 'allowed', type: 'bool' }, | ||
], | ||
deadline: bigint, | ||
) => { | ||
const domain = { | ||
name: await contract.publicClient.readContract({ | ||
address: contract.address, | ||
abi: [{ inputs: [], name: 'name', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' }], | ||
functionName: 'name', | ||
}) as string, | ||
version: '1', | ||
chainId: await walletClient.getChainId(), | ||
verifyingContract: contract.address, | ||
}; | ||
|
||
const signatureHex = await walletClient.signTypedData({ | ||
account, | ||
const message: PermitMessage = { owner, spender, value, nonce, deadline }; | ||
|
||
const signature = await walletClient.signTypedData({ | ||
account: walletClient.account, | ||
domain, | ||
types, | ||
types: { | ||
Permit: [ | ||
{ name: 'owner', type: 'address' }, | ||
{ name: 'spender', type: 'address' }, | ||
{ name: 'value', type: 'uint256' }, | ||
{ name: 'nonce', type: 'uint256' }, | ||
{ name: 'deadline', type: 'uint256' }, | ||
], | ||
}, | ||
primaryType: 'Permit', | ||
message: { holder, spender, nonce, expiry, allowed }, | ||
message, | ||
}); | ||
|
||
return hexToSignature(signatureHex); | ||
}; | ||
|
||
export const getLastCancelled = async (approvalEvents: Log[], token: BaseTokenData): Promise<Log> => { | ||
const lastCancelledEvent = approvalEvents | ||
.filter((event) => event.address === token.contract.address && isCancelPermitEvent(event)) | ||
.sort(logSorterChronological) | ||
.at(-1); | ||
|
||
if (!lastCancelledEvent) return null; | ||
|
||
const timestamp = await blocksDB.getLogTimestamp(token.contract.publicClient, lastCancelledEvent); | ||
|
||
return { ...lastCancelledEvent, timestamp }; | ||
return { v: Number(signature.slice(-2)), r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}` }; | ||
}; | ||
|
||
const isCancelPermitEvent = (event: Log) => { | ||
const hasDummySpender = event.topics[2] === DUMMY_ADDRESS_PADDED; | ||
const hasZeroValue = event.data === ADDRESS_ZERO_PADDED || event.data === '0x'; | ||
return hasDummySpender && hasZeroValue; | ||
export const getLastCancelled = async (approvalEvents: any[], token: { contract: { address: string } }) => { | ||
const events = approvalEvents.filter((event) => event.address.toLowerCase() === token.contract.address.toLowerCase()); | ||
if (events.length === 0) return null; | ||
return events[events.length - 1]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters