@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/compliance/__tests__/kyc.service.int.test.js +2 -2
- package/dist/compliance/__tests__/kyc.service.int.test.js.map +1 -1
- package/dist/compliance/__tests__/rg-monitoring.service.int.test.js +1 -1
- package/dist/compliance/__tests__/rg-monitoring.service.int.test.js.map +1 -1
- package/dist/contracts/adapters/payment.d.ts +11 -3
- package/dist/contracts/adapters/payment.d.ts.map +1 -1
- package/dist/contracts/adapters/payment.js.map +1 -1
- package/dist/wallet/__tests__/admin-reporting.int.test.js +5 -4
- package/dist/wallet/__tests__/admin-reporting.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js +9 -5
- package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js +12 -11
- package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-commands.service.int.test.js +17 -9
- package/dist/wallet/__tests__/wallet-commands.service.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-currency-code.test.d.ts +2 -0
- package/dist/wallet/__tests__/wallet-currency-code.test.d.ts.map +1 -0
- package/dist/wallet/__tests__/wallet-currency-code.test.js +14 -0
- package/dist/wallet/__tests__/wallet-currency-code.test.js.map +1 -0
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js +8 -4
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js +19 -11
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js +8 -5
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.router.int.test.js +2 -4
- package/dist/wallet/__tests__/wallet.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +122 -25
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/contract/index.d.ts +39 -8
- package/dist/wallet/contract/index.d.ts.map +1 -1
- package/dist/wallet/contract/index.js +26 -5
- package/dist/wallet/contract/index.js.map +1 -1
- package/dist/wallet/drizzle/migrations/0008_gorgeous_colonel_america.sql +1 -0
- package/dist/wallet/drizzle/migrations/0009_smooth_sue_storm.sql +17 -0
- package/dist/wallet/drizzle/migrations/meta/0008_snapshot.json +716 -0
- package/dist/wallet/drizzle/migrations/meta/0009_snapshot.json +895 -0
- package/dist/wallet/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/wallet/index.d.ts.map +1 -1
- package/dist/wallet/index.js +0 -2
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/migrate.d.ts +0 -1
- package/dist/wallet/migrate.d.ts.map +1 -1
- package/dist/wallet/migrate.js +0 -4
- package/dist/wallet/migrate.js.map +1 -1
- package/dist/wallet/router/index.d.ts +19 -4
- package/dist/wallet/router/index.d.ts.map +1 -1
- package/dist/wallet/router/index.js +5 -2
- package/dist/wallet/router/index.js.map +1 -1
- package/dist/wallet/schema/index.d.ts +126 -17
- package/dist/wallet/schema/index.d.ts.map +1 -1
- package/dist/wallet/schema/index.js +24 -2
- package/dist/wallet/schema/index.js.map +1 -1
- package/dist/wallet/service/wallet-commands.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet-commands.service.js +7 -15
- package/dist/wallet/service/wallet-commands.service.js.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts +280 -6
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +110 -62
- package/dist/wallet/service/wallet.service.js.map +1 -1
- 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":"AAAA,OAAO,EACL,KAAK,QAAQ,
|
|
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:
|
|
178
|
+
return { balance: '0', currency: DEFAULT_WALLET_CURRENCY };
|
|
136
179
|
}
|
|
137
180
|
return {
|
|
138
|
-
balance: record.
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1074
|
+
return toDepositAddressResult(winner);
|
|
1030
1075
|
}
|
|
1031
1076
|
async creditDepositByAddress(event) {
|
|
1032
|
-
const depositAddress = await this.findDepositAddressByAddress(event
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|