From 4719e9549b252856035ddad0467981414fc240fb Mon Sep 17 00:00:00 2001 From: xgram Date: Mon, 27 Oct 2025 07:44:15 +0000 Subject: [PATCH 1/3] + partner --- src/partners/xgram.ts | 163 ++++++++++++++++++++++++++++++++++++++++++ src/queryEngine.ts | 6 +- 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/partners/xgram.ts diff --git a/src/partners/xgram.ts b/src/partners/xgram.ts new file mode 100644 index 00000000..2ac92c79 --- /dev/null +++ b/src/partners/xgram.ts @@ -0,0 +1,163 @@ +import { + asArray, + asMaybe, + asNumber, + asObject, + asString, + asUnknown, + asValue +} from 'cleaners' + +import { + asStandardPluginParams, + PartnerPlugin, + PluginParams, + PluginResult, + StandardTx, + Status +} from '../types' +import { datelog, retryFetch, snooze } from '../util' + +const asXgramStatus = asMaybe( + asValue('finished', 'waiting', 'time_expired'), + 'other' +) + +const asXgramTx = asObject({ + date: asString, + id: asString, + status: asString, + amountFrom: asMaybe(asNumber, null), + amountTo: asMaybe(asNumber, null), + depositAddress: asString, + depositHash: asMaybe(asString, undefined), + depositTag: asMaybe(asString, null), + destinationAddress: asString, + destinationTag: asMaybe(asString, null), + expectedAmountFrom: asNumber, + expectedAmountTo: asNumber, + from: asString, + refundAddress: asMaybe(asString, null), + refundTag: asMaybe(asString, null), + to: asString, + txId: asMaybe(asString, undefined) +}) + +const asXgramResult = asObject({ exchanges: asArray(asUnknown) }) + +type XgramTxTx = ReturnType +type XgramStatus = ReturnType + +const MAX_RETRIES = 5 +const LIMIT = 5 +const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 5 // 5 days + +const statusMap: { [key in XgramStatus]: Status } = { + finished: 'complete', + waiting: 'pending', + time_expired: 'expired', + other: 'other' +} + +export const queryXgram = async ( + pluginParams: PluginParams +): Promise => { + const { settings, apiKeys } = asStandardPluginParams(pluginParams) + const { apiKey } = apiKeys + let { latestIsoDate } = settings + + if (apiKey == null) { + return { settings: { latestIsoDate }, transactions: [] } + } + + const standardTxs: StandardTx[] = [] + let previousTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK + if (previousTimestamp < 0) previousTimestamp = 0 + const previousLatestIsoDate = new Date(previousTimestamp).toISOString() + + let offset = 1 + let retry = 0 + while (true) { + const url = `https://xgram.io/api/v1/exchange-history?page=${offset}&limit=${LIMIT}` + try { + const response = await retryFetch(url, { + method: 'GET', + headers: { + 'x-api-key': apiKey, + 'Content-Type': 'application/json' + } + }) + const result = await response.json() + const txs = asXgramResult(result).exchanges + + if (txs.length === 0) { + break + } + for (const rawTx of txs) { + const standardTx = processXgramTx(rawTx) + standardTxs.push(standardTx) + if (standardTx.isoDate > latestIsoDate) { + latestIsoDate = standardTx.isoDate + } + } + datelog(`Xgram offset ${offset} latestIsoDate ${latestIsoDate}`) + offset += txs.length + retry = 0 + } catch (e) { + datelog(e) + // Retry a few times with time delay to prevent throttling + retry++ + if (retry <= MAX_RETRIES) { + datelog(`Snoozing ${5 * retry}s`) + await snooze(5000 * retry) + } else { + // We can safely save our progress since we go from oldest to newest. + break + } + } + } + const out: PluginResult = { + settings: { latestIsoDate }, + transactions: standardTxs + } + return out +} + +export const xgram: PartnerPlugin = { + // queryFunc will take PluginSettings as arg and return PluginResult + queryFunc: queryXgram, + // results in a PluginResult + pluginName: 'xgram', + pluginId: 'xgram' +} + +export function processXgramTx(rawTx: unknown): StandardTx { + const tx: XgramTxTx = asXgramTx(rawTx) + const [date, time] = tx.date.split(" "); +const [day, month, year] = date.split("."); +const dateN = new Date(`${year}-${month}-${day}T${time}`) +const isoString = dateN.toISOString(); + const timestamp = dateN.getTime() / 1000 + const standardTx: StandardTx = { + status: statusMap[tx.status], + orderId: tx.id, + countryCode: null, + depositTxid: tx.depositHash, + depositAddress: tx.depositAddress, + depositCurrency: tx.to.toUpperCase(), + depositAmount: tx.amountTo ?? tx.expectedAmountTo ?? 0, + direction: null, + exchangeType: 'swap', + paymentType: null, + payoutTxid: tx.txId, + payoutAddress: tx.destinationAddress, + payoutCurrency: tx.from.toUpperCase(), + payoutAmount: tx.amountFrom ?? tx.expectedAmountFrom ?? 0, + timestamp, + isoDate: isoString, + usdValue: -1, + rawTx + } + + return standardTx +} diff --git a/src/queryEngine.ts b/src/queryEngine.ts index 5c3dada6..ae3dba9b 100644 --- a/src/queryEngine.ts +++ b/src/queryEngine.ts @@ -32,6 +32,7 @@ import { maya, thorchain } from './partners/thorchain' import { transak } from './partners/transak' import { wyre } from './partners/wyre' import { xanpool } from './partners/xanpool' +import { xgram } from './partners/xgram' import { asApp, asApps, asProgressSettings, DbTx, StandardTx } from './types' import { datelog, promiseTimeout, standardizeNames } from './util' @@ -68,7 +69,8 @@ const plugins = [ thorchain, transak, wyre, - xanpool + xanpool, + xgram ] const QUERY_FREQ_MS = 60 * 1000 const MAX_CONCURRENT_QUERIES = 3 @@ -257,7 +259,7 @@ async function runPlugin( console.log(e) } }) - + // initialize progress settings if unrecognized format let progressSettings: ReturnType try { From 5d366f6f99b353ff0da3828c8a6974de3cfbe6d3 Mon Sep 17 00:00:00 2001 From: xgram Date: Mon, 27 Oct 2025 07:47:05 +0000 Subject: [PATCH 2/3] + new partner xgram --- src/partners/xgram.ts | 10 +++++----- src/queryEngine.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/partners/xgram.ts b/src/partners/xgram.ts index 2ac92c79..f41ca7c8 100644 --- a/src/partners/xgram.ts +++ b/src/partners/xgram.ts @@ -30,7 +30,7 @@ const asXgramTx = asObject({ amountFrom: asMaybe(asNumber, null), amountTo: asMaybe(asNumber, null), depositAddress: asString, - depositHash: asMaybe(asString, undefined), + depositHash: asMaybe(asString, undefined), depositTag: asMaybe(asString, null), destinationAddress: asString, destinationTag: asMaybe(asString, null), @@ -133,10 +133,10 @@ export const xgram: PartnerPlugin = { export function processXgramTx(rawTx: unknown): StandardTx { const tx: XgramTxTx = asXgramTx(rawTx) - const [date, time] = tx.date.split(" "); -const [day, month, year] = date.split("."); -const dateN = new Date(`${year}-${month}-${day}T${time}`) -const isoString = dateN.toISOString(); + const [date, time] = tx.date.split(' ') + const [day, month, year] = date.split('.') + const dateN = new Date(`${year}-${month}-${day}T${time}`) + const isoString = dateN.toISOString() const timestamp = dateN.getTime() / 1000 const standardTx: StandardTx = { status: statusMap[tx.status], diff --git a/src/queryEngine.ts b/src/queryEngine.ts index ae3dba9b..0989a4af 100644 --- a/src/queryEngine.ts +++ b/src/queryEngine.ts @@ -259,7 +259,7 @@ async function runPlugin( console.log(e) } }) - + // initialize progress settings if unrecognized format let progressSettings: ReturnType try { From 3f7ab7961cda3b8c0e22ae436a70a4ca1d3c66b6 Mon Sep 17 00:00:00 2001 From: xgram Date: Tue, 25 Nov 2025 09:19:37 +0000 Subject: [PATCH 3/3] Added Xgram partner reports --- src/partners/xgram.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/partners/xgram.ts b/src/partners/xgram.ts index f41ca7c8..27bc5aa1 100644 --- a/src/partners/xgram.ts +++ b/src/partners/xgram.ts @@ -19,7 +19,7 @@ import { import { datelog, retryFetch, snooze } from '../util' const asXgramStatus = asMaybe( - asValue('finished', 'waiting', 'time_expired'), + asValue('success', 'deposit_waiting', 'time_expired'), 'other' ) @@ -53,8 +53,8 @@ const LIMIT = 5 const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 5 // 5 days const statusMap: { [key in XgramStatus]: Status } = { - finished: 'complete', - waiting: 'pending', + success: 'complete', + deposit_waiting: 'pending', time_expired: 'expired', other: 'other' }