Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/keychain/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self' https:"
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' https://fonts.googleapis.com 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self' https:"
/>

<link rel="icon" type="image/png" href="/favicon-48x48.png" sizes="48x48" />
Expand Down
91 changes: 55 additions & 36 deletions packages/keychain/src/components/provider/tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { Price } from "@cartridge/ui/utils/api/cartridge";
import { useQuery } from "react-query";
import { getChecksumAddress } from "starknet";
import { fetchSwapQuoteInUsdc } from "@/utils/ekubo";
import { fetchSwapQuoteInUsdc, type ExtendedError } from "@/utils/ekubo";

export const DEFAULT_TOKENS = [
{
Expand Down Expand Up @@ -207,61 +207,80 @@ export function TokensProvider({
},
);

const [invalidatedAddresses, setInvalidatedAddresses] = useState<string[]>(
[],
);

// Fetch prices using Ekubo
const {
data: priceData,
isLoading: isPriceLoading,
error: priceError,
} = useQuery(
["token-prices-ekubo", debouncedAddresses.join(","), chainId],
[
"token-prices-ekubo",
debouncedAddresses.join(","),
invalidatedAddresses.join(","),
chainId,
],
async () => {
if (debouncedAddresses.length === 0 || !chainId) return [];

const USDC_DECIMALS = 6;
const ONE_USDC = BigInt(10 ** USDC_DECIMALS); // 1 USDC

const prices = await Promise.allSettled(
debouncedAddresses.map(async (address) => {
try {
const checksumAddress = getChecksumAddress(address);

// Get token decimals - tokens should exist by the time price query runs
const token = tokens[checksumAddress];
const tokenDecimals = token?.decimals ?? 18; // Default to 18 if not found
debouncedAddresses
.filter((address) => !invalidatedAddresses.includes(address))
.map(async (address) => {
try {
const checksumAddress = getChecksumAddress(address);

// Get token decimals - tokens should exist by the time price query runs
const token = tokens[checksumAddress];
const tokenDecimals = token?.decimals ?? 18; // Default to 18 if not found

// USDC price is always 1:1
if (
checksumAddress === getChecksumAddress(USDC_CONTRACT_ADDRESS)
) {
return {
base: address,
amount: String(ONE_USDC),
decimals: USDC_DECIMALS,
quote: "USDC",
};
}

// Fetch quote from Ekubo: how many token base units = 1 USDC
const tokenAmount = await fetchSwapQuoteInUsdc(
address,
BigInt(10 ** (tokenDecimals + 1)),
chainId,
);

// USDC price is always 1:1
if (checksumAddress === getChecksumAddress(USDC_CONTRACT_ADDRESS)) {
return {
base: address,
amount: String(ONE_USDC),
amount: String(tokenAmount / BigInt(10)),
decimals: USDC_DECIMALS,
quote: "USDC",
};
} catch (error) {
const ekuboError = error as ExtendedError;
// Only log non-429 errors (rate limiting is expected)
const is429 = ekuboError.message.includes("429");
if (!is429) {
console.warn(
`Failed to fetch price for ${address}:`,
ekuboError.message,
);
}
if (ekuboError.noRetry) {
setInvalidatedAddresses((prev) => [...prev, address]);
}
return null;
}

// Fetch quote from Ekubo: how many token base units = 1 USDC
const tokenAmount = await fetchSwapQuoteInUsdc(
address,
BigInt(10 ** (tokenDecimals + 1)),
chainId,
);

return {
base: address,
amount: String(tokenAmount / BigInt(10)),
decimals: USDC_DECIMALS,
quote: "USDC",
};
} catch (error) {
// Only log non-429 errors (rate limiting is expected)
const is429 =
error instanceof Error && error.message.includes("429");
if (!is429) {
console.error(`Failed to fetch price for ${address}:`, error);
}
return null;
}
}),
}),
);

return prices
Expand Down
8 changes: 6 additions & 2 deletions packages/keychain/src/utils/ekubo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ExternalWalletError } from "@/utils/errors";
/**
* Extended Error type with retry control
*/
interface ExtendedError extends Error {
export interface ExtendedError extends Error {
noRetry?: boolean;
}

Expand Down Expand Up @@ -319,7 +319,11 @@ export async function fetchSwapQuoteInUsdc(
): Promise<bigint> {
const usdcAddress = USDC_ADDRESSES[chainId];
if (!usdcAddress) {
throw new Error(`USDC address not found for chain ID: ${chainId}`);
const error = new Error(
`USDC address not found for chain ID: ${chainId}`,
) as ExtendedError;
error.noRetry = true;
throw error;
}
const quote = await fetchSwapQuote(
amount,
Expand Down
2 changes: 1 addition & 1 deletion packages/keychain/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self' https:; frame-ancestors 'self' https: http://localhost:* http://127.0.0.1:* capacitor:;"
"value": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' https://fonts.googleapis.com 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self' https:; frame-ancestors 'self' https: http://localhost:* http://127.0.0.1:* capacitor:;"
},
{
"key": "X-Content-Type-Options",
Expand Down
Loading