Skip to content
Closed
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
196 changes: 196 additions & 0 deletions scripts/compare-rates.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env node

// Compare Bridge vs Frankfurter (and optionally local /api/exchange-rate) rates
// Usage examples:
// node scripts/compare-rates.mjs
// node scripts/compare-rates.mjs --pairs USD:EUR,USD:MXN --api http://localhost:3000/api/exchange-rate
// BRIDGE_API_KEY=xxx node scripts/compare-rates.mjs

import { readFileSync } from 'fs'
import { resolve } from 'path'

// Load .env files
function loadEnv() {
const envFiles = ['.env.local', '.env']
for (const file of envFiles) {
try {
const envPath = resolve(file)
const envContent = readFileSync(envPath, 'utf8')
envContent.split('\n').forEach((line) => {
const trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=')
if (key && valueParts.length > 0) {
const value = valueParts.join('=').replace(/^["']|["']$/g, '')
if (!process.env[key]) {
process.env[key] = value
}
}
}
})
console.log(`Loaded environment from ${file}`)
break
} catch (e) {
// File doesn't exist, continue to next
}
}
}

loadEnv()

const DEFAULT_PAIRS = [
['USD', 'EUR'],
['USD', 'MXN'],
['USD', 'BRL'],
['EUR', 'USD'],
['EUR', 'GBP'],
]

const params = process.argv.slice(2)
const pairsArg = getArg('--pairs')
const apiArg = getArg('--api')

const PAIRS = pairsArg ? pairsArg.split(',').map((p) => p.split(':').map((s) => s.trim().toUpperCase())) : DEFAULT_PAIRS

const BRIDGE_API_KEY = process.env.BRIDGE_API_KEY

function getArg(name) {
const i = params.indexOf(name)
if (i === -1) return null
return params[i + 1] || null
}

async function fetchBridge(from, to) {
if (!BRIDGE_API_KEY) {
return { error: 'Missing BRIDGE_API_KEY' }
}
const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}`
const res = await fetch(url, {
method: 'GET',
headers: { 'Api-Key': BRIDGE_API_KEY },
})
if (!res.ok) {
return { error: `${res.status} ${res.statusText}` }
}
const data = await res.json()
const { midmarket_rate, buy_rate, sell_rate } = data || {}
return { midmarket_rate, buy_rate, sell_rate }
}

async function fetchFrankfurter(from, to) {
const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}`
const res = await fetch(url, { method: 'GET' })
if (!res.ok) {
return { error: `${res.status} ${res.statusText}` }
}
const data = await res.json()
const rate = data?.rates?.[to]
return { rate, rate_995: typeof rate === 'number' ? rate * 0.995 : undefined }
}

async function fetchLocalApi(from, to) {
if (!apiArg) return {}
try {
const url = `${apiArg}?from=${from}&to=${to}`
const res = await fetch(url, { method: 'GET' })
if (!res.ok) {
return { error: `${res.status} ${res.statusText}` }
}
const data = await res.json()
return { rate: data?.rate }
} catch (error) {
return { error: `Connection failed: ${error.message}` }
}
}

function fmt(n, digits = 6) {
return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(digits) : '-'
}

function bps(a, b) {
if (typeof a !== 'number' || typeof b !== 'number' || !Number.isFinite(a) || !Number.isFinite(b) || b === 0)
return '-'
const rel = (a / b - 1) * 10000
return `${rel.toFixed(1)} bps`
}

async function run() {
console.log('Comparing rates...')
if (!BRIDGE_API_KEY) {
console.warn('Warning: BRIDGE_API_KEY not set. Bridge calls will be skipped or return errors.')
}
if (apiArg) {
console.log(`Also querying local API: ${apiArg}`)
}

for (const [from, to] of PAIRS) {
const [bridge, frankData, local] = await Promise.all([
fetchBridge(from, to).catch((e) => ({ error: e?.message || String(e) })),
fetchFrankfurter(from, to).catch((e) => ({ error: e?.message || String(e) })),
fetchLocalApi(from, to).catch((e) => ({ error: e?.message || String(e) })),
])

const bridgeBuy = bridge?.buy_rate ? Number(bridge.buy_rate) : undefined
const bridgeMid = bridge?.midmarket_rate ? Number(bridge.midmarket_rate) : undefined
const bridgeSell = bridge?.sell_rate ? Number(bridge.sell_rate) : undefined
const frank = typeof frankData?.rate === 'number' ? frankData.rate : undefined
const frank995 = typeof frankData?.rate_995 === 'number' ? frankData.rate_995 : undefined
const localRate = typeof local?.rate === 'number' ? local.rate : undefined

console.log(`\nPair: ${from} -> ${to}`)
console.table([
{
source: 'Bridge',
buy: fmt(bridgeBuy),
mid: fmt(bridgeMid),
sell: fmt(bridgeSell),
note: bridge?.error || '',
},
{
source: 'Frankfurter',
rate: fmt(frank),
rate_995: fmt(frank995),
note: frankData?.error || '',
},
{
source: 'Local API',
rate: fmt(localRate),
note: local?.error || '',
},
])

// Delta analysis table
console.log(`\nDelta Analysis for ${from} -> ${to}:`)
console.table([
{
comparison: 'Mid vs Frankfurt',
delta: bps(bridgeMid, frank),
},
{
comparison: 'Mid vs Frankfurt×0.995',
delta: bps(bridgeMid, frank995),
},
{
comparison: 'Sell vs Frankfurt×0.995',
delta: bps(bridgeSell, frank995),
},
{
comparison: 'Sell vs Mid',
delta: bps(bridgeSell, bridgeMid),
},
{
comparison: 'Buy vs Mid',
delta: bps(bridgeBuy, bridgeMid),
},
{
comparison: 'Local vs Frankfurt×0.995',
delta: bps(localRate, frank995),
},
])
}
}

