diff --git a/apps/sdk-demo/src/utils/abis.ts b/apps/sdk-demo/src/utils/abis.ts
new file mode 100644
index 0000000..2a01390
--- /dev/null
+++ b/apps/sdk-demo/src/utils/abis.ts
@@ -0,0 +1,41 @@
+export const PermitAbi = [
+ {
+ inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],
+ name: 'nonces',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'version',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'name',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+];
+
+export const Permit2Abi = [
+ {
+ inputs: [
+ { internalType: 'address', name: '', type: 'address' },
+ { internalType: 'address', name: '', type: 'address' },
+ { internalType: 'address', name: '', type: 'address' },
+ ],
+ name: 'allowance',
+ outputs: [
+ { internalType: 'uint160', name: 'amount', type: 'uint160' },
+ { internalType: 'uint48', name: 'expiration', type: 'uint48' },
+ { internalType: 'uint48', name: 'nonce', type: 'uint48' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
\ No newline at end of file
diff --git a/apps/sdk-demo/src/utils/index.ts b/apps/sdk-demo/src/utils/index.ts
index bb9f438..1c50261 100644
--- a/apps/sdk-demo/src/utils/index.ts
+++ b/apps/sdk-demo/src/utils/index.ts
@@ -16,3 +16,15 @@ export function getAvailableStables(chain: RoutesSupportedChainId): MyTokenConfi
export function findTokenByAddress(chain: RoutesSupportedChainId, address: string): MyTokenConfig | undefined {
return getAvailableStables(chain).find((token) => token.address === address)
}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const replaceBigIntsWithStrings = (obj: any): any => {
+ if (typeof obj === "bigint") return obj.toString();
+ if (Array.isArray(obj))
+ return obj.map((x) => replaceBigIntsWithStrings(x));
+ if (obj && typeof obj === "object")
+ return Object.fromEntries(
+ Object.entries(obj).map(([k, v]) => [k, replaceBigIntsWithStrings(v)]),
+ );
+ return obj;
+};
diff --git a/apps/sdk-demo/src/utils/permit.ts b/apps/sdk-demo/src/utils/permit.ts
new file mode 100644
index 0000000..89acb75
--- /dev/null
+++ b/apps/sdk-demo/src/utils/permit.ts
@@ -0,0 +1,144 @@
+import { Hex } from "viem";
+import { signTypedData } from "@wagmi/core";
+import { config } from "../wagmi";
+import { Permit2DataDetails } from "@eco-foundation/routes-sdk";
+import { stableAddresses } from "@eco-foundation/routes-sdk";
+
+// Global permit2 address (same across all chains)
+export const PERMIT2_ADDRESS: Hex = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
+
+/**
+ * Checks if a token is USDC
+ * @param address Token address
+ * @returns Boolean indicating if token is USDC
+ */
+export function isUSDC(address: Hex): boolean {
+ // Check against known USDC addresses by lowercase comparison
+ const usdcAddresses = Object.values(stableAddresses)
+ .map(tokens => tokens.USDC)
+ .filter(Boolean)
+ .map(addr => addr?.toLowerCase());
+
+ return usdcAddresses.includes(address.toLowerCase());
+}
+
+/**
+ * Signs a permit for a given ERC-2612 ERC20 token using wagmi's signTypedData
+ */
+export type SignPermitProps = {
+ /** Address of the token to approve */
+ contractAddress: Hex
+ /** Name of the token to approve.
+ * Corresponds to the `name` method on the ERC-20 contract. Please note this must match exactly byte-for-byte */
+ erc20Name: string
+ /** Owner of the tokens. Usually the currently connected address. */
+ ownerAddress: Hex
+ /** Address to grant allowance to */
+ spenderAddress: Hex
+ /** Expiration of this approval, in SECONDS */
+ deadline: bigint
+ /** Numerical chainId of the token contract */
+ chainId: number
+ /** Defaults to 1. Some tokens need a different version, check the [PERMIT INFORMATION](https://github.com/vacekj/wagmi-permit/blob/main/PERMIT.md) for more information */
+ permitVersion?: string
+ /** Permit nonce for the specific address and token contract. You can get the nonce from the `nonces` method on the token contract. */
+ nonce: bigint
+ /** Amount to approve */
+ value: bigint
+}
+
+export async function signPermit({
+ contractAddress,
+ erc20Name,
+ ownerAddress,
+ spenderAddress,
+ value,
+ deadline,
+ nonce,
+ chainId,
+ permitVersion = "1",
+}: SignPermitProps): Promise {
+ const types = {
+ Permit: [
+ { name: 'owner', type: 'address' },
+ { name: 'spender', type: 'address' },
+ { name: 'value', type: 'uint256' },
+ { name: 'nonce', type: 'uint256' },
+ { name: 'deadline', type: 'uint256' },
+ ],
+ };
+
+ const domainData = {
+ name: erc20Name,
+ version: permitVersion,
+ chainId: chainId,
+ verifyingContract: contractAddress,
+ };
+
+ const message = {
+ owner: ownerAddress,
+ spender: spenderAddress,
+ value,
+ nonce,
+ deadline,
+ };
+
+ return signTypedData(config, {
+ domain: domainData,
+ message,
+ primaryType: 'Permit',
+ types,
+ });
+}
+
+export type SignPermit2Props = {
+ chainId: number
+ expiration: bigint
+ spender: Hex
+ details: Permit2DataDetails[]
+}
+
+export async function signPermit2({
+ chainId,
+ expiration,
+ spender,
+ details,
+}: SignPermit2Props): Promise {
+ const types = {
+ PermitDetails: [
+ { name: 'token', type: 'address' },
+ { name: 'amount', type: 'uint160' },
+ { name: 'expiration', type: 'uint48' },
+ { name: 'nonce', type: 'uint48' },
+ ],
+ PermitSingle: [
+ { name: 'details', type: 'PermitDetails' },
+ { name: 'spender', type: 'address' },
+ { name: 'sigDeadline', type: 'uint256' },
+ ],
+ PermitBatch: [
+ { name: 'details', type: 'PermitDetails[]' },
+ { name: 'spender', type: 'address' },
+ { name: 'sigDeadline', type: 'uint256' }
+ ],
+ };
+
+ const domainData = {
+ name: "Permit2",
+ chainId: chainId,
+ verifyingContract: PERMIT2_ADDRESS,
+ };
+
+ const message = {
+ details: details.length > 1 ? details : details[0],
+ spender,
+ sigDeadline: expiration,
+ };
+
+ return signTypedData(config, {
+ domain: domainData,
+ message,
+ primaryType: details.length > 1 ? 'PermitBatch' : 'PermitSingle',
+ types,
+ });
+}
diff --git a/apps/sdk-demo/src/views/demo/components/create-intent.tsx b/apps/sdk-demo/src/views/demo/components/create-intent.tsx
index 8b0982f..caca137 100644
--- a/apps/sdk-demo/src/views/demo/components/create-intent.tsx
+++ b/apps/sdk-demo/src/views/demo/components/create-intent.tsx
@@ -9,12 +9,16 @@ import { chains } from "../../../wagmi";
type Props = {
routesService: RoutesService,
onNewIntent: (intent: IntentType, isNative: boolean) => void,
+ quoteType: "receive" | "spend",
+ setQuoteType: (type: "receive" | "spend") => void
onIntentCleared?: () => void
}
export default function CreateIntent({
routesService,
onNewIntent,
+ quoteType,
+ setQuoteType,
onIntentCleared
}: Props) {
const { address } = useAccount();
@@ -106,7 +110,7 @@ export default function CreateIntent({
creator: address,
originChainID: effectiveOriginChain,
spendingToken: originToken,
- spendingTokenLimit: balance,
+ spendingTokenLimit: quoteType === "receive" ? balance : BigInt(amount),
destinationChainID: effectiveDestinationChain,
receivingToken: destinationToken,
amount: BigInt(amount),
@@ -127,7 +131,7 @@ export default function CreateIntent({
setIsIntentValid(false)
onIntentCleared?.()
}
- }, [isNativeIntent, balance, nativeBalance, address, effectiveOriginChain, originToken, effectiveDestinationChain, destinationToken, amount, recipient, prover, onNewIntent, onIntentCleared, routesService]);
+ }, [isNativeIntent, balance, nativeBalance, address, effectiveOriginChain, originToken, effectiveDestinationChain, destinationToken, amount, recipient, prover, onNewIntent, onIntentCleared, routesService, quoteType]);
const originTokensAvailable = useMemo(() => effectiveOriginChain ? getAvailableStables(effectiveOriginChain) : [], [effectiveOriginChain]);
const destinationTokensAvailable = useMemo(() => effectiveDestinationChain ? getAvailableStables(effectiveDestinationChain) : [], [effectiveDestinationChain]);
@@ -301,13 +305,35 @@ export default function CreateIntent({
>
)}
-
- Desired Amount {isNativeIntent ? '(in wei)' : ''}
+ Amount {isNativeIntent ? '(in wei)' : ''}
setAmount(e.target.value)} />
{amount && isNativeIntent && ({formatUnits(BigInt(amount), 18)} {nativeBalance?.symbol || 'ETH'})}
{amount && !isNativeIntent && decimals && ({formatUnits(BigInt(amount), decimals)})}
-
+
+
+ |
+
+
+