Skip to content

Commit 7cd6768

Browse files
committed
Address cursor bot review findings
- swapter: buffer each page and only advance latestIsoDate on clean completion, so a mid-page throw no longer re-appends rows (duplicate orderIds / Couch _id conflicts) and an error-driven exit keeps the pre-run progress marker instead of skipping older unfetched pages (High + Medium). - swapter: relax strict cleaners on fields that never feed StandardTx (info.type/link, deposit/withdraw.network, the partner block) to asMaybe so an unexpected encoding degrades that field instead of aborting the page (Medium). - swapter: add the missing demo partners.ts entry so getPartnerIds surfaces it (Medium). - nexchange: switch non-critical cleaners (address, txid, countryCode, nextCursor, network, contract_address) from asOptional to asMaybe so a single odd encoding degrades that field rather than aborting the query block (Medium). - nym: make apiKey optional and return [] on a null/empty key so an unprovisioned partner entry no-ops like the other couch plugins instead of erroring every cycle (Medium). - nym test: replace live-derived addresses and txids with clearly-synthetic placeholders; only field structure and amount math are load-bearing (High, privacy).
1 parent 10c59fb commit 7cd6768

5 files changed

Lines changed: 83 additions & 40 deletions

File tree

src/demo/partners.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export default {
133133
type: 'swap',
134134
color: '#E35852'
135135
},
136+
swapter: {
137+
type: 'swap',
138+
color: '#00C9A7'
139+
},
136140
swapuz: {
137141
type: 'swap',
138142
color: '#56BD7C'

src/partners/nexchange.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
asArray,
33
asBoolean,
44
asEither,
5+
asMaybe,
56
asNull,
67
asObject,
78
asOptional,
@@ -30,8 +31,8 @@ const CURRENCY_URL = 'https://api.n.exchange/en/api/v2/currency/'
3031
const asNexchangeTransfer = asObject({
3132
currency: asString,
3233
amount: asString,
33-
address: asOptional(asEither(asString, asNull), null),
34-
txid: asOptional(asEither(asString, asNull), null)
34+
address: asMaybe(asEither(asString, asNull), null),
35+
txid: asMaybe(asEither(asString, asNull), null)
3536
})
3637

3738
const asNexchangeOrder = asObject({
@@ -40,12 +41,12 @@ const asNexchangeOrder = asObject({
4041
createdAt: asString,
4142
deposit: asNexchangeTransfer,
4243
payout: asNexchangeTransfer,
43-
countryCode: asOptional(asEither(asString, asNull), null)
44+
countryCode: asMaybe(asEither(asString, asNull), null)
4445
})
4546

4647
const asNexchangeOrdersResponse = asObject({
4748
orders: asArray(asUnknown),
48-
nextCursor: asOptional(asEither(asString, asNull), null),
49+
nextCursor: asMaybe(asEither(asString, asNull), null),
4950
hasMore: asBoolean
5051
})
5152

@@ -55,9 +56,9 @@ const asNexchangeOrdersResponse = asObject({
5556
const asNexchangeCurrencyMeta = asObject({
5657
code: asString,
5758
is_fiat: asOptional(asBoolean, false),
58-
network: asOptional(asEither(asString, asNull), null),
59-
contract_address: asOptional(asEither(asString, asNull), null),
60-
common_symbol: asOptional(asEither(asString, asNull), null)
59+
network: asMaybe(asEither(asString, asNull), null),
60+
contract_address: asMaybe(asEither(asString, asNull), null),
61+
common_symbol: asMaybe(asEither(asString, asNull), null)
6162
})
6263

6364
const asNexchangeCurrencyList = asArray(asNexchangeCurrencyMeta)

src/partners/nym.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ export const asNymPluginParams = asObject({
8585
apiKeys: asObject({
8686
// Partner-issued reporting key, human/ops-set in production CouchDB
8787
// (reports_apps partnerIds.nymswap.apiKeys). Never fetched or set by code.
88-
apiKey: asString
88+
// Optional so an unprovisioned partner entry no-ops (returns []) instead of
89+
// throwing every query cycle, matching the other couch plugins.
90+
apiKey: asMaybe(asString)
8991
})
9092
})
9193

@@ -206,6 +208,13 @@ export async function queryNym(
206208
const { apiKey } = apiKeys
207209
let { latestIsoDate } = settings
208210

211+
// A null/empty apiKey means the partner entry is unprovisioned. Skip silently
212+
// (return no transactions) rather than erroring, so an unconfigured nymswap
213+
// key does not fail every query cycle.
214+
if (apiKey == null || apiKey === '') {
215+
return { settings: { latestIsoDate }, transactions: [] }
216+
}
217+
209218
// Progress persisted before this run. Only advanced past when the full cursor
210219
// walk COMPLETES (nextCursor null); on an error-driven early exit we keep this
211220
// value so a partial fetch never skips orders we did not page through. This

src/partners/swapter.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,30 @@ const asSwapterStatus = asMaybe(
3333
'other'
3434
)
3535

36+
// Only the fields consumed by processSwapterTx are required strictly. Fields
37+
// that never feed StandardTx (info.type, info.link, deposit/withdraw.network,
38+
// the partner block) use asMaybe so an unexpected encoding degrades that field
39+
// instead of throwing out of the whole page (which would abort or, with the
40+
// retry loop, re-fetch the page).
3641
const asSwapterTx = asObject({
3742
info: asObject({
3843
uid: asString,
3944
status: asSwapterStatus,
40-
type: asString,
41-
link: asString,
45+
type: asMaybe(asString),
46+
link: asMaybe(asString),
4247
equivalent: asNumber
4348
}),
4449
deposit: asObject({
4550
coin: asString,
46-
network: asString,
51+
network: asMaybe(asString),
4752
amount: asNumber,
4853
actual: asMaybe(asNumber),
4954
address: asString,
5055
memo: asMaybe(asString)
5156
}),
5257
withdraw: asObject({
5358
coin: asString,
54-
network: asString,
59+
network: asMaybe(asString),
5560
amount: asNumber,
5661
address: asString,
5762
memo: asMaybe(asString)
@@ -64,13 +69,17 @@ const asSwapterTx = asObject({
6469
success: asMaybe(asNumber),
6570
overdue: asMaybe(asNumber)
6671
}),
67-
partner: asObject({
68-
name: asString,
69-
profit: asObject({
70-
amount: asNumber,
71-
percent: asNumber
72+
partner: asMaybe(
73+
asObject({
74+
name: asMaybe(asString),
75+
profit: asMaybe(
76+
asObject({
77+
amount: asMaybe(asNumber),
78+
percent: asMaybe(asNumber)
79+
})
80+
)
7281
})
73-
})
82+
)
7483
})
7584

7685
const asSwapterResult = asObject({
@@ -105,7 +114,7 @@ export const querySwapter = async (
105114
const { log } = pluginParams
106115
const { settings, apiKeys } = asStandardPluginParams(pluginParams)
107116
const { apiKey } = apiKeys
108-
let latestIsoDate =
117+
const latestIsoDate =
109118
typeof settings.latestIsoDate === 'string'
110119
? settings.latestIsoDate
111120
: new Date(0).toISOString()
@@ -115,6 +124,13 @@ export const querySwapter = async (
115124
}
116125

117126
const standardTxs: StandardTx[] = []
127+
// Preserve the pre-run progress marker. latestIsoDate only advances once
128+
// pagination completes cleanly; if retries are exhausted mid-run we return
129+
// the original marker so the next cycle re-fetches the unfinished window
130+
// rather than skipping older, never-fetched pages.
131+
const startIsoDate = latestIsoDate
132+
let newLatestIsoDate = latestIsoDate
133+
let completed = false
118134

119135
let previousTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK
120136
if (previousTimestamp < 0) previousTimestamp = 0
@@ -150,22 +166,32 @@ export const querySwapter = async (
150166
const result = asSwapterResult(await response.json())
151167
const txs = result.data
152168

153-
if (txs.length === 0) break
169+
if (txs.length === 0) {
170+
completed = true
171+
break
172+
}
154173

174+
// Buffer this page so a mid-page throw is retried idempotently: the
175+
// buffer is discarded on error, so already-processed rows are never
176+
// appended twice (which would create duplicate orderIds and Couch _id
177+
// conflicts on bulk insert).
178+
const pageTxs: StandardTx[] = []
155179
for (const rawTx of txs) {
156180
const standardTx = processSwapterTx(rawTx, pluginParams)
157-
158-
standardTxs.push(standardTx)
159-
160-
if (standardTx.isoDate > latestIsoDate) {
161-
latestIsoDate = standardTx.isoDate
181+
pageTxs.push(standardTx)
182+
if (standardTx.isoDate > newLatestIsoDate) {
183+
newLatestIsoDate = standardTx.isoDate
162184
}
163185
}
186+
standardTxs.push(...pageTxs)
164187

165-
log(`Swapter page ${page} latestIsoDate ${latestIsoDate}`)
188+
log(`Swapter page ${page} latestIsoDate ${newLatestIsoDate}`)
166189

167190
const loaded = page * LIMIT
168-
if (loaded >= result.total) break
191+
if (loaded >= result.total) {
192+
completed = true
193+
break
194+
}
169195

170196
page++
171197
retry = 0
@@ -183,7 +209,7 @@ export const querySwapter = async (
183209
}
184210

185211
return {
186-
settings: { latestIsoDate },
212+
settings: { latestIsoDate: completed ? newLatestIsoDate : startIsoDate },
187213
transactions: standardTxs
188214
}
189215
}

test/nym.test.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { describe, it } from 'mocha'
33

44
import { processNymTx } from '../src/partners/nym'
55

6-
// Fixtures mirror real EdgeTransactionRecord payloads captured from NYM's live
7-
// GET /api/partner/v1/reports/transactions endpoint. Amounts are native-unit
8-
// strings; processNymTx converts them to major units via the asset decimals
9-
// (DEFAULT_DECIMALS when no live map is passed).
6+
// Fixtures follow the shape of NYM's GET /api/partner/v1/reports/transactions
7+
// payloads. Addresses and txids are SYNTHETIC placeholders (never live
8+
// user-linked identifiers); only the field structure and the native-unit amount
9+
// math are load-bearing. processNymTx converts native-unit amount strings to
10+
// major units via the asset decimals (DEFAULT_DECIMALS when no live map is
11+
// passed). The USDT contract address in the decimals-map case is the public
12+
// canonical USDT token contract, not user data.
1013
describe('processNymTx', function() {
1114
it('maps a completed order to a StandardTx with major-unit amounts', function() {
1215
const rawTx = {
@@ -25,12 +28,12 @@ describe('processNymTx', function() {
2528
destinationCurrencyCode: 'NYM',
2629
destinationAmount: '1633042311', // 1633.042311 NYM (6 decimals)
2730
destinationEvmChainId: null,
28-
payinAddress: '0x84F2089CBa3c9680F301bEd56C82e108a6Ab416a',
29-
payoutAddress: 'n1fj7fapafrrjxgf8p8qpk0sles7nt8230clvamt',
31+
payinAddress: '0x1111111111111111111111111111111111111111',
32+
payoutAddress: 'n1exampledepositaddr00000000000000000000',
3033
payinTxid:
31-
'0xb78ecf0e0a9e44afd030d2f74cbc9f8e3a10fef09559794363b328fac90702df',
34+
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
3235
payoutTxid:
33-
'DAB0D9F9531464D687C3E5993B1A5D7645622CAF055CEB96BDFDA1283CF29151'
36+
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
3437
}
3538

3639
const standardTx = processNymTx(rawTx)
@@ -41,13 +44,13 @@ describe('processNymTx', function() {
4144
expect(standardTx.depositCurrency).to.equal('ETH')
4245
expect(standardTx.depositAmount).to.equal(0.0158998)
4346
expect(standardTx.depositAddress).to.equal(
44-
'0x84F2089CBa3c9680F301bEd56C82e108a6Ab416a'
47+
'0x1111111111111111111111111111111111111111'
4548
)
4649
expect(standardTx.depositTxid).to.equal(rawTx.payinTxid)
4750
expect(standardTx.payoutCurrency).to.equal('NYM')
4851
expect(standardTx.payoutAmount).to.equal(1633.042311)
4952
expect(standardTx.payoutAddress).to.equal(
50-
'n1fj7fapafrrjxgf8p8qpk0sles7nt8230clvamt'
53+
'n1exampledepositaddr00000000000000000000'
5154
)
5255
expect(standardTx.payoutTxid).to.equal(rawTx.payoutTxid)
5356
// Timestamp keys off createdDate (completedDate is null here).
@@ -66,12 +69,12 @@ describe('processNymTx', function() {
6669
sourceCurrencyCode: 'USDT',
6770
sourceTokenId: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
6871
sourceAmount: '32468489', // 32.468489 USDT (6 decimals)
69-
payinAddress: '0x84F2089CBa3c9680F301bEd56C82e108a6Ab416a',
72+
payinAddress: '0x1111111111111111111111111111111111111111',
7073
payinTxid: null,
7174
destinationCurrencyCode: 'NYM',
7275
destinationTokenId: null,
7376
destinationAmount: '1901660695', // 1901.660695 NYM (6 decimals)
74-
payoutAddress: 'n1uyqr62rpsn5wjxux74h8lrdypt6pnxgqltds2g',
77+
payoutAddress: 'n1examplepayoutaddr000000000000000000000',
7578
payoutTxid: null
7679
},
7780
{

0 commit comments

Comments
 (0)