企业级钱包实战:地址预生成+地址池实现

技术栈:node+PostgreSQL,实现钱包地址预生成及地址池的完整思路及实战

技术栈:node + PostgreSQL

核心思路

一套固定算法 + 一个主助记词 = 无限预生成地址算法永远不变 → 结果永远不变 → 可离线提前算。

  • Keygen 机器 = 负责预生成地址

  • 数据库 = 地址池

  • 钱包服务 = 分配地址

  • 定时任务 cron = 监控库存,少了自动补

一、环境搭建

1.1 安装依赖

mkdir address-pool && cd address-pool 
npm init -y 
npm install ethers pg dotenv cron

1.2 建表

地址池

CREATE TABLE address_pool (
    id SERIAL PRIMARY KEY, 
    address VARCHAR(42) NOT NULL UNIQUE, 
    public_key TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE', -- AVAILABLE / ASSIGNED / RECYCLED 
    user_id INT NULL, 
    created_at TIMESTAMP DEFAULT NOW(), 
    assigned_at TIMESTAMP NULL 
); 
CREATE INDEX idx_address_pool_status ON address_pool(status);

1.3 环境配置

.env

DB_HOST=localhost 
DB_PORT=5432 
DB_USER=postgres 
DB_PASS=你的数据库密码 
DB_NAME=你的数据库名 
# 每次预生成的地址数量(这里先设1000测试) 
BATCH_SIZE=1000 
# 库存预警线(这里设100测试) 
THRESHOLD=100 # 助记词(离线保管,线上只存地址) 
MNEMONIC="你的 12/24 位助记词"

二、核心代码实现

require('dotenv').config();
const { ethers } = require('ethers');
const { Pool } = require('pg');
const cron = require('cron');
// --------------------------
// 1. 初始化数据库连接
// --------------------------
const db = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
});
// --------------------------
// 2. 初始化 HD 钱包(BIP44 标准,企业级推荐)
// --------------------------
// 助记词 → 根钱包 → 批量派生地址
const mnemonic = process.env.MNEMONIC;
if (!mnemonic) throw new Error("MNEMONIC 未配置!请在 .env 中设置");
const hdWallet = ethers.HDNodeWallet.fromPhrase(mnemonic);
console.log("✅ HD 钱包初始化完成,根地址:", hdWallet.address);
// --------------------------
// 3. 批量地址预生成(对应图里的「批量地址生成任务」)
// --------------------------
/**
● 批量生成 BIP44 派生地址
● @param {number} count 要生成的数量
● @param {number} startIndex 起始索引(避免重复)
● @returns {Array} 地址列表
 */
async function batchGenerateAddresses(count, startIndex = 0) {
  const addresses = [];
  for (let i = 0; i < count; i++) {
 // BIP44 以太坊标准路径:m/44'/60'/0'/0/index
 const index = startIndex + i;
 const child = hdWallet.derivePath(`m/44'/60'/0'/0/${index}`); 
 addresses.push({
   address: child.address.toLowerCase(), // 统一小写存库,避免大小写问题
   publicKey: child.publicKey,
   index: index,
 });
  }
  return addresses;
}
/**
● 把生成的地址批量存入数据库
● @param {Array} addresses 
 */
async function saveAddressesToDB(addresses) {
  const client = await db.connect();
  try {
 await client.query('BEGIN'); // 开启事务,防止部分失败
 for (const addr of addresses) {
   await client.query(
 INSERT INTO address_pool (address, public_key, status)
  VALUES ($1, $2, 'AVAILABLE')
  ON CONFLICT (address) DO NOTHING,
 [addr.address, addr.publicKey]
   );
 }
 await client.query('COMMIT');
 console.log(✅ 成功存入 ${addresses.length} 个地址);
  } catch (err) {
 await client.query('ROLLBACK');
 console.error("❌ 存入数据库失败:", err);
 throw err;
  } finally {
 client.release();
  }
}
// --------------------------
// 4. 给用户分配地址(对应图里的「业务层请求地址」)
// --------------------------
/**
● 为用户分配一个可用地址
● @param {number} userId 用户ID
● @returns {string} 分配的地址
 */
