Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/dfx-api-dev.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: DFX API DEV CI/CD

on:
# Auto-deploy is disabled until the DFX server migration cutover.
# The API still runs on Azure (see api-dev.yaml). Server-side prerequisites
# (deploy.sh dfx-api case, authorized_keys, compose) are not yet in place,
# so every develop push failed at the SSH deploy step. Re-add the push
# trigger when the migration goes live.
push:
branches: [develop]
paths-ignore:
- '**.md'
- 'infrastructure/**'
workflow_dispatch:

env:
Expand Down Expand Up @@ -39,6 +39,7 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
build-args: GIT_COMMIT=${{ github.sha }}

- name: Install cloudflared
run: |
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/dfx-api-prd.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: DFX API PRD CI/CD

on:
# Auto-deploy is disabled until the DFX server migration cutover.
# The API still runs on Azure (see api-prd.yaml). Server-side prerequisites
# (deploy.sh dfx-api case, authorized_keys, compose) are not yet in place,
# so every main push failed at the SSH deploy step. Re-add the push
# trigger when the migration goes live.
# Auto-deploy re-enabled for DEV (dfxdev prerequisites are in place).
# PRD trigger stays disabled until the PRD cutover is complete.
workflow_dispatch:

env:
Expand Down Expand Up @@ -39,6 +36,7 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
build-args: GIT_COMMIT=${{ github.sha }}

- name: Install cloudflared
run: |
Expand Down
11 changes: 10 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ RUN npm run build
# the already-compiled native modules without needing python3 + g++.
RUN npm prune --omit=dev

# Write git commit SHA into dist/ so /version endpoint can read it.
ARG GIT_COMMIT=unknown
RUN echo "$GIT_COMMIT" > dist/version.txt


FROM node:20-alpine

Expand All @@ -31,7 +35,12 @@ COPY --from=builder /home/node/package.json /home/node/package-lock.json ./
COPY --from=builder /home/node/node_modules ./node_modules
COPY --from=builder /home/node/dist ./dist
COPY --from=builder /home/node/migration ./migration
# Runtime assets referenced by source path (not dist/) in the app config:
# - i18n translations: config.ts → join(process.cwd(), 'src/shared/i18n/')
# - notification templates: *.hbs files
COPY --from=builder /home/node/src/shared/i18n ./src/shared/i18n
COPY --from=builder /home/node/src/subdomains/supporting/notification/templates ./src/subdomains/supporting/notification/templates

EXPOSE 3000

CMD ["node", "dist/main.js"]
CMD ["npm", "run", "start:prod"]
28 changes: 28 additions & 0 deletions scripts/db-debug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@
# Requirements:
# - curl
# - jq (optional, for pretty output)
#
# Safety:
# - DEBUG_API_URL defaults to PRODUCTION. Use read-only SELECT statements only;
# never run writes or DDL through this tool.
#
# Writing custom SQL (PostgreSQL backend):
# Table names are snake_case (e.g. user_data, log, asset). Column names are camelCase and
# case-sensitive, so wrap them in double quotes ("errorMessage", "ruleId") -- unquoted
# identifiers are folded to lowercase by Postgres and fail with "column does not exist".
# System schemas (information_schema, pg_catalog) are blocked server-side; derive table and
# column names from the TypeORM entities, not from the catalog.
#
# Financial balance semantics (read before interpreting --balance / --anomalies / --stats):
# These query the FinancialDataLog, which records the whole book valued in CHF. In the
# balancesTotal object: totalBalanceChf = plusBalanceChf - minusBalanceChf, where plus =
# assets DFX holds and minus = liabilities owed to customers, each valued at its current
# priceChf. Customer flow is balance-neutral: a deposit raises plus AND minus equally, and
# completing the order lowers both again, leaving only the fee. So totalBalanceChf is
# effectively the operating equity of the flow business and moves ONLY due to:
# 1. operating profit / fees (gradual, positive, realised on order completion)
# 2. FX (plus and minus are different asset baskets, so their CHF marks drift
# independently while orders are open -- the normal intraday noise)
# 3. an error or a realised loss (a discrete, persisting step)
# A sudden step (especially negative) is therefore suspicious rather than customer activity.
# The `valid` column is false when the jump vs. the previous entry exceeds
# Config.financeLogTotalBalanceChangeLimit and that entry is under 15 minutes old (a larger
# gap suppresses the flag); --anomalies lists these valid=false rows. Full reference: the
# BalancesTotal type in src/subdomains/supporting/log/dto/log.dto.ts and LogJobService.

