Skip to content

Commit a2eff8c

Browse files
committed
Fix MoonPay and Rango parsing.
Add EOS, xRPL, qtum, rvn, Dash, Tezos, and others. Also add card type.
1 parent 41deb64 commit a2eff8c

2 files changed

Lines changed: 71 additions & 9 deletions

File tree

src/partners/moonpay.ts

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,40 @@ const MOONPAY_NETWORK_TO_PLUGIN_ID: ChainNameToPluginIdMapping = {
3434
arbitrum: 'arbitrum',
3535
avalanche_c_chain: 'avalanche',
3636
base: 'base',
37+
// Moonpay `networkCode`: `bnb_chain` = Beacon Chain, `binance_smart_chain` = BSC.
38+
bnb_chain: 'binance',
3739
binance_smart_chain: 'binancesmartchain',
3840
bitcoin: 'bitcoin',
3941
bitcoin_cash: 'bitcoincash',
4042
cardano: 'cardano',
43+
celo: 'celo',
4144
cosmos: 'cosmoshub',
45+
dash: 'dash',
46+
digibyte: 'digibyte',
4247
dogecoin: 'dogecoin',
48+
eosio: 'eos',
4349
ethereum: 'ethereum',
4450
ethereum_classic: 'ethereumclassic',
51+
fantom: 'fantom',
52+
filecoin: 'filecoin',
4553
hedera: 'hedera',
4654
litecoin: 'litecoin',
55+
monero: 'monero',
4756
optimism: 'optimism',
57+
osmosis: 'osmosis',
58+
polkadot: 'polkadot',
4859
polygon: 'polygon',
60+
qtum: 'qtum',
61+
ravencoin: 'ravencoin',
4962
ripple: 'ripple',
63+
rsk: 'rsk',
5064
solana: 'solana',
5165
stellar: 'stellar',
5266
sui: 'sui',
67+
tezos: 'tezos',
5368
ton: 'ton',
5469
tron: 'tron',
70+
zcash: 'zcash',
5571
zksync: 'zksync'
5672
}
5773

@@ -145,13 +161,17 @@ const asMoonpayCurrency = asObject({
145161
metadata: asOptional(asMoonpayCurrencyMetadata)
146162
})
147163

