@@ -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.
149168const 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
174194const 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
179201type MoonpayTxBase = ReturnType < typeof asMoonpayTxBase >
@@ -189,6 +211,11 @@ const asMoonpayResult = asArray(asUnknown)
189211
190212const PARTNER_START_DATE = '2024-06-17T00:00:00.000Z'
191213const 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
192219const PER_REQUEST_LIMIT = 50
193220
194221export 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' ) {
0 commit comments