Skip to content

Commit 4418e19

Browse files
committed
✨ server: add maturity check and worker
1 parent 4ee5ab5 commit 4418e19

13 files changed

Lines changed: 790 additions & 23 deletions

File tree

.changeset/dull-candies-cheer.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 maturity check and worker

app.config.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ import versionCode from "./src/generated/versionCode.js";
2121
import type { IntercomPluginProps } from "@intercom/intercom-react-native/lib/typescript/module/expo-plugins/@types";
2222
import type { withSentry } from "@sentry/react-native/expo";
2323
import type { ExpoConfig } from "expo/config";
24-
import type * as OneSignalPlugin from "onesignal-expo-plugin/types/types";
24+
import type * as OneSignalPlugin from "onesignal-expo-plugin/build/types/types";
2525

26-
const { Mode } = require("onesignal-expo-plugin/build/types/types") as typeof OneSignalPlugin; // eslint-disable-line unicorn/prefer-module
2726

2827
if (env.EAS_BUILD_RUNNER === "eas-build") env.APP_DOMAIN ??= "web.exactly.app";
2928
if (env.APP_DOMAIN) env.EXPO_PUBLIC_DOMAIN = env.APP_DOMAIN;
@@ -121,7 +120,7 @@ export default {
121120
[
122121
"onesignal-expo-plugin",
123122
{
124-
mode: env.NODE_ENV === "production" ? Mode.Prod : Mode.Dev,
123+
mode: env.NODE_ENV === "production" ? "production" : "development",
125124
smallIcons: ["src/assets/notifications_default.png"],
126125
largeIcons: ["src/assets/notifications_default_large.png"],
127126
} satisfies OneSignalPlugin.OneSignalPluginProps,
@@ -168,7 +167,7 @@ export default {
168167
}`;
169168
let contents = readFileSync(buildGradle, "utf8");
170169
if (!contents.includes("nexus.ext.meawallet.com")) {
171-
contents = contents.replace(/(allprojects[\s\S]*?repositories\s*\{)/, `$1\n${meaRepo}`);
170+
contents = contents.replace(/(allprojects[\s\S]*?repositories\s*\{)/, `$1\n${meaRepo}`); // cspell:ignore allprojects
172171
writeFileSync(buildGradle, contents);
173172
}
174173
return c;

cspell.json

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
{
22
"language": "en",
3-
"import": [
4-
"@cspell/dict-es-es/cspell-ext.json"
5-
],
3+
"import": ["@cspell/dict-es-es/cspell-ext.json", "@cspell/dict-pt-br/cspell-ext.json"],
64
"ignorePaths": [
75
"*.svg",
86
"*.local.yaml",
@@ -213,13 +211,85 @@
213211
"montos",
214212
"refinancia",
215213
"refinanciamiento",
216-
"refinanciar"
214+
"refinanciar",
215+
"repágala",
216+
"verifícate"
217217
]
218+
},
219+
{
220+
"filename": "**/i18n/**/es-AR.json",
221+
"language": "en,es",
222+
"words": [
223+
"accedé",
224+
"aceptás",
225+
"activá",
226+
"actualizá",
227+
"agregá",
228+
"ahorrá",
229+
"ajustá",
230+
"aprendé",
231+
"asegurate",
232+
"biometría",
233+
"cambiá",
234+
"completá",
235+
"conectate",
236+
"conectá",
237+
"conectás",
238+
"configurá",
239+
"conocé",
240+
"conservá",
241+
"copiá",
242+
"creá",
243+
"cripto",
244+
"debés",
245+
"depositá",
246+
"desbloqueá",
247+
"empezá",
248+
"enviá",
249+
"enviás",
250+
"esperá",
251+
"evitá",
252+
"habilitá",
253+
"hacé",
254+
"hacés",
255+
"ingresá",
256+
"iniciá",
257+
"intentalo",
258+
"intentá",
259+
"introducí",
260+
"mantenete",
261+
"maximizá",
262+
"mejorá",
263+
"montos",
264+
"obtené",
265+
"pagá",
266+
"pasá",
267+
"podés",
268+
"ponete",
269+
"presioná",
270+
"probá",
271+
"querés",
272+
"recargá",
273+
"refinanciar",
274+
"refinanciá",
275+
"registrate",
276+
"renová",
277+
"reportá",
278+
"revisá",
279+
"seleccioná",
280+
"simulá",
281+
"tocá",
282+
"usalo",
283+
"usás",
284+
"verificate",
285+
"verificá"
286+
]
287+
},
288+
{
289+
"filename": "**/i18n/**/pt.json",
290+
"language": "en,pt",
291+
"words": ["autocustódia", "criptomoedas"]
218292
}
219293
],
220-
"ignoreRegExpList": [
221-
"\\bitmpl_\\w+\\b",
222-
"\\b(w|stat)?aBas\\w+\\b",
223-
"\\baOpt\\w+\\b"
224-
]
294+
"ignoreRegExpList": ["\\bitmpl_\\w+\\b", "\\b(w|stat)?aBas\\w+\\b", "\\baOpt\\w+\\b"]
225295
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"@changesets/cli": "^2.29.8",
127127
"@changesets/types": "^6.1.0",
128128
"@cspell/dict-es-es": "^3.0.8",
129+
"@cspell/dict-pt-br": "^2.4.2",
129130
"@eslint/mcp": "^0.2.0",
130131
"@expo/fingerprint": "^0.15.4",
131132
"@nx/esbuild": "22.4.5",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/i18n/es.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
"Card purchase": "Compra con tarjeta",
55
"Credit mode activated": "Modo crédito activado",
66
"Credit mode is active": "El modo crédito está activo",
7+
"Debt Maturity Alert": "Alerta de vencimiento de deuda",
78
"Deposited funds": "Fondos depositados",
89
"Fiat onramp activated": "Cuenta fiat activada",
910
"Funds received": "Fondos recibidos",
1011
"Refund processed": "Reembolso procesado",
1112
"Withdraw completed": "Retiro completado",
13+
"Your debt is due in 1 hour. Repay now to avoid liquidation.": "Tu deuda vence en 1 hora. Repágala ahora para evitar la liquidación.",
14+
"Your debt is due in 24 hours. Repay now to avoid liquidation.": "Tu deuda vence en 24 horas. Repágala ahora para evitar la liquidación.",
1215
"Your fiat onramp account has been activated": "Tu cuenta para depositar dinero fiat ha sido activada",
1316
"{{amount}} at {{merchantName}}. Paid in {{count}} installments_zero": "{{amount}} en {{merchantName}}. Pagado con USDC",
1417
"{{amount}} at {{merchantName}}. Paid in {{count}} installments_one": "{{amount}} en {{merchantName}}. Pagado con crédito",

server/i18n/pt.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
"Card purchase": "Compra no cartão",
55
"Credit mode activated": "Modo crédito ativado",
66
"Credit mode is active": "O modo crédito está ativo",
7+
"Debt Maturity Alert": "Alerta de vencimento de dívida",
78
"Deposited funds": "Fundos depositados",
89
"Fiat onramp activated": "Conta fiat ativada",
910
"Funds received": "Fundos recebidos",
1011
"Refund processed": "Reembolso processado",
1112
"Withdraw completed": "Saque concluído",
13+
"Your debt is due in 1 hour. Repay now to avoid liquidation.": "Sua dívida vence em 1 hora. Pague agora para evitar a liquidação.",
14+
"Your debt is due in 24 hours. Repay now to avoid liquidation.": "Sua dívida vence em 24 horas. Pague agora para evitar a liquidação.",
1215
"Your fiat onramp account has been activated": "Sua conta para depositar dinheiro fiat foi ativada",
1316
"{{amount}} at {{merchantName}}. Paid in {{count}} installments_zero": "{{amount}} em {{merchantName}}. Pago com USDC",
1417
"{{amount}} at {{merchantName}}. Paid in {{count}} installments_one": "{{amount}} em {{merchantName}}. Pago no crédito",

server/index.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidFingerprints from "./utils/android/fingerprints";
2121
import appOrigin from "./utils/appOrigin";
2222
import auth from "./utils/auth";
2323
import { closeQueue as closeAccountQueue } from "./utils/createCredential";
24+
import { closeQueue as closeMaturityQueue, scheduleMaturityChecks } from "./utils/maturity";
2425
import { close as closeRedis } from "./utils/redis";
2526
import { closeAndFlush as closeSegment } from "./utils/segment";
2627

@@ -319,20 +320,29 @@ export default app;
319320
const server = serve(app);
320321

321322
export async function close() {
322-
return new Promise((resolve, reject) => {
323-
server.close((error) => {
324-
Promise.allSettled([closeSentry(), closeRedis(), closeSegment(), database.$client.end(), closeAccountQueue()])
325-
.then((results) => {
326-
if (error) reject(error);
327-
else if (results.some((result) => result.status === "rejected")) reject(new Error("closing services failed"));
328-
else resolve(null);
329-
})
330-
.catch(reject);
331-
});
323+
const serverError = await new Promise<Error | undefined>((resolve) => {
324+
server.close((error) => resolve(error));
332325
});
326+
const results = await Promise.allSettled([
327+
closeSentry(),
328+
closeSegment(),
329+
database.$client.end(),
330+
closeAccountQueue(),
331+
closeMaturityQueue(),
332+
closeRedis(),
333+
]);
334+
const errors = [
335+
...(serverError ? [serverError] : []),
336+
...results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason as unknown),
337+
];
338+
if (errors.length > 0) throw new AggregateError(errors, "closing services failed");
333339
}
334340

335341
if (!process.env.VITEST) {
342+
scheduleMaturityChecks().catch((error: unknown) => {
343+
captureException(error, { level: "error", tags: { unhandled: true } });
344+
});
345+
336346
["SIGINT", "SIGTERM"].map((code) => {
337347
process.on(code, () => {
338348
close()

server/test/hooks/block.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,57 @@ describe("proposal", () => {
171171
});
172172
expect(hasExpectedTransfers(receipts, expected)).toBe(true);
173173
});
174+
175+
it("sends withdraw notification after proposal execution", async () => {
176+
const [withdraw, anotherWithdraw] = proposals;
177+
if (!withdraw || !anotherWithdraw) throw new Error("missing proposals");
178+
const sendPushNotification = vi.spyOn(onesignal, "sendPushNotification").mockResolvedValue({});
179+
vi.spyOn(ensClient, "getEnsName").mockResolvedValue("alice.eth");
180+
vi.spyOn(publicClient, "readContract").mockImplementation(({ functionName }) => {
181+
if (functionName === "decimals") return Promise.resolve(6);
182+
if (functionName === "symbol") return Promise.resolve("exaUSDC");
183+
return Promise.reject(new Error("unexpected readContract call"));
184+
});
185+
const proposalExecutions = waitForSuccessfulProposalExecutions([withdraw.args.nonce, anotherWithdraw.args.nonce]);
186+
187+
await Promise.all([
188+
appClient.index.$post({
189+
...withdrawProposal,
190+
json: {
191+
...withdrawProposal.json,
192+
event: {
193+
...withdrawProposal.json.event,
194+
data: {
195+
...withdrawProposal.json.event.data,
196+
block: {
197+
...withdrawProposal.json.event.data.block,
198+
logs: [
199+
{ topics: withdraw.topics, data: withdraw.data, account: { address: withdraw.address } },
200+
{
201+
topics: anotherWithdraw.topics,
202+
data: anotherWithdraw.data,
203+
account: { address: anotherWithdraw.address },
204+
},
205+
],
206+
},
207+
},
208+
},
209+
},
210+
}),
211+
proposalExecutions,
212+
]);
213+
await vi.waitUntil(() => sendPushNotification.mock.calls.length >= 2, 26_666);
214+
expect(sendPushNotification).toHaveBeenCalledWith({
215+
userId: bobAccount,
216+
headings: { en: "Withdraw completed", es: "Retiro completado" }, // cspell:ignore Retiro completado
217+
contents: { en: "3 USDC sent to alice.eth", es: "3 USDC enviados a alice.eth" }, // cspell:ignore enviados
218+
});
219+
expect(sendPushNotification).toHaveBeenCalledWith({
220+
userId: bobAccount,
221+
headings: { en: "Withdraw completed", es: "Retiro completado" },
222+
contents: { en: "4 USDC sent to alice.eth", es: "4 USDC enviados a alice.eth" },
223+
});
224+
});
174225
});
175226

176227
describe("with weth withdraw proposal", () => {

server/test/hooks/panda.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,7 @@ describe("card operations", () => {
11691169
});
11701170

11711171
it("captures partial refund feedback errors", async () => {
1172+
const sendPushNotification = vi.spyOn(onesignal, "sendPushNotification");
11721173
const error = new Error("feedback failed");
11731174
vi.spyOn(sardine, "feedback").mockRejectedValue(error);
11741175
const cardId = "partial-refund-feedback-error";
@@ -1213,6 +1214,14 @@ describe("card operations", () => {
12131214

12141215
expect(captureException).toHaveBeenCalledWith(error, { level: "error" });
12151216
expect(response.status).toBe(200);
1217+
expect(sendPushNotification).toHaveBeenCalledWith({
1218+
userId: account,
1219+
headings: t("Refund processed"),
1220+
contents: t("{{refundAmount}} USDC from {{merchantName}} have been refunded to your account", {
1221+
refundAmount: f(0.05),
1222+
merchantName: "99999",
1223+
}),
1224+
});
12161225
});
12171226

12181227
it("returns ok on reversal replay", async () => {

0 commit comments

Comments
 (0)