Skip to content

Commit 4f99113

Browse files
committed
✨ server: add bridge fee window tracking
1 parent 0f204ad commit 4f99113

8 files changed

Lines changed: 818 additions & 61 deletions

File tree

.changeset/swift-foxes-track.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/server": patch
3+
---
4+
5+
✨ add bridge fee window tracking

server/api/ramp.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Hono } from "hono";
55
import {
66
array,
77
literal,
8+
number,
89
object,
910
optional,
1011
parse,
@@ -238,6 +239,12 @@ async function getOrCreateInquiry(credentialId: string, template: string) {
238239

239240
type QuoteResponse = undefined | { buyRate: string; sellRate: string };
240241

242+
const SponsoredFees = object({
243+
count: object({ available: string(), threshold: string() }),
244+
volume: object({ available: string(), threshold: string(), symbol: string() }),
245+
window: number(),
246+
});
247+
241248
// eslint-disable-next-line @typescript-eslint/no-unused-vars
242249
const DepositDetails = variant("network", [
243250
object({
@@ -265,6 +272,7 @@ const DepositDetails = variant("network", [
265272
displayName: literal("PIX BR"),
266273
beneficiaryName: string(),
267274
fee: string(),
275+
sponsoredFees: optional(SponsoredFees),
268276
estimatedProcessingTime: string(),
269277
}),
270278
object({
@@ -277,6 +285,7 @@ const DepositDetails = variant("network", [
277285
bankAddress: string(),
278286
beneficiaryAddress: string(),
279287
fee: string(),
288+
sponsoredFees: optional(SponsoredFees),
280289
estimatedProcessingTime: string(),
281290
}),
282291
object({
@@ -289,6 +298,7 @@ const DepositDetails = variant("network", [
289298
bankName: string(),
290299
beneficiaryAddress: string(),
291300
fee: string(),
301+
sponsoredFees: optional(SponsoredFees),
292302
estimatedProcessingTime: string(),
293303
}),
294304
object({
@@ -297,6 +307,7 @@ const DepositDetails = variant("network", [
297307
beneficiaryName: string(),
298308
iban: string(), // cspell:ignore iban
299309
fee: string(),
310+
sponsoredFees: optional(SponsoredFees),
300311
estimatedProcessingTime: string(),
301312
}),
302313
object({
@@ -305,27 +316,31 @@ const DepositDetails = variant("network", [
305316
beneficiaryName: string(),
306317
clabe: string(), // cspell:ignore clabe
307318
fee: string(),
319+
sponsoredFees: optional(SponsoredFees),
308320
estimatedProcessingTime: string(),
309321
}),
310322
object({
311323
network: literal("TRON"),
312324
displayName: literal("TRON"),
313325
address: string(),
314326
fee: string(),
327+
sponsoredFees: optional(SponsoredFees),
315328
estimatedProcessingTime: string(),
316329
}),
317330
object({
318331
network: literal("SOLANA"),
319332
displayName: literal("SOLANA"),
320333
address: string(),
321334
fee: string(),
335+
sponsoredFees: optional(SponsoredFees),
322336
estimatedProcessingTime: string(),
323337
}),
324338
object({
325339
network: literal("STELLAR"),
326340
displayName: literal("STELLAR"),
327341
address: string(),
328342
fee: string(),
343+
sponsoredFees: optional(SponsoredFees),
329344
estimatedProcessingTime: string(),
330345
}),
331346
object({
@@ -337,6 +352,7 @@ const DepositDetails = variant("network", [
337352
bankName: string(),
338353
bankAddress: string(),
339354
fee: string(),
355+
sponsoredFees: optional(SponsoredFees),
340356
estimatedProcessingTime: string(),
341357
}),
342358
]);

server/hooks/bridge.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { and, DrizzleQueryError, eq, isNull } from "drizzle-orm";
55
import { Hono } from "hono";
66
import { validator } from "hono/validator";
77
import { createHash, createVerify } from "node:crypto";
8-
import { literal, object, parse, picklist, string, unknown, variant } from "valibot";
8+
import { check, literal, object, parse, picklist, pipe, string, unknown, variant } from "valibot";
99

1010
import { Address } from "@exactly/common/validation";
1111

1212
import database, { credentials } from "../database";
1313
import { sendPushNotification } from "../utils/onesignal";
1414
import { searchAccounts } from "../utils/persona";
15-
import { BridgeCurrency, getCustomer, publicKey } from "../utils/ramps/bridge";
15+
import { BridgeCurrency, feeRule, getCustomer, publicKey } from "../utils/ramps/bridge";
1616
import { track } from "../utils/segment";
1717
import validatorHook from "../utils/validatorHook";
1818

@@ -52,6 +52,10 @@ export default new Hono().post(
5252
object({
5353
event_type: literal("liquidation_address.drain.updated.status_transitioned"),
5454
event_object: object({
55+
created_at: pipe(
56+
string(),
57+
check((v) => !Number.isNaN(new Date(v).getTime()), "invalid date"),
58+
),
5559
currency: picklist(BridgeCurrency),
5660
customer_id: string(),
5761
id: string(),
@@ -70,6 +74,10 @@ export default new Hono().post(
7074
object({ type: literal("in_review"), id: string(), customer_id: string() }),
7175
object({ type: literal("microdeposit"), id: string(), customer_id: string() }), // cspell:ignore microdeposit
7276
object({
77+
created_at: pipe(
78+
string(),
79+
check((v) => !Number.isNaN(new Date(v).getTime()), "invalid created at date"),
80+
),
7381
customer_id: string(),
7482
currency: picklist(BridgeCurrency),
7583
id: string(),
@@ -190,6 +198,18 @@ export default new Hono().post(
190198
}).catch((error: unknown) => captureException(error, { level: "error" }));
191199
}
192200
if (payload.event_object.type === "payment_processed") {
201+
const usdcAmount = Number(payload.event_object.receipt.final_amount);
202+
if (Number.isNaN(usdcAmount)) {
203+
captureException(new Error("invalid amount"), {
204+
level: "error",
205+
contexts: { details: { usdcAmount: payload.event_object.receipt.final_amount } },
206+
});
207+
return c.json({ code: "invalid amount" }, 200);
208+
}
209+
await feeRule.report(
210+
{ bridgeId, eventId: payload.event_object.id, amount: Math.round(usdcAmount * 100) },
211+
new Date(payload.event_object.created_at),
212+
);
193213
track({
194214
userId: account,
195215
event: "Onramp",
@@ -198,13 +218,25 @@ export default new Hono().post(
198218
amount: Number(payload.event_object.receipt.initial_amount),
199219
provider: "bridge",
200220
source: credential.source,
201-
usdcAmount: Number(payload.event_object.receipt.final_amount),
221+
usdcAmount,
202222
},
203223
});
204224
}
205225
return c.json({ code: "ok" }, 200);
206-
case "liquidation_address.drain.updated.status_transitioned":
226+
case "liquidation_address.drain.updated.status_transitioned": {
207227
if (payload.event_object.state !== "payment_submitted") return c.json({ code: "ok" }, 200);
228+
const usdcAmount = Number(payload.event_object.receipt.outgoing_amount);
229+
if (Number.isNaN(usdcAmount)) {
230+
captureException(new Error("invalid amount"), {
231+
level: "error",
232+
contexts: { details: { usdcAmount: payload.event_object.receipt.outgoing_amount } },
233+
});
234+
return c.json({ code: "invalid amount" }, 200);
235+
}
236+
await feeRule.report(
237+
{ bridgeId, eventId: payload.event_object.id, amount: Math.round(usdcAmount * 100) },
238+
new Date(payload.event_object.created_at),
239+
);
208240
sendPushNotification({
209241
userId: account,
210242
headings: { en: "Deposited funds" },
@@ -220,10 +252,11 @@ export default new Hono().post(
220252
amount: Number(payload.event_object.receipt.initial_amount),
221253
provider: "bridge",
222254
source: credential.source,
223-
usdcAmount: Number(payload.event_object.receipt.outgoing_amount),
255+
usdcAmount,
224256
},
225257
});
226258
return c.json({ code: "ok" }, 200);
259+
}
227260
}
228261
},
229262
);

server/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import panda from "./hooks/panda";
2020
import persona from "./hooks/persona";
2121
import androidFingerprints from "./utils/android/fingerprints";
2222
import appOrigin from "./utils/appOrigin";
23+
import { feeRule } from "./utils/ramps/bridge";
2324
import { close as closeRedis } from "./utils/redis";
2425
import { closeAndFlush as closeSegment } from "./utils/segment";
2526

@@ -322,8 +323,9 @@ const server = serve(app);
322323
export async function close() {
323324
return new Promise((resolve, reject) => {
324325
server.close((error) => {
325-
Promise.allSettled([closeSentry(), closeRedis(), closeSegment(), database.$client.end()])
326-
.then((results) => {
326+
Promise.allSettled([feeRule.stop(), closeSentry(), closeSegment(), database.$client.end()])
327+
.then(async (results) => {
328+
await closeRedis();
327329
if (error) reject(error);
328330
else if (results.some((result) => result.status === "rejected")) reject(new Error("closing services failed"));
329331
else resolve(null);

0 commit comments

Comments
 (0)