feat: update

This commit is contained in:
2026-05-14 01:16:26 +03:00
parent 47eec7117b
commit c1a6c15773
2 changed files with 145 additions and 118 deletions

View File

@@ -1,3 +1,4 @@
import {AsyncLocalStorage} from 'node:async_hooks';
import pg from 'pg'; import pg from 'pg';
import type {DatabaseSecret} from '../secrets/VaultClient.js'; import type {DatabaseSecret} from '../secrets/VaultClient.js';
@@ -25,6 +26,8 @@ export interface PaymentClaimResult {
export class PostgresClient { export class PostgresClient {
private readonly pool: pg.Pool; private readonly pool: pg.Pool;
private readonly queryClient = new AsyncLocalStorage<pg.PoolClient>();
constructor(secret: DatabaseSecret) { constructor(secret: DatabaseSecret) {
this.pool = new Pool({ this.pool = new Pool({
host: secret.host, host: secret.host,
@@ -38,12 +41,34 @@ export class PostgresClient {
}); });
} }
async withOrderProcessingLock<T>(orderId: string, fn: () => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query('SELECT pg_advisory_lock(hashtext($1::text))', [orderId]);
return await this.queryClient.run(client, fn);
} finally {
await client.query('SELECT pg_advisory_unlock(hashtext($1::text))', [orderId]);
client.release();
}
}
private async query<T extends pg.QueryResultRow>(text: string, params?: unknown[]): Promise<pg.QueryResult<T>> {
const pinned = this.queryClient.getStore();
if (pinned) {
return pinned.query<T>(text, params);
}
return this.pool.query<T>(text, params);
}
async ping(): Promise<void> { async ping(): Promise<void> {
await this.pool.query('SELECT 1'); await this.query('SELECT 1');
} }
async getOrderUsdtAmount(orderId: string): Promise<string | null> { async getOrderUsdtAmount(orderId: string): Promise<string | null> {
const result = await this.pool.query<{ usdt_amount: string }>( const result = await this.query<{ usdt_amount: string }>(
'SELECT usdt_amount::text AS usdt_amount FROM orders WHERE id = $1', 'SELECT usdt_amount::text AS usdt_amount FROM orders WHERE id = $1',
[orderId] [orderId]
); );
@@ -51,7 +76,7 @@ export class PostgresClient {
} }
async getUserEthWalletAddress(userId: string): Promise<string | null> { async getUserEthWalletAddress(userId: string): Promise<string | null> {
const result = await this.pool.query<{ address: string }>( const result = await this.query<{ address: string }>(
`SELECT address FROM wallets WHERE user_id = $1 AND chain = 'ETH' LIMIT 1`, `SELECT address FROM wallets WHERE user_id = $1 AND chain = 'ETH' LIMIT 1`,
[userId] [userId]
); );
@@ -59,7 +84,7 @@ export class PostgresClient {
} }
async getPaymentByOrderId(orderId: string): Promise<PaymentRecord | null> { async getPaymentByOrderId(orderId: string): Promise<PaymentRecord | null> {
const result = await this.pool.query<PaymentRecord>( const result = await this.query<PaymentRecord>(
'SELECT status, web3_transaction_hash, updated_at FROM payments WHERE order_id = $1', 'SELECT status, web3_transaction_hash, updated_at FROM payments WHERE order_id = $1',
[orderId] [orderId]
); );
@@ -67,13 +92,13 @@ export class PostgresClient {
} }
async claimPaymentForTransfer(orderId: string): Promise<PaymentClaimResult> { async claimPaymentForTransfer(orderId: string): Promise<PaymentClaimResult> {
const claimed = await this.pool.query<PaymentRecord>( const claimed = await this.query<PaymentRecord>(
`UPDATE payments `UPDATE payments
SET status = 'web3_processing', SET status = 'web3_processing',
updated_at = now() updated_at = now()
WHERE order_id = $1 WHERE order_id = $1
AND web3_transaction_hash IS NULL AND web3_transaction_hash IS NULL
AND status = 'money_accepted' AND status IN ('money_accepted', 'web3_processing')
RETURNING status, web3_transaction_hash, updated_at`, RETURNING status, web3_transaction_hash, updated_at`,
[orderId] [orderId]
); );
@@ -92,7 +117,7 @@ export class PostgresClient {
} }
async releaseStaleWeb3Processing(orderId: string, staleMs: number): Promise<boolean> { async releaseStaleWeb3Processing(orderId: string, staleMs: number): Promise<boolean> {
const result = await this.pool.query<{ order_id: string }>( const result = await this.query<{ order_id: string }>(
`UPDATE payments `UPDATE payments
SET status = 'money_accepted', SET status = 'money_accepted',
updated_at = now() updated_at = now()
@@ -107,7 +132,7 @@ export class PostgresClient {
} }
async setPaymentTxHash(orderId: string, txHash: string): Promise<boolean> { async setPaymentTxHash(orderId: string, txHash: string): Promise<boolean> {
const result = await this.pool.query( const result = await this.query(
`UPDATE payments `UPDATE payments
SET web3_transaction_hash = $2, SET web3_transaction_hash = $2,
updated_at = now() updated_at = now()
@@ -119,7 +144,7 @@ export class PostgresClient {
} }
async markPaymentDelivered(orderId: string, txHash: string): Promise<void> { async markPaymentDelivered(orderId: string, txHash: string): Promise<void> {
await this.pool.query( await this.query(
`UPDATE payments `UPDATE payments
SET status = 'usdt_delivered', SET status = 'usdt_delivered',
web3_transaction_hash = $2, web3_transaction_hash = $2,
@@ -132,7 +157,7 @@ export class PostgresClient {
} }
async markPaymentHashError(orderId: string): Promise<void> { async markPaymentHashError(orderId: string): Promise<void> {
await this.pool.query( await this.query(
`UPDATE payments `UPDATE payments
SET status = 'web3_hash_error', SET status = 'web3_hash_error',
updated_at = now() updated_at = now()
@@ -143,7 +168,7 @@ export class PostgresClient {
} }
async markPaymentBalanceProblem(orderId: string, txHash: string): Promise<void> { async markPaymentBalanceProblem(orderId: string, txHash: string): Promise<void> {
await this.pool.query( await this.query(
`UPDATE payments `UPDATE payments
SET status = 'web3_balance_problem', SET status = 'web3_balance_problem',
web3_transaction_hash = $2, web3_transaction_hash = $2,

View File

@@ -29,6 +29,7 @@ export class TransferOrchestrator {
} }
async handle(message: CryptoTransferRequest, log: Logger): Promise<void> { async handle(message: CryptoTransferRequest, log: Logger): Promise<void> {
await this.db.withOrderProcessingLock(message.order_id, async () => {
let claim = await this.db.claimPaymentForTransfer(message.order_id); let claim = await this.db.claimPaymentForTransfer(message.order_id);
let existing = claim.payment; let existing = claim.payment;
if (!existing) { if (!existing) {
@@ -149,6 +150,7 @@ export class TransferOrchestrator {
await this.db.markPaymentDelivered(message.order_id, txHash); await this.db.markPaymentDelivered(message.order_id, txHash);
log.info({ event: 'transfer.delivered', tx_hash: txHash }, 'marked usdt_delivered'); log.info({ event: 'transfer.delivered', tx_hash: txHash }, 'marked usdt_delivered');
await this.publishCompleted(message, log); await this.publishCompleted(message, log);
});
} }
private async assertHotWalletReady(amountUnits: bigint): Promise<void> { private async assertHotWalletReady(amountUnits: bigint): Promise<void> {