security: remove .env from tracking (contains secrets)
This commit is contained in:
121
apps/api/src/services/wallet-generator.service.ts
Normal file
121
apps/api/src/services/wallet-generator.service.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Wallet generation: BIP39 mnemonic + multi-chain address derivation.
|
||||
* Server-side для custodial-флоу.
|
||||
*
|
||||
* Поддерживаемые chains (BIP44):
|
||||
* ETH/BSC — m/44'/60'/0'/0/0 (secp256k1, EIP-55 checksum)
|
||||
* BTC — m/84'/0'/0'/0/0 (P2WPKH bech32)
|
||||
* TRX — m/44'/195'/0'/0/0 (secp256k1, base58check + prefix 0x41)
|
||||
* SOL — m/44'/501'/0'/0' (ed25519)
|
||||
*/
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
import { createHash } from 'crypto';
|
||||
import bs58 from 'bs58';
|
||||
import * as bip39 from 'bip39';
|
||||
import { BIP32Factory } from 'bip32';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import { Keypair } from '@solana/web3.js';
|
||||
import { derivePath } from 'ed25519-hd-key';
|
||||
import type { ChainCode } from '../lib/address-validators';
|
||||
|
||||
const bip32 = BIP32Factory(ecc);
|
||||
|
||||
export const DERIVATION_PATHS: Record<ChainCode, string> = {
|
||||
ETH: "m/44'/60'/0'/0/0",
|
||||
BSC: "m/44'/60'/0'/0/0",
|
||||
BTC: "m/84'/0'/0'/0/0",
|
||||
TRX: "m/44'/195'/0'/0/0",
|
||||
SOL: "m/44'/501'/0'/0'",
|
||||
};
|
||||
|
||||
export const ALL_CHAINS: ChainCode[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL'];
|
||||
|
||||
export interface DerivedWallet {
|
||||
chain: ChainCode;
|
||||
address: string;
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сгенерить 12-словную BIP39 мнемонику.
|
||||
*/
|
||||
export function generateMnemonic(): string {
|
||||
return bip39.generateMnemonic(128);
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация существующей mnemonic (не используется в текущем флоу — оставлено на будущее).
|
||||
*/
|
||||
export function validateMnemonic(m: string): boolean {
|
||||
return bip39.validateMnemonic(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Деривить адреса для всех chains из одной mnemonic.
|
||||
*/
|
||||
export async function deriveAllAddresses(mnemonic: string): Promise<DerivedWallet[]> {
|
||||
if (!bip39.validateMnemonic(mnemonic)) {
|
||||
throw new Error('Invalid mnemonic');
|
||||
}
|
||||
|
||||
const seed = await bip39.mnemonicToSeed(mnemonic);
|
||||
const seedHex = seed.toString('hex');
|
||||
|
||||
// ETH (BSC использует тот же адрес)
|
||||
const ethWallet = ethers.Wallet.fromMnemonic(mnemonic, DERIVATION_PATHS.ETH);
|
||||
const ethAddress = ethers.utils.getAddress(ethWallet.address); // EIP-55 checksum
|
||||
|
||||
// BTC (P2WPKH bech32)
|
||||
const btcRoot = bip32.fromSeed(seed);
|
||||
const btcChild = btcRoot.derivePath(DERIVATION_PATHS.BTC);
|
||||
if (!btcChild.publicKey) {
|
||||
throw new Error('BTC derivation failed: no public key');
|
||||
}
|
||||
const btcPayment = bitcoin.payments.p2wpkh({
|
||||
pubkey: Buffer.from(btcChild.publicKey),
|
||||
network: bitcoin.networks.bitcoin,
|
||||
});
|
||||
if (!btcPayment.address) {
|
||||
throw new Error('BTC derivation failed: no address');
|
||||
}
|
||||
|
||||
// TRX (derive privkey same curve as ETH, convert pubkey → TRX base58check address)
|
||||
const trxWallet = ethers.Wallet.fromMnemonic(mnemonic, DERIVATION_PATHS.TRX);
|
||||
const trxAddress = ethAddressToTron(trxWallet.address);
|
||||
|
||||
// SOL (ed25519 derivation)
|
||||
const { key: solKey } = derivePath(DERIVATION_PATHS.SOL, seedHex);
|
||||
if (!solKey || solKey.length !== 32) {
|
||||
throw new Error('SOL derivation produced invalid seed length');
|
||||
}
|
||||
const solKeypair = Keypair.fromSeed(solKey);
|
||||
const solAddress = solKeypair.publicKey.toBase58();
|
||||
|
||||
return [
|
||||
{ chain: 'ETH', address: ethAddress, derivationPath: DERIVATION_PATHS.ETH },
|
||||
{ chain: 'BSC', address: ethAddress, derivationPath: DERIVATION_PATHS.BSC },
|
||||
{ chain: 'BTC', address: btcPayment.address, derivationPath: DERIVATION_PATHS.BTC },
|
||||
{ chain: 'TRX', address: trxAddress, derivationPath: DERIVATION_PATHS.TRX },
|
||||
{ chain: 'SOL', address: solAddress, derivationPath: DERIVATION_PATHS.SOL },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* ETH-style address (0x...) → TRX base58check (T...).
|
||||
* TRX и ETH используют одну curve и одну keccak256-логику для получения 20-байтного хеша.
|
||||
* Различие только в префиксе (0x41 vs ничего) и в кодировке (base58check vs hex).
|
||||
*/
|
||||
export function ethAddressToTron(ethAddr: string): string {
|
||||
const hex = ethAddr.toLowerCase().replace(/^0x/, '');
|
||||
if (hex.length !== 40) {
|
||||
throw new Error('ethAddressToTron: invalid input length');
|
||||
}
|
||||
const bytes = Buffer.from(hex, 'hex');
|
||||
const prefixed = Buffer.concat([Buffer.from([0x41]), bytes]); // 21 байт
|
||||
const h1 = createHash('sha256').update(prefixed).digest();
|
||||
const h2 = createHash('sha256').update(h1).digest();
|
||||
const checksum = h2.subarray(0, 4);
|
||||
return bs58.encode(new Uint8Array(Buffer.concat([prefixed, checksum])));
|
||||
}
|
||||
Reference in New Issue
Block a user