set -e

Expand Down
4 changes: 4 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1253,13 +1253,17 @@ export class Configuration {
};
}

// max recvWindow (60s) to tolerate MEXC round-trip latency spikes that otherwise reject signed requests with error 700003
mexcRecvWindow = 60000;

get mexc(): ConstructorArgs {
return {
apiKey: process.env.MEXC_KEY,
secret: process.env.MEXC_SECRET,
withdrawKeys: splitWithdrawKeys(process.env.MEXC_WITHDRAW_KEYS),
...this.exchange,
timeout: 30_000,
options: { recvWindow: this.mexcRecvWindow },
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/integration/exchange/services/mexc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,19 @@ export class MexcService extends ExchangeService {
private readonly baseUrl = 'https://api.mexc.com/api/v3';

private async request<T>(method: Method, path: string, params: Record<string, string>): Promise<T> {
// retry once with a fresh timestamp if a latency spike pushed the request outside the recvWindow (error 700003)
return Util.retry(
() => this.signedRequest<T>(method, path, { ...params }),
2,
0,
undefined,
(e) => e.message?.includes('700003'),
);
}

private async signedRequest<T>(method: Method, path: string, params: Record<string, string>): Promise<T> {
params.timestamp = Date.now().toString();
params.recvWindow = `${Config.mexcRecvWindow}`;

const searchParams = new URLSearchParams(params);

Expand Down
23 changes: 23 additions & 0 deletions src/subdomains/supporting/log/dto/log.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ export interface FinanceLog {
balancesTotal: BalancesTotal;
}

/**
* Aggregate CHF valuation of the whole book at log time, written by LogJobService.
*
* `totalBalanceChf = plusBalanceChf - minusBalanceChf`, where plus/minus are each
* asset's positive/negative balance valued at its current `priceChf`. Plus = assets
* DFX holds, minus = liabilities owed to customers.
*
* Invariant: customer flow is balance-neutral. A deposit raises plus AND minus by the
* same amount; completing the order lowers both again, leaving only the fee in plus.
* So `totalBalanceChf` never moves because a customer deposits or withdraws — it is
* effectively the operating equity of the flow business and only moves due to:
* 1. operating profit / fees — gradual, positive, realised on order completion
* 2. FX — plus and minus are different asset baskets, so their CHF marks drift
* independently while orders are open (the normal intraday noise)
* 3. an error or a realised loss — a discrete, persisting step
*
* A sudden step (especially negative) is therefore suspicious rather than customer
* activity. Two guardrails act on this signal in LogJobService: the entry is flagged
* `valid: false` when the jump vs. the previous entry exceeds
* `Config.financeLogTotalBalanceChangeLimit` and that entry is under 15 minutes old (a
* larger logging gap suppresses the flag), and safety mode is triggered when
* `totalBalanceChf` drops below the `minTotalBalanceChf` setting.
*/
export interface BalancesTotal {
plusBalanceChf: number;
minusBalanceChf: number;
Expand Down
3 changes: 2 additions & 1 deletion src/subdomains/supporting/log/log-job.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export class LogJobService {
// changes
const changeLog = await this.getChangeLog();

// total balances
// total balances — customer flow is balance-neutral, so totalBalanceChf moves only on
// operating profit, FX, or an error/realised loss (see BalancesTotal). Hence the guardrails below.
const plusBalanceChf = Util.sumObjValue(Object.values(balancesByFinancialType), 'plusBalanceChf');
const minusBalanceChf = Util.sumObjValue(Object.values(balancesByFinancialType), 'minusBalanceChf');

Expand Down
Loading