@openora/core 0.4.1-canary.91 → 0.4.1-canary.93
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/admin-console/contract/index.d.ts +12 -0
- package/dist/admin-console/contract/index.d.ts.map +1 -1
- package/dist/admin-console/plugin.d.ts +18 -1
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/admin-console/router/index.d.ts +6 -0
- package/dist/admin-console/router/index.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +18 -1
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +18 -1
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +18 -1
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +18 -1
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +18 -1
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +18 -1
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.d.ts +1 -1
- package/dist/contracts/adapters/audit.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.js.map +1 -1
- package/dist/contracts/adapters/index.d.ts +4 -2
- package/dist/contracts/adapters/index.d.ts.map +1 -1
- package/dist/contracts/adapters/index.js +2 -1
- package/dist/contracts/adapters/index.js.map +1 -1
- package/dist/contracts/adapters/payment.d.ts +99 -0
- package/dist/contracts/adapters/payment.d.ts.map +1 -1
- package/dist/contracts/adapters/payment.js +6 -0
- package/dist/contracts/adapters/payment.js.map +1 -1
- package/dist/contracts/adapters/rate-limit.d.ts +1 -0
- package/dist/contracts/adapters/rate-limit.d.ts.map +1 -1
- package/dist/contracts/adapters/rate-limit.js +1 -0
- package/dist/contracts/adapters/rate-limit.js.map +1 -1
- package/dist/contracts/adapters/wallet-asset-catalog.d.ts +34 -0
- package/dist/contracts/adapters/wallet-asset-catalog.d.ts.map +1 -0
- package/dist/contracts/adapters/wallet-asset-catalog.js +3 -0
- package/dist/contracts/adapters/wallet-asset-catalog.js.map +1 -0
- package/dist/contracts/schemas/__tests__/common-schemas.test.js +2 -1
- package/dist/contracts/schemas/__tests__/common-schemas.test.js.map +1 -1
- package/dist/contracts/schemas/common.d.ts.map +1 -1
- package/dist/contracts/schemas/common.js +7 -4
- package/dist/contracts/schemas/common.js.map +1 -1
- package/dist/contracts/schemas/events.d.ts +15 -0
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +16 -2
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/contracts/schemas/platform-config.d.ts +45 -0
- package/dist/contracts/schemas/platform-config.d.ts.map +1 -1
- package/dist/contracts/schemas/platform-config.js +47 -0
- package/dist/contracts/schemas/platform-config.js.map +1 -1
- package/dist/contracts/schemas/wallet-tx.d.ts +38 -1
- package/dist/contracts/schemas/wallet-tx.d.ts.map +1 -1
- package/dist/contracts/schemas/wallet-tx.js +32 -3
- package/dist/contracts/schemas/wallet-tx.js.map +1 -1
- package/dist/engagement/chat/plugin.d.ts +18 -1
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +18 -1
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +18 -1
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/social/plugin.d.ts +18 -1
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/engagement/social-transfers/plugin.d.ts +18 -1
- package/dist/engagement/social-transfers/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.d.ts +18 -1
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/iam/service/iam.service.d.ts +1 -1
- package/dist/pam/identity/plugin.d.ts +18 -1
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +18 -1
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +18 -1
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +18 -1
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +18 -1
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/auth/permissions.d.ts +36 -0
- package/dist/server/auth/permissions.d.ts.map +1 -1
- package/dist/server/auth/permissions.js +6 -0
- package/dist/server/auth/permissions.js.map +1 -1
- package/dist/server/db/__tests__/query-helpers.test.js +38 -1
- package/dist/server/db/__tests__/query-helpers.test.js.map +1 -1
- package/dist/server/db/index.d.ts +1 -1
- package/dist/server/db/index.d.ts.map +1 -1
- package/dist/server/db/index.js +1 -1
- package/dist/server/db/index.js.map +1 -1
- package/dist/server/db/query-helpers.d.ts +4 -0
- package/dist/server/db/query-helpers.d.ts.map +1 -1
- package/dist/server/db/query-helpers.js +31 -0
- package/dist/server/db/query-helpers.js.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +18 -1
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.js +3 -1
- package/dist/server/runtime/core-token-catalog.js.map +1 -1
- package/dist/testing/mock.d.ts +12 -1
- package/dist/testing/mock.d.ts.map +1 -1
- package/dist/testing/mock.js +16 -0
- package/dist/testing/mock.js.map +1 -1
- package/dist/wallet/__tests__/settlement-network.test.d.ts +2 -0
- package/dist/wallet/__tests__/settlement-network.test.d.ts.map +1 -0
- package/dist/wallet/__tests__/settlement-network.test.js +54 -0
- package/dist/wallet/__tests__/settlement-network.test.js.map +1 -0
- package/dist/wallet/__tests__/wallet-asset.router.int.test.d.ts +2 -0
- package/dist/wallet/__tests__/wallet-asset.router.int.test.d.ts.map +1 -0
- package/dist/wallet/__tests__/wallet-asset.router.int.test.js +234 -0
- package/dist/wallet/__tests__/wallet-asset.router.int.test.js.map +1 -0
- package/dist/wallet/__tests__/wallet-auto-rule.router.int.test.js +5 -3
- package/dist/wallet/__tests__/wallet-auto-rule.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js +4 -2
- 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 +6 -3
- package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-custody-stubs.router.test.d.ts +2 -0
- package/dist/wallet/__tests__/wallet-custody-stubs.router.test.d.ts.map +1 -0
- package/dist/wallet/__tests__/wallet-custody-stubs.router.test.js +80 -0
- package/dist/wallet/__tests__/wallet-custody-stubs.router.test.js.map +1 -0
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js +2 -1
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js +119 -4
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js +2 -1
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.router.int.test.js +23 -3
- package/dist/wallet/__tests__/wallet.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +265 -12
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/adapters/wallet-asset-catalog.service.d.ts +14 -0
- package/dist/wallet/adapters/wallet-asset-catalog.service.d.ts.map +1 -0
- package/dist/wallet/adapters/wallet-asset-catalog.service.js +37 -0
- package/dist/wallet/adapters/wallet-asset-catalog.service.js.map +1 -0
- package/dist/wallet/admin-reporting.d.ts +2 -2
- package/dist/wallet/contract/index.d.ts +428 -4
- package/dist/wallet/contract/index.d.ts.map +1 -1
- package/dist/wallet/contract/index.js +203 -5
- package/dist/wallet/contract/index.js.map +1 -1
- package/dist/wallet/drizzle/migrations/0010_worthless_quasimodo.sql +84 -0
- package/dist/wallet/drizzle/migrations/meta/0010_snapshot.json +1565 -0
- package/dist/wallet/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/wallet/plugin.d.ts +18 -1
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/dist/wallet/plugin.js +19 -2
- package/dist/wallet/plugin.js.map +1 -1
- package/dist/wallet/router/index.d.ts +235 -3
- package/dist/wallet/router/index.d.ts.map +1 -1
- package/dist/wallet/router/index.js +117 -20
- package/dist/wallet/router/index.js.map +1 -1
- package/dist/wallet/schema/index.d.ts +984 -3
- package/dist/wallet/schema/index.d.ts.map +1 -1
- package/dist/wallet/schema/index.js +141 -2
- package/dist/wallet/schema/index.js.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts +70 -6
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +379 -16
- package/dist/wallet/service/wallet.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
import { makeNotFoundError, makeConflictError, createDomainError, findOneOrThrow, pageToOffset, withAdvisoryXactLock, assertRateLimit, createLogger, moneyToNumber, moneyEquals, } from '@openora/core/server';
|
|
2
|
-
import { normalizeKycStatus, } from '@openora/core/contracts';
|
|
1
|
+
import { makeNotFoundError, serializeRow, makeConflictError, createDomainError, findOneOrThrow, pageToOffset, withAdvisoryXactLock, assertRateLimit, createLogger, moneyToNumber, moneyEquals, moneyAdd, moneyCompare, moneySubtract, } from '@openora/core/server';
|
|
2
|
+
import { normalizeKycStatus, DEFAULT_PAYMENT_PROVIDER, } from '@openora/core/contracts';
|
|
3
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, walletBalance, walletTransaction, autoWithdrawalRule, walletAutoWithdrawalConfig, walletDepositAddress, } from '../schema/index.js';
|
|
5
|
+
import { wallet, walletBalance, walletTransaction, autoWithdrawalRule, walletAutoWithdrawalConfig, walletDepositAddress, walletAsset, } from '../schema/index.js';
|
|
6
6
|
const logger = createLogger('wallet');
|
|
7
7
|
export const WalletNotFoundError = makeNotFoundError('Wallet');
|
|
8
8
|
export const WithdrawalNotFoundError = makeNotFoundError('Withdrawal');
|
|
9
9
|
export const AutoWithdrawalConfigNotFoundError = makeNotFoundError('AutoWithdrawalConfig');
|
|
10
|
+
export const PlayerNotFoundError = makeNotFoundError('Player');
|
|
10
11
|
export const InsufficientBalanceError = createDomainError('InsufficientBalanceError', (available, requested) => `Insufficient balance: available ${available}, requested ${requested}`);
|
|
11
12
|
export const WithdrawalNotPendingError = makeConflictError('WithdrawalNotPendingError', 'Withdrawal is not pending and cannot be reviewed');
|
|
12
13
|
export const KycRequiredError = makeConflictError('KycRequiredError', 'KYC verification required before withdrawal');
|
|
13
14
|
export const IdempotencyKeyReuseError = makeConflictError('IdempotencyKeyReuseError', 'Idempotency key was already used with a different amount');
|
|
14
15
|
export const DepositAddressUnsupportedError = makeConflictError('DepositAddressUnsupportedError', 'The bound payment adapter does not support address-based deposits');
|
|
15
16
|
export const DestinationAddressRequiredError = makeConflictError('DestinationAddressRequiredError', 'A destination address is required for a crypto-rail withdrawal');
|
|
17
|
+
export const WalletAssetNotFoundError = makeNotFoundError('WalletAsset');
|
|
18
|
+
export const WalletAssetAlreadyExistsError = makeConflictError('WalletAssetAlreadyExistsError', 'An asset is already configured for this currency and network');
|
|
19
|
+
export const WalletAssetUnsupportedError = makeConflictError('WalletAssetUnsupportedError', 'The bound payment adapter cannot serve this currency and network');
|
|
20
|
+
export const WalletAssetInUseError = makeConflictError('WalletAssetInUseError', 'Players still hold a balance in this currency');
|
|
21
|
+
export const WalletAssetUnknownProviderError = makeConflictError('WalletAssetUnknownProviderError', 'providerName is not a registered payment provider');
|
|
22
|
+
export const WalletAssetHasInFlightTransactionsError = makeConflictError('WalletAssetHasInFlightTransactionsError', 'A pending or processing transaction exists for this currency and network');
|
|
23
|
+
export const AmbiguousNetworkError = createDomainError('AmbiguousNetworkError', (currency, networks) => `Currency ${currency} settles on several networks (${networks}) - a network is required`);
|
|
24
|
+
export const UnsupportedNetworkError = createDomainError('UnsupportedNetworkError', (currency, network) => `Currency ${currency} is not settled on network ${network}`);
|
|
25
|
+
export const WithdrawalDisabledError = createDomainError('WithdrawalDisabledError', (currency) => `Withdrawals are disabled for ${currency} on every configured network`);
|
|
26
|
+
export const BelowMinimumWithdrawalError = createDomainError('BelowMinimumWithdrawalError', (amount, minimum, currency, network) => `Withdrawal of ${amount} ${currency} on ${network} is below the ${minimum} ${currency} minimum`);
|
|
16
27
|
const KYC_PASS_STATUSES = new Set(['approved', 'manually_overridden']);
|
|
17
28
|
export const CurrencyMismatchError = createDomainError('CurrencyMismatchError', (requested, walletCurrency) => `Currency mismatch: requested ${requested}, wallet holds ${walletCurrency}`);
|
|
18
29
|
export const AmbiguousDepositAddressError = createDomainError('AmbiguousDepositAddressError', (address, network) => `Deposit address ${address} on ${network ?? 'an unknown network'} is issued to more than one user`);
|
|
19
|
-
// Crypto currencies settle on the crypto rail
|
|
20
|
-
// fiat rail (a PSP). The concrete provider is recorded per transaction, not
|
|
21
|
-
// Overridable per-operator via `platformConfig.wallet.cryptoCurrencies` - see `railFor`.
|
|
30
|
+
// Crypto currencies settle on the crypto rail through a custody/MPC vendor; everything
|
|
31
|
+
// else on the fiat rail (a PSP). The concrete provider is recorded per transaction, not
|
|
32
|
+
// here. Overridable per-operator via `platformConfig.wallet.cryptoCurrencies` - see `railFor`.
|
|
22
33
|
const DEFAULT_CRYPTO_CURRENCIES = new Set(['BTC', 'ETH', 'USDT', 'USDC']);
|
|
23
34
|
// Mirrors the `wallet.currency` column default: what a player without a wallet row
|
|
24
35
|
// reads as their active currency before one is created on first deposit.
|
|
@@ -27,6 +38,49 @@ const DEFAULT_WALLET_CURRENCY = 'USD';
|
|
|
27
38
|
// fraud (idempotency + the ledger guard cover correctness). An overlay rebinds
|
|
28
39
|
// RATE_LIMITER to change the backend, not this policy.
|
|
29
40
|
const WALLET_MUTATION_RATE_LIMIT = { limit: 30, windowMs: 60 * 1000 };
|
|
41
|
+
/**
|
|
42
|
+
* Pins the chain a payout settles on. With one payable network the choice is implied; with
|
|
43
|
+
* several an explicit network is mandatory, because picking one silently would send a
|
|
44
|
+
* player's USDT over a chain their receiving wallet may not support.
|
|
45
|
+
*
|
|
46
|
+
* `assets` is every catalog row for the currency, enabled or not, so the two empty cases
|
|
47
|
+
* stay distinguishable: no rows at all means the operator never configured this currency
|
|
48
|
+
* (a fiat PSP), and the caller's choice passes through unchecked; rows that are all
|
|
49
|
+
* withdrawal-disabled is a deliberate operator decision and fails closed.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveWithdrawalNetwork(assets, currency, network) {
|
|
52
|
+
if (assets.length === 0) {
|
|
53
|
+
return network?.toUpperCase() ?? null;
|
|
54
|
+
}
|
|
55
|
+
const payable = assets.filter((asset) => asset.withdrawalEnabled);
|
|
56
|
+
const [only, ...rest] = payable;
|
|
57
|
+
if (!only) {
|
|
58
|
+
throw new WithdrawalDisabledError(currency);
|
|
59
|
+
}
|
|
60
|
+
if (network === undefined) {
|
|
61
|
+
if (rest.length > 0) {
|
|
62
|
+
throw new AmbiguousNetworkError(currency, payable.map((asset) => asset.network).join(', '));
|
|
63
|
+
}
|
|
64
|
+
return only.network.toUpperCase();
|
|
65
|
+
}
|
|
66
|
+
const wanted = network.toUpperCase();
|
|
67
|
+
if (!payable.some((asset) => asset.network.toUpperCase() === wanted)) {
|
|
68
|
+
throw new UnsupportedNetworkError(currency, wanted);
|
|
69
|
+
}
|
|
70
|
+
return wanted;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rejects a payout worth less than the operator's per-network floor. The floor is per chain,
|
|
74
|
+
* not per currency: moving USDT costs cents on BEP20 and dollars on ERC20, so one
|
|
75
|
+
* currency-wide minimum is either too high for the cheap chain or below the fee on the
|
|
76
|
+
* expensive one.
|
|
77
|
+
*/
|
|
78
|
+
export function assertAboveMinimumWithdrawal(assets, amount, currency, network) {
|
|
79
|
+
const asset = assets.find((candidate) => candidate.withdrawalEnabled && candidate.network.toUpperCase() === network);
|
|
80
|
+
if (asset && moneyCompare(amount, asset.minWithdrawal) < 0) {
|
|
81
|
+
throw new BelowMinimumWithdrawalError(amount, asset.minWithdrawal, currency, asset.network);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
30
84
|
export function railFor(currency, cryptoCurrencies) {
|
|
31
85
|
const set = cryptoCurrencies
|
|
32
86
|
? new Set(cryptoCurrencies.map((c) => c.toUpperCase()))
|
|
@@ -69,6 +123,7 @@ export async function readWalletBalance(txn, walletId, currency) {
|
|
|
69
123
|
// prefix won't parse - hash namespace + key into a stable pseudo-uuid instead.
|
|
70
124
|
const DEPOSIT_IDEMPOTENCY_NAMESPACE = 'deposit';
|
|
71
125
|
const WITHDRAW_IDEMPOTENCY_NAMESPACE = 'withdraw';
|
|
126
|
+
const MANUAL_ADJUSTMENT_IDEMPOTENCY_NAMESPACE = 'manual-adjustment';
|
|
72
127
|
function namespacedIdempotencyKey(namespace, rawKey) {
|
|
73
128
|
const hex = createHash('sha256').update(`${namespace}:${rawKey}`).digest('hex');
|
|
74
129
|
return [
|
|
@@ -119,10 +174,17 @@ function toAutoWithdrawalConfigDto(row) {
|
|
|
119
174
|
createdAt: row.createdAt.toISOString(),
|
|
120
175
|
};
|
|
121
176
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
const PUBLIC_ASSET_COLUMNS = {
|
|
178
|
+
currency: walletAsset.currency,
|
|
179
|
+
network: walletAsset.network,
|
|
180
|
+
minDeposit: walletAsset.minDeposit,
|
|
181
|
+
minWithdrawal: walletAsset.minWithdrawal,
|
|
182
|
+
withdrawalFee: walletAsset.withdrawalFee,
|
|
183
|
+
depositEnabled: walletAsset.depositEnabled,
|
|
184
|
+
withdrawalEnabled: walletAsset.withdrawalEnabled,
|
|
185
|
+
};
|
|
186
|
+
function toWalletAssetDto(row) {
|
|
187
|
+
return serializeRow(row, { dateFields: ['createdAt', 'updatedAt'], decimalFields: [] });
|
|
126
188
|
}
|
|
127
189
|
/**
|
|
128
190
|
* Owns all wallet balance mutations and the withdrawal review/auto-approval
|
|
@@ -136,6 +198,7 @@ export class WalletService {
|
|
|
136
198
|
drizzle;
|
|
137
199
|
events;
|
|
138
200
|
payment;
|
|
201
|
+
paymentProviders;
|
|
139
202
|
identityReader;
|
|
140
203
|
directory;
|
|
141
204
|
platformConfig;
|
|
@@ -143,10 +206,11 @@ export class WalletService {
|
|
|
143
206
|
riskTags;
|
|
144
207
|
tagEvaluationCommands;
|
|
145
208
|
audit;
|
|
146
|
-
constructor({ drizzle, events, payment, directory, identityReader, platformConfig, limiter, riskTags, tagEvaluationCommands, audit, }) {
|
|
209
|
+
constructor({ drizzle, events, payment, paymentProviders, directory, identityReader, platformConfig, limiter, riskTags, tagEvaluationCommands, audit, }) {
|
|
147
210
|
this.drizzle = drizzle;
|
|
148
211
|
this.events = events;
|
|
149
212
|
this.payment = payment;
|
|
213
|
+
this.paymentProviders = paymentProviders;
|
|
150
214
|
this.directory = directory;
|
|
151
215
|
this.identityReader = identityReader;
|
|
152
216
|
this.platformConfig = platformConfig;
|
|
@@ -155,9 +219,36 @@ export class WalletService {
|
|
|
155
219
|
this.tagEvaluationCommands = tagEvaluationCommands;
|
|
156
220
|
this.audit = audit;
|
|
157
221
|
}
|
|
222
|
+
// Every catalog row for the currency, enabled or not - resolveWithdrawalNetwork needs
|
|
223
|
+
// both to tell "never configured" from "deliberately disabled".
|
|
224
|
+
assetsForCurrency(currency) {
|
|
225
|
+
return this.drizzle.db
|
|
226
|
+
.select({
|
|
227
|
+
network: walletAsset.network,
|
|
228
|
+
minWithdrawal: walletAsset.minWithdrawal,
|
|
229
|
+
withdrawalEnabled: walletAsset.withdrawalEnabled,
|
|
230
|
+
})
|
|
231
|
+
.from(walletAsset)
|
|
232
|
+
.where(eq(walletAsset.currency, currency.toUpperCase()));
|
|
233
|
+
}
|
|
158
234
|
resolveRail(currency) {
|
|
159
235
|
return railFor(currency, this.platformConfig?.wallet?.cryptoCurrencies);
|
|
160
236
|
}
|
|
237
|
+
// The concrete vendor a transaction/deposit-address settles through: the catalog row
|
|
238
|
+
// for the exact (currency, network) pair names the bound provider. Reconciliation
|
|
239
|
+
// scopes its diff by this column, so a null network (an unconfigured pair - no
|
|
240
|
+
// catalog row could ever match it) and a configured-but-null providerName column both
|
|
241
|
+
// resolve to the single default binding rather than leaving the column null.
|
|
242
|
+
async providerNameFor(currency, network) {
|
|
243
|
+
if (network === null) {
|
|
244
|
+
return DEFAULT_PAYMENT_PROVIDER;
|
|
245
|
+
}
|
|
246
|
+
const [row] = await this.drizzle.db
|
|
247
|
+
.select({ providerName: walletAsset.providerName })
|
|
248
|
+
.from(walletAsset)
|
|
249
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)));
|
|
250
|
+
return row?.providerName ?? DEFAULT_PAYMENT_PROVIDER;
|
|
251
|
+
}
|
|
161
252
|
rateLimit(userId) {
|
|
162
253
|
return this.limiter
|
|
163
254
|
? assertRateLimit(this.limiter, `wallet-mutation:${userId}`, WALLET_MUTATION_RATE_LIMIT)
|
|
@@ -281,6 +372,117 @@ export class WalletService {
|
|
|
281
372
|
}
|
|
282
373
|
return { transactionId, status: 'completed' };
|
|
283
374
|
}
|
|
375
|
+
async manualAdjust({ adminId, userId, direction, amount, currency, reason, idempotencyKey, ip, userAgent, }) {
|
|
376
|
+
const playerId = await this.identityReader.getPlayerIdByUserId(userId);
|
|
377
|
+
if (!playerId) {
|
|
378
|
+
throw new PlayerNotFoundError(userId);
|
|
379
|
+
}
|
|
380
|
+
const { result, emitted } = await this.drizzle.db.transaction(async (txn) => {
|
|
381
|
+
let walletRecord = (await txn.select().from(wallet).where(eq(wallet.userId, userId)).for('update')).at(0);
|
|
382
|
+
if (!walletRecord) {
|
|
383
|
+
if (direction === 'debit') {
|
|
384
|
+
throw new InsufficientBalanceError('0', amount);
|
|
385
|
+
}
|
|
386
|
+
const [created] = await txn
|
|
387
|
+
.insert(wallet)
|
|
388
|
+
.values({ userId, currency })
|
|
389
|
+
.onConflictDoNothing()
|
|
390
|
+
.returning();
|
|
391
|
+
walletRecord =
|
|
392
|
+
created ??
|
|
393
|
+
findOneOrThrow(await txn.select().from(wallet).where(eq(wallet.userId, userId)).for('update'), new WalletNotFoundError(userId));
|
|
394
|
+
}
|
|
395
|
+
const existing = await this.findByIdempotencyKey(txn, walletRecord.id, namespacedIdempotencyKey(MANUAL_ADJUSTMENT_IDEMPOTENCY_NAMESPACE, idempotencyKey));
|
|
396
|
+
if (existing) {
|
|
397
|
+
this.assertManualAdjustmentReplayMatches(existing, {
|
|
398
|
+
adminId,
|
|
399
|
+
direction,
|
|
400
|
+
amount,
|
|
401
|
+
currency,
|
|
402
|
+
reason,
|
|
403
|
+
});
|
|
404
|
+
return { result: { transactionId: existing.id, status: existing.status }, emitted: false };
|
|
405
|
+
}
|
|
406
|
+
const { row, replayed } = await this.insertIdempotentTransaction(txn, {
|
|
407
|
+
namespace: MANUAL_ADJUSTMENT_IDEMPOTENCY_NAMESPACE,
|
|
408
|
+
walletId: walletRecord.id,
|
|
409
|
+
rawIdempotencyKey: idempotencyKey,
|
|
410
|
+
amount,
|
|
411
|
+
currency,
|
|
412
|
+
values: {
|
|
413
|
+
walletId: walletRecord.id,
|
|
414
|
+
type: direction === 'credit' ? 'manual_credit' : 'manual_debit',
|
|
415
|
+
amount,
|
|
416
|
+
currency,
|
|
417
|
+
status: 'completed',
|
|
418
|
+
reviewedBy: adminId,
|
|
419
|
+
reviewedAt: new Date(),
|
|
420
|
+
reviewReason: reason,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
if (replayed) {
|
|
424
|
+
this.assertManualAdjustmentReplayMatches(row, {
|
|
425
|
+
adminId,
|
|
426
|
+
direction,
|
|
427
|
+
amount,
|
|
428
|
+
currency,
|
|
429
|
+
reason,
|
|
430
|
+
});
|
|
431
|
+
return { result: { transactionId: row.id, status: row.status }, emitted: false };
|
|
432
|
+
}
|
|
433
|
+
const balances = direction === 'credit'
|
|
434
|
+
? await creditWalletBalance(txn, walletRecord.id, currency, amount)
|
|
435
|
+
: await debitWalletBalance(txn, walletRecord.id, currency, amount);
|
|
436
|
+
const [balance] = balances;
|
|
437
|
+
if (!balance) {
|
|
438
|
+
throw new InsufficientBalanceError(await readWalletBalance(txn, walletRecord.id, currency), amount);
|
|
439
|
+
}
|
|
440
|
+
// Derived from the row the update returned, never read separately beforehand. The
|
|
441
|
+
// wallet row is locked here but the deposit credit path does not take that lock, so
|
|
442
|
+
// a deposit can commit between a pre-read and this update - and `before` is going
|
|
443
|
+
// into an append-only audit record that cannot be corrected later.
|
|
444
|
+
const balanceBefore = direction === 'credit'
|
|
445
|
+
? moneySubtract(balance.amount, amount)
|
|
446
|
+
: moneyAdd(balance.amount, amount);
|
|
447
|
+
await this.audit.recordInTransaction(txn, {
|
|
448
|
+
actorId: adminId,
|
|
449
|
+
actorType: 'admin',
|
|
450
|
+
action: 'wallet.manual_adjustment.created',
|
|
451
|
+
resourceType: 'wallet_transaction',
|
|
452
|
+
resourceId: row.id,
|
|
453
|
+
before: { balance: balanceBefore, currency },
|
|
454
|
+
after: {
|
|
455
|
+
balance: balance.amount,
|
|
456
|
+
currency,
|
|
457
|
+
transactionId: row.id,
|
|
458
|
+
direction,
|
|
459
|
+
amount,
|
|
460
|
+
reason,
|
|
461
|
+
},
|
|
462
|
+
ip,
|
|
463
|
+
userAgent,
|
|
464
|
+
});
|
|
465
|
+
return { result: { transactionId: row.id, status: row.status }, emitted: true };
|
|
466
|
+
});
|
|
467
|
+
// Every other balance-mutating path on this service emits; without this one an admin
|
|
468
|
+
// correction moves a real balance while responsible-gaming monitoring, analytics and
|
|
469
|
+
// any operator subscriber see nothing at all.
|
|
470
|
+
if (emitted) {
|
|
471
|
+
this.events.emit('wallet.manual_adjustment.created', {
|
|
472
|
+
userId,
|
|
473
|
+
playerId,
|
|
474
|
+
adminId,
|
|
475
|
+
amount,
|
|
476
|
+
currency,
|
|
477
|
+
transactionId: result.transactionId,
|
|
478
|
+
direction,
|
|
479
|
+
reason,
|
|
480
|
+
ip,
|
|
481
|
+
userAgent,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
284
486
|
// Pre-PSP replay check for deposit; also returns the resolved wallet so the transaction
|
|
285
487
|
// below can skip re-selecting it. Throws IdempotencyKeyReuseError on an amount/currency mismatch.
|
|
286
488
|
async findDepositReplay({ userId, idempotencyKey, amount, currency, }) {
|
|
@@ -307,6 +509,14 @@ export class WalletService {
|
|
|
307
509
|
throw new IdempotencyKeyReuseError();
|
|
308
510
|
}
|
|
309
511
|
}
|
|
512
|
+
assertManualAdjustmentReplayMatches(existing, { adminId, direction, amount, currency, reason, }) {
|
|
513
|
+
if (existing.type !== (direction === 'credit' ? 'manual_credit' : 'manual_debit') ||
|
|
514
|
+
existing.reviewedBy !== adminId ||
|
|
515
|
+
existing.reviewReason !== reason) {
|
|
516
|
+
throw new IdempotencyKeyReuseError();
|
|
517
|
+
}
|
|
518
|
+
this.assertReplayMatches(existing, amount, currency);
|
|
519
|
+
}
|
|
310
520
|
// Two concurrent requests with the same key can both pass the pre-insert check and race
|
|
311
521
|
// on the unique index; onConflictDoNothing makes the loser's insert a no-op so it re-reads
|
|
312
522
|
// the winner's committed row and returns it as a replay instead of aborting the transaction.
|
|
@@ -345,12 +555,15 @@ export class WalletService {
|
|
|
345
555
|
* return an already-`completed`/`processing` result if the request
|
|
346
556
|
* qualifies for (and passes) auto-approval - see `maybeAutoApprove`.
|
|
347
557
|
*/
|
|
348
|
-
async withdraw({ userId, amount, currency, idempotencyKey, destinationAddress, ip, userAgent, }) {
|
|
558
|
+
async withdraw({ userId, amount, currency, network, idempotencyKey, destinationAddress, ip, userAgent, }) {
|
|
349
559
|
await this.rateLimit(userId);
|
|
350
560
|
await this.assertKycForWithdrawal(userId);
|
|
351
561
|
if (this.resolveRail(currency) === 'crypto' && !destinationAddress) {
|
|
352
562
|
throw new DestinationAddressRequiredError();
|
|
353
563
|
}
|
|
564
|
+
const assets = await this.assetsForCurrency(currency);
|
|
565
|
+
const settlementNetwork = resolveWithdrawalNetwork(assets, currency, network);
|
|
566
|
+
assertAboveMinimumWithdrawal(assets, amount, currency, settlementNetwork);
|
|
354
567
|
const { transactionId, status, replayed, walletId, rail } = await this.drizzle.db.transaction(async (txn) => {
|
|
355
568
|
// FOR UPDATE serializes concurrent withdrawals against a double-debit TOCTOU under READ COMMITTED.
|
|
356
569
|
const current = findOneOrThrow(await txn.select().from(wallet).where(eq(wallet.userId, userId)).for('update'), new WalletNotFoundError(userId));
|
|
@@ -381,6 +594,7 @@ export class WalletService {
|
|
|
381
594
|
currency,
|
|
382
595
|
status: 'pending',
|
|
383
596
|
rail: this.resolveRail(currency),
|
|
597
|
+
network: settlementNetwork,
|
|
384
598
|
destinationAddress: destinationAddress ?? null,
|
|
385
599
|
},
|
|
386
600
|
});
|
|
@@ -513,6 +727,7 @@ export class WalletService {
|
|
|
513
727
|
username: summary?.username ?? '',
|
|
514
728
|
amount: r.tx.amount,
|
|
515
729
|
currency: r.tx.currency,
|
|
730
|
+
network: r.tx.network,
|
|
516
731
|
rail: r.tx.rail ?? null,
|
|
517
732
|
status: r.tx.status,
|
|
518
733
|
kycStatus: summary?.kycStatus ?? null,
|
|
@@ -584,6 +799,8 @@ export class WalletService {
|
|
|
584
799
|
rail: tx.rail,
|
|
585
800
|
adminId,
|
|
586
801
|
destinationAddress: tx.destinationAddress,
|
|
802
|
+
// Pinned at request time; without it an adapter cannot tell ERC20 USDT from TRC20.
|
|
803
|
+
network: tx.network,
|
|
587
804
|
});
|
|
588
805
|
}
|
|
589
806
|
catch (err) {
|
|
@@ -598,7 +815,10 @@ export class WalletService {
|
|
|
598
815
|
if (result.status !== 'completed') {
|
|
599
816
|
await this.drizzle.db
|
|
600
817
|
.update(walletTransaction)
|
|
601
|
-
.set({
|
|
818
|
+
.set({
|
|
819
|
+
providerName: await this.providerNameFor(tx.currency, tx.network),
|
|
820
|
+
providerRefId: result.externalId,
|
|
821
|
+
})
|
|
602
822
|
.where(eq(walletTransaction.id, tx.id));
|
|
603
823
|
return { transactionId: tx.id, status: 'processing' };
|
|
604
824
|
}
|
|
@@ -606,7 +826,7 @@ export class WalletService {
|
|
|
606
826
|
.update(walletTransaction)
|
|
607
827
|
.set({
|
|
608
828
|
status: 'completed',
|
|
609
|
-
providerName: providerNameFor(tx.
|
|
829
|
+
providerName: await this.providerNameFor(tx.currency, tx.network),
|
|
610
830
|
providerRefId: result.externalId,
|
|
611
831
|
})
|
|
612
832
|
.where(eq(walletTransaction.id, tx.id));
|
|
@@ -878,6 +1098,142 @@ export class WalletService {
|
|
|
878
1098
|
return config;
|
|
879
1099
|
});
|
|
880
1100
|
}
|
|
1101
|
+
async listWalletAssets() {
|
|
1102
|
+
const rows = await this.drizzle.db
|
|
1103
|
+
.select()
|
|
1104
|
+
.from(walletAsset)
|
|
1105
|
+
.orderBy(asc(walletAsset.currency), asc(walletAsset.network));
|
|
1106
|
+
return rows.map(toWalletAssetDto);
|
|
1107
|
+
}
|
|
1108
|
+
listEnabledWalletAssets() {
|
|
1109
|
+
return this.drizzle.db
|
|
1110
|
+
.select(PUBLIC_ASSET_COLUMNS)
|
|
1111
|
+
.from(walletAsset)
|
|
1112
|
+
.where(or(eq(walletAsset.depositEnabled, true), eq(walletAsset.withdrawalEnabled, true)))
|
|
1113
|
+
.orderBy(asc(walletAsset.currency), asc(walletAsset.network));
|
|
1114
|
+
}
|
|
1115
|
+
async getWalletAsset(currency, network) {
|
|
1116
|
+
const [row] = await this.drizzle.db
|
|
1117
|
+
.select()
|
|
1118
|
+
.from(walletAsset)
|
|
1119
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)));
|
|
1120
|
+
return row ? toWalletAssetDto(row) : null;
|
|
1121
|
+
}
|
|
1122
|
+
// The catalog is operator-editable, so an admin can name a pair the bound vendor has
|
|
1123
|
+
// never heard of. Reject at write time rather than at a player's deposit request.
|
|
1124
|
+
assertAdapterSupports(currency, network) {
|
|
1125
|
+
if (this.payment.supportsAsset && !this.payment.supportsAsset(currency, network)) {
|
|
1126
|
+
throw new WalletAssetUnsupportedError();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
// Same discipline as assertAdapterSupports, for the provider key itself: an
|
|
1130
|
+
// unvalidated typo silently falls back to the default adapter, which means eg a
|
|
1131
|
+
// crypto payout attempted through a PSP. Undefined (the default binding) always passes.
|
|
1132
|
+
assertProviderNameValid(providerName) {
|
|
1133
|
+
if (providerName !== undefined && !this.paymentProviders.names().includes(providerName)) {
|
|
1134
|
+
throw new WalletAssetUnknownProviderError();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async createWalletAsset(adminId, input, meta) {
|
|
1138
|
+
this.assertAdapterSupports(input.currency, input.network);
|
|
1139
|
+
this.assertProviderNameValid(input.providerName);
|
|
1140
|
+
return this.drizzle.db.transaction(async (txn) => {
|
|
1141
|
+
const rows = await txn.insert(walletAsset).values(input).onConflictDoNothing().returning();
|
|
1142
|
+
// Empty => the (currency, network) unique index rejected it.
|
|
1143
|
+
const asset = toWalletAssetDto(findOneOrThrow(rows, new WalletAssetAlreadyExistsError()));
|
|
1144
|
+
await this.audit.recordInTransaction(txn, {
|
|
1145
|
+
actorId: adminId,
|
|
1146
|
+
actorType: 'admin',
|
|
1147
|
+
action: 'wallet.wallet_asset.created',
|
|
1148
|
+
resourceType: 'wallet_asset',
|
|
1149
|
+
resourceId: asset.id,
|
|
1150
|
+
before: null,
|
|
1151
|
+
after: asset,
|
|
1152
|
+
...meta,
|
|
1153
|
+
});
|
|
1154
|
+
return asset;
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
// Deliberately touches only the catalog row: a withdrawal already pending or processing
|
|
1158
|
+
// in this currency keeps its own terms and is never cancelled by disabling the pair.
|
|
1159
|
+
async updateWalletAsset(adminId, { currency, network, ...changes }, meta) {
|
|
1160
|
+
if (changes.providerAssetId !== undefined) {
|
|
1161
|
+
this.assertAdapterSupports(currency, network);
|
|
1162
|
+
}
|
|
1163
|
+
return this.drizzle.db.transaction(async (txn) => {
|
|
1164
|
+
const [before] = await txn
|
|
1165
|
+
.select()
|
|
1166
|
+
.from(walletAsset)
|
|
1167
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)));
|
|
1168
|
+
if (!before) {
|
|
1169
|
+
throw new WalletAssetNotFoundError(`${currency}/${network}`);
|
|
1170
|
+
}
|
|
1171
|
+
const rows = await txn
|
|
1172
|
+
.update(walletAsset)
|
|
1173
|
+
.set(changes)
|
|
1174
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)))
|
|
1175
|
+
.returning();
|
|
1176
|
+
const asset = toWalletAssetDto(findOneOrThrow(rows, new WalletAssetNotFoundError(`${currency}/${network}`)));
|
|
1177
|
+
await this.audit.recordInTransaction(txn, {
|
|
1178
|
+
actorId: adminId,
|
|
1179
|
+
actorType: 'admin',
|
|
1180
|
+
action: 'wallet.wallet_asset.updated',
|
|
1181
|
+
resourceType: 'wallet_asset',
|
|
1182
|
+
resourceId: asset.id,
|
|
1183
|
+
before: toWalletAssetDto(before),
|
|
1184
|
+
after: asset,
|
|
1185
|
+
...meta,
|
|
1186
|
+
});
|
|
1187
|
+
return asset;
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
async deleteWalletAsset(adminId, currency, network, meta) {
|
|
1191
|
+
return this.drizzle.db.transaction(async (txn) => {
|
|
1192
|
+
const [before] = await txn
|
|
1193
|
+
.select()
|
|
1194
|
+
.from(walletAsset)
|
|
1195
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)));
|
|
1196
|
+
if (!before) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
// wallet_balance is keyed by currency only (no network column), so this guard is
|
|
1200
|
+
// necessarily currency-wide: removing one network of a currency players still hold
|
|
1201
|
+
// is blocked even if their balance arrived over another network. Fails safe.
|
|
1202
|
+
const [held] = await txn
|
|
1203
|
+
.select({ n: count() })
|
|
1204
|
+
.from(walletBalance)
|
|
1205
|
+
.where(and(eq(walletBalance.currency, currency), sql `${walletBalance.amount} > 0`));
|
|
1206
|
+
if ((held?.n ?? 0) > 0) {
|
|
1207
|
+
throw new WalletAssetInUseError();
|
|
1208
|
+
}
|
|
1209
|
+
// Renaming a pair is a delete plus a create (the (currency, network) key AND
|
|
1210
|
+
// providerName are immutable), so this delete is the only way providerName ever
|
|
1211
|
+
// effectively changes. Block it while a pending/processing transaction exists for
|
|
1212
|
+
// this exact (currency, network) pair - otherwise the vendor reference an in-flight
|
|
1213
|
+
// payout is settling through gets rewritten out from under it.
|
|
1214
|
+
const [inFlight] = await txn
|
|
1215
|
+
.select({ n: count() })
|
|
1216
|
+
.from(walletTransaction)
|
|
1217
|
+
.where(and(eq(walletTransaction.currency, currency), eq(walletTransaction.network, network), inArray(walletTransaction.status, ['pending', 'processing'])));
|
|
1218
|
+
if ((inFlight?.n ?? 0) > 0) {
|
|
1219
|
+
throw new WalletAssetHasInFlightTransactionsError();
|
|
1220
|
+
}
|
|
1221
|
+
await txn
|
|
1222
|
+
.delete(walletAsset)
|
|
1223
|
+
.where(and(eq(walletAsset.currency, currency), eq(walletAsset.network, network)));
|
|
1224
|
+
await this.audit.recordInTransaction(txn, {
|
|
1225
|
+
actorId: adminId,
|
|
1226
|
+
actorType: 'admin',
|
|
1227
|
+
action: 'wallet.wallet_asset.deleted',
|
|
1228
|
+
resourceType: 'wallet_asset',
|
|
1229
|
+
resourceId: before.id,
|
|
1230
|
+
before: toWalletAssetDto(before),
|
|
1231
|
+
after: null,
|
|
1232
|
+
...meta,
|
|
1233
|
+
});
|
|
1234
|
+
return true;
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
881
1237
|
async autoApprovalKycStatus(userId) {
|
|
882
1238
|
// No directory bound => cannot verify KYC => fail closed.
|
|
883
1239
|
if (!this.directory) {
|
|
@@ -1001,7 +1357,7 @@ export class WalletService {
|
|
|
1001
1357
|
});
|
|
1002
1358
|
return { transactionId: tx.id, status: 'rejected' };
|
|
1003
1359
|
}
|
|
1004
|
-
async getTransactions({ userId, page, limit, sortBy, sortOrder, }) {
|
|
1360
|
+
async getTransactions({ userId, page, limit, sortBy, sortOrder, includeInternal = false, }) {
|
|
1005
1361
|
const db = this.drizzle.db;
|
|
1006
1362
|
const [walletRecord] = await db.select().from(wallet).where(eq(wallet.userId, userId));
|
|
1007
1363
|
if (!walletRecord) {
|
|
@@ -1035,8 +1391,12 @@ export class WalletService {
|
|
|
1035
1391
|
type: tx.type,
|
|
1036
1392
|
amount: tx.amount,
|
|
1037
1393
|
currency: tx.currency,
|
|
1394
|
+
network: tx.network,
|
|
1038
1395
|
status: tx.status,
|
|
1039
1396
|
createdAt: tx.createdAt.toISOString(),
|
|
1397
|
+
reviewedBy: includeInternal ? tx.reviewedBy : null,
|
|
1398
|
+
reviewedAt: includeInternal ? (tx.reviewedAt?.toISOString() ?? null) : null,
|
|
1399
|
+
reviewReason: includeInternal ? tx.reviewReason : null,
|
|
1040
1400
|
})),
|
|
1041
1401
|
total: Number(n),
|
|
1042
1402
|
page,
|
|
@@ -1060,7 +1420,7 @@ export class WalletService {
|
|
|
1060
1420
|
network: network ?? null,
|
|
1061
1421
|
address: issued.address,
|
|
1062
1422
|
tag: issued.tag ?? null,
|
|
1063
|
-
providerName:
|
|
1423
|
+
providerName: await this.providerNameFor(currency, network ?? null),
|
|
1064
1424
|
})
|
|
1065
1425
|
.onConflictDoNothing()
|
|
1066
1426
|
.returning();
|
|
@@ -1103,6 +1463,9 @@ export class WalletService {
|
|
|
1103
1463
|
currency: event.currency,
|
|
1104
1464
|
status: 'completed',
|
|
1105
1465
|
rail: this.resolveRail(event.currency),
|
|
1466
|
+
// Prefer the chain the vendor reported; fall back to the one the address was
|
|
1467
|
+
// issued on, which is the only network an address-only webhook can imply.
|
|
1468
|
+
network: event.network ?? depositAddress.network,
|
|
1106
1469
|
providerName: depositAddress.providerName,
|
|
1107
1470
|
providerRefId: event.externalId,
|
|
1108
1471
|
destinationAddress: event.address,
|