run().catch((e) => {
console.error(e)
process.exit(1)
})
2 changes: 1 addition & 1 deletion src/app/(mobile-ui)/claim/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export async function generateMetadata({
description,
...(siteUrl ? { metadataBase: new URL(siteUrl) } : {}),
icons: {
icon: '/logo-favicon.png',
icon: '/favicon.ico',
},
openGraph: {
title,
Expand Down
8 changes: 4 additions & 4 deletions src/app/(mobile-ui)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import { AccountType } from '@/interfaces'
import { formatUnits } from 'viem'
import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants'
import { PostSignupActionManager } from '@/components/Global/PostSignupActionManager'
import { useGuestFlow } from '@/context/GuestFlowContext'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { useClaimBankFlow } from '@/context/ClaimBankFlowContext'

const BALANCE_WARNING_THRESHOLD = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD ?? '500')
const BALANCE_WARNING_EXPIRY = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_EXPIRY ?? '1814400') // 21 days in seconds
Expand All @@ -48,7 +48,7 @@ export default function Home() {
const { balance, address, isFetchingBalance, isFetchingRewardBalance } = useWallet()
const { rewardWalletBalance } = useWalletStore()
const [isRewardsModalOpen, setIsRewardsModalOpen] = useState(false)
const { resetGuestFlow } = useGuestFlow()
const { resetFlow: resetClaimBankFlow } = useClaimBankFlow()
const { resetWithdrawFlow } = useWithdrawFlow()
const [isBalanceHidden, setIsBalanceHidden] = useState(() => {
const prefs = getUserPreferences()
Expand Down Expand Up @@ -84,9 +84,9 @@ export default function Home() {
const isLoading = isFetchingUser && !username

useEffect(() => {
resetGuestFlow()
resetClaimBankFlow()
resetWithdrawFlow()
}, [resetGuestFlow, resetWithdrawFlow])
}, [resetClaimBankFlow, resetWithdrawFlow])

useEffect(() => {
// We have some users that didn't have the peanut wallet created
Expand Down
2 changes: 1 addition & 1 deletion src/app/(mobile-ui)/request/pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function generateMetadata({
title,
description: 'Request cryptocurrency from friends, family, or anyone else using Peanut on any chain.',
icons: {
icon: '/logo-favicon.png',
icon: '/favicon.ico',
},
openGraph: {
images: [
Expand Down
2 changes: 1 addition & 1 deletion src/app/[...recipient]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export async function generateMetadata({ params, searchParams }: any) {
description,
...(siteUrl ? { metadataBase: new URL(siteUrl) } : {}),
icons: {
icon: '/logo-favicon.png',
icon: '/favicon.ico',
},
openGraph: {
title,
Expand Down
17 changes: 9 additions & 8 deletions src/app/actions/squid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ const getSquidTokensCache = unstable_cache(
)

export const getSquidChainsAndTokens = unstable_cache(
async (): Promise<Record<string, interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] }>> => {
async (): Promise<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
> => {
const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])

const chainsById = chains.reduce<Record<string, interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] }>>(
(acc, chain) => {
acc[chain.chainId] = { ...chain, tokens: [] }
return acc
},
{}
)
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
return acc
}, {})

Comment on lines +37 to 48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unsound cast for networkName risks undefined at runtime — derive and set explicitly

{ ...(chain as ISquidChain & { networkName: string }), tokens: [] } trusts that SDK chains already include networkName. If the SDK doesn’t yet ship this field, downstream UI reading chain.networkName will render undefined.

Set networkName explicitly with safe fallbacks:

 export const getSquidChainsAndTokens = unstable_cache(
   async (): Promise<
     Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
   > => {
     const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])

-    const chainsById = chains.reduce<
-      Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
-    >((acc, chain) => {
-      acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
-      return acc
-    }, {})
+    const chainsById = chains.reduce<
+      Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
+    >((acc, chain) => {
+      const anyChain = chain as any
+      const networkName: string =
+        anyChain.networkName ??
+        anyChain.axelarChainName ?? // legacy field seen in prior codebase
+        anyChain.name ??
+        String(chain.chainId)
+      acc[chain.chainId] = { ...(chain as interfaces.ISquidChain), networkName, tokens: [] }
+      return acc
+    }, {})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async (): Promise<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
> => {
const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])
const chainsById = chains.reduce<Record<string, interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] }>>(
(acc, chain) => {
acc[chain.chainId] = { ...chain, tokens: [] }
return acc
},
{}
)
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
return acc
}, {})
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
const anyChain = chain as any
const networkName: string =
anyChain.networkName ??
anyChain.axelarChainName ?? // legacy field seen in prior codebase
anyChain.name ??
String(chain.chainId)
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain), networkName, tokens: [] }
return acc
}, {})
🤖 Prompt for AI Agents
In src/app/actions/squid.ts around lines 37 to 48, the code unsafely casts chain
to include networkName which may be missing at runtime; instead explicitly set
networkName with a safe fallback when building chainsById — e.g. avoid the cast
and construct the object by spreading the known chain properties and adding
networkName: chain.networkName ?? chain.name ?? String(chain.chainId) (or a
lookup map if you have canonical names), and keep tokens: [] so downstream
consumers never see undefined.

tokens.forEach((token) => {
if (token.active && token.chainId in chainsById) {
Expand Down
Loading
Loading