334 lines
9.1 KiB
TypeScript
334 lines
9.1 KiB
TypeScript
import * as crypto from 'crypto';
|
||
import { bech32 } from 'bech32';
|
||
|
||
// =============================================================================
|
||
// 链配置
|
||
// =============================================================================
|
||
|
||
export interface ChainConfig {
|
||
name: string;
|
||
prefix: string;
|
||
coinType: number;
|
||
curve: 'secp256k1' | 'ed25519';
|
||
derivationPath: string;
|
||
}
|
||
|
||
export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
|
||
kava: {
|
||
name: 'Kava',
|
||
prefix: 'kava',
|
||
coinType: 459,
|
||
curve: 'secp256k1',
|
||
derivationPath: "m/44'/459'/0'/0/0",
|
||
},
|
||
cosmos: {
|
||
name: 'Cosmos Hub',
|
||
prefix: 'cosmos',
|
||
coinType: 118,
|
||
curve: 'secp256k1',
|
||
derivationPath: "m/44'/118'/0'/0/0",
|
||
},
|
||
osmosis: {
|
||
name: 'Osmosis',
|
||
prefix: 'osmo',
|
||
coinType: 118,
|
||
curve: 'secp256k1',
|
||
derivationPath: "m/44'/118'/0'/0/0",
|
||
},
|
||
ethereum: {
|
||
name: 'Ethereum',
|
||
prefix: '0x',
|
||
coinType: 60,
|
||
curve: 'secp256k1',
|
||
derivationPath: "m/44'/60'/0'/0/0",
|
||
},
|
||
};
|
||
|
||
// =============================================================================
|
||
// 地址派生工具
|
||
// =============================================================================
|
||
|
||
/**
|
||
* 从公钥派生 Bech32 地址 (Cosmos 系列)
|
||
*
|
||
* 流程:
|
||
* 1. 公钥 → SHA256 → RIPEMD160 → 20字节地址
|
||
* 2. 20字节地址 → Bech32 编码
|
||
*/
|
||
export function deriveCosmosAddress(publicKeyHex: string, prefix: string): string {
|
||
// 移除可能的 0x 前缀
|
||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||
|
||
// 对于 secp256k1,需要压缩公钥 (33 bytes)
|
||
// 如果是未压缩公钥 (65 bytes),需要先压缩
|
||
let compressedKey: Buffer = publicKeyBytes;
|
||
if (publicKeyBytes.length === 65) {
|
||
compressedKey = compressSecp256k1PublicKey(publicKeyBytes);
|
||
} else if (publicKeyBytes.length === 64) {
|
||
// 没有前缀的未压缩公钥
|
||
const uncompressed = Buffer.concat([Buffer.from([0x04]), publicKeyBytes]);
|
||
compressedKey = compressSecp256k1PublicKey(uncompressed);
|
||
}
|
||
|
||
// SHA256 → RIPEMD160
|
||
const sha256Hash = crypto.createHash('sha256').update(compressedKey).digest();
|
||
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
|
||
|
||
// Bech32 编码
|
||
const words = bech32.toWords(ripemd160Hash);
|
||
const address = bech32.encode(prefix, words);
|
||
|
||
return address;
|
||
}
|
||
|
||
/**
|
||
* 从公钥派生以太坊地址
|
||
*
|
||
* 流程:
|
||
* 1. 未压缩公钥 (去掉 04 前缀) → Keccak256 → 取后 20 字节
|
||
*/
|
||
export function deriveEthereumAddress(publicKeyHex: string): string {
|
||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||
|
||
// 需要未压缩公钥的 x, y 坐标 (64 bytes)
|
||
let uncompressedKey: Buffer;
|
||
if (publicKeyBytes.length === 33) {
|
||
// 压缩公钥,需要解压
|
||
uncompressedKey = decompressSecp256k1PublicKey(publicKeyBytes);
|
||
} else if (publicKeyBytes.length === 65) {
|
||
// 未压缩公钥,去掉 04 前缀
|
||
uncompressedKey = publicKeyBytes.slice(1) as Buffer;
|
||
} else if (publicKeyBytes.length === 64) {
|
||
uncompressedKey = publicKeyBytes;
|
||
} else {
|
||
throw new Error(`Invalid public key length: ${publicKeyBytes.length}`);
|
||
}
|
||
|
||
// Keccak256 (使用 keccak256 而不是 sha3-256)
|
||
const { keccak_256 } = require('@noble/hashes/sha3');
|
||
const hash = keccak_256(uncompressedKey);
|
||
|
||
// 取后 20 字节
|
||
const addressBytes = hash.slice(-20);
|
||
const address = '0x' + Buffer.from(addressBytes).toString('hex');
|
||
|
||
return checksumAddress(address);
|
||
}
|
||
|
||
/**
|
||
* 从 Ed25519 公钥派生地址 (用于某些链)
|
||
*/
|
||
export function deriveEd25519Address(publicKeyHex: string, prefix: string): string {
|
||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||
|
||
// SHA256 → RIPEMD160
|
||
const sha256Hash = crypto.createHash('sha256').update(publicKeyBytes).digest();
|
||
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
|
||
|
||
// Bech32 编码
|
||
const words = bech32.toWords(ripemd160Hash);
|
||
const address = bech32.encode(prefix, words);
|
||
|
||
return address;
|
||
}
|
||
|
||
/**
|
||
* 压缩 secp256k1 公钥
|
||
*/
|
||
function compressSecp256k1PublicKey(uncompressed: Buffer): Buffer {
|
||
if (uncompressed.length !== 65 || uncompressed[0] !== 0x04) {
|
||
throw new Error('Invalid uncompressed public key');
|
||
}
|
||
|
||
const x = uncompressed.slice(1, 33);
|
||
const y = uncompressed.slice(33, 65);
|
||
|
||
// 判断 y 是奇数还是偶数
|
||
const prefix = y[31] % 2 === 0 ? 0x02 : 0x03;
|
||
|
||
return Buffer.concat([Buffer.from([prefix]), x]);
|
||
}
|
||
|
||
/**
|
||
* 解压缩 secp256k1 公钥
|
||
* 使用椭圆曲线数学: y² = x³ + 7 (mod p)
|
||
*/
|
||
function decompressSecp256k1PublicKey(compressed: Buffer): Buffer {
|
||
if (compressed.length !== 33) {
|
||
throw new Error('Invalid compressed public key');
|
||
}
|
||
|
||
// secp256k1 曲线参数
|
||
const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
|
||
|
||
const prefix = compressed[0];
|
||
const x = BigInt('0x' + compressed.slice(1).toString('hex'));
|
||
|
||
// 计算 y² = x³ + 7 (mod p)
|
||
const xCubed = modPow(x, 3n, p);
|
||
const ySquared = (xCubed + 7n) % p;
|
||
|
||
// 计算平方根 (p ≡ 3 mod 4, 所以 y = ySquared^((p+1)/4) mod p)
|
||
let y = modPow(ySquared, (p + 1n) / 4n, p);
|
||
|
||
// 根据前缀选择正确的 y 值
|
||
const isYOdd = y % 2n === 1n;
|
||
const shouldBeOdd = prefix === 0x03;
|
||
|
||
if (isYOdd !== shouldBeOdd) {
|
||
y = p - y;
|
||
}
|
||
|
||
// 转换为 Buffer (64 bytes: x || y)
|
||
const xBuffer = Buffer.from(x.toString(16).padStart(64, '0'), 'hex');
|
||
const yBuffer = Buffer.from(y.toString(16).padStart(64, '0'), 'hex');
|
||
|
||
return Buffer.concat([xBuffer, yBuffer]);
|
||
}
|
||
|
||
/**
|
||
* 模幂运算
|
||
*/
|
||
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
|
||
let result = 1n;
|
||
base = base % modulus;
|
||
|
||
while (exponent > 0n) {
|
||
if (exponent % 2n === 1n) {
|
||
result = (result * base) % modulus;
|
||
}
|
||
exponent = exponent / 2n;
|
||
base = (base * base) % modulus;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* EIP-55 校验和地址
|
||
*/
|
||
function checksumAddress(address: string): string {
|
||
const { keccak_256 } = require('@noble/hashes/sha3');
|
||
const addr = address.toLowerCase().replace('0x', '');
|
||
const hash = Buffer.from(keccak_256(Buffer.from(addr, 'utf8'))).toString('hex');
|
||
|
||
let result = '0x';
|
||
for (let i = 0; i < addr.length; i++) {
|
||
if (parseInt(hash[i], 16) >= 8) {
|
||
result += addr[i].toUpperCase();
|
||
} else {
|
||
result += addr[i];
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// =============================================================================
|
||
// 地址派生服务
|
||
// =============================================================================
|
||
|
||
export interface DerivedAddress {
|
||
chain: string;
|
||
chainName: string;
|
||
prefix: string;
|
||
address: string;
|
||
derivationPath: string;
|
||
publicKeyHex: string;
|
||
}
|
||
|
||
/**
|
||
* 地址派生服务
|
||
*
|
||
* 注意:TSS keygen 生成的是聚合公钥,不是派生公钥。
|
||
* 对于需要不同链的地址,我们直接使用聚合公钥派生地址,
|
||
* 而不是像 HD 钱包那样从种子派生。
|
||
*
|
||
* 这意味着:
|
||
* - 所有链使用相同的公钥
|
||
* - 不同链的地址只是编码方式不同
|
||
* - 这是 TSS 钱包的标准做法
|
||
*/
|
||
export class AddressDerivationService {
|
||
/**
|
||
* 从 TSS 聚合公钥派生指定链的地址
|
||
*/
|
||
deriveAddress(publicKeyHex: string, chain: string): DerivedAddress {
|
||
const config = CHAIN_CONFIGS[chain];
|
||
if (!config) {
|
||
throw new Error(`Unsupported chain: ${chain}`);
|
||
}
|
||
|
||
let address: string;
|
||
|
||
if (chain === 'ethereum') {
|
||
address = deriveEthereumAddress(publicKeyHex);
|
||
} else if (config.curve === 'ed25519') {
|
||
address = deriveEd25519Address(publicKeyHex, config.prefix);
|
||
} else {
|
||
// Cosmos 系列 (kava, cosmos, osmosis 等)
|
||
address = deriveCosmosAddress(publicKeyHex, config.prefix);
|
||
}
|
||
|
||
return {
|
||
chain,
|
||
chainName: config.name,
|
||
prefix: config.prefix,
|
||
address,
|
||
derivationPath: config.derivationPath,
|
||
publicKeyHex,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 派生所有支持的链地址
|
||
*/
|
||
deriveAllAddresses(publicKeyHex: string): DerivedAddress[] {
|
||
const addresses: DerivedAddress[] = [];
|
||
|
||
for (const chain of Object.keys(CHAIN_CONFIGS)) {
|
||
try {
|
||
const derived = this.deriveAddress(publicKeyHex, chain);
|
||
addresses.push(derived);
|
||
} catch (err) {
|
||
console.error(`Failed to derive ${chain} address:`, err);
|
||
}
|
||
}
|
||
|
||
return addresses;
|
||
}
|
||
|
||
/**
|
||
* 验证地址格式
|
||
*/
|
||
validateAddress(address: string, chain: string): boolean {
|
||
const config = CHAIN_CONFIGS[chain];
|
||
if (!config) {
|
||
return false;
|
||
}
|
||
|
||
if (chain === 'ethereum') {
|
||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||
}
|
||
|
||
try {
|
||
const decoded = bech32.decode(address);
|
||
return decoded.prefix === config.prefix;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取支持的链列表
|
||
*/
|
||
getSupportedChains(): ChainConfig[] {
|
||
return Object.values(CHAIN_CONFIGS);
|
||
}
|
||
}
|
||
|
||
// 导出单例
|
||
export const addressDerivationService = new AddressDerivationService();
|