From 2dd354a8a5d60f5678baba6c59b104d19d7bebb8 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Mon, 19 Feb 2024 15:42:22 -0800 Subject: [PATCH] Add Lifi reporting plugin --- src/demo/partners.ts | 4 + src/partners/lifi.ts | 190 +++++++++++++++++++++++++++++++++++++++++++ src/queryEngine.ts | 2 + 3 files changed, 196 insertions(+) create mode 100644 src/partners/lifi.ts diff --git a/src/demo/partners.ts b/src/demo/partners.ts index c1b549d5..c391340e 100644 --- a/src/demo/partners.ts +++ b/src/demo/partners.ts @@ -81,6 +81,10 @@ export default { type: 'fiat', color: '#2551E8' }, + lifi: { + type: 'swap', + color: '#EBB8FA' + }, moonpay: { type: 'fiat', color: '#7214F5' diff --git a/src/partners/lifi.ts b/src/partners/lifi.ts new file mode 100644 index 00000000..3d066d7d --- /dev/null +++ b/src/partners/lifi.ts @@ -0,0 +1,190 @@ +import { + asArray, + asMaybe, + asNumber, + asObject, + asOptional, + asString, + asValue +} from 'cleaners' + +import { + asStandardPluginParams, + PartnerPlugin, + PluginParams, + PluginResult, + StandardTx, + Status +} from '../types' +import { datelog, retryFetch, smartIsoDateFromTimestamp, snooze } from '../util' + +const asStatuses = asMaybe(asValue('DONE'), 'other') +const asToken = asObject({ + // address: asString, + // chainId: asNumber, + symbol: asString, + decimals: asNumber + // name: asString, + // coinKey: asString, + // logoURI: asString, + // priceUSD: asString +}) + +const asTransaction = asObject({ + txHash: asString, + // txLink: asString, + amount: asString, + token: asToken, + // chainId: asNumber, + // gasPrice: asString, + // gasUsed: asString, + // gasToken: asToken, + // gasAmount: asString, + // gasAmountUSD: asString, + amountUSD: asOptional(asString), + // value: asString, + timestamp: asOptional(asNumber) +}) + +const asTransfer = asObject({ + // transactionId: asString, + sending: asTransaction, + receiving: asTransaction, + // lifiExplorerLink: asString, + // fromAddress: asString, + toAddress: asString, + // tool: asString, + status: asString + // substatus: asString, + // substatusMessage: asString, + // metadata: asObject({ + // integrator: asString + // }) +}) + +// Define the cleaner for the whole JSON +const asTransfersResult = asObject({ + transfers: asArray(asTransfer) +}) + +type Transfer = ReturnType +type PartnerStatuses = ReturnType + +const MAX_RETRIES = 5 +const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 30 // 30 days +const QUERY_TIME_BLOCK_MS = QUERY_LOOKBACK + +const statusMap: { [key in PartnerStatuses]: Status } = { + DONE: 'complete', + other: 'other' +} + +export async function queryLifi( + pluginParams: PluginParams +): Promise { + const { settings, apiKeys } = asStandardPluginParams(pluginParams) + const { apiKey } = apiKeys + let { latestIsoDate } = settings + + if (latestIsoDate === '2018-01-01T00:00:00.000Z') { + latestIsoDate = new Date('2023-01-01T00:00:00.000Z').toISOString() + } + + let lastCheckedTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK + if (lastCheckedTimestamp < 0) lastCheckedTimestamp = 0 + + const ssFormatTxs: StandardTx[] = [] + let retry = 0 + let startTime = lastCheckedTimestamp + + while (true) { + const endTime = startTime + QUERY_TIME_BLOCK_MS + const now = Date.now() + + const startTimeS = startTime / 1000 + const endTimeS = endTime / 1000 + + const url = `https://li.quest/v1/analytics/transfers?integrator=${apiKey}&fromTimestamp=${startTimeS}&toTimestamp=${endTimeS}` + try { + const response = await retryFetch(url) + if (!response.ok) { + const text = await response.text() + throw new Error(text) + } + const jsonObj = await response.json() + const transferResults = asTransfersResult(jsonObj) + for (const tx of transferResults.transfers) { + const txts = tx.receiving.timestamp ?? tx.sending.timestamp ?? 0 + if (txts === 0) { + throw new Error('No timestamp') + } + const { isoDate, timestamp } = smartIsoDateFromTimestamp(txts) + + // Use biggystri + const depositAmount = + Number(tx.sending.amount) / 10 ** tx.sending.token.decimals + + const payoutAmount = + Number(tx.receiving.amount) / 10 ** tx.receiving.token.decimals + + const ssTx: StandardTx = { + status: statusMap[tx.status], + orderId: tx.sending.txHash, + depositTxid: tx.sending.txHash, + depositAddress: undefined, + depositCurrency: tx.sending.token.symbol, + depositAmount, + payoutTxid: undefined, + payoutAddress: tx.toAddress, + payoutCurrency: tx.receiving.token.symbol, + payoutAmount, + timestamp, + isoDate, + usdValue: Number( + tx.receiving.amountUSD ?? tx.sending.amountUSD ?? '-1' + ), + rawTx: tx + } + ssFormatTxs.push(ssTx) + if (ssTx.isoDate > latestIsoDate) { + latestIsoDate = ssTx.isoDate + } + } + const endDate = new Date(endTime) + startTime = endDate.getTime() + datelog( + `Lifi endDate:${new Date( + endDate + ).toISOString()} latestIsoDate:${latestIsoDate}` + ) + if (endTime > now) { + break + } + retry = 0 + } catch (e) { + datelog(e) + // Retry a few times with time delay to prevent throttling + retry++ + if (retry <= MAX_RETRIES) { + datelog(`Snoozing ${60 * retry}s`) + await snooze(61000 * retry) + } else { + // We can safely save our progress since we go from oldest to newest. + break + } + } + await snooze(3000) + } + + const out = { + settings: { latestIsoDate }, + transactions: ssFormatTxs + } + return out +} + +export const lifi: PartnerPlugin = { + queryFunc: queryLifi, + pluginName: 'Li.Fi', + pluginId: 'lifi' +} diff --git a/src/queryEngine.ts b/src/queryEngine.ts index 65a57f4d..ff091db0 100644 --- a/src/queryEngine.ts +++ b/src/queryEngine.ts @@ -19,6 +19,7 @@ import { ioniaGiftCards } from './partners/ioniagiftcard' import { ioniaVisaRewards } from './partners/ioniavisarewards' import { letsexchange } from './partners/letsexchange' import { libertyx } from './partners/libertyx' +import { lifi } from './partners/lifi' import { moonpay } from './partners/moonpay' import { paytrie } from './partners/paytrie' import { safello } from './partners/safello' @@ -52,6 +53,7 @@ const plugins = [ ioniaGiftCards, letsexchange, libertyx, + lifi, moonpay, paytrie, safello,