From 2eaf1b609aa80ea71a04dd20b31efff917d4e445 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 5 May 2026 18:06:56 -0600 Subject: [PATCH 1/6] feat(swap-service): WIP verificationStatus column + decoupled tx/verify polling Adds VerificationStatus enum (PENDING/SUCCESS/FAILED) and verificationStatus column to the Swap model, decouples affiliate verification from pollSwapStatus, and gates verification on tx-status terminal so each axis can succeed independently. Per-verifier mapping applied for Near and Relay; other verifiers blanket- mapped to SUCCESS. Manual backfill SQL after migration deploy: UPDATE swaps SET "verificationStatus"='SUCCESS' WHERE "isAffiliateVerified" IS NOT NULL; Open TODOs (out of scope for this PR): PENDING termination criterion, admin re-verify endpoint, per-verifier mapping for the remaining verifiers, per-call HTTP timeouts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/polling/swap-polling.service.ts | 55 +++--- apps/swap-service/src/swaps/swaps.service.ts | 69 +++++--- .../src/verification/__tests__/near.test.ts | 12 +- .../src/verification/__tests__/relay.test.ts | 16 +- .../verification/swap-verification.service.ts | 159 +++++++++++------- packages/shared-types/src/index.ts | 2 +- .../migration.sql | 8 + prisma/schema/swap-service.prisma | 20 ++- 8 files changed, 208 insertions(+), 133 deletions(-) create mode 100644 prisma/migrations/20260505235634_add_swap_verification_status/migration.sql diff --git a/apps/swap-service/src/polling/swap-polling.service.ts b/apps/swap-service/src/polling/swap-polling.service.ts index ae1c178..3121757 100644 --- a/apps/swap-service/src/polling/swap-polling.service.ts +++ b/apps/swap-service/src/polling/swap-polling.service.ts @@ -46,31 +46,48 @@ export class SwapPollingService { } private async pollOne(swap: Awaited>[number]): Promise { - const statusUpdate = await (async () => { + let current = swap + + if (current.status === 'IDLE' || current.status === 'PENDING') { try { - return await this.swapsService.pollSwapStatus(swap.swapId) + const statusUpdate = await this.swapsService.pollSwapStatus(current.swapId) + + if (statusUpdate.status !== current.status) { + this.logger.log(`Status changed for swap ${current.swapId}: ${current.status} -> ${statusUpdate.status}`) + + current = await this.swapsService.updateSwapStatus({ + swapId: current.swapId, + status: statusUpdate.status, + sellTxHash: statusUpdate.sellTxHash, + buyTxHash: statusUpdate.buyTxHash, + statusMessage: statusUpdate.statusMessage, + }) + + this.websocketGateway.sendSwapUpdateToUser(current.userId, current) + } } catch (err) { - this.logger.error(`Failed to poll swap ${swap.swapId}:`, err) - return + this.logger.error(`Failed to poll tx status for swap ${current.swapId}:`, err) } - })() - - if (!statusUpdate || statusUpdate.status === swap.status) return + } - this.logger.log(`Status changed for swap ${swap.swapId}: ${swap.status} -> ${statusUpdate.status}`) + if (current.verificationStatus !== 'PENDING') return try { - const updatedSwap = await this.swapsService.updateSwapStatus({ - swapId: swap.swapId, - status: statusUpdate.status, - sellTxHash: statusUpdate.sellTxHash, - buyTxHash: statusUpdate.buyTxHash, - statusMessage: statusUpdate.statusMessage, - }) - - this.websocketGateway.sendSwapUpdateToUser(swap.userId, updatedSwap) - } catch (error) { - this.logger.error(`Failed to persist status change for swap ${swap.swapId}:`, error) + if (current.status === 'SUCCESS') { + const updated = await this.swapsService.verifySwap(current) + if (updated.verificationStatus !== current.verificationStatus) { + this.logger.log( + `Verification changed for swap ${current.swapId}: ${current.verificationStatus} -> ${updated.verificationStatus}`, + ) + this.websocketGateway.sendSwapUpdateToUser(updated.userId, updated) + } + } else if (current.status === 'FAILED') { + const updated = await this.swapsService.markVerificationFailed(current.swapId) + this.logger.log(`Verification short-circuited to FAILED for swap ${current.swapId} (tx FAILED)`) + this.websocketGateway.sendSwapUpdateToUser(updated.userId, updated) + } + } catch (err) { + this.logger.error(`Failed to verify swap ${current.swapId}:`, err) } } } diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index a63c89f..2faff6d 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -234,7 +234,10 @@ export class SwapsService { async getPendingSwaps(): Promise { const swaps = await this.prisma.swap.findMany({ - where: { status: { in: ['IDLE', 'PENDING'] }, sellTxHash: { not: null } }, + where: { + sellTxHash: { not: null }, + OR: [{ status: { in: ['IDLE', 'PENDING'] } }, { verificationStatus: 'PENDING' }], + }, }) return swaps.map(toSwap) @@ -374,8 +377,6 @@ export class SwapsService { const statusMessage = Array.isArray(message) ? message[0] : message - await this.reconcileSwap(swap) - return { status: status === TxStatus.Confirmed ? 'SUCCESS' : status === TxStatus.Failed ? 'FAILED' : 'PENDING', sellTxHash: swap.sellTxHash, @@ -391,34 +392,39 @@ export class SwapsService { } } - private async reconcileSwap(swap: Swap): Promise { - try { - const verificationResult = await this.swapVerificationService.verifySwap(swap) + async verifySwap(swap: Swap): Promise { + const verificationResult = await this.swapVerificationService.verifySwap(swap) - logger.log( - [ - `Swap verified: ${swap.swapId}`, - verificationResult.isVerified ? 'ok' : 'failed', - verificationResult.hasAffiliate && - `affiliate ${verificationResult.affiliateAddress} (${verificationResult.affiliateBps} bps)`, - verificationResult.error && `error: ${verificationResult.error}`, - ] - .filter(Boolean) - .join(' | '), - ) + logger.log( + [ + `Swap verified: ${swap.swapId}`, + verificationResult.verificationStatus, + verificationResult.hasAffiliate && + `affiliate ${verificationResult.affiliateAddress} (${verificationResult.affiliateBps} bps)`, + verificationResult.error && `error: ${verificationResult.error}`, + ] + .filter(Boolean) + .join(' | '), + ) - const isAffiliateVerified = verificationResult.isVerified && verificationResult.hasAffiliate + // Transient — leave persisted state untouched and let the next polling tick retry. + if (verificationResult.verificationStatus === 'PENDING') return swap - const affiliateVerificationDetails: AffiliateVerificationDetails = { - hasAffiliate: verificationResult.hasAffiliate, - affiliateBps: verificationResult.affiliateBps, - affiliateAddress: verificationResult.affiliateAddress, - verifiedSellAmountCryptoBaseUnit: verificationResult.verifiedSellAmountCryptoBaseUnit, - } + const isAffiliateVerified = + verificationResult.verificationStatus === 'SUCCESS' && verificationResult.hasAffiliate + + const affiliateVerificationDetails: AffiliateVerificationDetails = { + hasAffiliate: verificationResult.hasAffiliate, + affiliateBps: verificationResult.affiliateBps, + affiliateAddress: verificationResult.affiliateAddress, + verifiedSellAmountCryptoBaseUnit: verificationResult.verifiedSellAmountCryptoBaseUnit, + } + return toSwap( await this.prisma.swap.update({ where: { swapId: swap.swapId }, data: { + verificationStatus: verificationResult.verificationStatus, isAffiliateVerified, affiliateFeeAssetId: verificationResult.actualAffiliateFeeAssetId, affiliateAssetUsd: verificationResult.actualAffiliateFeeUsd, @@ -426,9 +432,16 @@ export class SwapsService { actualBuyAmountCryptoBaseUnit: verificationResult.actualBuyAmountCryptoBaseUnit, actualAffiliateFeeAmountCryptoBaseUnit: verificationResult.actualAffiliateFeeAmountCryptoBaseUnit, }, - }) - } catch (error) { - logger.warn(`Failed to verify affiliate for swap ${swap.swapId}:`, error) - } + }), + ) + } + + async markVerificationFailed(swapId: string): Promise { + return toSwap( + await this.prisma.swap.update({ + where: { swapId }, + data: { verificationStatus: 'FAILED', isAffiliateVerified: false }, + }), + ) } } diff --git a/apps/swap-service/src/verification/__tests__/near.test.ts b/apps/swap-service/src/verification/__tests__/near.test.ts index 04801ab..9e2c3d7 100644 --- a/apps/swap-service/src/verification/__tests__/near.test.ts +++ b/apps/swap-service/src/verification/__tests__/near.test.ts @@ -26,7 +26,7 @@ describe('verifyNearIntents', () => { const result = await service.verifySwap(swap) expect(result).toMatchObject({ - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: true, affiliateBps: 30, affiliateAddress: 'shapeshifttokenomics.sputnik-dao.near', @@ -56,7 +56,7 @@ describe('verifyNearIntents', () => { const result = await service.verifySwap(swap) - expect(result.isVerified).toBe(true) + expect(result.verificationStatus).toBe('SUCCESS') expect(result.hasAffiliate).toBe(false) expect(result.affiliateBps).toBeUndefined() expect(result.affiliateAddress).toBeUndefined() @@ -104,24 +104,24 @@ describe('verifyNearIntents', () => { expect(result.verifiedSellAmountCryptoBaseUnit).toBe(response.quoteResponse.quote.amountIn) }) - it('returns unverified when nearIntentsSpecific.depositAddress is missing', async () => { + it('returns FAILED when nearIntentsSpecific.depositAddress is missing', async () => { const swapWithoutMetadata = { ...swap, metadata: {} as SwapperSpecificMetadata } as Swap const result = await service.verifySwap(swapWithoutMetadata) expect(result).toMatchObject({ - isVerified: false, + verificationStatus: 'FAILED', hasAffiliate: false, error: 'Missing depositAddress in nearIntentsSpecific metadata', }) }) - it('returns unverified when the SDK throws', async () => { + it('returns PENDING when the SDK throws (transient — retry next tick)', async () => { jest.spyOn(OneClickService, 'getExecutionStatus').mockRejectedValue(new Error('upstream 500')) const result = await service.verifySwap(swap) - expect(result.isVerified).toBe(false) + expect(result.verificationStatus).toBe('PENDING') expect(result.error).toBe('upstream 500') }) }) diff --git a/apps/swap-service/src/verification/__tests__/relay.test.ts b/apps/swap-service/src/verification/__tests__/relay.test.ts index 426b9d4..2defd3f 100644 --- a/apps/swap-service/src/verification/__tests__/relay.test.ts +++ b/apps/swap-service/src/verification/__tests__/relay.test.ts @@ -29,7 +29,7 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swap) expect(result).toMatchObject({ - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: true, affiliateBps: 60, affiliateAddress: DAO_TREASURY_BASE, @@ -77,7 +77,7 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swap) - expect(result.isVerified).toBe(true) + expect(result.verificationStatus).toBe('SUCCESS') expect(result.hasAffiliate).toBe(false) expect(result.affiliateBps).toBeUndefined() expect(result.affiliateAddress).toBeUndefined() @@ -183,7 +183,7 @@ describe('verifyRelay', () => { expect(result.actualAffiliateFeeAmountCryptoBaseUnit).toBe('999') }) - it('returns unverified when relayTransactionMetadata.relayId is missing', async () => { + it('returns FAILED when relayTransactionMetadata.relayId is missing', async () => { const swapWithoutMetadata = { ...swap, metadata: {} as SwapperSpecificMetadata } as Swap service = new SwapVerificationService(makeHttpMock(relayResponse)) @@ -191,22 +191,22 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swapWithoutMetadata) expect(result).toMatchObject({ - isVerified: false, + verificationStatus: 'FAILED', hasAffiliate: false, error: 'Missing relayId in relayTransactionMetadata', }) }) - it('returns unverified when API returns no requests', async () => { + it('returns PENDING when API returns no requests (transient — retry next tick)', async () => { service = new SwapVerificationService(makeHttpMock({ requests: [] })) const result = await service.verifySwap(swap) - expect(result.isVerified).toBe(false) + expect(result.verificationStatus).toBe('PENDING') expect(result.error).toBe('No request data found from Relay API') }) - it('returns unverified when the HTTP call fails', async () => { + it('returns PENDING when the HTTP call fails (transient — retry next tick)', async () => { const httpMock = { get: jest.fn().mockReturnValue(throwError(() => new Error('upstream 500'))), } as unknown as HttpService @@ -215,7 +215,7 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swap) - expect(result.isVerified).toBe(false) + expect(result.verificationStatus).toBe('PENDING') expect(result.error).toBe('upstream 500') }) }) diff --git a/apps/swap-service/src/verification/swap-verification.service.ts b/apps/swap-service/src/verification/swap-verification.service.ts index 83508e2..9472d62 100644 --- a/apps/swap-service/src/verification/swap-verification.service.ts +++ b/apps/swap-service/src/verification/swap-verification.service.ts @@ -61,14 +61,6 @@ export class SwapVerificationService { async verifySwap(swap: Swap): Promise { const { swapId, swapperName } = swap - const unverified = (error: string): SwapVerificationResult => ({ - isVerified: false, - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error, - }) - try { this.logger.log(`Verifying affiliate for swap ${swapId} on ${swapperName}`) @@ -107,7 +99,13 @@ export class SwapVerificationService { return await this.verifyAcross(swap) case SwapperName.Debridge: case SwapperName.Test: - return unverified(`Verification not implemented for ${swapperName}`) + return { + verificationStatus: 'SUCCESS', + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + error: `Verification not implemented for ${swapperName}`, + } default: { const _exhaustive: never = swapperName void _exhaustive @@ -117,7 +115,14 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying swap ${swapId} for ${swapperName}:`, error) - return unverified(error instanceof Error ? error.message : 'Unknown error') + // Unexpected exception — treat as transient and let the next polling tick retry. + return { + verificationStatus: 'PENDING', + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + error: error instanceof Error ? error.message : 'Unknown error', + } } } @@ -125,7 +130,15 @@ export class SwapVerificationService { const { swapId, metadata } = swap const depositAddress = metadata.nearIntentsSpecific?.depositAddress - if (!depositAddress) throw new Error('Missing depositAddress in nearIntentsSpecific metadata') + if (!depositAddress) { + return { + verificationStatus: 'FAILED', + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + error: 'Missing depositAddress in nearIntentsSpecific metadata', + } + } const status = await OneClickService.getExecutionStatus(depositAddress) @@ -143,7 +156,7 @@ export class SwapVerificationService { const verifiedSellAmountCryptoBaseUnit = swapDetails.depositedAmount || swapDetails.amountIn || quote.amountIn const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: Boolean(shapeshiftFee), affiliateBps: shapeshiftFee?.fee, affiliateAddress: shapeshiftFee?.recipient, @@ -162,14 +175,30 @@ export class SwapVerificationService { const { swapId, metadata } = swap const relayId = metadata.relayTransactionMetadata?.relayId - if (!relayId) throw new Error('Missing relayId in relayTransactionMetadata') + if (!relayId) { + return { + verificationStatus: 'FAILED', + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + error: 'Missing relayId in relayTransactionMetadata', + } + } const { data } = await firstValueFrom( this.httpService.get(`${env.VITE_RELAY_API_URL}/requests/v2?id=${relayId}`), ) const request = data?.requests?.[0] - if (!request?.data) throw new Error('No request data found from Relay API') + if (!request?.data) { + return { + verificationStatus: 'PENDING', + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + error: 'No request data found from Relay API', + } + } const appFees = request.data.appFees?.length ? request.data.appFees : (request.data.paidAppFees ?? []) @@ -212,7 +241,7 @@ export class SwapVerificationService { })() const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: Boolean(shapeshiftFee), affiliateBps, affiliateAddress: shapeshiftFee?.recipient, @@ -245,7 +274,7 @@ export class SwapVerificationService { if (!appDataHash) { this.logger.warn(`CowSwap - Missing appDataHash for swap ${swapId}`) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -293,7 +322,7 @@ export class SwapVerificationService { } const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, affiliateAddress: hasShapeshiftAffiliate ? affiliateAddress : undefined, @@ -308,7 +337,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying CowSwap for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -331,7 +360,7 @@ export class SwapVerificationService { if (!orderId) { this.logger.warn(`Portals - Missing orderId for swap ${swapId}`) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -346,7 +375,7 @@ export class SwapVerificationService { } catch { this.logger.warn(`Portals - Unsupported chain for treasury address: ${sellChainId}`) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -370,7 +399,7 @@ export class SwapVerificationService { if (!partner) { this.logger.warn(`Portals - No partner found in API response for swap ${swapId}`) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -386,7 +415,7 @@ export class SwapVerificationService { const verifiedSellAmountCryptoBaseUnit = orderData?.context?.inputAmount?.toString() ?? undefined const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: swap.affiliateBps ?? undefined, affiliateAddress: hasShapeshiftAffiliate ? expectedTreasuryAddress : undefined, @@ -401,7 +430,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Portals for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -416,7 +445,7 @@ export class SwapVerificationService { if (!txHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -436,7 +465,7 @@ export class SwapVerificationService { if (!observedTx || !observedTx.tx) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -447,7 +476,7 @@ export class SwapVerificationService { const memo: string | undefined = observedTx.tx.memo if (!memo) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -471,7 +500,7 @@ export class SwapVerificationService { : undefined const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftThorchainAffiliate : undefined, @@ -486,7 +515,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Thorchain for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -501,7 +530,7 @@ export class SwapVerificationService { if (!txHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -521,7 +550,7 @@ export class SwapVerificationService { if (!observedTx || !observedTx.tx) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -532,7 +561,7 @@ export class SwapVerificationService { const memo: string | undefined = observedTx.tx.memo if (!memo) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -556,7 +585,7 @@ export class SwapVerificationService { : undefined const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftMayaAffiliate : undefined, @@ -571,7 +600,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Maya for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -587,7 +616,7 @@ export class SwapVerificationService { if (!chainflipSwapId) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -609,7 +638,7 @@ export class SwapVerificationService { if (!swapData) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -630,7 +659,7 @@ export class SwapVerificationService { )?.toString() return { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: hasShapeshiftAffiliate && affiliateBps ? parseInt(String(affiliateBps)) : undefined, affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftChainflipAffiliate : undefined, @@ -641,7 +670,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Chainflip for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -658,7 +687,7 @@ export class SwapVerificationService { if (!tradeHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -684,7 +713,7 @@ export class SwapVerificationService { if (!trade) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -711,7 +740,7 @@ export class SwapVerificationService { const verifiedSellAmountCryptoBaseUnit = (trade.sellAmount ?? trade.inputTokenAmount ?? trade.amount)?.toString() return { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps, affiliateAddress: hasShapeshiftAffiliate ? this.shapeshift0xIntegrator : undefined, @@ -722,7 +751,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying 0x for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -737,7 +766,7 @@ export class SwapVerificationService { if (!txHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -790,7 +819,7 @@ export class SwapVerificationService { if (!trade) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -809,7 +838,7 @@ export class SwapVerificationService { const verifiedSellAmountCryptoBaseUnit = sellTokenEntries[0]?.amount?.toString() ?? undefined const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps, affiliateAddress: this.shapeshiftBebopSource, @@ -824,7 +853,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Bebop for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -837,7 +866,7 @@ export class SwapVerificationService { const { swapId } = swap const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -855,7 +884,7 @@ export class SwapVerificationService { if (!txHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -874,7 +903,7 @@ export class SwapVerificationService { if (!bridgeInfo) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -892,7 +921,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: true, + verificationStatus: 'SUCCESS', hasAffiliate: hasShapeshiftAffiliate, affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftButterswapEntrance : undefined, @@ -907,7 +936,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying ButterSwap for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -923,7 +952,7 @@ export class SwapVerificationService { if (!txHash) { return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -941,7 +970,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate, affiliateBps: hasAffiliate ? affiliateBps : undefined, verifiedSellAmountCryptoBaseUnit, @@ -955,7 +984,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Cetus for swap ${swapId}:`, error) return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -971,7 +1000,7 @@ export class SwapVerificationService { if (!txHash) { return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -989,7 +1018,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate, affiliateBps: hasAffiliate ? affiliateBps : undefined, verifiedSellAmountCryptoBaseUnit, @@ -1003,7 +1032,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Sun.io for swap ${swapId}:`, error) return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1019,7 +1048,7 @@ export class SwapVerificationService { if (!txHash) { return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1038,7 +1067,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate, affiliateBps: hasAffiliate ? affiliateBps : undefined, affiliateAddress, @@ -1053,7 +1082,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying AVNU for swap ${swapId}:`, error) return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1069,7 +1098,7 @@ export class SwapVerificationService { if (!txHash) { return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1092,7 +1121,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate, affiliateBps: hasAffiliate ? affiliateBps : undefined, affiliateAddress: referrerAddress, @@ -1107,7 +1136,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying STON.fi for swap ${swapId}:`, error) return Promise.resolve({ - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1123,7 +1152,7 @@ export class SwapVerificationService { if (!txHash) { return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, @@ -1152,7 +1181,7 @@ export class SwapVerificationService { )?.toString() const result: SwapVerificationResult = { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate, affiliateBps: hasAffiliate ? affiliateBps : undefined, affiliateAddress, @@ -1167,7 +1196,7 @@ export class SwapVerificationService { } catch (error) { this.logger.error(`Error verifying Across for swap ${swapId}:`, error) return { - isVerified: false, + verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index eed9f9b..a118233 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -114,7 +114,7 @@ export interface SwapStatusResponse { } export interface SwapVerificationResult { - isVerified: boolean + verificationStatus: 'PENDING' | 'SUCCESS' | 'FAILED' hasAffiliate: boolean affiliateBps?: number affiliateAddress?: string diff --git a/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql b/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql new file mode 100644 index 0000000..1cccc12 --- /dev/null +++ b/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql @@ -0,0 +1,8 @@ +-- CreateEnum +CREATE TYPE "public"."VerificationStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED'); + +-- AlterTable +ALTER TABLE "public"."swaps" ADD COLUMN "verificationStatus" "public"."VerificationStatus" NOT NULL DEFAULT 'PENDING'; + +-- CreateIndex +CREATE INDEX "swaps_verificationStatus_sellTxHash_idx" ON "public"."swaps"("verificationStatus", "sellTxHash"); diff --git a/prisma/schema/swap-service.prisma b/prisma/schema/swap-service.prisma index a7db75e..cb40b06 100644 --- a/prisma/schema/swap-service.prisma +++ b/prisma/schema/swap-service.prisma @@ -14,13 +14,13 @@ model Affiliate { } model Swap { - swapId String @id + swapId String @id sellAsset Json buyAsset Json sellAmountCryptoBaseUnit String expectedBuyAmountCryptoBaseUnit String actualBuyAmountCryptoBaseUnit String? - status String @default("PENDING") + status String @default("PENDING") source String swapperName String sellAccountId String @@ -30,9 +30,9 @@ model Swap { buyTxHash String? txLink String? statusMessage String? - isStreaming Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isStreaming Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt metadata Json userId String referralCode String? @@ -41,16 +41,24 @@ model Swap { affiliateAssetUsd String? isAffiliateVerified Boolean? affiliateVerificationDetails Json? - affiliateAddress String? @db.Citext + affiliateAddress String? @db.Citext affiliateBps Int? origin String? affiliateFeeAssetId String? actualAffiliateFeeAmountCryptoBaseUnit String? shapeshiftBps Int + verificationStatus VerificationStatus @default(PENDING) @@index([referralCode]) @@index([affiliateAddress]) @@index([status, sellTxHash]) + @@index([verificationStatus, sellTxHash]) @@index([userId]) @@map("swaps") } + +enum VerificationStatus { + PENDING + SUCCESS + FAILED +} From f137eaf76930077b34bcaf6e2fdf9eb019f6f031 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 6 May 2026 11:33:36 -0600 Subject: [PATCH 2/6] refactor(swap-service): consolidate verifier returns and centralize logging Replaces stamped-out short-circuit objects across all verifiers with a noAffiliateResult(status, reason) helper, and drops per-verifier try/catch wraps so HTTP/parse failures flow to the top-level catch (PENDING + retry) instead of incorrectly terminalizing as SUCCESS. Refines per-verifier status mappings under one rule: - missing identifier we need to query upstream -> FAILED - upstream returned no data for our tx -> PENDING (indexer lag) - got our tx, no affiliate marker present -> SUCCESS (definitive) Notable corrections: Thorchain/Maya !memo is now SUCCESS (memo is immutable on-chain - absence is definitive). ButterSwap !bridgeInfo is now PENDING (didn't get data for our tx). HTTP errors across all verifiers now route to PENDING via the top-level catch. Single boundary log in verifySwap replaces logVerification (per- verifier) and the redundant log in swaps.service.ts:verifySwap. Logs now fire for every path (happy/short-circuit/thrown). Renames error -> noAffiliateReason on SwapVerificationResult - the field is only populated when hasAffiliate is false, so "error" was misleading for SUCCESS-no-affiliate cases. Renames pollSwapStatus -> checkSwapStatus (polling lives in pollOne; this method does a single fetch). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/polling/swap-polling.service.ts | 2 +- apps/swap-service/src/swaps/swaps.service.ts | 22 +- .../__tests__/fixtures/near/swap.ts | 1 + .../__tests__/fixtures/relay/swap.ts | 1 + .../src/verification/__tests__/near.test.ts | 2 +- .../src/verification/__tests__/relay.test.ts | 4 +- .../verification/swap-verification.service.ts | 1275 ++++++----------- apps/swap-service/src/verification/utils.ts | 43 +- packages/shared-types/src/index.ts | 2 +- 9 files changed, 448 insertions(+), 904 deletions(-) diff --git a/apps/swap-service/src/polling/swap-polling.service.ts b/apps/swap-service/src/polling/swap-polling.service.ts index 3121757..cbd3074 100644 --- a/apps/swap-service/src/polling/swap-polling.service.ts +++ b/apps/swap-service/src/polling/swap-polling.service.ts @@ -50,7 +50,7 @@ export class SwapPollingService { if (current.status === 'IDLE' || current.status === 'PENDING') { try { - const statusUpdate = await this.swapsService.pollSwapStatus(current.swapId) + const statusUpdate = await this.swapsService.checkSwapStatus(current.swapId) if (statusUpdate.status !== current.status) { this.logger.log(`Status changed for swap ${current.swapId}: ${current.status} -> ${statusUpdate.status}`) diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index 2faff6d..9f4a6d9 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -350,8 +350,8 @@ export class SwapsService { } } - async pollSwapStatus(swapId: string): Promise { - logger.log(`Polling status for swap: ${swapId}`) + async checkSwapStatus(swapId: string): Promise { + logger.log(`Checking status for swap: ${swapId}`) const prismaSwap = await this.prisma.swap.findUnique({ where: { swapId } }) if (!prismaSwap) throw new NotFoundException(`Swap not found: ${swapId}`) @@ -384,7 +384,7 @@ export class SwapsService { statusMessage: typeof statusMessage === 'string' ? statusMessage : '', } } catch (error) { - logger.error(`Failed to poll swap status for ${swapId}:`, error) + logger.error(`Failed to check swap status for ${swapId}:`, error) return { status: 'PENDING', statusMessage: `Error polling status: ${error instanceof Error ? error.message : 'Unknown error'}`, @@ -395,23 +395,9 @@ export class SwapsService { async verifySwap(swap: Swap): Promise { const verificationResult = await this.swapVerificationService.verifySwap(swap) - logger.log( - [ - `Swap verified: ${swap.swapId}`, - verificationResult.verificationStatus, - verificationResult.hasAffiliate && - `affiliate ${verificationResult.affiliateAddress} (${verificationResult.affiliateBps} bps)`, - verificationResult.error && `error: ${verificationResult.error}`, - ] - .filter(Boolean) - .join(' | '), - ) - - // Transient — leave persisted state untouched and let the next polling tick retry. if (verificationResult.verificationStatus === 'PENDING') return swap - const isAffiliateVerified = - verificationResult.verificationStatus === 'SUCCESS' && verificationResult.hasAffiliate + const isAffiliateVerified = verificationResult.verificationStatus === 'SUCCESS' && verificationResult.hasAffiliate const affiliateVerificationDetails: AffiliateVerificationDetails = { hasAffiliate: verificationResult.hasAffiliate, diff --git a/apps/swap-service/src/verification/__tests__/fixtures/near/swap.ts b/apps/swap-service/src/verification/__tests__/fixtures/near/swap.ts index 61dbf59..f2fd508 100644 --- a/apps/swap-service/src/verification/__tests__/fixtures/near/swap.ts +++ b/apps/swap-service/src/verification/__tests__/fixtures/near/swap.ts @@ -71,6 +71,7 @@ export default { affiliateAssetUsd: '2288.4', isAffiliateVerified: null, affiliateVerificationDetails: null, + verificationStatus: 'PENDING', affiliateAddress: '0xa44c286ba83bb771cd0107b2c1df678435bd1535', affiliateBps: 60, origin: 'api', diff --git a/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts b/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts index 15d7174..e0a6da7 100644 --- a/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts +++ b/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts @@ -74,6 +74,7 @@ export default { buyAssetUsd: '0.9998', affiliateAssetUsd: '0.9998', isAffiliateVerified: true, + verificationStatus: 'SUCCESS', affiliateVerificationDetails: { affiliateBps: 60, hasAffiliate: true, diff --git a/apps/swap-service/src/verification/__tests__/near.test.ts b/apps/swap-service/src/verification/__tests__/near.test.ts index 9e2c3d7..105096b 100644 --- a/apps/swap-service/src/verification/__tests__/near.test.ts +++ b/apps/swap-service/src/verification/__tests__/near.test.ts @@ -122,6 +122,6 @@ describe('verifyNearIntents', () => { const result = await service.verifySwap(swap) expect(result.verificationStatus).toBe('PENDING') - expect(result.error).toBe('upstream 500') + expect(result.noAffiliateReason).toBe('upstream 500') }) }) diff --git a/apps/swap-service/src/verification/__tests__/relay.test.ts b/apps/swap-service/src/verification/__tests__/relay.test.ts index 2defd3f..96e8519 100644 --- a/apps/swap-service/src/verification/__tests__/relay.test.ts +++ b/apps/swap-service/src/verification/__tests__/relay.test.ts @@ -203,7 +203,7 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swap) expect(result.verificationStatus).toBe('PENDING') - expect(result.error).toBe('No request data found from Relay API') + expect(result.noAffiliateReason).toBe('No request data found from Relay API') }) it('returns PENDING when the HTTP call fails (transient — retry next tick)', async () => { @@ -216,6 +216,6 @@ describe('verifyRelay', () => { const result = await service.verifySwap(swap) expect(result.verificationStatus).toBe('PENDING') - expect(result.error).toBe('upstream 500') + expect(result.noAffiliateReason).toBe('upstream 500') }) }) diff --git a/apps/swap-service/src/verification/swap-verification.service.ts b/apps/swap-service/src/verification/swap-verification.service.ts index 9472d62..a225427 100644 --- a/apps/swap-service/src/verification/swap-verification.service.ts +++ b/apps/swap-service/src/verification/swap-verification.service.ts @@ -26,7 +26,7 @@ import { ZrxApiResponse, ZrxTrade, } from './types' -import { applyBps, logVerification, THORCHAIN_PRECISION, thorchainToNativePrecision } from './utils' +import { applyBps, noAffiliateResult, THORCHAIN_PRECISION, thorchainToNativePrecision } from './utils' @Injectable() export class SwapVerificationService { @@ -59,86 +59,93 @@ export class SwapVerificationService { } async verifySwap(swap: Swap): Promise { - const { swapId, swapperName } = swap - - try { - this.logger.log(`Verifying affiliate for swap ${swapId} on ${swapperName}`) - - switch (swapperName) { - case SwapperName.NearIntents: - return await this.verifyNearIntents(swap) - case SwapperName.Relay: - return await this.verifyRelay(swap) - case SwapperName.CowSwap: - return await this.verifyCowSwap(swap) - case SwapperName.Portals: - return await this.verifyPortals(swap) - case SwapperName.Thorchain: - return await this.verifyThorchain(swap) - case SwapperName.Mayachain: - return await this.verifyMaya(swap) - case SwapperName.Chainflip: - return await this.verifyChainflip(swap) - case SwapperName.Zrx: - return await this.verifyZrx(swap) - case SwapperName.Bebop: - return await this.verifyBebop(swap) - case SwapperName.ArbitrumBridge: - return await this.verifyArbitrumBridge(swap) - case SwapperName.ButterSwap: - return await this.verifyButterSwap(swap) - case SwapperName.Cetus: - return await this.verifyCetus(swap) - case SwapperName.Sunio: - return await this.verifySunio(swap) - case SwapperName.Avnu: - return await this.verifyAvnu(swap) - case SwapperName.Stonfi: - return await this.verifyStonfi(swap) - case SwapperName.Across: - return await this.verifyAcross(swap) - case SwapperName.Debridge: - case SwapperName.Test: - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: `Verification not implemented for ${swapperName}`, + const result = await (async () => { + try { + switch (swap.swapperName) { + case SwapperName.NearIntents: + return await this.verifyNearIntents(swap) + case SwapperName.Relay: + return await this.verifyRelay(swap) + case SwapperName.CowSwap: + return await this.verifyCowSwap(swap) + case SwapperName.Portals: + return await this.verifyPortals(swap) + case SwapperName.Thorchain: + return await this.verifyThorchain(swap) + case SwapperName.Mayachain: + return await this.verifyMaya(swap) + case SwapperName.Chainflip: + return await this.verifyChainflip(swap) + case SwapperName.Zrx: + return await this.verifyZrx(swap) + case SwapperName.Bebop: + return await this.verifyBebop(swap) + case SwapperName.ArbitrumBridge: + return await this.verifyArbitrumBridge(swap) + case SwapperName.ButterSwap: + return await this.verifyButterSwap(swap) + case SwapperName.Cetus: + return await this.verifyCetus(swap) + case SwapperName.Sunio: + return await this.verifySunio(swap) + case SwapperName.Avnu: + return await this.verifyAvnu(swap) + case SwapperName.Stonfi: + return await this.verifyStonfi(swap) + case SwapperName.Across: + return await this.verifyAcross(swap) + case SwapperName.Debridge: + case SwapperName.Test: + return noAffiliateResult('SUCCESS', `Verification not implemented`) + default: { + const _exhaustive: never = swap.swapperName + void _exhaustive + throw new Error('unreachable') } - default: { - const _exhaustive: never = swapperName - void _exhaustive - throw new Error('unreachable') } + } catch (error) { + return noAffiliateResult('PENDING', error instanceof Error ? error.message : 'Unknown error') } - } catch (error) { - this.logger.error(`Error verifying swap ${swapId} for ${swapperName}:`, error) - - // Unexpected exception — treat as transient and let the next polling tick retry. - return { - verificationStatus: 'PENDING', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Unknown error', - } + })() + + this.logResult(swap, result) + + return result + } + + private logResult(swap: Swap, result: SwapVerificationResult): void { + const detail = result.hasAffiliate + ? `affiliate=${result.affiliateAddress} (${result.affiliateBps} bps)` + : `noAffiliateReason=${result.noAffiliateReason ?? 'unknown'}` + + const segments = [ + `${swap.swapperName} verification`, + `swapId=${swap.swapId}`, + `status=${result.verificationStatus}`, + detail, + ] + + const hasAmounts = + result.verifiedSellAmountCryptoBaseUnit || + result.actualBuyAmountCryptoBaseUnit || + result.actualAffiliateFeeAmountCryptoBaseUnit + + if (hasAmounts) { + segments.push( + `sell=${result.verifiedSellAmountCryptoBaseUnit ?? 'none'}`, + `buy=${result.actualBuyAmountCryptoBaseUnit ?? 'none'}`, + `fee=${result.actualAffiliateFeeAmountCryptoBaseUnit ?? 'none'}`, + ) } + + this.logger.log(segments.join(' | ')) } private async verifyNearIntents(swap: Swap): Promise { - const { swapId, metadata } = swap + const { metadata } = swap const depositAddress = metadata.nearIntentsSpecific?.depositAddress - if (!depositAddress) { - return { - verificationStatus: 'FAILED', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing depositAddress in nearIntentsSpecific metadata', - } - } + if (!depositAddress) return noAffiliateResult('FAILED', 'Missing depositAddress in nearIntentsSpecific metadata') const status = await OneClickService.getExecutionStatus(depositAddress) @@ -155,7 +162,7 @@ export class SwapVerificationService { const verifiedSellAmountCryptoBaseUnit = swapDetails.depositedAmount || swapDetails.amountIn || quote.amountIn - const result: SwapVerificationResult = { + return { verificationStatus: 'SUCCESS', hasAffiliate: Boolean(shapeshiftFee), affiliateBps: shapeshiftFee?.fee, @@ -165,40 +172,20 @@ export class SwapVerificationService { // 1Click applies the appFee bps to the input amount, so realized fee = input × bps / 10000. actualAffiliateFeeAmountCryptoBaseUnit: applyBps(verifiedSellAmountCryptoBaseUnit, shapeshiftFee?.fee), } - - logVerification(this.logger, SwapperName.NearIntents, swapId, result, { status: status.status }) - - return result } private async verifyRelay(swap: Swap): Promise { - const { swapId, metadata } = swap + const { metadata } = swap const relayId = metadata.relayTransactionMetadata?.relayId - if (!relayId) { - return { - verificationStatus: 'FAILED', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing relayId in relayTransactionMetadata', - } - } + if (!relayId) return noAffiliateResult('FAILED', 'Missing relayId in relayTransactionMetadata') const { data } = await firstValueFrom( this.httpService.get(`${env.VITE_RELAY_API_URL}/requests/v2?id=${relayId}`), ) const request = data?.requests?.[0] - if (!request?.data) { - return { - verificationStatus: 'PENDING', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No request data found from Relay API', - } - } + if (!request?.data) return noAffiliateResult('PENDING', 'No request data found from Relay API') const appFees = request.data.appFees?.length ? request.data.appFees : (request.data.paidAppFees ?? []) @@ -240,7 +227,7 @@ export class SwapVerificationService { return priceUsd?.toString() })() - const result: SwapVerificationResult = { + return { verificationStatus: 'SUCCESS', hasAffiliate: Boolean(shapeshiftFee), affiliateBps, @@ -251,13 +238,6 @@ export class SwapVerificationService { actualAffiliateFeeAssetId, actualAffiliateFeeUsd: actualAffiliateFeeUsd ?? undefined, } - - logVerification(this.logger, SwapperName.Relay, swapId, result, { - status: request.status, - feeAsset: result.actualAffiliateFeeAssetId, - }) - - return result } private async verifyCowSwap(swap: Swap): Promise { @@ -271,79 +251,53 @@ export class SwapVerificationService { const appDataHash = (metadata?.cowswapQuoteSpecific as { quote?: { appDataHash?: string } } | undefined)?.quote ?.appDataHash - if (!appDataHash) { - this.logger.warn(`CowSwap - Missing appDataHash for swap ${swapId}`) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing appDataHash in metadata', - } - } + if (!appDataHash) return noAffiliateResult('FAILED', 'Missing appDataHash in metadata') - try { - // ALWAYS fetch appData from CowSwap API to verify it's legitimate - this.logger.log(`CowSwap - Fetching appData from API using hash ${appDataHash} for swap ${swapId}`) - const cowNetwork = assertGetCowNetwork(sellChainId) - const response = await firstValueFrom( - this.httpService.get( - `${this.cowswapApiUrl}/${cowNetwork}/api/v1/app_data/${appDataHash}`, - ), - ) + // ALWAYS fetch appData from CowSwap API to verify it's legitimate + this.logger.log(`CowSwap - Fetching appData from API using hash ${appDataHash} for swap ${swapId}`) + const cowNetwork = assertGetCowNetwork(sellChainId) + const response = await firstValueFrom( + this.httpService.get( + `${this.cowswapApiUrl}/${cowNetwork}/api/v1/app_data/${appDataHash}`, + ), + ) - const decodedAppData = JSON.parse(response.data.fullAppData) as CowSwapDecodedAppData - - // Check if appCode is "shapeshift" - const appCode = decodedAppData?.appCode - const hasShapeshiftAppCode = appCode?.toLowerCase() === this.shapeshiftCowswapAppCode.toLowerCase() - - // Extract partner fee information from metadata.partnerFee - const partnerFee = decodedAppData?.metadata?.partnerFee - const affiliateBps = partnerFee?.bps - const affiliateAddress = partnerFee?.recipient - - // We have ShapeShift affiliate if appCode is shapeshift AND we have partnerFee - const hasShapeshiftAffiliate = hasShapeshiftAppCode && !!partnerFee - - let verifiedSellAmountCryptoBaseUnit: string | undefined - const orderUid = txHash || (metadata?.cowswapOrderUid as string | undefined) - if (orderUid) { - try { - const orderResponse = await firstValueFrom( - this.httpService.get(`${this.cowswapApiUrl}/${cowNetwork}/api/v1/orders/${orderUid}`), - ) - verifiedSellAmountCryptoBaseUnit = - orderResponse.data?.executedSellAmountBeforeFees?.toString() ?? - orderResponse.data?.executedSellAmount?.toString() - } catch (orderErr) { - this.logger.warn(`CowSwap - Failed to fetch order ${orderUid} for amount verification:`, orderErr) - } - } + const decodedAppData = JSON.parse(response.data.fullAppData) as CowSwapDecodedAppData - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, - affiliateAddress: hasShapeshiftAffiliate ? affiliateAddress : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + const appCode = decodedAppData?.appCode + const hasShapeshiftAppCode = appCode?.toLowerCase() === this.shapeshiftCowswapAppCode.toLowerCase() + + const partnerFee = decodedAppData?.metadata?.partnerFee + const affiliateBps = partnerFee?.bps + const affiliateAddress = partnerFee?.recipient + + const hasShapeshiftAffiliate = hasShapeshiftAppCode && !!partnerFee - logVerification(this.logger, SwapperName.CowSwap, swapId, result) - - return result - } catch (error) { - this.logger.error(`Error verifying CowSwap for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to decode CowSwap appData', + // Order-amount fetch is best-effort — failure here shouldn't fail the whole verification. + let verifiedSellAmountCryptoBaseUnit: string | undefined + const orderUid = txHash || (metadata?.cowswapOrderUid as string | undefined) + if (orderUid) { + try { + const orderResponse = await firstValueFrom( + this.httpService.get(`${this.cowswapApiUrl}/${cowNetwork}/api/v1/orders/${orderUid}`), + ) + verifiedSellAmountCryptoBaseUnit = + orderResponse.data?.executedSellAmountBeforeFees?.toString() ?? + orderResponse.data?.executedSellAmount?.toString() + } catch (orderErr) { + this.logger.warn(`CowSwap - Failed to fetch order ${orderUid} for amount verification:`, orderErr) } } + + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, + affiliateAddress: hasShapeshiftAffiliate ? affiliateAddress : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + } } private async verifyPortals(swap: Swap): Promise { @@ -356,852 +310,475 @@ export class SwapVerificationService { // Get the orderId from the swap (stored as the quote id) const orderId = (metadata?.portalsTransactionMetadata as { orderId?: string } | undefined)?.orderId - - if (!orderId) { - this.logger.warn(`Portals - Missing orderId for swap ${swapId}`) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing orderId in metadata', - } - } + if (!orderId) return noAffiliateResult('FAILED', 'Missing orderId in metadata') // Get the expected treasury address for this chain let expectedTreasuryAddress: string try { expectedTreasuryAddress = getTreasuryAddressFromChainId(sellChainId) } catch { - this.logger.warn(`Portals - Unsupported chain for treasury address: ${sellChainId}`) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: `Unsupported chain for treasury address: ${sellChainId}`, - } + return noAffiliateResult('FAILED', `Unsupported chain for treasury address: ${sellChainId}`) } - try { - // ALWAYS fetch order status from Portals API to verify it's legitimate - this.logger.log(`Portals - Fetching order status from API using orderId ${orderId} for swap ${swapId}`) - const response = await firstValueFrom( - this.httpService.get(`${this.portalsApiUrl}/v2/portal/status?orderId=${orderId}`), - ) - - const orderData = response.data - this.logger.log(`Portals - Fetched and verified order from API for swap ${swapId}`) - - // Get partner from the API response context - const partner = orderData?.context?.partner - - if (!partner) { - this.logger.warn(`Portals - No partner found in API response for swap ${swapId}`) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No partner found in Portals API response', - } - } + // ALWAYS fetch order status from Portals API to verify it's legitimate + this.logger.log(`Portals - Fetching order status from API using orderId ${orderId} for swap ${swapId}`) + const response = await firstValueFrom( + this.httpService.get(`${this.portalsApiUrl}/v2/portal/status?orderId=${orderId}`), + ) - // Verify partner matches the expected treasury address (case-insensitive for EVM addresses) - const hasShapeshiftAffiliate = partner.toLowerCase() === expectedTreasuryAddress.toLowerCase() + const orderData = response.data + this.logger.log(`Portals - Fetched and verified order from API for swap ${swapId}`) - const feeAmount = orderData?.context?.feeAmount + const partner = orderData?.context?.partner + if (!partner) return noAffiliateResult('SUCCESS', 'No partner found in Portals API response') - const verifiedSellAmountCryptoBaseUnit = orderData?.context?.inputAmount?.toString() ?? undefined + const hasShapeshiftAffiliate = partner.toLowerCase() === expectedTreasuryAddress.toLowerCase() - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: swap.affiliateBps ?? undefined, - affiliateAddress: hasShapeshiftAffiliate ? expectedTreasuryAddress : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + const verifiedSellAmountCryptoBaseUnit = orderData?.context?.inputAmount?.toString() ?? undefined - logVerification(this.logger, SwapperName.Portals, swapId, result, { feeAmount }) - - return result - } catch (error) { - this.logger.error(`Error verifying Portals for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify Portals order', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: swap.affiliateBps ?? undefined, + affiliateAddress: hasShapeshiftAffiliate ? expectedTreasuryAddress : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: orderData?.context?.feeAmount, } } private async verifyThorchain(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined - if (!txHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Thorchain verification', - } - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Thorchain verification') - try { - // SECURITY: Query Thorchain node API to verify memo contains affiliate info - const txUrl = `${this.thorchainNodeUrl}/thorchain/tx/${txHash}` + // SECURITY: Query Thorchain node API to verify memo contains affiliate info + const txUrl = `${this.thorchainNodeUrl}/thorchain/tx/${txHash}` - this.logger.log(`Thorchain - Fetching tx from node API: ${txUrl}`) + this.logger.log(`Thorchain - Fetching tx from node API: ${txUrl}`) - const response = await firstValueFrom(this.httpService.get(txUrl)) + const response = await firstValueFrom(this.httpService.get(txUrl)) - const observedTx = response.data?.observed_tx + const observedTx = response.data?.observed_tx - if (!observedTx || !observedTx.tx) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No observed transaction found', - } - } + if (!observedTx || !observedTx.tx) return noAffiliateResult('PENDING', 'No observed transaction found') - const memo: string | undefined = observedTx.tx.memo - if (!memo) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No memo found in transaction', - } - } + const memo: string | undefined = observedTx.tx.memo + // Observed tx's memo is immutable on chain — absence is definitive, not transient. + if (!memo) return noAffiliateResult('SUCCESS', 'No memo found in transaction') - // Parse memo format: =:r:thor1dz68dtlzrxnjflha9vvs7yt7p77mqdnf5yugww:131082237:ss:0 - // The affiliate code is after the 4th colon, followed by fee in bps - const memoPattern = new RegExp(`:${this.shapeshiftThorchainAffiliate}:(\\d+)`, 'i') - const memoMatch = memo.match(memoPattern) + // Parse memo format: =:r:thor1dz68dtlzrxnjflha9vvs7yt7p77mqdnf5yugww:131082237:ss:0 + // The affiliate code is after the 4th colon, followed by fee in bps + const memoPattern = new RegExp(`:${this.shapeshiftThorchainAffiliate}:(\\d+)`, 'i') + const memoMatch = memo.match(memoPattern) - const hasShapeshiftAffiliate = !!memoMatch - const affiliateBps = memoMatch ? parseInt(memoMatch[1]) : undefined + const hasShapeshiftAffiliate = !!memoMatch + const affiliateBps = memoMatch ? parseInt(memoMatch[1]) : undefined - const coins = observedTx.tx.coins - const sellAssetPrecision = swap.sellAsset.precision ?? THORCHAIN_PRECISION - const firstCoinAmount = coins?.[0]?.amount - const verifiedSellAmountCryptoBaseUnit = firstCoinAmount - ? thorchainToNativePrecision(firstCoinAmount, sellAssetPrecision) - : undefined + const coins = observedTx.tx.coins + const sellAssetPrecision = swap.sellAsset.precision ?? THORCHAIN_PRECISION + const firstCoinAmount = coins?.[0]?.amount + const verifiedSellAmountCryptoBaseUnit = firstCoinAmount + ? thorchainToNativePrecision(firstCoinAmount, sellAssetPrecision) + : undefined - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, - affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftThorchainAffiliate : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - - logVerification(this.logger, SwapperName.Thorchain, swapId, result, { memo }) - - return result - } catch (error) { - this.logger.error(`Error verifying Thorchain for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to fetch Thorchain data from node', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, + affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftThorchainAffiliate : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } private async verifyMaya(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined - if (!txHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Maya verification', - } - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Maya verification') - try { - // SECURITY: Query Maya node API to verify memo contains affiliate info - const txUrl = `${this.mayachainNodeUrl}/mayachain/tx/${txHash}` + // SECURITY: Query Maya node API to verify memo contains affiliate info + const txUrl = `${this.mayachainNodeUrl}/mayachain/tx/${txHash}` - this.logger.log(`Maya - Fetching tx from node API: ${txUrl}`) + this.logger.log(`Maya - Fetching tx from node API: ${txUrl}`) - const response = await firstValueFrom(this.httpService.get(txUrl)) + const response = await firstValueFrom(this.httpService.get(txUrl)) - const observedTx = response.data?.observed_tx + const observedTx = response.data?.observed_tx - if (!observedTx || !observedTx.tx) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No observed transaction found', - } - } + if (!observedTx || !observedTx.tx) return noAffiliateResult('PENDING', 'No observed transaction found') - const memo: string | undefined = observedTx.tx.memo - if (!memo) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No memo found in transaction', - } - } - - // Parse memo format: =:r:maya1dz68dtlzrxnjflha9vvs7yt7p77mqdnf5yugww:131082237:ss:0 - // The affiliate code is after the 4th colon, followed by fee in bps - const memoPattern = new RegExp(`:${this.shapeshiftMayaAffiliate}:(\\d+)`, 'i') - const memoMatch = memo.match(memoPattern) + const memo: string | undefined = observedTx.tx.memo + // Observed tx's memo is immutable on chain — absence is definitive, not transient. + if (!memo) return noAffiliateResult('SUCCESS', 'No memo found in transaction') - const hasShapeshiftAffiliate = !!memoMatch - const affiliateBps = memoMatch ? parseInt(memoMatch[1]) : undefined + // Parse memo format: =:r:maya1dz68dtlzrxnjflha9vvs7yt7p77mqdnf5yugww:131082237:ss:0 + // The affiliate code is after the 4th colon, followed by fee in bps + const memoPattern = new RegExp(`:${this.shapeshiftMayaAffiliate}:(\\d+)`, 'i') + const memoMatch = memo.match(memoPattern) - const coins = observedTx.tx.coins - const sellAssetPrecision = swap.sellAsset.precision ?? THORCHAIN_PRECISION - const firstCoinAmount = coins?.[0]?.amount - const verifiedSellAmountCryptoBaseUnit = firstCoinAmount - ? thorchainToNativePrecision(firstCoinAmount, sellAssetPrecision) - : undefined + const hasShapeshiftAffiliate = !!memoMatch + const affiliateBps = memoMatch ? parseInt(memoMatch[1]) : undefined - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, - affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftMayaAffiliate : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + const coins = observedTx.tx.coins + const sellAssetPrecision = swap.sellAsset.precision ?? THORCHAIN_PRECISION + const firstCoinAmount = coins?.[0]?.amount + const verifiedSellAmountCryptoBaseUnit = firstCoinAmount + ? thorchainToNativePrecision(firstCoinAmount, sellAssetPrecision) + : undefined - logVerification(this.logger, SwapperName.Mayachain, swapId, result, { memo }) - - return result - } catch (error) { - this.logger.error(`Error verifying Maya for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to fetch Maya data from node', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, + affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftMayaAffiliate : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } private async verifyChainflip(swap: Swap): Promise { - const { swapId } = swap const metadata = swap.metadata as Record const chainflipSwapId = metadata?.chainflipSwapId as string | undefined - if (!chainflipSwapId) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing chainflipSwapId in metadata', - } + if (!chainflipSwapId) return noAffiliateResult('FAILED', 'Missing chainflipSwapId in metadata') + + const statusUrl = `${this.chainflipApiUrl}/swaps/${chainflipSwapId}` + + const headers: Record = {} + if (this.chainflipApiKey) { + headers['Authorization'] = `Bearer ${this.chainflipApiKey}` } - try { - const statusUrl = `${this.chainflipApiUrl}/swaps/${chainflipSwapId}` + const response = await firstValueFrom(this.httpService.get(statusUrl, { headers })) - const headers: Record = {} - if (this.chainflipApiKey) { - headers['Authorization'] = `Bearer ${this.chainflipApiKey}` - } + const swapData = response.data - const response = await firstValueFrom(this.httpService.get(statusUrl, { headers })) + if (!swapData) return noAffiliateResult('PENDING', 'No swap data found from Chainflip API') - const swapData = response.data + const affiliate = swapData.affiliate || swapData.affiliateName + const affiliateBps = swapData.affiliateBps || swapData.affiliateFee - if (!swapData) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No swap data found from Chainflip API', - } - } + const hasShapeshiftAffiliate = affiliate?.toLowerCase() === this.shapeshiftChainflipAffiliate.toLowerCase() - // Check for affiliate information in the swap data - const affiliate = swapData.affiliate || swapData.affiliateName - const affiliateBps = swapData.affiliateBps || swapData.affiliateFee - - const hasShapeshiftAffiliate = affiliate?.toLowerCase() === this.shapeshiftChainflipAffiliate.toLowerCase() - - const verifiedSellAmountCryptoBaseUnit = ( - swapData.depositAmount ?? - swapData.ingressAmount ?? - swapData.sourceAmount - )?.toString() - - return { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: hasShapeshiftAffiliate && affiliateBps ? parseInt(String(affiliateBps)) : undefined, - affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftChainflipAffiliate : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - } catch (error) { - this.logger.error(`Error verifying Chainflip for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to fetch Chainflip swap data', - } + const verifiedSellAmountCryptoBaseUnit = ( + swapData.depositAmount ?? + swapData.ingressAmount ?? + swapData.sourceAmount + )?.toString() + + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: hasShapeshiftAffiliate && affiliateBps ? parseInt(String(affiliateBps)) : undefined, + affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftChainflipAffiliate : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } private async verifyZrx(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record const tradeHash = txHash || (metadata?.tradeHash as string | undefined) || (metadata?.txHash as string | undefined) - if (!tradeHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing tradeHash in metadata', - } - } - - try { - // Use 0x Trade Analytics API via ShapeShift proxy to verify the trade - const requestUrl = `${this.zrxApiUrl}/trade-analytics/swap?txHash=${tradeHash}` + if (!tradeHash) return noAffiliateResult('FAILED', 'Missing tradeHash in metadata') - const response = await firstValueFrom(this.httpService.get(requestUrl)) + // Use 0x Trade Analytics API via ShapeShift proxy to verify the trade + const requestUrl = `${this.zrxApiUrl}/trade-analytics/swap?txHash=${tradeHash}` - const trades: ZrxTrade[] = Array.isArray(response.data) - ? response.data - : response.data?.trades || response.data?.results || [] + const response = await firstValueFrom(this.httpService.get(requestUrl)) - const trade = trades.find( - (t: ZrxTrade) => - t.txHash?.toLowerCase() === tradeHash.toLowerCase() || - t.transactionHash?.toLowerCase() === tradeHash.toLowerCase(), - ) + const trades: ZrxTrade[] = Array.isArray(response.data) + ? response.data + : response.data?.trades || response.data?.results || [] - if (!trade) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: `Trade not found in 0x analytics (searched ${trades.length} trades)`, - } - } + const trade = trades.find( + (t: ZrxTrade) => + t.txHash?.toLowerCase() === tradeHash.toLowerCase() || + t.transactionHash?.toLowerCase() === tradeHash.toLowerCase(), + ) - // Check for ShapeShift's partner/integrator name - // The field could be integratorId, integratorName, or affiliateName - const integratorId = trade.integratorId || trade.integratorName || trade.affiliateName - const hasShapeshiftAffiliate = integratorId?.toLowerCase() === this.shapeshift0xIntegrator.toLowerCase() + if (!trade) + return noAffiliateResult('PENDING', `Trade not found in 0x analytics (searched ${trades.length} trades)`) - // Extract fee information - // The fee could be in integratorFee, affiliateFee, or partnerFee fields - // Note: 0x fees are typically in decimal format (e.g., 0.0015 for 15 bps) - const integratorFee = trade.integratorFee || trade.affiliateFee || trade.partnerFee - let affiliateBps: number | undefined + // The integrator field could be integratorId, integratorName, or affiliateName + const integratorId = trade.integratorId || trade.integratorName || trade.affiliateName + const hasShapeshiftAffiliate = integratorId?.toLowerCase() === this.shapeshift0xIntegrator.toLowerCase() - if (integratorFee) { - // Convert decimal fee to basis points (e.g., 0.0015 -> 15 bps) - affiliateBps = parseFloat(integratorFee) * 10000 - } + // 0x fees are decimal (e.g., 0.0015 == 15 bps) + const integratorFee = trade.integratorFee || trade.affiliateFee || trade.partnerFee + const affiliateBps = integratorFee ? parseFloat(integratorFee) * 10000 : undefined - const verifiedSellAmountCryptoBaseUnit = (trade.sellAmount ?? trade.inputTokenAmount ?? trade.amount)?.toString() + const verifiedSellAmountCryptoBaseUnit = (trade.sellAmount ?? trade.inputTokenAmount ?? trade.amount)?.toString() - return { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps, - affiliateAddress: hasShapeshiftAffiliate ? this.shapeshift0xIntegrator : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - } catch (error) { - this.logger.error(`Error verifying 0x for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify 0x trade', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps, + affiliateAddress: hasShapeshiftAffiliate ? this.shapeshift0xIntegrator : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } private async verifyBebop(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined - if (!txHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Bebop verification', - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Bebop verification') + + // Query trade history with source filter, scoped to swap createdAt +/- 1 hour (nanoseconds) + const swapTimestamp = swap.createdAt.getTime() + const oneHour = 60 * 60 * 1000 + const startNano = (swapTimestamp - oneHour) * 1_000_000 + const endNano = (swapTimestamp + oneHour) * 1_000_000 + + const queryParams = new URLSearchParams({ + start: startNano.toString(), + end: endNano.toString(), + source: this.shapeshiftBebopSource, + }) + + const headers = { + 'source-auth': this.bebopApiKey, } - try { - // Get swap timestamp to create time range (swap createdAt +/- 1 hour) - const swapTimestamp = swap.createdAt.getTime() - const oneHour = 60 * 60 * 1000 - const startNano = (swapTimestamp - oneHour) * 1_000_000 // Convert to nanoseconds - const endNano = (swapTimestamp + oneHour) * 1_000_000 - - // Query trade history with source filter and time range - const queryParams = new URLSearchParams({ + const requestUrl = `${this.bebopApiUrl}/history/v2/trades?${queryParams.toString()}` + + this.logger.log(`Bebop API Request - URL: ${requestUrl}`) + this.logger.log( + `Bebop API Request - Params: ${JSON.stringify({ start: startNano.toString(), end: endNano.toString(), source: this.shapeshiftBebopSource, - }) - - const headers = { - 'source-auth': this.bebopApiKey, - } - - const requestUrl = `${this.bebopApiUrl}/history/v2/trades?${queryParams.toString()}` - - // Log request details - this.logger.log(`Bebop API Request - URL: ${requestUrl}`) - this.logger.log( - `Bebop API Request - Params: ${JSON.stringify({ - start: startNano.toString(), - end: endNano.toString(), - source: this.shapeshiftBebopSource, - swapTimestamp: new Date(swapTimestamp).toISOString(), - })}`, - ) - this.logger.log(`Bebop API Request - Headers: { 'source-auth': '[REDACTED]' }`) - this.logger.log(`Bebop API Request - Looking for txHash: ${txHash}`) + swapTimestamp: new Date(swapTimestamp).toISOString(), + })}`, + ) + this.logger.log(`Bebop API Request - Looking for txHash: ${txHash}`) - const response = await firstValueFrom(this.httpService.get(requestUrl, { headers })) + const response = await firstValueFrom(this.httpService.get(requestUrl, { headers })) - this.logger.log(`Bebop API Response - Status: ${response.status}`) - this.logger.log(`Bebop API Response - Data: ${JSON.stringify(response.data)}`) + this.logger.log(`Bebop API Response - Status: ${response.status}`) + this.logger.log(`Bebop API Response - Data: ${JSON.stringify(response.data)}`) - const trades = response.data?.results || [] - this.logger.log(`Bebop API Response - Found ${trades.length} trades`) + const trades = response.data?.results || [] + this.logger.log(`Bebop API Response - Found ${trades.length} trades`) - const trade = trades.find((t: BebopTrade) => t.txHash?.toLowerCase() === txHash.toLowerCase()) + const trade = trades.find((t: BebopTrade) => t.txHash?.toLowerCase() === txHash.toLowerCase()) - if (!trade) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Trade not found in Bebop history', - } - } + if (!trade) return noAffiliateResult('PENDING', 'Trade not found in Bebop history') - // Since we filtered by source=shapeshift, finding the trade means it was made through ShapeShift - const hasShapeshiftAffiliate = true + // Filtered by source=shapeshift, so finding the trade implies a ShapeShift route. + const hasShapeshiftAffiliate = true - // Extract partner fee from the response (partnerFeeBps is in basis points) - const partnerFeeBps = trade.partnerFeeBps - const affiliateBps = partnerFeeBps != null ? Number(partnerFeeBps) : undefined + const partnerFeeBps = trade.partnerFeeBps + const affiliateBps = partnerFeeBps != null ? Number(partnerFeeBps) : undefined - const sellTokenEntries = trade.sellTokens ? Object.values(trade.sellTokens) : [] - const verifiedSellAmountCryptoBaseUnit = sellTokenEntries[0]?.amount?.toString() ?? undefined + const sellTokenEntries = trade.sellTokens ? Object.values(trade.sellTokens) : [] + const verifiedSellAmountCryptoBaseUnit = sellTokenEntries[0]?.amount?.toString() ?? undefined - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps, - affiliateAddress: this.shapeshiftBebopSource, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - - logVerification(this.logger, SwapperName.Bebop, swapId, result) - - return result - } catch (error) { - this.logger.error(`Error verifying Bebop for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify Bebop trade', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps, + affiliateAddress: this.shapeshiftBebopSource, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } - private verifyArbitrumBridge(swap: Swap): Promise { - const { swapId } = swap - - const result: SwapVerificationResult = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private verifyArbitrumBridge(_swap: Swap): Promise { + return Promise.resolve({ verificationStatus: 'SUCCESS', hasAffiliate: false, actualBuyAmountCryptoBaseUnit: undefined, actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - - logVerification(this.logger, SwapperName.ArbitrumBridge, swapId, result) - - return Promise.resolve(result) + }) } private async verifyButterSwap(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for ButterSwap verification', - } - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for ButterSwap verification') - try { - const apiUrl = `https://bs-app-api.chainservice.io/api/queryBridgeInfoBySourceHash?hash=${txHash}` - - this.logger.log(`ButterSwap - Fetching bridge info from API: ${apiUrl}`) - - const response = await firstValueFrom(this.httpService.get(apiUrl)) + const apiUrl = `https://bs-app-api.chainservice.io/api/queryBridgeInfoBySourceHash?hash=${txHash}` - const bridgeInfo = response.data?.data?.info + this.logger.log(`ButterSwap - Fetching bridge info from API: ${apiUrl}`) - if (!bridgeInfo) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'No bridge info found', - } - } + const response = await firstValueFrom(this.httpService.get(apiUrl)) - const entrance = bridgeInfo.entrance - const hasShapeshiftAffiliate = entrance?.toLowerCase() === this.shapeshiftButterswapEntrance.toLowerCase() + const bridgeInfo = response.data?.data?.info + if (!bridgeInfo) return noAffiliateResult('PENDING', 'No bridge info found') - const affiliateBps = swap.affiliateBps ?? undefined + const entrance = bridgeInfo.entrance + const hasShapeshiftAffiliate = entrance?.toLowerCase() === this.shapeshiftButterswapEntrance.toLowerCase() - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() + const affiliateBps = swap.affiliateBps ?? undefined - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate: hasShapeshiftAffiliate, - affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, - affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftButterswapEntrance : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - logVerification(this.logger, SwapperName.ButterSwap, swapId, result) - - return result - } catch (error) { - this.logger.error(`Error verifying ButterSwap for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify ButterSwap trade', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate: hasShapeshiftAffiliate, + affiliateBps: hasShapeshiftAffiliate && affiliateBps ? affiliateBps : undefined, + affiliateAddress: hasShapeshiftAffiliate ? this.shapeshiftButterswapEntrance : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } - private verifyCetus(swap: Swap): Promise { - const { swapId } = swap + // eslint-disable-next-line @typescript-eslint/require-await + private async verifyCetus(swap: Swap): Promise { const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Cetus verification', - }) - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Cetus verification') - try { - // TODO: Implement on-chain/API verification for Cetus - const affiliateBps = swap.affiliateBps ?? undefined - const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 - - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() - - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate, - affiliateBps: hasAffiliate ? affiliateBps : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + // TODO: Implement on-chain/API verification for Cetus + const affiliateBps = swap.affiliateBps ?? undefined + const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 + + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - logVerification(this.logger, SwapperName.Cetus, swapId, result) - - return Promise.resolve(result) - } catch (error) { - this.logger.error(`Error verifying Cetus for swap ${swapId}:`, error) - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify Cetus trade', - }) + return { + verificationStatus: 'SUCCESS', + hasAffiliate, + affiliateBps: hasAffiliate ? affiliateBps : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } - private verifySunio(swap: Swap): Promise { - const { swapId } = swap + // eslint-disable-next-line @typescript-eslint/require-await + private async verifySunio(swap: Swap): Promise { const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Sun.io verification', - }) - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Sun.io verification') - try { - // TODO: Implement on-chain/API verification for Sun.io - const affiliateBps = swap.affiliateBps ?? undefined - const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 - - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() - - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate, - affiliateBps: hasAffiliate ? affiliateBps : undefined, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + // TODO: Implement on-chain/API verification for Sun.io + const affiliateBps = swap.affiliateBps ?? undefined + const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 + + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - logVerification(this.logger, SwapperName.Sunio, swapId, result) - - return Promise.resolve(result) - } catch (error) { - this.logger.error(`Error verifying Sun.io for swap ${swapId}:`, error) - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify Sun.io trade', - }) + return { + verificationStatus: 'SUCCESS', + hasAffiliate, + affiliateBps: hasAffiliate ? affiliateBps : undefined, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } - private verifyAvnu(swap: Swap): Promise { - const { swapId } = swap + // eslint-disable-next-line @typescript-eslint/require-await + private async verifyAvnu(swap: Swap): Promise { const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for AVNU verification', - }) - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for AVNU verification') - try { - // TODO: Implement on-chain/API verification for AVNU - const affiliateBps = swap.affiliateBps ?? undefined - const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 - const affiliateAddress = swap.affiliateAddress ?? undefined - - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() - - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate, - affiliateBps: hasAffiliate ? affiliateBps : undefined, - affiliateAddress, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + // TODO: Implement on-chain/API verification for AVNU + const affiliateBps = swap.affiliateBps ?? undefined + const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 + const affiliateAddress = swap.affiliateAddress ?? undefined + + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - logVerification(this.logger, SwapperName.Avnu, swapId, result) - - return Promise.resolve(result) - } catch (error) { - this.logger.error(`Error verifying AVNU for swap ${swapId}:`, error) - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify AVNU trade', - }) + return { + verificationStatus: 'SUCCESS', + hasAffiliate, + affiliateBps: hasAffiliate ? affiliateBps : undefined, + affiliateAddress, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } - private verifyStonfi(swap: Swap): Promise { - const { swapId } = swap + // eslint-disable-next-line @typescript-eslint/require-await + private async verifyStonfi(swap: Swap): Promise { const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for STON.fi verification', - }) - } - - try { - // TODO: Implement on-chain/API verification for STON.fi - const stonfiSpecific = metadata?.stonfiSpecific as StonfiQuoteMetadata | undefined + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for STON.fi verification') - const referrerAddress = stonfiSpecific?.referrerAddress + // TODO: Implement on-chain/API verification for STON.fi + const stonfiSpecific = metadata?.stonfiSpecific as StonfiQuoteMetadata | undefined - const affiliateBps = swap.affiliateBps ?? stonfiSpecific?.referrerFeeBps ?? undefined + const referrerAddress = stonfiSpecific?.referrerAddress - const hasAffiliate = !!referrerAddress && (affiliateBps !== undefined ? affiliateBps > 0 : false) + const affiliateBps = swap.affiliateBps ?? stonfiSpecific?.referrerFeeBps ?? undefined - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() + const hasAffiliate = !!referrerAddress && (affiliateBps !== undefined ? affiliateBps > 0 : false) - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate, - affiliateBps: hasAffiliate ? affiliateBps : undefined, - affiliateAddress: referrerAddress, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - logVerification(this.logger, SwapperName.Stonfi, swapId, result) - - return Promise.resolve(result) - } catch (error) { - this.logger.error(`Error verifying STON.fi for swap ${swapId}:`, error) - return Promise.resolve({ - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify STON.fi trade', - }) + return { + verificationStatus: 'SUCCESS', + hasAffiliate, + affiliateBps: hasAffiliate ? affiliateBps : undefined, + affiliateAddress: referrerAddress, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } private async verifyAcross(swap: Swap): Promise { - const { swapId } = swap const txHash = swap.sellTxHash || undefined const metadata = swap.metadata as Record - if (!txHash) { - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: 'Missing txHash for Across verification', - } - } + if (!txHash) return noAffiliateResult('FAILED', 'Missing txHash for Across verification') - try { - const statusUrl = `${this.acrossApiUrl}/deposit/status?depositTxnRef=${txHash}` + const statusUrl = `${this.acrossApiUrl}/deposit/status?depositTxnRef=${txHash}` - this.logger.log(`Across - Fetching deposit status from API: ${statusUrl}`) + this.logger.log(`Across - Fetching deposit status from API: ${statusUrl}`) - const response = await firstValueFrom(this.httpService.get(statusUrl)) + const response = await firstValueFrom(this.httpService.get(statusUrl)) - // TODO: Implement on-chain/API verification for Across - const depositStatus = response.data + // TODO: Implement on-chain/API verification for Across + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const depositStatus = response.data - const affiliateBps = swap.affiliateBps ?? undefined - const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 + const affiliateBps = swap.affiliateBps ?? undefined + const hasAffiliate = affiliateBps !== undefined && affiliateBps > 0 - const affiliateAddress = - (metadata?.appFeeRecipient as string | undefined) || (metadata?.integratorId as string | undefined) + const affiliateAddress = + (metadata?.appFeeRecipient as string | undefined) || (metadata?.integratorId as string | undefined) - const verifiedSellAmountCryptoBaseUnit = ( - (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit - )?.toString() + const verifiedSellAmountCryptoBaseUnit = ( + (metadata?.sellAmountIncludingProtocolFeesCryptoBaseUnit as string | undefined) ?? swap.sellAmountCryptoBaseUnit + )?.toString() - const result: SwapVerificationResult = { - verificationStatus: 'SUCCESS', - hasAffiliate, - affiliateBps: hasAffiliate ? affiliateBps : undefined, - affiliateAddress, - verifiedSellAmountCryptoBaseUnit, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - } - - logVerification(this.logger, SwapperName.Across, swapId, result, { status: depositStatus?.status }) - - return result - } catch (error) { - this.logger.error(`Error verifying Across for swap ${swapId}:`, error) - return { - verificationStatus: 'SUCCESS', - hasAffiliate: false, - actualBuyAmountCryptoBaseUnit: undefined, - actualAffiliateFeeAmountCryptoBaseUnit: undefined, - error: error instanceof Error ? error.message : 'Failed to verify Across deposit', - } + return { + verificationStatus: 'SUCCESS', + hasAffiliate, + affiliateBps: hasAffiliate ? affiliateBps : undefined, + affiliateAddress, + verifiedSellAmountCryptoBaseUnit, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, } } } diff --git a/apps/swap-service/src/verification/utils.ts b/apps/swap-service/src/verification/utils.ts index b74d92d..e1f0e91 100644 --- a/apps/swap-service/src/verification/utils.ts +++ b/apps/swap-service/src/verification/utils.ts @@ -1,8 +1,6 @@ -import type { Logger } from '@nestjs/common' - import type { SwapVerificationResult } from '@shapeshift/shared-types' -import type { SwapperName } from '@shapeshiftoss/swapper' +export const BPS_DENOMINATOR = 10000n export const THORCHAIN_PRECISION = 8 export const thorchainToNativePrecision = (thorchainAmount: string, nativePrecision: number): string => { @@ -13,7 +11,16 @@ export const thorchainToNativePrecision = (thorchainAmount: string, nativePrecis return trimmed || '0' } -const BPS_DENOMINATOR = 10000n +export const noAffiliateResult = ( + verificationStatus: SwapVerificationResult['verificationStatus'], + noAffiliateReason: string, +): SwapVerificationResult => ({ + verificationStatus, + hasAffiliate: false, + actualBuyAmountCryptoBaseUnit: undefined, + actualAffiliateFeeAmountCryptoBaseUnit: undefined, + noAffiliateReason, +}) export const applyBps = (amount: string | undefined, bps: number | undefined): string | undefined => { if (!amount || bps === undefined) return undefined @@ -24,31 +31,3 @@ export const applyBps = (amount: string | undefined, bps: number | undefined): s return undefined } } - -export const logVerification = ( - logger: Logger, - swapperName: SwapperName, - swapId: string, - result: SwapVerificationResult, - extra?: Record, -): void => { - const affiliate = result.hasAffiliate - ? `affiliate=${result.affiliateAddress} (${result.affiliateBps} bps)` - : 'affiliate=none' - - const extras = Object.entries(extra ?? {}) - .filter(([, v]) => v !== undefined && v !== null && v !== '') - .map(([k, v]) => `${k}=${v}`) - - const segments = [ - `${swapperName} verification`, - `swapId=${swapId}`, - affiliate, - `sell=${result.verifiedSellAmountCryptoBaseUnit ?? 'none'}`, - `buy=${result.actualBuyAmountCryptoBaseUnit ?? 'none'}`, - `fee=${result.actualAffiliateFeeAmountCryptoBaseUnit ?? 'none'}`, - ...extras, - ] - - logger.log(segments.join(' | ')) -} diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index a118233..325eeea 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -123,7 +123,7 @@ export interface SwapVerificationResult { actualAffiliateFeeAmountCryptoBaseUnit: string | undefined actualAffiliateFeeAssetId?: string actualAffiliateFeeUsd?: string - error?: string + noAffiliateReason?: string } export interface Fees { From 08251a3b0090d4eb42d7bfe73920fe5884289012 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 6 May 2026 13:17:46 -0600 Subject: [PATCH 3/6] refactor(swap-service): split tx-status and verification polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decouples the two axes that were sharing a single 5s cron + worker pool. CowSwap and other multi-call verifications could starve tx-status workers, and verification retries were hammering upstream APIs at tx-status cadence (5s). Polling layer now has two crons with independent isPolling guards and shared worker concurrency cap (10): - pollPendingTxStatus every 5s - pollPendingVerification every 30s Each fetches its own focused swap set: - getPendingTxSwaps status IN ('IDLE','PENDING') - getPendingVerificationSwaps verificationStatus='PENDING' AND status IN ('SUCCESS','FAILED') The status filters are mutually exclusive, so swaps never appear in both queues. Worker pools are also independent — long-running verifier I/O can't block tx-status checks. Collapses markVerificationFailed into verifySwap as a status='FAILED' short-circuit (the only caller was the poll loop's else branch). Drops unused getPendingSwaps service method and GET /swaps/pending controller route — no callers in the monorepo or web repo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/polling/swap-polling.service.ts | 120 ++++++++++-------- .../src/swaps/swaps.controller.ts | 5 - apps/swap-service/src/swaps/swaps.service.ts | 35 +++-- 3 files changed, 88 insertions(+), 72 deletions(-) diff --git a/apps/swap-service/src/polling/swap-polling.service.ts b/apps/swap-service/src/polling/swap-polling.service.ts index cbd3074..10ac88e 100644 --- a/apps/swap-service/src/polling/swap-polling.service.ts +++ b/apps/swap-service/src/polling/swap-polling.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common' import { Cron, CronExpression } from '@nestjs/schedule' import { SwapsService } from '../swaps/swaps.service' +import type { Swap } from '../swaps/types' import { WebsocketGateway } from '../websocket/websocket.gateway' const POLL_CONCURRENCY = 10 @@ -10,7 +11,8 @@ const POLL_CONCURRENCY = 10 export class SwapPollingService { private readonly logger = new Logger(SwapPollingService.name) - private isPolling = false + private isPollingTx = false + private isPollingVerification = false constructor( private swapsService: SwapsService, @@ -18,76 +20,84 @@ export class SwapPollingService { ) {} @Cron(CronExpression.EVERY_5_SECONDS) - async pollPendingSwaps() { - if (this.isPolling) return - this.isPolling = true + async pollPendingTxStatus() { + if (this.isPollingTx) return + this.isPollingTx = true try { - // TODO: paginate with a batch size + oldest-first ordering once the in-flight - // queue grows enough that one cron tick can't drain it within the 5s interval. - const pendingSwaps = await this.swapsService.getPendingSwaps() - if (pendingSwaps.length === 0) return - - this.logger.log(`Polling ${pendingSwaps.length} pending swaps`) - - const queue = [...pendingSwaps] - const workers = Array.from({ length: Math.min(POLL_CONCURRENCY, queue.length) }, async () => { - while (queue.length > 0) { - const swap = queue.shift() - if (swap) await this.pollOne(swap) - } - }) - await Promise.all(workers) + const swaps = await this.swapsService.getPendingTxSwaps() + if (swaps.length === 0) return + + this.logger.log(`Polling tx status for ${swaps.length} swaps`) + await this.runWorkers(swaps, (swap) => this.pollTxStatus(swap)) } catch (err) { - this.logger.error('Failed to poll pending swaps:', err) + this.logger.error('Failed to poll pending tx status:', err) } finally { - this.isPolling = false + this.isPollingTx = false } } - private async pollOne(swap: Awaited>[number]): Promise { - let current = swap - - if (current.status === 'IDLE' || current.status === 'PENDING') { - try { - const statusUpdate = await this.swapsService.checkSwapStatus(current.swapId) + @Cron(CronExpression.EVERY_30_SECONDS) + async pollPendingVerification() { + if (this.isPollingVerification) return + this.isPollingVerification = true - if (statusUpdate.status !== current.status) { - this.logger.log(`Status changed for swap ${current.swapId}: ${current.status} -> ${statusUpdate.status}`) + try { + const swaps = await this.swapsService.getPendingVerificationSwaps() + if (swaps.length === 0) return - current = await this.swapsService.updateSwapStatus({ - swapId: current.swapId, - status: statusUpdate.status, - sellTxHash: statusUpdate.sellTxHash, - buyTxHash: statusUpdate.buyTxHash, - statusMessage: statusUpdate.statusMessage, - }) + this.logger.log(`Polling verification for ${swaps.length} swaps`) + await this.runWorkers(swaps, (swap) => this.pollVerification(swap)) + } catch (err) { + this.logger.error('Failed to poll pending verification:', err) + } finally { + this.isPollingVerification = false + } + } - this.websocketGateway.sendSwapUpdateToUser(current.userId, current) - } - } catch (err) { - this.logger.error(`Failed to poll tx status for swap ${current.swapId}:`, err) + private async runWorkers(swaps: Swap[], handler: (swap: Swap) => Promise): Promise { + const queue = [...swaps] + const workers = Array.from({ length: Math.min(POLL_CONCURRENCY, queue.length) }, async () => { + while (queue.length > 0) { + const swap = queue.shift() + if (swap) await handler(swap) } - } + }) + await Promise.all(workers) + } - if (current.verificationStatus !== 'PENDING') return + private async pollTxStatus(swap: Swap): Promise { + try { + const statusUpdate = await this.swapsService.checkSwapStatus(swap.swapId) + + if (statusUpdate.status === swap.status) return + + this.logger.log(`Status changed for swap ${swap.swapId}: ${swap.status} -> ${statusUpdate.status}`) + + const updated = await this.swapsService.updateSwapStatus({ + swapId: swap.swapId, + status: statusUpdate.status, + sellTxHash: statusUpdate.sellTxHash, + buyTxHash: statusUpdate.buyTxHash, + statusMessage: statusUpdate.statusMessage, + }) + + this.websocketGateway.sendSwapUpdateToUser(updated.userId, updated) + } catch (err) { + this.logger.error(`Failed to poll tx status for swap ${swap.swapId}:`, err) + } + } + private async pollVerification(swap: Swap): Promise { try { - if (current.status === 'SUCCESS') { - const updated = await this.swapsService.verifySwap(current) - if (updated.verificationStatus !== current.verificationStatus) { - this.logger.log( - `Verification changed for swap ${current.swapId}: ${current.verificationStatus} -> ${updated.verificationStatus}`, - ) - this.websocketGateway.sendSwapUpdateToUser(updated.userId, updated) - } - } else if (current.status === 'FAILED') { - const updated = await this.swapsService.markVerificationFailed(current.swapId) - this.logger.log(`Verification short-circuited to FAILED for swap ${current.swapId} (tx FAILED)`) - this.websocketGateway.sendSwapUpdateToUser(updated.userId, updated) + const updated = await this.swapsService.verifySwap(swap) + if (updated.verificationStatus !== swap.verificationStatus) { + this.logger.log( + `Verification changed for swap ${swap.swapId}: ${swap.verificationStatus} -> ${updated.verificationStatus}`, + ) } } catch (err) { - this.logger.error(`Failed to verify swap ${current.swapId}:`, err) + this.logger.error(`Failed to verify swap ${swap.swapId}:`, err) } } } diff --git a/apps/swap-service/src/swaps/swaps.controller.ts b/apps/swap-service/src/swaps/swaps.controller.ts index 2bf6094..ab67b4a 100644 --- a/apps/swap-service/src/swaps/swaps.controller.ts +++ b/apps/swap-service/src/swaps/swaps.controller.ts @@ -16,11 +16,6 @@ export class SwapsController { return this.swapsService.createSwap(data) } - @Get('pending') - async getPendingSwaps() { - return this.swapsService.getPendingSwaps() - } - @Get(':swapId') async getSwapById(@Param('swapId') swapId: string) { const swap = await this.swapsService.getSwapById(swapId) diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index 9f4a6d9..c8b6a77 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -232,11 +232,23 @@ export class SwapsService { return { swaps: rows.map(toSwap), nextCursor: getNextCursor(rows, limit) } } - async getPendingSwaps(): Promise { + async getPendingTxSwaps(): Promise { const swaps = await this.prisma.swap.findMany({ where: { sellTxHash: { not: null }, - OR: [{ status: { in: ['IDLE', 'PENDING'] } }, { verificationStatus: 'PENDING' }], + status: { in: ['IDLE', 'PENDING'] }, + }, + }) + + return swaps.map(toSwap) + } + + async getPendingVerificationSwaps(): Promise { + const swaps = await this.prisma.swap.findMany({ + where: { + sellTxHash: { not: null }, + verificationStatus: 'PENDING', + status: { in: ['SUCCESS', 'FAILED'] }, }, }) @@ -393,8 +405,16 @@ export class SwapsService { } async verifySwap(swap: Swap): Promise { - const verificationResult = await this.swapVerificationService.verifySwap(swap) + if (swap.status === 'FAILED') { + return toSwap( + await this.prisma.swap.update({ + where: { swapId: swap.swapId }, + data: { verificationStatus: 'FAILED', isAffiliateVerified: false }, + }), + ) + } + const verificationResult = await this.swapVerificationService.verifySwap(swap) if (verificationResult.verificationStatus === 'PENDING') return swap const isAffiliateVerified = verificationResult.verificationStatus === 'SUCCESS' && verificationResult.hasAffiliate @@ -421,13 +441,4 @@ export class SwapsService { }), ) } - - async markVerificationFailed(swapId: string): Promise { - return toSwap( - await this.prisma.swap.update({ - where: { swapId }, - data: { verificationStatus: 'FAILED', isAffiliateVerified: false }, - }), - ) - } } From ff1d2477c45902d9e986784b9c90f3c17545f0ed Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 6 May 2026 13:18:37 -0600 Subject: [PATCH 4/6] fix(swap-service): preserve error context when notification send fails Previous catch silently dropped the error and logged only a generic message. Capture err and pass it to logger.error so the stack trace makes it to the log output. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/swap-service/src/swaps/swaps.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index c8b6a77..1232744 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -183,8 +183,8 @@ export class SwapsService { try { await this.sendStatusUpdateNotification(swap) - } catch { - logger.error(`Failed to send notification for swap ${swap.swapId}`) + } catch (err) { + logger.error(`Failed to send notification for swap ${swap.swapId}:`, err) } logger.log(`Swap status updated: ${swap.swapId} -> ${swap.status}`) From 4a09ffa436c39b08c8a65284115acedf7e680721 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 6 May 2026 14:36:57 -0600 Subject: [PATCH 5/6] fix(swap-service): correct stale error field name in verifier tests Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/swap-service/src/swaps/swaps.service.ts | 2 +- .../src/verification/__tests__/near.test.ts | 2 +- .../src/verification/__tests__/relay.test.ts | 2 +- .../verification/swap-verification.service.ts | 28 ++++--------------- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index 1232744..892553f 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -187,7 +187,7 @@ export class SwapsService { logger.error(`Failed to send notification for swap ${swap.swapId}:`, err) } - logger.log(`Swap status updated: ${swap.swapId} -> ${swap.status}`) + logger.log(`Swap status updated for swap: ${swap.swapId} (${swap.status})`) return swap } catch (error) { diff --git a/apps/swap-service/src/verification/__tests__/near.test.ts b/apps/swap-service/src/verification/__tests__/near.test.ts index 105096b..40ae2ce 100644 --- a/apps/swap-service/src/verification/__tests__/near.test.ts +++ b/apps/swap-service/src/verification/__tests__/near.test.ts @@ -112,7 +112,7 @@ describe('verifyNearIntents', () => { expect(result).toMatchObject({ verificationStatus: 'FAILED', hasAffiliate: false, - error: 'Missing depositAddress in nearIntentsSpecific metadata', + noAffiliateReason: 'Missing depositAddress in nearIntentsSpecific metadata', }) }) diff --git a/apps/swap-service/src/verification/__tests__/relay.test.ts b/apps/swap-service/src/verification/__tests__/relay.test.ts index 96e8519..b4e3dc0 100644 --- a/apps/swap-service/src/verification/__tests__/relay.test.ts +++ b/apps/swap-service/src/verification/__tests__/relay.test.ts @@ -193,7 +193,7 @@ describe('verifyRelay', () => { expect(result).toMatchObject({ verificationStatus: 'FAILED', hasAffiliate: false, - error: 'Missing relayId in relayTransactionMetadata', + noAffiliateReason: 'Missing relayId in relayTransactionMetadata', }) }) diff --git a/apps/swap-service/src/verification/swap-verification.service.ts b/apps/swap-service/src/verification/swap-verification.service.ts index a225427..5a76b8c 100644 --- a/apps/swap-service/src/verification/swap-verification.service.ts +++ b/apps/swap-service/src/verification/swap-verification.service.ts @@ -114,31 +114,13 @@ export class SwapVerificationService { } private logResult(swap: Swap, result: SwapVerificationResult): void { - const detail = result.hasAffiliate - ? `affiliate=${result.affiliateAddress} (${result.affiliateBps} bps)` + const affiliateDetails = result.hasAffiliate + ? `${result.affiliateAddress} - ${result.affiliateBps} bps` : `noAffiliateReason=${result.noAffiliateReason ?? 'unknown'}` - const segments = [ - `${swap.swapperName} verification`, - `swapId=${swap.swapId}`, - `status=${result.verificationStatus}`, - detail, - ] - - const hasAmounts = - result.verifiedSellAmountCryptoBaseUnit || - result.actualBuyAmountCryptoBaseUnit || - result.actualAffiliateFeeAmountCryptoBaseUnit - - if (hasAmounts) { - segments.push( - `sell=${result.verifiedSellAmountCryptoBaseUnit ?? 'none'}`, - `buy=${result.actualBuyAmountCryptoBaseUnit ?? 'none'}`, - `fee=${result.actualAffiliateFeeAmountCryptoBaseUnit ?? 'none'}`, - ) - } - - this.logger.log(segments.join(' | ')) + this.logger.log( + `${swap.swapperName} verification for swap: ${swap.swapId} ${result.verificationStatus} (${affiliateDetails})`, + ) } private async verifyNearIntents(swap: Swap): Promise { From 8af27a8b48c915fb7cfd61fc5c5200d769f1ef98 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 6 May 2026 15:44:45 -0600 Subject: [PATCH 6/6] fix(swap-service): address coderabbit findings on verification PR - Fix relay fixture: status was PENDING but completion fields (buyTxHash, statusMessage, actualBuy) indicated a finished swap. Match the new invariant that verification only runs after tx-status terminalizes. - Round 0x integrator fee bps to integer to avoid float artifacts when fees are decimal-encoded. - Re-target the verification poll index to (verificationStatus, status) so the 30s cron's predicate is fully covered by the index. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/verification/__tests__/fixtures/relay/swap.ts | 2 +- apps/swap-service/src/verification/swap-verification.service.ts | 2 +- .../20260505235634_add_swap_verification_status/migration.sql | 2 +- prisma/schema/swap-service.prisma | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts b/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts index e0a6da7..6c2edc4 100644 --- a/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts +++ b/apps/swap-service/src/verification/__tests__/fixtures/relay/swap.ts @@ -35,7 +35,7 @@ export default { sellAmountCryptoBaseUnit: '1000000000000000', expectedBuyAmountCryptoBaseUnit: '2272662', actualBuyAmountCryptoBaseUnit: '2272662', - status: 'PENDING', + status: 'SUCCESS', source: 'Relay', swapperName: SwapperName.Relay, sellAccountId: '3c70e97c6f86a5b5cfdf82dfd3380ac4ed3d8b89dabf86f631bdf739372926be', diff --git a/apps/swap-service/src/verification/swap-verification.service.ts b/apps/swap-service/src/verification/swap-verification.service.ts index 5a76b8c..4602fde 100644 --- a/apps/swap-service/src/verification/swap-verification.service.ts +++ b/apps/swap-service/src/verification/swap-verification.service.ts @@ -493,7 +493,7 @@ export class SwapVerificationService { // 0x fees are decimal (e.g., 0.0015 == 15 bps) const integratorFee = trade.integratorFee || trade.affiliateFee || trade.partnerFee - const affiliateBps = integratorFee ? parseFloat(integratorFee) * 10000 : undefined + const affiliateBps = integratorFee ? Math.round(parseFloat(integratorFee) * 10000) : undefined const verifiedSellAmountCryptoBaseUnit = (trade.sellAmount ?? trade.inputTokenAmount ?? trade.amount)?.toString() diff --git a/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql b/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql index 1cccc12..a9568ae 100644 --- a/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql +++ b/prisma/migrations/20260505235634_add_swap_verification_status/migration.sql @@ -5,4 +5,4 @@ CREATE TYPE "public"."VerificationStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED ALTER TABLE "public"."swaps" ADD COLUMN "verificationStatus" "public"."VerificationStatus" NOT NULL DEFAULT 'PENDING'; -- CreateIndex -CREATE INDEX "swaps_verificationStatus_sellTxHash_idx" ON "public"."swaps"("verificationStatus", "sellTxHash"); +CREATE INDEX "swaps_verificationStatus_status_idx" ON "public"."swaps"("verificationStatus", "status"); diff --git a/prisma/schema/swap-service.prisma b/prisma/schema/swap-service.prisma index cb40b06..b6f655d 100644 --- a/prisma/schema/swap-service.prisma +++ b/prisma/schema/swap-service.prisma @@ -52,7 +52,7 @@ model Swap { @@index([referralCode]) @@index([affiliateAddress]) @@index([status, sellTxHash]) - @@index([verificationStatus, sellTxHash]) + @@index([verificationStatus, status]) @@index([userId]) @@map("swaps") }