Skip to content

Commit

Permalink
feat: save on storage permit data
Browse files Browse the repository at this point in the history
  • Loading branch information
yvesfracari committed Nov 19, 2024
1 parent 4a2256b commit 83f5f22
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 10 deletions.
3 changes: 2 additions & 1 deletion packages/cow-hooks-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"dependencies": {
"@1inch/permit-signed-approvals-utils": "1.5.2",
"@balancer/sdk": "0.26.1",
"@bleu/tsconfig": "workspace:*",
"@bleu.builders/ui": "0.1.133",
"@bleu/tsconfig": "workspace:*",
"@bleu/utils": "workspace:*",
"@cowprotocol/contracts": "1.6.0",
"@cowprotocol/cow-sdk": "^5.5.1",
Expand All @@ -25,6 +25,7 @@
"@uniswap/sdk-core": "5.4.0",
"cmdk": "^1.0.0",
"graphql-request": "^6.1.0",
"jotai": "^2.10.2",
"preline": "^2.5.1",
"viem": "2.21.14"
},
Expand Down
17 changes: 10 additions & 7 deletions packages/cow-hooks-ui/src/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Provider as JotaiProvider } from "jotai";
import type { PropsWithChildren } from "react";
import { IFrameContextProvider } from "./context/iframe";
import { TokenLogoContextProvider } from "./context/tokenLogo";
import { Scrollbar } from "./ui/Scrollbar";

export function RootLayout({ children }: PropsWithChildren) {
return (
<IFrameContextProvider>
<TokenLogoContextProvider>
<body className="bg-transparent">
<Scrollbar>{children}</Scrollbar>
</body>
</TokenLogoContextProvider>
</IFrameContextProvider>
<JotaiProvider>
<IFrameContextProvider>
<TokenLogoContextProvider>
<body className="bg-transparent">
<Scrollbar>{children}</Scrollbar>
</body>
</TokenLogoContextProvider>
</IFrameContextProvider>
</JotaiProvider>
);
}
197 changes: 197 additions & 0 deletions packages/cow-hooks-ui/src/hooks/tokenAllowance/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import type { SupportedChainId } from "@cowprotocol/cow-sdk";
import type { PermitHookData } from "@cowprotocol/permit-utils";
import { atom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils";

// Types
export interface PermitCacheKeyParams {
chainId: SupportedChainId;
tokenAddress: string;
account?: string;
nonce?: number;
spender: string;
}

export type StorePermitCacheParams = PermitCacheKeyParams & {
hookData: PermitHookData;
};

export interface CachedPermitData {
hookData: PermitHookData;
nonce?: number;
}

export type PermitCache = Record<string, string>;

// Constants
const STORAGE_KEY = "hookDapp:userPermitCache:v1" as const;

// Check if window is defined (for SSR compatibility)
const isBrowser = typeof window !== "undefined";

// Create a storage that's SSR-safe
const storage = createJSONStorage<PermitCache>(() => ({
getItem: (key) => {
if (!isBrowser) return null;

try {
const item = localStorage.getItem(key);
return item;
} catch (e) {
console.error("[Storage] Error getting item:", e);
return null;
}
},
setItem: (key, value) => {
if (!isBrowser) return;

try {
localStorage.setItem(key, value);
} catch (e) {
console.error("[Storage] Error setting item:", e);
}
},
removeItem: (key) => {
if (!isBrowser) return;

try {
localStorage.removeItem(key);
} catch (e) {
console.error("[Storage] Error removing item:", e);
}
},
}));

// Helper functions
const safeJsonParse = <T>(str: string): T | undefined => {
try {
return JSON.parse(str) as T;
} catch (e) {
console.error("[PermitCache] Failed to parse JSON:", e);
return undefined;
}
};

const safeJsonStringify = (data: unknown): string => {
try {
return JSON.stringify(data);
} catch (e) {
console.error("[PermitCache] Failed to stringify data:", e);
return "{}";
}
};

const buildKey = ({
chainId,
tokenAddress,
account,
spender,
}: PermitCacheKeyParams): string => {
if (!tokenAddress || !spender) {
throw new Error(
"Required parameters missing: tokenAddress and spender are required",
);
}

const base = `${chainId}-${tokenAddress.toLowerCase()}-${spender.toLowerCase()}`;
return account ? `${base}-${account.toLowerCase()}` : base;
};

const removePermitCacheBuilder =
(key: string) =>
(permitCache: PermitCache): PermitCache => {
const { [key]: _, ...newPermitCache } = { ...permitCache };
return newPermitCache;
};

export const getStoredPermitCache = (): PermitCache => {
if (!isBrowser) return {};

try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return {};

const parsed = JSON.parse(stored);
return parsed;
} catch (e) {
console.error("[PermitCache] Failed to read from storage:", e);
return {};
}
};

