@openora/core 0.4.1-canary.76 → 0.4.1-canary.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/compliance/__tests__/kyc.service.int.test.js +2 -2
  3. package/dist/compliance/__tests__/kyc.service.int.test.js.map +1 -1
  4. package/dist/compliance/__tests__/rg-monitoring.service.int.test.js +1 -1
  5. package/dist/compliance/__tests__/rg-monitoring.service.int.test.js.map +1 -1
  6. package/dist/contracts/adapters/payment.d.ts +11 -3
  7. package/dist/contracts/adapters/payment.d.ts.map +1 -1
  8. package/dist/contracts/adapters/payment.js.map +1 -1
  9. package/dist/wallet/__tests__/admin-reporting.int.test.js +5 -4
  10. package/dist/wallet/__tests__/admin-reporting.int.test.js.map +1 -1
  11. package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js +9 -5
  12. package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js.map +1 -1
  13. package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js +12 -11
  14. package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js.map +1 -1
  15. package/dist/wallet/__tests__/wallet-commands.service.int.test.js +17 -9
  16. package/dist/wallet/__tests__/wallet-commands.service.int.test.js.map +1 -1
  17. package/dist/wallet/__tests__/wallet-currency-code.test.d.ts +2 -0
  18. package/dist/wallet/__tests__/wallet-currency-code.test.d.ts.map +1 -0
  19. package/dist/wallet/__tests__/wallet-currency-code.test.js +14 -0
  20. package/dist/wallet/__tests__/wallet-currency-code.test.js.map +1 -0
  21. package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js +8 -4
  22. package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js.map +1 -1
  23. package/dist/wallet/__tests__/wallet-webhook.router.int.test.js +19 -11
  24. package/dist/wallet/__tests__/wallet-webhook.router.int.test.js.map +1 -1
  25. package/dist/wallet/__tests__/wallet.rate-limit.int.test.js +8 -5
  26. package/dist/wallet/__tests__/wallet.rate-limit.int.test.js.map +1 -1
  27. package/dist/wallet/__tests__/wallet.router.int.test.js +2 -4
  28. package/dist/wallet/__tests__/wallet.router.int.test.js.map +1 -1
  29. package/dist/wallet/__tests__/wallet.service.int.test.js +122 -25
  30. package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
  31. package/dist/wallet/contract/index.d.ts +39 -8
  32. package/dist/wallet/contract/index.d.ts.map +1 -1
  33. package/dist/wallet/contract/index.js +26 -5
  34. package/dist/wallet/contract/index.js.map +1 -1
  35. package/dist/wallet/drizzle/migrations/0008_gorgeous_colonel_america.sql +1 -0
  36. package/dist/wallet/drizzle/migrations/0009_smooth_sue_storm.sql +17 -0
  37. package/dist/wallet/drizzle/migrations/meta/0008_snapshot.json +716 -0
  38. package/dist/wallet/drizzle/migrations/meta/0009_snapshot.json +895 -0
  39. package/dist/wallet/drizzle/migrations/meta/_journal.json +14 -0
  40. package/dist/wallet/index.d.ts.map +1 -1
  41. package/dist/wallet/index.js +0 -2
  42. package/dist/wallet/index.js.map +1 -1
  43. package/dist/wallet/migrate.d.ts +0 -1
  44. package/dist/wallet/migrate.d.ts.map +1 -1
  45. package/dist/wallet/migrate.js +0 -4
  46. package/dist/wallet/migrate.js.map +1 -1
  47. package/dist/wallet/router/index.d.ts +19 -4
  48. package/dist/wallet/router/index.d.ts.map +1 -1
  49. package/dist/wallet/router/index.js +5 -2
  50. package/dist/wallet/router/index.js.map +1 -1
  51. package/dist/wallet/schema/index.d.ts +126 -17
  52. package/dist/wallet/schema/index.d.ts.map +1 -1
  53. package/dist/wallet/schema/index.js +24 -2
  54. package/dist/wallet/schema/index.js.map +1 -1
  55. package/dist/wallet/service/wallet-commands.service.d.ts.map +1 -1
  56. package/dist/wallet/service/wallet-commands.service.js +7 -15
  57. package/dist/wallet/service/wallet-commands.service.js.map +1 -1
  58. package/dist/wallet/service/wallet.service.d.ts +280 -6
  59. package/dist/wallet/service/wallet.service.d.ts.map +1 -1
  60. package/dist/wallet/service/wallet.service.js +110 -62
  61. package/dist/wallet/service/wallet.service.js.map +1 -1
  62. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { type EventBus, DrizzleService } from '@openora/core/server';
