189 lines
5.3 KiB
TypeScript
189 lines
5.3 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { keccak256, getBytes, sha256, ripemd160 } from 'ethers';
|
|
import { bech32 } from 'bech32';
|
|
import { EvmAddress } from '@/domain/value-objects';
|
|
import { ChainTypeEnum } from '@/domain/enums';
|
|
|
|
export interface DerivedAddress {
|
|
chainType: ChainTypeEnum;
|
|
address: string;
|
|
}
|
|
|
|
/**
|
|
* 地址派生适配器
|
|
* 从 MPC 公钥派生多链钱包地址
|
|
*/
|
|
@Injectable()
|
|
export class AddressDerivationAdapter {
|
|
private readonly logger = new Logger(AddressDerivationAdapter.name);
|
|
|
|
/**
|
|
* 从压缩公钥派生 EVM 地址
|
|
*
|
|
* @param compressedPublicKey 压缩格式的公钥 (33 bytes, 0x02/0x03 开头)
|
|
* @returns EVM 地址
|
|
*/
|
|
deriveEvmAddress(compressedPublicKey: string): string {
|
|
// 移除 0x 前缀
|
|
const pubKeyHex = compressedPublicKey.replace('0x', '');
|
|
|
|
// 验证压缩公钥格式
|
|
if (pubKeyHex.length !== 66) {
|
|
throw new Error(`Invalid compressed public key length: ${pubKeyHex.length}, expected 66`);
|
|
}
|
|
|
|
const prefix = pubKeyHex.slice(0, 2);
|
|
if (prefix !== '02' && prefix !== '03') {
|
|
throw new Error(`Invalid compressed public key prefix: ${prefix}, expected 02 or 03`);
|
|
}
|
|
|
|
// 解压缩公钥
|
|
const uncompressedPubKey = this.decompressPublicKey(pubKeyHex);
|
|
|
|
// 移除 04 前缀(非压缩公钥标识)
|
|
const pubKeyWithoutPrefix = uncompressedPubKey.slice(2);
|
|
|
|
// Keccak256 哈希
|
|
const hash = keccak256(getBytes('0x' + pubKeyWithoutPrefix));
|
|
|
|
// 取最后 20 bytes 作为地址
|
|
const address = '0x' + hash.slice(-40);
|
|
|
|
return address;
|
|
}
|
|
|
|
/**
|
|
* 解压缩公钥
|
|
*
|
|
* 使用 secp256k1 曲线解压缩
|
|
*/
|
|
private decompressPublicKey(compressedPubKeyHex: string): string {
|
|
const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
|
|
|
|
const prefix = parseInt(compressedPubKeyHex.slice(0, 2), 16);
|
|
const x = BigInt('0x' + compressedPubKeyHex.slice(2));
|
|
|
|
// y² = x³ + 7 (mod p)
|
|
const ySquared = (x ** 3n + 7n) % p;
|
|
|
|
// 计算模平方根
|
|
let y = this.modPow(ySquared, (p + 1n) / 4n, p);
|
|
|
|
// 根据前缀选择 y 的奇偶性
|
|
const isYOdd = y % 2n === 1n;
|
|
const shouldBeOdd = prefix === 0x03;
|
|
|
|
if (isYOdd !== shouldBeOdd) {
|
|
y = p - y;
|
|
}
|
|
|
|
// 返回非压缩格式 (04 + x + y)
|
|
const xHex = x.toString(16).padStart(64, '0');
|
|
const yHex = y.toString(16).padStart(64, '0');
|
|
|
|
return '04' + xHex + yHex;
|
|
}
|
|
|
|
/**
|
|
* 模幂运算
|
|
*/
|
|
private modPow(base: bigint, exp: bigint, mod: bigint): bigint {
|
|
let result = 1n;
|
|
base = base % mod;
|
|
while (exp > 0n) {
|
|
if (exp % 2n === 1n) {
|
|
result = (result * base) % mod;
|
|
}
|
|
exp = exp / 2n;
|
|
base = (base * base) % mod;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 从压缩公钥派生 Cosmos 地址 (bech32 格式)
|
|
*
|
|
* @param compressedPublicKey 压缩格式的公钥 (33 bytes, 0x02/0x03 开头)
|
|
* @param prefix bech32 地址前缀 (如 'kava', 'dst')
|
|
* @returns bech32 格式的地址
|
|
*/
|
|
deriveCosmosAddress(compressedPublicKey: string, prefix: string): string {
|
|
// 移除 0x 前缀
|
|
const pubKeyHex = compressedPublicKey.replace('0x', '');
|
|
|
|
// 验证压缩公钥格式
|
|
if (pubKeyHex.length !== 66) {
|
|
throw new Error(`Invalid compressed public key length: ${pubKeyHex.length}, expected 66`);
|
|
}
|
|
|
|
// SHA256 哈希
|
|
const pubKeyBytes = getBytes('0x' + pubKeyHex);
|
|
const sha256Hash = sha256(pubKeyBytes);
|
|
|
|
// RIPEMD160 哈希 (得到 20 bytes)
|
|
const ripemd160Hash = ripemd160(sha256Hash);
|
|
|
|
// 转换为 5-bit words 用于 bech32 编码
|
|
const hashBytes = getBytes(ripemd160Hash);
|
|
const words = bech32.toWords(Buffer.from(hashBytes));
|
|
|
|
// bech32 编码
|
|
const address = bech32.encode(prefix, words);
|
|
|
|
this.logger.debug(`Derived Cosmos address with prefix '${prefix}': ${address}`);
|
|
|
|
return address;
|
|
}
|
|
|
|
/**
|
|
* 从公钥派生所有支持链的地址
|
|
*/
|
|
deriveAllAddresses(compressedPublicKey: string): DerivedAddress[] {
|
|
const addresses: DerivedAddress[] = [];
|
|
|
|
this.logger.log(`[DERIVE] Starting address derivation for public key: ${compressedPublicKey.slice(0, 20)}...`);
|
|
|
|
// EVM 链共用同一个地址
|
|
const evmAddress = this.deriveEvmAddress(compressedPublicKey);
|
|
this.logger.log(`[DERIVE] EVM address derived: ${evmAddress}`);
|
|
|
|
// KAVA (EVM 格式 - 0x...) - Kava EVM 兼容链
|
|
addresses.push({
|
|
chainType: ChainTypeEnum.KAVA,
|
|
address: evmAddress,
|
|
});
|
|
this.logger.log(`[DERIVE] KAVA address (EVM): ${evmAddress}`);
|
|
|
|
// DST (Cosmos bech32 格式 - dst1...)
|
|
const dstAddress = this.deriveCosmosAddress(compressedPublicKey, 'dst');
|
|
addresses.push({
|
|
chainType: ChainTypeEnum.DST,
|
|
address: dstAddress,
|
|
});
|
|
this.logger.log(`[DERIVE] DST address (Cosmos): ${dstAddress}`);
|
|
|
|
// BSC (EVM 格式 - 0x...)
|
|
addresses.push({
|
|
chainType: ChainTypeEnum.BSC,
|
|
address: evmAddress,
|
|
});
|
|
this.logger.log(`[DERIVE] BSC address (EVM): ${evmAddress}`);
|
|
|
|
this.logger.log(`[DERIVE] Successfully derived ${addresses.length} addresses from public key`);
|
|
|
|
return addresses;
|
|
}
|
|
|
|
/**
|
|
* 验证地址格式
|
|
*/
|
|
validateEvmAddress(address: string): boolean {
|
|
try {
|
|
EvmAddress.create(address);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|