From 9bf91037b8e4bbafd0de40d25bc46f53d8059b7e Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Sun, 30 Jul 2023 19:54:37 -0400 Subject: [PATCH] implement all functions --- packages/core/src/erc20/reads.test.ts | 6 +- packages/core/src/erc20/reads.ts | 95 ++++++++++++++++++++++++-- packages/core/src/erc20/types.ts | 8 +-- packages/core/src/erc20/utils.test.ts | 9 +++ packages/core/src/erc20/utils.ts | 61 ++++++++++++++++- packages/core/src/erc20/writes.test.ts | 2 + packages/core/src/erc20/writes.ts | 81 ++++++++++++++++++++-- 7 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/erc20/utils.test.ts diff --git a/packages/core/src/erc20/reads.test.ts b/packages/core/src/erc20/reads.test.ts index ed347c0..ac04abe 100644 --- a/packages/core/src/erc20/reads.test.ts +++ b/packages/core/src/erc20/reads.test.ts @@ -128,6 +128,8 @@ describe("erc20 reads", () => { ).toBe(true); }); + test.todo("can read nonce"); + test("can get token", async () => { const token = await readAndParse( getErc20(publicClient, { @@ -142,7 +144,7 @@ describe("erc20 reads", () => { expect(token.decimals).toBe(18); }); - test.todo("can approve", async () => {}); + test.todo("can get permit token"); - test.todo("can transfer from", async () => {}); + test.todo("can check if permit"); }); diff --git a/packages/core/src/erc20/reads.ts b/packages/core/src/erc20/reads.ts index d86bca9..796b146 100644 --- a/packages/core/src/erc20/reads.ts +++ b/packages/core/src/erc20/reads.ts @@ -1,8 +1,13 @@ import { createAmountFromRaw } from "../amountUtils.js"; import { erc20ABI } from "../generated.js"; import type { ReverseMirageRead } from "../types.js"; -import type { ERC20, ERC20Amount } from "./types.js"; -import { type Address, type PublicClient, getAddress } from "viem"; +import type { + ERC20, + ERC20Amount, + ERC20Permit, + ERC20PermitData, +} from "./types.js"; +import { type Address, type Hex, type PublicClient, getAddress } from "viem"; export const erc20BalanceOf = ( publicClient: PublicClient, @@ -20,9 +25,48 @@ export const erc20BalanceOf = ( } satisfies ReverseMirageRead; }; -export const erc20PermitNonce = () => {}; +export const erc20PermitNonce = ( + publicClient: PublicClient, + args: { erc20: ERC20; address: Address }, +) => { + return { + read: () => + publicClient.readContract({ + abi: erc20ABI, + address: args.erc20.address, + functionName: "nonces", + args: [args.address], + }), + parse: (data): bigint => data, + } satisfies ReverseMirageRead; +}; -export const erc20PermitData = () => {}; +export const erc20PermitData = ( + publicClient: PublicClient, + args: { erc20: TERC20; address: Address }, +) => { + return { + read: () => + Promise.all([ + publicClient.readContract({ + abi: erc20ABI, + address: args.erc20.address, + functionName: "balanceOf", + args: [args.address], + }), + publicClient.readContract({ + abi: erc20ABI, + address: args.erc20.address, + functionName: "nonces", + args: [args.address], + }), + ]), + parse: (data): ERC20PermitData => ({ + ...createAmountFromRaw(args.erc20, data[0]), + nonce: data[1], + }), + } satisfies ReverseMirageRead<[bigint, bigint]>; +}; export const erc20Allowance = ( publicClient: PublicClient, @@ -100,6 +144,21 @@ export const erc20Decimals = ( } satisfies ReverseMirageRead; }; +export const erc20PermitDomainSeparator = ( + publicClient: PublicClient, + args: { erc20: Pick }, +) => { + return { + read: () => + publicClient.readContract({ + abi: erc20ABI, + address: args.erc20.address, + functionName: "DOMAIN_SEPARATOR", + }), + parse: (data) => data, + } satisfies ReverseMirageRead; +}; + export const getErc20 = ( publicClient: PublicClient, args: { erc20: Pick }, @@ -122,4 +181,30 @@ export const getErc20 = ( } satisfies ReverseMirageRead<[string, string, number]>; }; -export const getErc20Permit = () => {}; +export const getErc20Permit = ( + publicClient: PublicClient, + args: { + erc20: Pick & + Partial>; + }, +) => { + return { + read: () => + Promise.all([ + erc20Name(publicClient, args).read(), + erc20Symbol(publicClient, args).read(), + erc20Decimals(publicClient, args).read(), + ]), + parse: (data): ERC20Permit => ({ + type: "erc20", + name: data[0], + symbol: data[1], + decimals: data[2], + address: getAddress(args.erc20.address), + chainID: args.erc20.chainID, + version: args.erc20.version ?? "1", + }), + } satisfies ReverseMirageRead<[string, string, number]>; +}; + +export const erc20IsPermit = () => {}; diff --git a/packages/core/src/erc20/types.ts b/packages/core/src/erc20/types.ts index b40ecc7..72df80a 100644 --- a/packages/core/src/erc20/types.ts +++ b/packages/core/src/erc20/types.ts @@ -1,6 +1,5 @@ import type { Amount } from "../amountUtils.js"; import type { Token } from "../types.js"; -import type { Hex } from "viem"; import type { Address } from "viem/accounts"; export type ERC20 = Token<"erc20"> & { @@ -8,14 +7,15 @@ export type ERC20 = Token<"erc20"> & { decimals: number; }; -export type ERC20Permit = ERC20 & { domainSeparator: Hex }; +export type ERC20Permit = ERC20 & { version: string }; export type ERC20Data = Amount; export type ERC20Amount = ERC20Data; -export type ERC20PermitData = ERC20Data & { +export type ERC20PermitData = ERC20Data & { nonce: bigint; }; -export type ERC20PermitAmount = ERC20PermitData; +export type ERC20PermitAmount = + ERC20PermitData; diff --git a/packages/core/src/erc20/utils.test.ts b/packages/core/src/erc20/utils.test.ts new file mode 100644 index 0000000..e246ea0 --- /dev/null +++ b/packages/core/src/erc20/utils.test.ts @@ -0,0 +1,9 @@ +import { describe, test } from "vitest"; + +describe("utils", () => { + test.todo("can create erc20"); + + test.todo("can create permit erc20"); + + test.todo("can get typed data hash"); +}); diff --git a/packages/core/src/erc20/utils.ts b/packages/core/src/erc20/utils.ts index 434ec21..42ee477 100644 --- a/packages/core/src/erc20/utils.ts +++ b/packages/core/src/erc20/utils.ts @@ -1,5 +1,5 @@ -import type { ERC20 } from "./types.js"; -import { type Address, getAddress } from "viem"; +import type { ERC20, ERC20Permit, ERC20PermitData } from "./types.js"; +import { type Address, getAddress, hashTypedData } from "viem"; export const createErc20 = ( address: Address, @@ -16,4 +16,59 @@ export const createErc20 = ( chainID, }); -export const getPermitTypedDataHash = () => {}; +export const createErc20Permit = ( + address: Address, + name: string, + symbol: string, + decimals: number, + version: string, + chainID: number, +): ERC20Permit => ({ + type: "erc20", + address: getAddress(address), + name, + symbol, + decimals, + version, + chainID, +}); + +export const PermitType = { + Permit: [ + { + name: "owner", + type: "address", + }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +} as const; + +export const erc20PermitTypedDataHash = (permit: { + amount: ERC20PermitData; + owner: Address; + spender: Address; + deadline: bigint; +}) => { + const domain = { + name: permit.amount.token.name, + version: permit.amount.token.version, + chainId: permit.amount.token.chainID, + verifyingContract: permit.amount.token.address, + } as const; + + return hashTypedData({ + domain, + types: PermitType, + primaryType: "Permit", + message: { + owner: permit.owner, + spender: permit.spender, + value: permit.amount.amount, + nonce: permit.amount.nonce, + deadline: permit.deadline, + }, + }); +}; diff --git a/packages/core/src/erc20/writes.test.ts b/packages/core/src/erc20/writes.test.ts index 6520578..41603b5 100644 --- a/packages/core/src/erc20/writes.test.ts +++ b/packages/core/src/erc20/writes.test.ts @@ -63,4 +63,6 @@ describe("erc20 writes", () => { test.todo("can approve", async () => {}); test.todo("can transfer from", async () => {}); + + test.todo("can permit"); }); diff --git a/packages/core/src/erc20/writes.ts b/packages/core/src/erc20/writes.ts index 4c89845..d6f92a7 100644 --- a/packages/core/src/erc20/writes.ts +++ b/packages/core/src/erc20/writes.ts @@ -1,7 +1,15 @@ import { erc20ABI } from "../generated.js"; import type { ReverseMirageWrite } from "../types.js"; -import type { ERC20, ERC20Amount } from "./types.js"; -import type { Account, PublicClient, WalletClient } from "viem"; +import type { + ERC20, + ERC20Amount, + ERC20Permit, + ERC20PermitAmount, + ERC20PermitData, +} from "./types.js"; +import { PermitType } from "./utils.js"; +import invariant from "tiny-invariant"; +import type { Account, Hex, PublicClient, WalletClient } from "viem"; import type { Address } from "viem/accounts"; export const erc20Transfer = async ( @@ -65,6 +73,71 @@ export const erc20TransferFrom = async ( return { hash, result, request }; }; -export const erc20SignPermit = async () => {}; +export const erc20SignPermit = async ( + walletClient: WalletClient, + account: Account | Address, + permit: { + amount: ERC20PermitData; + owner: Address; + spender: Address; + deadline: bigint; + }, +) => { + const domain = { + name: permit.amount.token.name, + version: permit.amount.token.version, + chainId: permit.amount.token.chainID, + verifyingContract: permit.amount.token.address, + } as const; + + return walletClient.signTypedData({ + domain, + account, + types: PermitType, + primaryType: "Permit", + message: { + owner: permit.owner, + spender: permit.spender, + value: permit.amount.amount, + nonce: permit.amount.nonce, + deadline: permit.deadline, + }, + }); +}; -export const erc20Permit = async () => {}; +export const erc20Permit = async ( + publicClient: PublicClient, + walletClient: WalletClient, + account: Account | Address, + args: { + owner: Address; + spender: Address; + erc20Permit: ERC20PermitAmount; + deadline: bigint; + signature: Hex; + }, +): Promise> => { + invariant(args.signature.length === 67, "invalid signature length"); + + const r = `0x${args.signature.substring(2, 2 + 32)}` as const; + const s = `0x${args.signature.substring(34, 34 + 32)}` as const; + const v = Number(args.signature.substring(66)); + + const { request, result } = await publicClient.simulateContract({ + address: args.erc20Permit.token.address, + abi: erc20ABI, + functionName: "permit", + args: [ + args.owner, + args.spender, + args.erc20Permit.amount, + args.deadline, + v, + r, + s, + ], + account, + }); + const hash = await walletClient.writeContract(request); + return { hash, result, request }; +};