1
+ import { type EventBus, type DrizzleDb, DrizzleService } from '@openora/core/server';
2
2
  import { type PaymentAdapter, type PaymentWebhookEvent, type AdminUserDirectory, type PlatformConfig, type RateLimiterAdapter, type RateLimitKey, type WalletRail, type PlayerTags, type AuditWritePort, type TagEvaluationCommands, type TagKey, type User, type IdentityReader, type ClientMeta, type PaginationOptions } from '@openora/core/contracts';
3
- import { type WalletTransaction } from '../schema/index.js';
3
+ import { type Wallet, type WalletTransaction } from '../schema/index.js';
4
4
  import type { TransactionResult, WithdrawalQueueFilter, AutoWithdrawalRule, WalletAutoWithdrawalConfig, WalletTransactionSortBy } from '../contract/index.js';
5
5
  export declare const WalletNotFoundError: new (id: string) => Error;
6
6
  export declare const WithdrawalNotFoundError: new (id: string) => Error;
@@ -12,7 +12,273 @@ export declare const IdempotencyKeyReuseError: new () => Error;
12
12
  export declare const DepositAddressUnsupportedError: new () => Error;
13
13
  export declare const DestinationAddressRequiredError: new () => Error;
14
14
  export declare const CurrencyMismatchError: new (requested: unknown, walletCurrency: unknown) => Error;
15
+ export declare const AmbiguousDepositAddressError: new (address: unknown, network: unknown) => Error;
15
16
  export declare function railFor(currency: string, cryptoCurrencies?: readonly string[]): WalletRail;