148-
// Base cleaner with fields common to both buy and sell transactions
164+
// Base cleaner with fields common to both buy and sell transactions.
165+
// `country` is the only Moonpay-supplied country field (verified via
166+
// src/bin/moonpayCountryFieldSurvey.ts across 122k txs / 2 years), but is
167+
// optional because some legacy rows omit it.
149168
const asMoonpayTxBase = asObject({
150169
baseCurrency: asMoonpayCurrency,
151170
baseCurrencyAmount: asNumber,
152171
baseCurrencyId: asString,
153-
cardType: asOptional(asValue('apple_pay', 'google_pay')),
154-
country: asString,
172+
// apple_pay / google_pay with paymentMethod mobile_wallet; "card" with credit_debit_card
173+
cardType: asOptional(asValue('apple_pay', 'google_pay', 'card')),
174+
country: asOptional(asString),
155175
createdAt: asDate,
156176
id: asString,
157177
status: asString,
@@ -173,7 +193,9 @@ const asMoonpayBuyFields = asObject({
173193

174194
const asMoonpaySellFields = asObject({
175195
quoteCurrency: asMoonpayCurrency,
176-
quoteCurrencyAmount: asNumber
196+
// Pending sell txs may have null quoteCurrencyAmount until the deposit is
197+
// confirmed and the payout is calculated.
198+
quoteCurrencyAmount: asOptional(asNumber, 0)
177199
})
178200

179201
type MoonpayTxBase = ReturnType<typeof asMoonpayTxBase>
@@ -189,6 +211,11 @@ const asMoonpayResult = asArray(asUnknown)
189211

190212
const PARTNER_START_DATE = '2024-06-17T00:00:00.000Z'
191213
const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 7
214+
// Cap each queryFunc invocation to ~6 months of transaction data. The runner
215+
// snoozes between invocations and persists progress in between, so during
216+
// long backfills the work is split across multiple wakeups instead of one
217+
// invocation having to traverse years of weekly windows.
218+
const MAX_QUERY_RANGE = 1000 * 60 * 60 * 24 * 30 * 6
192219
const PER_REQUEST_LIMIT = 50
193220

194221
export async function queryMoonpay(
@@ -222,6 +249,13 @@ export async function queryMoonpay(
222249
).toISOString()
223250

224251
const isoNow = new Date().toISOString()
252+
// Cap this invocation to MAX_QUERY_RANGE past the entry latestIsoDate.
253+
// When we hit the cap we early-exit and let the next invocation resume
254+
// from the saved progress.
255+
const capIso = new Date(
256+
new Date(latestIsoDate).getTime() + MAX_QUERY_RANGE
257+
).toISOString()
258+
const targetIsoDate = capIso < isoNow ? capIso : isoNow
225259

226260
try {
227261
do {
@@ -291,8 +325,13 @@ export async function queryMoonpay(
291325
latestIsoDate = new Date(
292326
new Date(latestIsoDate).getTime() + QUERY_LOOKBACK
293327
).toISOString()
294-
} while (isoNow > latestIsoDate)
295-
latestIsoDate = isoNow
328+
} while (targetIsoDate > latestIsoDate)
329+
latestIsoDate = targetIsoDate
330+
if (targetIsoDate < isoNow) {
331+
log(
332+
`Early exit at 6-month cap: saving progress up until ${targetIsoDate} (current time: ${isoNow})`
333+
)
334+
}
296335
} catch (e) {
297336
log.error(`Error: ${e}`)
298337
log(`Saving progress up until ${queryIsoDate}`)
@@ -325,8 +364,23 @@ export function processMoonpayTx(rawTx: unknown): StandardTx {
325364
// Map Moonpay status to Edge status
326365
const status: Status = statusMap[tx.status] ?? 'other'
327366

328-
// Buy transactions have paymentMethod, sell transactions have payoutMethod
329-
const direction: 'buy' | 'sell' = tx.paymentMethod != null ? 'buy' : 'sell'
367+
// A completed transaction must have a finalized payout amount. If this
368+
// assertion ever trips, Moonpay has changed something about how completed
369+
// transactions are reported and the parser needs to be revisited.
370+
if (tx.quoteCurrencyAmount == null && tx.status === 'completed') {
371+
throw new Error(
372+
`Moonpay tx ${tx.id} is status=completed but has no quoteCurrencyAmount`
373+
)
374+
}
375+
376+
// Determine direction from baseCurrency.type:
377+
// fiat baseCurrency => buy (fiat in, crypto out)
378+
// crypto baseCurrency => sell (crypto in, fiat out)
379+
// Older buy txs from /v1/transactions can have paymentMethod=null
380+
// (e.g. legacy card payments with cardType="card"), so we cannot rely on
381+
// paymentMethod presence alone to distinguish buy vs sell.
382+
const direction: 'buy' | 'sell' =
383+
tx.baseCurrency.type === 'fiat' ? 'buy' : 'sell'
330384

331385
if (direction === 'buy') {
332386
const buyFields = asMoonpayBuyFields(rawTx)
@@ -418,9 +472,15 @@ function getFiatPaymentType(tx: MoonpayTxBase): FiatPaymentType | null {
418472
let paymentMethod: FiatPaymentType | null = null
419473
switch (tx.paymentMethod) {
420474
case undefined:
475+
// Legacy buy transactions can omit paymentMethod entirely. Fall back to
476+
// cardType which Moonpay set on older card payments.
477+
if (tx.cardType === 'card') return 'credit'
478+
if (tx.cardType === 'apple_pay') return 'applepay'
479+
if (tx.cardType === 'google_pay') return 'googlepay'
421480
return null
422481
case 'mobile_wallet':
423-
// Older versions of Moonpay data had a separate cardType field.
482+
// Moonpay uses cardType to distinguish wallet brands; plain cards use
483+
// paymentMethod credit_debit_card with cardType "card" (see paymentMethodMap).
424484
if (tx.cardType === 'apple_pay') {
425485
paymentMethod = 'applepay'
426486
} else if (tx.cardType === 'google_pay') {

src/partners/rango.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ const RANGO_BLOCKCHAIN_TO_PLUGIN_ID: Record<string, string> = {
119119
OSMOSIS: 'osmosis',
120120
POLYGON: 'polygon',
121121
SOLANA: 'solana',
122+
TON: 'ton',
122123
TRON: 'tron',
124+
XRPL: 'ripple',
123125
ZKSYNC: 'zksync'
124126
}
125127

0 commit comments

Comments
 (0)