Skip to content

Commit c0ccf95

Browse files
committed
fix: ARM zero-address activeMarket and persistent CoinGecko cache
1 parent d17708a commit c0ccf95

3 files changed

Lines changed: 105 additions & 27 deletions

File tree

src/mainnet/post-processors/ogn-daily-stats.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,17 @@ export const process = async (ctx: Context) => {
3232

3333
const ognDailyStats = [] as OGNDailyStat[]
3434

35-
const coingeckoData = await getCoingeckoData(ctx, {
36-
coinId: 'origin-protocol',
37-
vsCurrency: 'usd',
38-
})
35+
let coingeckoData: Awaited<ReturnType<typeof getCoingeckoData>> = {}
36+
try {
37+
coingeckoData = await getCoingeckoData(ctx, {
38+
coinId: 'origin-protocol',
39+
vsCurrency: 'usd',
40+
})
41+
} catch (err) {
42+
// First-time deploys may have neither fresh API data nor DB cache. Skip
43+
// the price-dependent fields rather than crashing the whole processor.
44+
console.warn(`OGN daily stats: coingecko unavailable (${(err as Error).message}); skipping price data`)
45+
}
3946

4047
for (const date of dates) {
4148
const newDailyStat = await createDailyStat(ctx, date)

src/templates/origin-arm/origin-arm.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,15 @@ export const createOriginARMProcessors = ({
264264
armContract.previewRedeem(10n ** 18n),
265265
marketFrom && block.header.height >= marketFrom ? armContract.activeMarket() : Promise.resolve(undefined),
266266
])
267-
const marketBalanceOf = activeMarket
268-
? await new erc20Abi.Contract(ctx, block.header, activeMarket).balanceOf(armAddress)
267+
// Guard against the zero address: an ARM may expose activeMarket()
268+
// from its deploy block but not have a market wired up until later.
269+
// Calling balanceOf on 0x0 returns empty bytes and crashes the decoder.
270+
const hasMarket = !!activeMarket && activeMarket !== ADDRESS_ZERO
271+
const marketBalanceOf = hasMarket
272+
? await new erc20Abi.Contract(ctx, block.header, activeMarket!).balanceOf(armAddress)
269273
: 0n
270-
const activeMarketContract = activeMarket
271-
? new originOsArmAbi.Contract(ctx, block.header, activeMarket)
274+
const activeMarketContract = hasMarket
275+
? new originOsArmAbi.Contract(ctx, block.header, activeMarket!)
272276
: undefined
273277
const marketAssets =
274278
activeMarketContract && marketBalanceOf > 0n

src/utils/coingecko2.ts

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,101 @@
1+
import { CoinGeckoCoinData } from '@model'
12
import { Context } from '@originprotocol/squid-utils'
23

3-
import { processCoingeckoData } from './coingecko'
4+
import { CoingeckoDataOutput, processCoingeckoData } from './coingecko'
45
import { queryClient } from './queryClient'
56

7+
type CoinId = 'origin-dollar' | 'origin-ether' | 'super-oeth' | 'origin-dollar-governance' | 'origin-protocol'
8+
9+
// Maps the CoinGecko coinId to the `product` value coingeckoProcessor writes
10+
// into CoinGeckoCoinData. Used for the DB fallback below.
11+
const COIN_TO_PRODUCT: Record<CoinId, string> = {
12+
'origin-protocol': 'OGN',
13+
'origin-dollar': 'OUSD',
14+
'origin-ether': 'OETH',
15+
'super-oeth': 'superOETHb',
16+
'origin-dollar-governance': 'OGV',
17+
}
18+
19+
async function readCachedData(
20+
ctx: Context,
21+
coinId: CoinId,
22+
vsCurrency: 'usd' | 'eth',
23+
): Promise<CoingeckoDataOutput | null> {
24+
const product = COIN_TO_PRODUCT[coinId]
25+
const rows = await ctx.store.find(CoinGeckoCoinData, {
26+
where: { product, vsCurrency: vsCurrency.toUpperCase() as 'USD' | 'ETH' },
27+
})
28+
if (rows.length === 0) return null
29+
const result: CoingeckoDataOutput = {}
30+
for (const row of rows) {
31+
result[row.date] = {
32+
prices: row.price,
33+
market_caps: row.marketCap,
34+
total_volumes: row.tradingVolume,
35+
}
36+
}
37+
return result
38+
}
39+
40+
// Persistent throttle: if today's row exists in the DB we already have
41+
// today's data, so skip the API entirely. Survives process restarts (unlike
42+
// the in-memory throttle in coingeckoProcessor) and amortizes nicely across
43+
// the 4-coin burst that runs every batch during catch-up.
44+
async function todayCached(ctx: Context, coinId: CoinId, vsCurrency: 'usd' | 'eth'): Promise<boolean> {
45+
const product = COIN_TO_PRODUCT[coinId]
46+
const today = new Date().toISOString().slice(0, 10)
47+
const row = await ctx.store.get(CoinGeckoCoinData, `${today}-${product}`)
48+
return row != null
49+
}
50+
651
export async function getCoingeckoData(
752
ctx: Context,
853
props: {
9-
coinId: 'origin-dollar' | 'origin-ether' | 'super-oeth' | 'origin-dollar-governance' | 'origin-protocol'
54+
coinId: CoinId
1055
vsCurrency?: 'eth' | 'usd'
1156
startTimestamp?: number
1257
},
1358
) {
1459
const vsCurrency = props.vsCurrency || 'usd'
60+
61+
// If today's data is already cached, return the DB rows without touching
62+
// the API. Saves one fresh API call per coin per restart during catch-up.
63+
if (await todayCached(ctx, props.coinId, vsCurrency)) {
64+
const cached = await readCachedData(ctx, props.coinId, vsCurrency)
65+
if (cached) return cached
66+
}
67+
1568
const coingeckoURL = `https://api.coingecko.com/api/v3/coins/${props.coinId}/market_chart?vs_currency=${vsCurrency}&days=365&interval=daily&precision=18`
16-
const coingeckoJson = await queryClient.fetchQuery({
17-
queryKey: [coingeckoURL, new Date().toISOString().slice(0, 10)],
18-
queryFn: async () => {
19-
console.log('Fetching Coingecko market data')
20-
const response = await fetch(coingeckoURL)
21-
if (response.status === 429) {
22-
throw new Error('Coingecko rate limited')
23-
}
24-
const result = await response.json()
25-
console.log(`Found ${result.prices.length} prices`)
26-
return result
27-
},
28-
29-
staleTime: 600_000, // 10 minutes
30-
})
69+
try {
70+
const coingeckoJson = await queryClient.fetchQuery({
71+
queryKey: [coingeckoURL, new Date().toISOString().slice(0, 10)],
72+
queryFn: async () => {
73+
console.log('Fetching Coingecko market data')
74+
const response = await fetch(coingeckoURL)
75+
if (response.status === 429) {
76+
throw new Error('Coingecko rate limited')
77+
}
78+
const result = await response.json()
79+
console.log(`Found ${result.prices.length} prices`)
80+
return result
81+
},
82+
staleTime: 600_000, // 10 minutes
83+
// Don't burn rate-limit budget on retries — if we're 429'd, retries 429 too.
84+
retry: 0,
85+
})
3186

32-
const result = processCoingeckoData(coingeckoJson)
33-
return result
87+
return processCoingeckoData(coingeckoJson)
88+
} catch (err) {
89+
// API failed (rate limit, network, etc.). Fall back to whatever the
90+
// coingeckoProcessor last persisted — better than crashing the processor.
91+
const cached = await readCachedData(ctx, props.coinId, vsCurrency)
92+
if (cached) {
93+
console.log(
94+
`Coingecko fetch failed for ${props.coinId} (${(err as Error).message}); ` +
95+
`using ${Object.keys(cached).length} cached rows from DB`,
96+
)
97+
return cached
98+
}
99+
throw err
100+
}
34101
}

0 commit comments

Comments
 (0)