17
+ export declare function creditWalletBalance(txn: DrizzleDb, walletId: Wallet['id'], currency: string, amount: string): Omit<import("drizzle-orm/pg-core").PgInsertBase<import("drizzle-orm/pg-core").PgTableWithColumns<{
18
+ name: "wallet_balance";
19
+ schema: undefined;
20
+ columns: {
21
+ id: import("drizzle-orm/pg-core").PgColumn<{
22
+ name: "id";
23
+ tableName: "wallet_balance";
24
+ dataType: "string";
25
+ columnType: "PgUUID";
26
+ data: string;
27
+ driverParam: string;
28
+ notNull: true;
29
+ hasDefault: true;
30
+ isPrimaryKey: true;
31
+ isAutoincrement: false;
32
+ hasRuntimeDefault: false;
33
+ enumValues: undefined;
34
+ baseColumn: never;
35
+ identity: undefined;
36
+ generated: undefined;
37
+ }, {}, {}>;
38
+ walletId: import("drizzle-orm/pg-core").PgColumn<{
39
+ name: "walletId";
40
+ tableName: "wallet_balance";
41
+ dataType: "string";
42
+ columnType: "PgUUID";
43
+ data: string;
44
+ driverParam: string;
45
+ notNull: true;
46
+ hasDefault: false;
47
+ isPrimaryKey: false;
48
+ isAutoincrement: false;
49
+ hasRuntimeDefault: false;
50
+ enumValues: undefined;
51
+ baseColumn: never;
52
+ identity: undefined;
53
+ generated: undefined;
54
+ }, {}, {}>;
55
+ currency: import("drizzle-orm/pg-core").PgColumn<{
56
+ name: "currency";
57
+ tableName: "wallet_balance";
58
+ dataType: "string";
59
+ columnType: "PgText";
60
+ data: string;
61
+ driverParam: string;
62
+ notNull: true;
63
+ hasDefault: false;
64
+ isPrimaryKey: false;
65
+ isAutoincrement: false;
66
+ hasRuntimeDefault: false;
67
+ enumValues: [string, ...string[]];
68
+ baseColumn: never;
69
+ identity: undefined;
70
+ generated: undefined;
71
+ }, {}, {}>;
72
+ amount: import("drizzle-orm/pg-core").PgColumn<{
73
+ name: "amount";
74
+ tableName: "wallet_balance";
75
+ dataType: "string";
76
+ columnType: "PgNumeric";
77
+ data: string;
78
+ driverParam: string;
79
+ notNull: true;
80
+ hasDefault: true;
81
+ isPrimaryKey: false;
82
+ isAutoincrement: false;
83
+ hasRuntimeDefault: false;
84
+ enumValues: undefined;
85
+ baseColumn: never;
86
+ identity: undefined;
87
+ generated: undefined;
88
+ }, {}, {}>;
89
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
90
+ name: "createdAt";
91
+ tableName: "wallet_balance";
92
+ dataType: "date";
93
+ columnType: "PgTimestamp";
94
+ data: Date;
95
+ driverParam: string;
96
+ notNull: true;
97
+ hasDefault: true;
98
+ isPrimaryKey: false;
99
+ isAutoincrement: false;
100
+ hasRuntimeDefault: false;
101
+ enumValues: undefined;
102
+ baseColumn: never;
103
+ identity: undefined;
104
+ generated: undefined;
105
+ }, {}, {}>;
106
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
107
+ name: "updatedAt";
108
+ tableName: "wallet_balance";
109
+ dataType: "date";
110
+ columnType: "PgTimestamp";
111
+ data: Date;
112
+ driverParam: string;
113
+ notNull: true;
114
+ hasDefault: true;
115
+ isPrimaryKey: false;
116
+ isAutoincrement: false;
117
+ hasRuntimeDefault: false;
118
+ enumValues: undefined;
119
+ baseColumn: never;
120
+ identity: undefined;
121
+ generated: undefined;
122
+ }, {}, {}>;
123
+ };
124
+ dialect: "pg";
125
+ }>, import("drizzle-orm/node-postgres").NodePgQueryResultHKT, {
126
+ amount: import("drizzle-orm/pg-core").PgColumn<{
127
+ name: "amount";
128
+ tableName: "wallet_balance";
129
+ dataType: "string";
130
+ columnType: "PgNumeric";
131
+ data: string;
132
+ driverParam: string;
133
+ notNull: true;
134
+ hasDefault: true;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: undefined;
139
+ baseColumn: never;
140
+ identity: undefined;
141
+ generated: undefined;
142
+ }, {}, {}>;
143
+ }, {
144
+ amount: string;
145
+ }, false, "onConflictDoNothing" | "onConflictDoUpdate" | "returning">, "onConflictDoNothing" | "onConflictDoUpdate" | "returning">;
146
+ export declare function debitWalletBalance(txn: DrizzleDb, walletId: Wallet['id'], currency: string, amount: string): Omit<import("drizzle-orm/pg-core").PgUpdateBase<import("drizzle-orm/pg-core").PgTableWithColumns<{
147
+ name: "wallet_balance";
148
+ schema: undefined;
149
+ columns: {
150
+ id: import("drizzle-orm/pg-core").PgColumn<{
151
+ name: "id";
152
+ tableName: "wallet_balance";
153
+ dataType: "string";
154
+ columnType: "PgUUID";
155
+ data: string;
156
+ driverParam: string;
157
+ notNull: true;
158
+ hasDefault: true;
159
+ isPrimaryKey: true;
160
+ isAutoincrement: false;
161
+ hasRuntimeDefault: false;
162
+ enumValues: undefined;
163
+ baseColumn: never;
164
+ identity: undefined;
165
+ generated: undefined;
166
+ }, {}, {}>;
167
+ walletId: import("drizzle-orm/pg-core").PgColumn<{
168
+ name: "walletId";
169
+ tableName: "wallet_balance";
170
+ dataType: "string";
171
+ columnType: "PgUUID";
172
+ data: string;
173
+ driverParam: string;
174
+ notNull: true;
175
+ hasDefault: false;
176
+ isPrimaryKey: false;
177
+ isAutoincrement: false;
178
+ hasRuntimeDefault: false;
179
+ enumValues: undefined;
180
+ baseColumn: never;
181
+ identity: undefined;
182
+ generated: undefined;
183
+ }, {}, {}>;
184
+ currency: import("drizzle-orm/pg-core").PgColumn<{
185
+ name: "currency";
186
+ tableName: "wallet_balance";
187
+ dataType: "string";
188
+ columnType: "PgText";
189
+ data: string;
190
+ driverParam: string;
191
+ notNull: true;
192
+ hasDefault: false;
193
+ isPrimaryKey: false;
194
+ isAutoincrement: false;
195
+ hasRuntimeDefault: false;
196
+ enumValues: [string, ...string[]];
197
+ baseColumn: never;
198
+ identity: undefined;
199
+ generated: undefined;
200
+ }, {}, {}>;
201
+ amount: import("drizzle-orm/pg-core").PgColumn<{
202
+ name: "amount";
203
+ tableName: "wallet_balance";
204
+ dataType: "string";
205
+ columnType: "PgNumeric";
206
+ data: string;
207
+ driverParam: string;
208
+ notNull: true;
209
+ hasDefault: true;
210
+ isPrimaryKey: false;
211
+ isAutoincrement: false;
212
+ hasRuntimeDefault: false;
213
+ enumValues: undefined;
214
+ baseColumn: never;
215
+ identity: undefined;
216
+ generated: undefined;
217
+ }, {}, {}>;
218
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
219
+ name: "createdAt";
220
+ tableName: "wallet_balance";
221
+ dataType: "date";
222
+ columnType: "PgTimestamp";
223
+ data: Date;
224
+ driverParam: string;
225
+ notNull: true;
226
+ hasDefault: true;
227
+ isPrimaryKey: false;
228
+ isAutoincrement: false;
229
+ hasRuntimeDefault: false;
230
+ enumValues: undefined;
231
+ baseColumn: never;
232
+ identity: undefined;
233
+ generated: undefined;
234
+ }, {}, {}>;
235
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
236
+ name: "updatedAt";
237
+ tableName: "wallet_balance";
238
+ dataType: "date";
239
+ columnType: "PgTimestamp";
240
+ data: Date;
241
+ driverParam: string;
242
+ notNull: true;
243
+ hasDefault: true;
244
+ isPrimaryKey: false;
245
+ isAutoincrement: false;
246
+ hasRuntimeDefault: false;
247
+ enumValues: undefined;
248
+ baseColumn: never;
249
+ identity: undefined;
250
+ generated: undefined;
251
+ }, {}, {}>;
252
+ };
253
+ dialect: "pg";
254
+ }>, import("drizzle-orm/node-postgres").NodePgQueryResultHKT, undefined, {
255
+ amount: import("drizzle-orm/pg-core").PgColumn<{
256
+ name: "amount";
257
+ tableName: "wallet_balance";
258
+ dataType: "string";
259
+ columnType: "PgNumeric";
260
+ data: string;
261
+ driverParam: string;
262
+ notNull: true;
263
+ hasDefault: true;
264
+ isPrimaryKey: false;
265
+ isAutoincrement: false;
266
+ hasRuntimeDefault: false;
267
+ enumValues: undefined;
268
+ baseColumn: never;
269
+ identity: undefined;
270
+ generated: undefined;
271
+ }, {}, {}>;
272
+ }, {
273
+ amount: string;
274
+ }, Record<"wallet_balance", "not-null">, [], false, "returning" | "where" | "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">, "returning" | "where" | "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">;
275
+ export declare function readWalletBalance(txn: DrizzleDb, walletId: Wallet['id'], currency: string): Promise<string>;
276
+ type DepositAddressResult = {
277
+ address: string;
278
+ currency: string;
279
+ network?: string;
280
+ tag?: string;
281
+ };
16
282
  export type WalletServiceDeps = {
17
283
  drizzle: DrizzleService;
18
284
  events: EventBus;
@@ -52,6 +318,16 @@ export declare class WalletService {
52
318
  balance: string;
53
319
  currency: string;
54
320
  }>;
321
+ getBalances(userId: User['id']): Promise<{
322
+ activeCurrency: string;
323
+ balances: {
324
+ currency: string;
325
+ balance: string;
326
+ }[];
327
+ }>;
328
+ setActiveCurrency(userId: User['id'], currency: string): Promise<{
329
+ activeCurrency: string;
330
+ }>;
55
331
  /**
56
332
  * `idempotencyKey` (when supplied) makes a retried deposit a no-op: a replay
57
333
  * returns the original committed result WITHOUT calling the PSP again or
@@ -160,10 +436,7 @@ export declare class WalletService {
160
436
  page: number;
161
437
  limit: number;
162
438
  }>;
163
- getOrCreateDepositAddress(userId: User['id'], currency: string): Promise<{
164
- address: string;
165
- currency: string;
166
- }>;
439
+ getOrCreateDepositAddress(userId: User['id'], currency: string, network?: string): Promise<DepositAddressResult>;
167
440
  creditDepositByAddress(event: Extract<PaymentWebhookEvent, {
168
441
  kind: 'deposit';
169
442
  }>): Promise<void>;
@@ -172,4 +445,5 @@ export declare class WalletService {
172
445
  private findDepositAddress;
173
446
  private findDepositAddressByAddress;
174
447
  }
448
+ export {};
175
449
  //# sourceMappingURL=wallet.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wallet.service.d.ts","sourceRoot":"","sources":["../../../src/wallet/service/wallet.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wallet.service.d.ts","sourceRoot":"","sources":["../../../src/wallet/service/wallet.service.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { makeNotFoundError, makeConflictError, createDomainError, findOneOrThrow, pageToOffset, withAdvisoryXactLock, assertRateLimit, createLogger, moneyToNumber, moneyEquals, } from '@openora/core/server';
2
2
  import { normalizeKycStatus, } from '@openora/core/contracts';
3
- import { eq, asc, desc, sql, and, gte, lte, count, inArray } from 'drizzle-orm';
3
+ import { eq, asc, desc, sql, and, gte, lte, count, inArray, isNull, or } from 'drizzle-orm';
4
4
  import { createHash } from 'node:crypto';
5
- import { wallet, walletTransaction, autoWithdrawalRule, walletAutoWithdrawalConfig, walletDepositAddress, } from '../schema/index.js';
5
+ import { wallet, walletBalance, walletTransaction, autoWithdrawalRule, walletAutoWithdrawalConfig, walletDepositAddress, } from '../schema/index.js';
6
6
  const logger = createLogger('wallet');
7
7
  export const WalletNotFoundError = makeNotFoundError('Wallet');
8
8
  export const WithdrawalNotFoundError = makeNotFoundError('Withdrawal');
@@ -15,10 +15,14 @@ export const DepositAddressUnsupportedError = makeConflictError('DepositAddressU
15
15
  export const DestinationAddressRequiredError = makeConflictError('DestinationAddressRequiredError', 'A destination address is required for a crypto-rail withdrawal');
16
16
  const KYC_PASS_STATUSES = new Set(['approved', 'manually_overridden']);
17
17
  export const CurrencyMismatchError = createDomainError('CurrencyMismatchError', (requested, walletCurrency) => `Currency mismatch: requested ${requested}, wallet holds ${walletCurrency}`);
18
+ export const AmbiguousDepositAddressError = createDomainError('AmbiguousDepositAddressError', (address, network) => `Deposit address ${address} on ${network ?? 'an unknown network'} is issued to more than one user`);
18
19
  // Crypto currencies settle on the crypto rail (Fireblocks); everything else on the
19
20
  // fiat rail (a PSP). The concrete provider is recorded per transaction, not here.
20
21
  // Overridable per-operator via `platformConfig.wallet.cryptoCurrencies` - see `railFor`.
21
22
  const DEFAULT_CRYPTO_CURRENCIES = new Set(['BTC', 'ETH', 'USDT', 'USDC']);
23
+ // Mirrors the `wallet.currency` column default: what a player without a wallet row
24
+ // reads as their active currency before one is created on first deposit.
25
+ const DEFAULT_WALLET_CURRENCY = 'USD';
22
26
  // Per-user throttle on money mutations - guards a runaway/misbehaving client, not
23
27
  // fraud (idempotency + the ledger guard cover correctness). An overlay rebinds
24
28
  // RATE_LIMITER to change the backend, not this policy.
@@ -29,6 +33,37 @@ export function railFor(currency, cryptoCurrencies) {
29
33
  : DEFAULT_CRYPTO_CURRENCIES;
30
34
  return set.has(currency.toUpperCase()) ? 'crypto' : 'fiat';
31
35
  }
36
+ // Currency checks elsewhere are case-insensitive, so `usd` reaches here for a `USD`
37
+ // wallet. Every read and write of wallet_balance funnels through these three helpers,
38
+ // so normalizing the key here is enough to keep one row per wallet+currency.
39
+ const balanceKey = (currency) => currency.toUpperCase();
40
+ export function creditWalletBalance(txn, walletId, currency, amount) {
41
+ return txn
42
+ .insert(walletBalance)
43
+ .values({ walletId, currency: balanceKey(currency), amount })
44
+ .onConflictDoUpdate({
45
+ target: [walletBalance.walletId, walletBalance.currency],
46
+ set: {
47
+ amount: sql `${walletBalance.amount} + ${amount}::numeric`,
48
+ updatedAt: new Date(),
49
+ },
50
+ })
51
+ .returning({ amount: walletBalance.amount });
52
+ }
53
+ export function debitWalletBalance(txn, walletId, currency, amount) {
54
+ return txn
55
+ .update(walletBalance)
56
+ .set({ amount: sql `${walletBalance.amount} - ${amount}::numeric` })
57
+ .where(and(eq(walletBalance.walletId, walletId), eq(walletBalance.currency, balanceKey(currency)), gte(walletBalance.amount, amount)))
58
+ .returning({ amount: walletBalance.amount });
59
+ }
60
+ export async function readWalletBalance(txn, walletId, currency) {
61
+ const [row] = await txn
62
+ .select({ amount: walletBalance.amount })
63
+ .from(walletBalance)
64
+ .where(and(eq(walletBalance.walletId, walletId), eq(walletBalance.currency, balanceKey(currency))));
65
+ return row?.amount ?? '0';
66
+ }
32
67
  // Namespace the key per operation so the same raw key on a deposit then a withdraw can't
33
68
  // collide on the (walletId, idempotencyKey) unique index. The column is a uuid, so a string
34
69
  // prefix won't parse - hash namespace + key into a stable pseudo-uuid instead.
@@ -54,6 +89,14 @@ const HIGH_FREQUENCY_MIN_COUNT = 3;
54
89
  const DAILY_CAP_WINDOW_MS = 24 * 60 * 60 * 1000;
55
90
  // Also the marker the cumulative-cap query reads back to identify auto-approved payouts - keep write and read in lockstep.
56
91
  const AUTO_APPROVED_REASON = 'auto-approved';
92
+ function toDepositAddressResult(row) {
93
+ return {
94
+ address: row.address,
95
+ currency: row.currency,
96
+ ...(row.network === null ? {} : { network: row.network }),
97
+ ...(row.tag === null ? {} : { tag: row.tag }),
98
+ };
99
+ }
57
100
  function toAutoWithdrawalRuleDto(row) {
58
101
  return {
59
102
  id: row.id,
@@ -132,13 +175,40 @@ export class WalletService {
132
175
  async getBalance(userId) {
133
176
  const [record] = await this.drizzle.db.select().from(wallet).where(eq(wallet.userId, userId));
134
177
  if (!record) {
135
- return { balance: '0', currency: 'USD' };
178
+ return { balance: '0', currency: DEFAULT_WALLET_CURRENCY };
136
179
  }
137
180
  return {
138
- balance: record.balance,
181
+ balance: await readWalletBalance(this.drizzle.db, record.id, record.currency),
139
182
  currency: record.currency,
140
183
  };
141
184
  }
185
+ async getBalances(userId) {
186
+ const [record] = await this.drizzle.db.select().from(wallet).where(eq(wallet.userId, userId));
187
+ if (!record) {
188
+ return { activeCurrency: DEFAULT_WALLET_CURRENCY, balances: [] };
189
+ }
190
+ const rows = await this.drizzle.db
191
+ .select({ currency: walletBalance.currency, balance: walletBalance.amount })
192
+ .from(walletBalance)
193
+ .where(eq(walletBalance.walletId, record.id))
194
+ .orderBy(walletBalance.currency);
195
+ return { activeCurrency: record.currency, balances: rows };
196
+ }
197
+ // TODO: validate `currency` against a canonical supported-currency list once one
198
+ // exists - `platformConfig.wallet.cryptoCurrencies` classifies rails, it is not an
199
+ // allowlist, so today any non-empty string is accepted and persisted.
200
+ async setActiveCurrency(userId, currency) {
201
+ const updated = await this.drizzle.db
202
+ .update(wallet)
203
+ .set({ currency })
204
+ .where(eq(wallet.userId, userId))
205
+ .returning({ currency: wallet.currency });
206
+ const record = updated[0];
207
+ if (!record) {
208
+ throw new WalletNotFoundError(userId);
209
+ }
210
+ return { activeCurrency: record.currency };
211
+ }
142
212
  /**
143
213
  * `idempotencyKey` (when supplied) makes a retried deposit a no-op: a replay
144
214
  * returns the original committed result WITHOUT calling the PSP again or
@@ -169,12 +239,6 @@ export class WalletService {
169
239
  .from(wallet)
170
240
  .where(eq(wallet.userId, userId));
171
241
  }
172
- // Single-currency wallet: reject a mismatch BEFORE the PSP call so a wrong-currency
173
- // request never charges the vendor. A brand-new wallet has no currency to mismatch
174
- // against yet - it adopts this deposit's currency on insert below.
175
- if (preResolvedWallet && currency.toUpperCase() !== preResolvedWallet.currency.toUpperCase()) {
176
- throw new CurrencyMismatchError(currency, preResolvedWallet.currency);
177
- }
178
242
  const psp = await this.payment.processDeposit(amount, currency, { userId, provider });
179
243
  const { transactionId, replayed } = await this.drizzle.db.transaction(async (txn) => {
180
244
  let walletRecord = preResolvedWallet;
@@ -182,11 +246,7 @@ export class WalletService {
182
246
  [walletRecord] = await txn.select().from(wallet).where(eq(wallet.userId, userId));
183
247
  }
184
248
  if (!walletRecord) {
185
- walletRecord = findOneOrThrow(await txn.insert(wallet).values({ userId, balance: '0', currency }).returning(), new WalletNotFoundError(userId));
186
- }
187
- // Single-currency wallet: reject a mismatch rather than coerce it onto the wrong rail.
188
- if (currency.toUpperCase() !== walletRecord.currency.toUpperCase()) {
189
- throw new CurrencyMismatchError(currency, walletRecord.currency);
249
+ walletRecord = findOneOrThrow(await txn.insert(wallet).values({ userId, currency }).returning(), new WalletNotFoundError(userId));
190
250
  }
191
251
  const { row, replayed } = await this.insertIdempotentTransaction(txn, {
192
252
  namespace: DEPOSIT_IDEMPOTENCY_NAMESPACE,
@@ -206,10 +266,7 @@ export class WalletService {
206
266
  },
207
267
  });
208
268
  if (!replayed) {
209
- await txn
210
- .update(wallet)
211
- .set({ balance: sql `${wallet.balance} + ${amount}::numeric` })
212
- .where(eq(wallet.id, walletRecord.id));
269
+ await creditWalletBalance(txn, walletRecord.id, currency, amount);
213
270
  }
214
271
  return { transactionId: row.id, replayed };
215
272
  });
@@ -297,10 +354,6 @@ export class WalletService {
297
354
  const { transactionId, status, replayed, walletId, rail } = await this.drizzle.db.transaction(async (txn) => {
298
355
  // FOR UPDATE serializes concurrent withdrawals against a double-debit TOCTOU under READ COMMITTED.
299
356
  const current = findOneOrThrow(await txn.select().from(wallet).where(eq(wallet.userId, userId)).for('update'), new WalletNotFoundError(userId));
300
- // Single-currency wallet: reject a mismatch rather than coerce it onto the wrong rail.
301
- if (currency.toUpperCase() !== current.currency.toUpperCase()) {
302
- throw new CurrencyMismatchError(currency, current.currency);
303
- }
304
357
  // Replay of an already-committed key: return the original untouched. The new-key race is caught by the insert below.
305
358
  if (idempotencyKey) {
306
359
  const existing = await this.findByIdempotencyKey(txn, current.id, namespacedIdempotencyKey(WITHDRAW_IDEMPOTENCY_NAMESPACE, idempotencyKey));
@@ -341,13 +394,9 @@ export class WalletService {
341
394
  };
342
395
  }
343
396
  // Guarded conditional debit: the WHERE makes balance>=amount atomic with the write; 0 rows back rolls back the whole tx.
344
- const debited = await txn
345
- .update(wallet)
346
- .set({ balance: sql `${wallet.balance} - ${amount}::numeric` })
347
- .where(and(eq(wallet.id, current.id), gte(wallet.balance, amount)))
348
- .returning({ id: wallet.id });
397
+ const debited = await debitWalletBalance(txn, current.id, currency, amount);
349
398
  if (debited.length !== 1) {
350
- throw new InsufficientBalanceError(current.balance, amount);
399
+ throw new InsufficientBalanceError(await readWalletBalance(txn, current.id, currency), amount);
351
400
  }
352
401
  if (this.tagEvaluationCommands) {
353
402
  await this.tagEvaluationCommands.evaluateWithdrawalRequested(txn, { userId, amount });
@@ -580,10 +629,7 @@ export class WalletService {
580
629
  if (updated.length === 0) {
581
630
  return false;
582
631
  }
583
- await txn
584
- .update(wallet)
585
- .set({ balance: sql `${wallet.balance} + ${amount}::numeric` })
586
- .where(eq(wallet.id, tx.walletId));
632
+ await creditWalletBalance(txn, tx.walletId, tx.currency, amount);
587
633
  return true;
588
634
  });
589
635
  if (transitioned && adminId) {
@@ -939,10 +985,7 @@ export class WalletService {
939
985
  .set({ status: 'rejected', reviewedBy: adminId, reviewedAt, reviewReason: reason })
940
986
  .where(eq(walletTransaction.id, withdrawalId))
941
987
  .returning(), new WithdrawalNotFoundError(withdrawalId));
942
- await txn
943
- .update(wallet)
944
- .set({ balance: sql `${wallet.balance} + ${updated.amount}::numeric` })
945
- .where(eq(wallet.id, updated.walletId));
988
+ await creditWalletBalance(txn, updated.walletId, updated.currency, updated.amount);
946
989
  return updated;
947
990
  });
948
991
  const userId = await this.userIdForWallet(tx.walletId);
@@ -1000,41 +1043,44 @@ export class WalletService {
1000
1043
  limit,
1001
1044
  };
1002
1045
  }
1003
- async getOrCreateDepositAddress(userId, currency) {
1004
- const existing = await this.findDepositAddress(userId, currency);
1046
+ async getOrCreateDepositAddress(userId, currency, network) {
1047
+ const existing = await this.findDepositAddress(userId, currency, network);
1005
1048
  if (existing) {
1006
- return { address: existing.address, currency: existing.currency };
1049
+ return toDepositAddressResult(existing);
1007
1050
  }
1008
1051
  if (!this.payment.issueDepositAddress) {
1009
1052
  throw new DepositAddressUnsupportedError();
1010
1053
  }
1011
- const issued = await this.payment.issueDepositAddress(userId, currency);
1054
+ const issued = await this.payment.issueDepositAddress(userId, currency, network);
1012
1055
  const [row] = await this.drizzle.db
1013
1056
  .insert(walletDepositAddress)
1014
1057
  .values({
1015
1058
  userId,
1016
1059
  currency,
1060
+ network: network ?? null,
1017
1061
  address: issued.address,
1062
+ tag: issued.tag ?? null,
1018
1063
  providerName: providerNameFor(this.resolveRail(currency)),
1019
1064
  })
1020
1065
  .onConflictDoNothing()
1021
1066
  .returning();
1022
1067
  if (row) {
1023
- return { address: row.address, currency: row.currency };
1068
+ return toDepositAddressResult(row);
1024
1069
  }
1025
- const winner = await this.findDepositAddress(userId, currency);
1070
+ const winner = await this.findDepositAddress(userId, currency, network);
1026
1071
  if (!winner) {
1027
- throw new Error(`wallet deposit address: idempotency conflict but no row found (userId=${userId}, currency=${currency})`);
1072
+ throw new Error(`wallet deposit address: idempotency conflict but no row found (userId=${userId}, currency=${currency}, network=${network ?? 'none'})`);
1028
1073
  }
1029
- return { address: winner.address, currency: winner.currency };
1074
+ return toDepositAddressResult(winner);
1030
1075
  }
1031
1076
  async creditDepositByAddress(event) {
1032
- const depositAddress = await this.findDepositAddressByAddress(event.address);
1077
+ const depositAddress = await this.findDepositAddressByAddress(event);
1033
1078
  if (!depositAddress) {
1034
- logger.warn({ address: event.address }, 'payment webhook: no wallet_deposit_address for inbound deposit');
1079
+ logger.warn({ address: event.address, network: event.network, tag: event.tag }, 'payment webhook: no wallet_deposit_address for inbound deposit');
1035
1080
  return;
1036
1081
  }
1037
- if (event.currency.toUpperCase() !== depositAddress.currency.toUpperCase()) {
1082
+ if (event.network === undefined &&
1083
+ event.currency.toUpperCase() !== depositAddress.currency.toUpperCase()) {
1038
1084
  throw new CurrencyMismatchError(event.currency, depositAddress.currency);
1039
1085
  }
1040
1086
  const { transactionId, replayed } = await this.drizzle.db.transaction(async (txn) => {
@@ -1045,12 +1091,9 @@ export class WalletService {
1045
1091
  if (!walletRecord) {
1046
1092
  walletRecord = findOneOrThrow(await txn
1047
1093
  .insert(wallet)
1048
- .values({ userId: depositAddress.userId, balance: '0', currency: event.currency })
1094
+ .values({ userId: depositAddress.userId, currency: event.currency })
1049
1095
  .returning(), new WalletNotFoundError(depositAddress.userId));
1050
1096
  }
1051
- else if (walletRecord.currency.toUpperCase() !== event.currency.toUpperCase()) {
1052
- throw new CurrencyMismatchError(event.currency, walletRecord.currency);
1053
- }
1054
1097
  const [inserted] = await txn
1055
1098
  .insert(walletTransaction)
1056
1099
  .values({
@@ -1068,10 +1111,7 @@ export class WalletService {
1068
1111
  .onConflictDoNothing()
1069
1112
  .returning();
1070
1113
  if (inserted) {
1071
- await txn
1072
- .update(wallet)
1073
- .set({ balance: sql `${wallet.balance} + ${event.amount}::numeric` })
1074
- .where(eq(wallet.id, walletRecord.id));
1114
+ await creditWalletBalance(txn, walletRecord.id, event.currency, event.amount);
1075
1115
  return { transactionId: inserted.id, replayed: false };
1076
1116
  }
1077
1117
  const [winner] = await txn
@@ -1107,19 +1147,27 @@ export class WalletService {
1107
1147
  .where(and(eq(walletTransaction.walletId, walletId), eq(walletTransaction.idempotencyKey, idempotencyKey)));
1108
1148
  return existing;
1109
1149
  }
1110
- async findDepositAddress(userId, currency) {
1150
+ async findDepositAddress(userId, currency, network) {
1111
1151
  const [row] = await this.drizzle.db
1112
1152
  .select()
1113
1153
  .from(walletDepositAddress)
1114
- .where(and(eq(walletDepositAddress.userId, userId), eq(walletDepositAddress.currency, currency)));
1154
+ .where(and(eq(walletDepositAddress.userId, userId), eq(walletDepositAddress.currency, currency), network === undefined
1155
+ ? isNull(walletDepositAddress.network)
1156
+ : eq(walletDepositAddress.network, network)));
1115
1157
  return row;
1116
1158
  }
1117
- async findDepositAddressByAddress(address) {
1118
- const [row] = await this.drizzle.db
1159
+ async findDepositAddressByAddress({ address, network, tag, }) {
1160
+ const rows = await this.drizzle.db
1119
1161
  .select()
1120
1162
  .from(walletDepositAddress)
1121
- .where(eq(walletDepositAddress.address, address));
1122
- return row;
1163
+ .where(and(eq(walletDepositAddress.address, address), tag === undefined ? isNull(walletDepositAddress.tag) : eq(walletDepositAddress.tag, tag), network === undefined
1164
+ ? undefined
1165
+ : or(eq(walletDepositAddress.network, network), isNull(walletDepositAddress.network))));
1166
+ const owners = new Set(rows.map((row) => row.userId));
1167
+ if (owners.size > 1) {
1168
+ throw new AmbiguousDepositAddressError(address, network);
1169
+ }
1170
+ return rows.find((row) => row.network === network) ?? rows[0];
1123
1171
  }
1124
1172
  }
1125
1173
  //# sourceMappingURL=wallet.service.js.map