技术栈:node+PostgreSQL,实现钱包地址预生成及地址池的完整思路及实战
技术栈:node + PostgreSQL
一套固定算法 + 一个主助记词 = 无限预生成地址算法永远不变 → 结果永远不变 → 可离线提前算。
Keygen 机器 = 负责预生成地址
数据库 = 地址池
钱包服务 = 分配地址
定时任务 cron = 监控库存,少了自动补
mkdir address-pool && cd address-pool
npm init -y
npm install ethers pg dotenv cron
地址池
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);
.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
};
FOR UPDATE 行锁?cron?如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码