Розробка системи генерації унікальних адрес для платежів
Одна адреса для всіх платежів з коментарем — працює, доки користувачі не забувають вказувати memo. Але коли 5% платежів приходять без ідентифікатора та потребують ручного розбору — це операційна проблема. Unique address per payment вирішує її повністю: кожне замовлення отримує свою адресу, будь-який вхідний платіж однозначно ідентифікується.
HD Wallet деривація: математика за кулісами
Основа системи — ієрархічні детерміновані гаманці (BIP-32/BIP-44). З одного master seed можна детерміновано вивести скільки завгодно дочірніх ключів — адреси відтворювані, приватний ключ будь-якої дочірної адреси восстанавлюється з seed в будь-який момент.
Master Seed (128/256 біт)
↓ BIP-39
Master Mnemonic (12/24 слова)
↓ BIP-32 HMAC-SHA512
Master Extended Key (xprv)
↓ BIP-44 деривація
m / purpose' / coin_type' / account' / change / index
Для Ethereum (coin_type = 60):
m/44'/60'/0'/0/0 → перша адреса
m/44'/60'/0'/0/1 → друга адреса
m/44'/60'/0'/0/N → N+1-ша адреса
Важливий момент: деривація address-only не потребує приватного ключа. Extended public key (xpub) достатньо для генерації адрес. Це дозволяє розмежувати компоненти: сервер генерації адрес зберігає тільки xpub (компрометація = витік адрес, не коштів), сервер підписання транзакцій зберігає xprv в ізольованому середовищі (HSM, offline).
Реалізація
import { HDNodeWallet, Mnemonic } from 'ethers';
class PaymentAddressGenerator {
private hdNode: HDNodeWallet;
private currentIndex: number;
constructor(xpub: string, startIndex: number = 0) {
// З xpub — тільки публічна деривація
this.hdNode = HDNodeWallet.fromExtendedKey(xpub);
this.currentIndex = startIndex;
}
generateAddress(orderId: string): { address: string; index: number } {
// Використовуємо атомарний лічильник з БД, не локальне стан
const index = this.currentIndex++;
const childNode = this.hdNode.deriveChild(index);
return {
address: childNode.address.toLowerCase(),
index,
};
}
}
// Атомарна генерація: отримуємо наступний index з PostgreSQL sequence
async function getNextIndex(db: Pool): Promise<number> {
const result = await db.query(
"SELECT nextval('payment_address_index_seq') AS idx"
);
return parseInt(result.rows[0].idx);
}
Чому не просто інкремент у коді? При горизонтальному масштабуванні кілька інстансів сервісу можуть одночасно отримати один і той же index. PostgreSQL sequence атомарний — nextval завжди повертає унікальне значення.
База даних: схема
CREATE SEQUENCE payment_address_index_seq START 1;
CREATE TABLE payment_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
address VARCHAR(42) NOT NULL UNIQUE,
derivation_index INTEGER NOT NULL UNIQUE,
order_id UUID NOT NULL REFERENCES orders(id),
network VARCHAR(20) NOT NULL, -- 'ethereum', 'bsc', 'polygon'...
currency VARCHAR(20) NOT NULL, -- 'ETH', 'USDT', 'USDC'...
expected_amount NUMERIC(36, 18),
received_amount NUMERIC(36, 18) DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
expires_at TIMESTAMPTZ NOT NULL,
confirmed_tx VARCHAR(66), -- tx hash
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_payment_addresses_address ON payment_addresses(address);
CREATE INDEX idx_payment_addresses_status ON payment_addresses(status)
WHERE status = 'pending';
Індекс по address — критичний для O(1) lookup при отриманні транзакції від blockchain-листенера.
Моніторинг: відстежуємо тільки активні адреси
Наївний підхід — підписатися на все згенеровані адреси. У великій системі це тисячі адрес. Краще: тримати in-memory set активних (pending) адрес, який оновлюється при створенні/закритті платежів.
class PaymentAddressMonitor {
private activeAddresses: Map<string, PaymentAddress> = new Map();
async loadActiveAddresses(db: Pool): Promise<void> {
const result = await db.query(
`SELECT address, order_id, expected_amount, currency, expires_at
FROM payment_addresses
WHERE status = 'pending' AND expires_at > NOW()`
);
for (const row of result.rows) {
this.activeAddresses.set(row.address.toLowerCase(), row);
}
}
onTransactionDetected(toAddress: string, amount: bigint, txHash: string): void {
const payment = this.activeAddresses.get(toAddress.toLowerCase());
if (!payment) return; // Не наша адреса
// Перевірка суми з толерантністю 1%
const tolerance = payment.expectedAmount * 99n / 100n;
if (amount >= tolerance) {
this.confirmPayment(payment, txHash);
}
}
}
Multi-network: один index, різні адреси
EVM-сумісні мережі використовують один і той же приватний ключ — адреса одинакова на Ethereum, BNB Chain, Polygon, Arbitrum. Зручно: один index у таблиці можна використовувати для прийняття платежів у різних мережах на один адрес, але моніторинг потрібен окремий для кожної мережи.
Для non-EVM (TON, Solana, Bitcoin) — окремі HD деривації з різними master seed або різними шляхами деривації.
Sweeping: консолідація коштів
Кошти на дочірніх адресах потрібно періодично збирати на основну адресу (холодний гаманець або multisig). Sweep повинен відбуватися після підтвердження платежу:
async function sweepAddress(index: number): Promise<void> {
// Деривація приватного ключа — тільки на signing сервері
const privateKey = masterHdNode.deriveChild(index).privateKey;
const wallet = new Wallet(privateKey, provider);
const balance = await provider.getBalance(wallet.address);
const gasEstimate = 21000n;
const gasPrice = await provider.getFeeData().then(d => d.gasPrice!);
const gasCost = gasEstimate * gasPrice;
if (balance <= gasCost) return; // Нічого не свипати
await wallet.sendTransaction({
to: HOT_WALLET_ADDRESS,
value: balance - gasCost,
gasLimit: gasEstimate,
});
}
Для ERC-20 токенів sweep складніший: спочатку переконатися, що на адресі є ETH для gas, відправити його з основної адреси, потім забрати токени. Або використовувати паттерн з Permit/EIP-2612 — тоді газ платить основна адреса.
Безпека
- Master seed — у HSM або AWS KMS. Ніколи в змінних середовища.
- xpub для генерації адрес — окремо від xprv для підписання.
- Signing сервіс — ізольований мікросервіс з мінімальними привілеями.
- Аудит всіх операцій sweep — кожна транзакція логується з обґрунтуванням.
- Gap limit — BIP-44 рекомендує не дивитися глибше за 20 послідовних невикористаних адрес при восстановленні гаманця.