async function assignAddressToUser(userId) {
  const client = await db.connect();
  try {
 await client.query('BEGIN');
 // 取一个未分配的地址(FOR UPDATE 行锁,防止并发抢同一个地址)
 const { rows } = await client.query(
   SELECT id, address FROM address_pool 
WHERE status = 'AVAILABLE' 
LIMIT 1 FOR UPDATE
 ); if (rows.length === 0) {
   throw new Error("地址池为空!请先预生成地址");
 } const addr = rows[0];
 // 更新状态为已分配
 await client.query(
   UPDATE address_pool 
SET status = 'ASSIGNED', user_id = $1, assigned_at = NOW() 
WHERE id = $2,
   [userId, addr.id]
 ); await client.query('COMMIT');
 console.log(✅ 给用户 ${userId} 分配地址:${addr.address});
 return addr.address;
  } catch (err) {
    await client.query('ROLLBACK');
    console.error("❌ 分配地址失败:", err);
    throw err;
  } finally {
    client.release();
  }
}
// --------------------------
// 5. 库存监控 + 自动补货
// --------------------------
/**
● 查询当前可用地址数量
● @returns {number} 可用地址数
 */
async function getAvailableAddressCount() {
  const { rows } = await db.query(
 SELECT COUNT(*) as count FROM address_pool WHERE status = 'AVAILABLE'
  );
  return parseInt(rows[0].count);
}
/**
● 自动生成一批地址补充库存
 */
async function replenishAddressPool() {
  const availableCount = await getAvailableAddressCount();
  const threshold = parseInt(process.env.THRESHOLD);
  const batchSize = parseInt(process.env.BATCH_SIZE);
  console.log(📊 当前可用地址:${availableCount},预警线:${threshold});
  if (availableCount < threshold) {
    console.log("⚠️ 库存不足,开始自动补货...");
    // 先查当前最大的索引,避免重复生成
    const { rows } = await db.query(
      SELECT MAX(CAST(SUBSTRING(public_key FROM 'm/44''/60''/0''/0/[0-9]+') AS INTEGER)) as max_index 
       FROM address_pool
    );
    const startIndex = rows[0].max_index ? rows[0].max_index + 1 : 0;
// 生成新一批地址
const newAddresses = await batchGenerateAddresses(batchSize, startIndex);
await saveAddressesToDB(newAddresses);
console.log(`✅ 补货完成,新增 ${batchSize} 个地址`);
  } else {
    console.log("✅ 库存充足,无需补货");
  }
}
// --------------------------
// 6. 定时任务(每分钟检查一次库存)
// --------------------------
function startCronJob() {
  const job = new cron.CronJob('* * * * *', async () => {
    console.log("\n🕒 执行库存检查定时任务...");
    await replenishAddressPool();
  });
  job.start();
  console.log("✅ 定时任务已启动,每分钟检查一次地址库存");
}
// --------------------------
// 7. 测试入口
// --------------------------
async function main() {
  // 1. 首次运行,先生成一批初始地址
  const initialAddresses = await batchGenerateAddresses(1000, 0);
  await saveAddressesToDB(initialAddresses);
  // 2. 启动定时补货任务
  startCronJob();
  // 3. 模拟给用户分配地址
  const testUserId = 1001;
  const addr = await assignAddressToUser(testUserId);
  console.log(\n🧪 测试分配结果:用户 ${testUserId} → ${addr});
}
// 启动程序
main().catch(err => {
  console.error("❌ 程序出错:", err);
  process.exit(1);
});
module.exports = {
  assignAddressToUser,
  getAvailableAddressCount,
  replenishAddressPool
};

三、细节说明

1. 为什么用 BIP44 助记词派生,而不是随机私钥?

  • 私钥不存数据库,只存助记词(离线冷备份),数据库只存地址和公钥
  • 一套助记词可以派生百万级地址,不会出现私钥分散、管理混乱的问题
  • 提现时通过索引号反向派生私钥签名,不需要把所有私钥都存在线上

2. 并发分配地址为什么用 FOR UPDATE 行锁?

  • 防止高并发注册时,多个请求抢到同一个地址
  • 事务 + 行锁保证了 “取地址→更新状态” 的原子性,不会出现重复分配

3. 定时任务补货为什么用 cron

  • 每分钟检查一次库存,低于预警线自动补货,不会出现用户注册时地址池空了的情况
  • 补货过程不影响用户分配地址,业务无感知
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mengbuluo222
mengbuluo222
0x9ff1...faa5
前端开发求职中... 8年+开发经验,拥有丰富的开发经验,擅长VUE、React开发。