// Atoms
export const userPermitCacheAtom = atomWithStorage<PermitCache>(
STORAGE_KEY,
getStoredPermitCache(), // Initialize with current storage value
storage,
{
getOnInit: true,
},
);

export const storePermitCacheAtom = atom(
null,
(get, set, params: StorePermitCacheParams) => {
if (!params?.hookData) {
console.error("[storePermitCacheAtom] Missing required hookData");
return;
}

const key = buildKey(params);
const dataToCache: CachedPermitData = {
hookData: params.hookData,
nonce: params.nonce,
};

const currentCache = get(userPermitCacheAtom);

const newCache = {
...currentCache,
[key]: safeJsonStringify(dataToCache),
};
set(userPermitCacheAtom, newCache);
},
);

export const getPermitCacheAtom = atom(
null,
(get, set, params: PermitCacheKeyParams) => {
const permitCache = get(userPermitCacheAtom);

const key = buildKey(params);
const cachedData = permitCache[key];

if (!cachedData) {
return undefined;
}

try {
const parsedData = safeJsonParse<CachedPermitData>(cachedData);

if (!parsedData) {
set(userPermitCacheAtom, removePermitCacheBuilder(key));
return undefined;
}

const { hookData, nonce: storedNonce } = parsedData;

// Check nonce validity for user-specific permits
if (
params.account &&
params.nonce !== undefined &&
storedNonce !== undefined
) {
if (storedNonce < params.nonce) {
set(userPermitCacheAtom, removePermitCacheBuilder(key));
return undefined;
}
}

return hookData;
} catch (e) {
console.error("[getPermitCacheAtom] Failed to parse cached data:", e);
set(userPermitCacheAtom, removePermitCacheBuilder(key));
return undefined;
}
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
getTokenPermitInfo,
} from "@cowprotocol/permit-utils";
import { BigNumber } from "ethers";
import { useSetAtom } from "jotai";
import { useCallback } from "react";
import { type Address, erc20Abi, maxUint256 } from "viem";
import { useIFrameContext } from "../../context/iframe";
import { getPermitCacheAtom, storePermitCacheAtom } from "./state";
import { handleTokenApprove } from "./useHandleTokenApprove";

export function useHandleTokenAllowance({
Expand All @@ -18,6 +20,9 @@ export function useHandleTokenAllowance({
}) {
const { web3Provider, publicClient, jsonRpcProvider, context, signer } =
useIFrameContext();
const storePermit = useSetAtom(storePermitCacheAtom);
const getCachedPermit = useSetAtom(getPermitCacheAtom);

return useCallback(
async (amount: BigNumber, tokenAddress: Address) => {
if (
Expand Down Expand Up @@ -75,6 +80,18 @@ export function useHandleTokenAllowance({
eip2162Utils.getTokenNonce(tokenAddress, account),
]).catch(() => [undefined, undefined]);

const cachedPermit = getCachedPermit({
chainId: context.chainId,
tokenAddress,
account: context.account,
spender,
nonce,
});

if (cachedPermit) {
return cachedPermit;
}

if (!permitInfo || !checkIsPermitInfo(permitInfo)) {
await handleTokenApprove({
signer,
Expand All @@ -99,9 +116,26 @@ export function useHandleTokenAllowance({
nonce,
});
if (!hook) throw new Error("User rejected permit");
storePermit({
chainId,
tokenAddress,
account,
nonce,
spender,
hookData: hook,
});
return hook;
},
[jsonRpcProvider, context, publicClient, spender, signer, web3Provider],
[
jsonRpcProvider,
context,
publicClient,
spender,
signer,
web3Provider,
getCachedPermit,
storePermit,
],
);
}

Expand Down
48 changes: 47 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 83f5f22

Please sign in to comment.