305 lines
8.6 KiB
TypeScript
305 lines
8.6 KiB
TypeScript
/**
|
||
* Kava EVM 地址派生工具
|
||
* 从 ECDSA 公钥派生 EVM 兼容地址
|
||
*/
|
||
|
||
// secp256k1 曲线参数
|
||
const SECP256K1_P = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
|
||
const SECP256K1_B = 7n;
|
||
|
||
/**
|
||
* 将 Uint8Array 转换为 BigInt
|
||
*/
|
||
function bytesToBigInt(bytes: Uint8Array): bigint {
|
||
let result = 0n;
|
||
for (let i = 0; i < bytes.length; i++) {
|
||
result = (result << 8n) | BigInt(bytes[i]);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 将 BigInt 转换为 32 字节的 Uint8Array
|
||
*/
|
||
function bigIntToBytes32(n: bigint): Uint8Array {
|
||
const bytes = new Uint8Array(32);
|
||
let temp = n;
|
||
for (let i = 31; i >= 0; i--) {
|
||
bytes[i] = Number(temp & 0xffn);
|
||
temp = temp >> 8n;
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
/**
|
||
* 模幂运算 (快速幂算法)
|
||
*/
|
||
function modPow(base: bigint, exp: bigint, mod: bigint): bigint {
|
||
let result = 1n;
|
||
base = ((base % mod) + mod) % mod;
|
||
while (exp > 0n) {
|
||
if (exp & 1n) {
|
||
result = (result * base) % mod;
|
||
}
|
||
exp = exp >> 1n;
|
||
base = (base * base) % mod;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 计算 secp256k1 曲线上给定 x 坐标的 y 坐标
|
||
* y² = x³ + 7 (mod p)
|
||
*/
|
||
function decompressY(x: bigint, isOdd: boolean): bigint {
|
||
// 计算 y² = x³ + 7 (mod p)
|
||
const x3 = modPow(x, 3n, SECP256K1_P);
|
||
const ySquared = (x3 + SECP256K1_B) % SECP256K1_P;
|
||
|
||
// 计算平方根: y = ySquared^((p+1)/4) mod p
|
||
// 这是因为 p ≡ 3 (mod 4) 对于 secp256k1
|
||
const exp = (SECP256K1_P + 1n) / 4n;
|
||
let y = modPow(ySquared, exp, SECP256K1_P);
|
||
|
||
// 根据奇偶性选择正确的 y 值
|
||
const yIsOdd = (y & 1n) === 1n;
|
||
if (yIsOdd !== isOdd) {
|
||
y = SECP256K1_P - y;
|
||
}
|
||
|
||
return y;
|
||
}
|
||
|
||
/**
|
||
* 解压缩 secp256k1 公钥
|
||
* 输入: 33 字节压缩公钥 (02/03 前缀 + 32 字节 x 坐标)
|
||
* 输出: 64 字节未压缩公钥 (32 字节 x + 32 字节 y,无前缀)
|
||
*/
|
||
function decompressPublicKey(compressedPubKey: Uint8Array): Uint8Array {
|
||
if (compressedPubKey.length !== 33) {
|
||
throw new Error('压缩公钥必须是 33 字节');
|
||
}
|
||
|
||
const prefix = compressedPubKey[0];
|
||
if (prefix !== 0x02 && prefix !== 0x03) {
|
||
throw new Error('无效的压缩公钥前缀');
|
||
}
|
||
|
||
const isOdd = prefix === 0x03;
|
||
const xBytes = compressedPubKey.slice(1);
|
||
const x = bytesToBigInt(xBytes);
|
||
const y = decompressY(x, isOdd);
|
||
|
||
// 组合 x 和 y 坐标
|
||
const result = new Uint8Array(64);
|
||
result.set(xBytes, 0);
|
||
result.set(bigIntToBytes32(y), 32);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 将十六进制字符串转换为 Uint8Array
|
||
*/
|
||
function hexToBytes(hex: string): Uint8Array {
|
||
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
||
const bytes = new Uint8Array(cleanHex.length / 2);
|
||
for (let i = 0; i < cleanHex.length; i += 2) {
|
||
bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
/**
|
||
* 将 Uint8Array 转换为十六进制字符串
|
||
*/
|
||
function bytesToHex(bytes: Uint8Array): string {
|
||
return Array.from(bytes)
|
||
.map((b) => b.toString(16).padStart(2, '0'))
|
||
.join('');
|
||
}
|
||
|
||
/**
|
||
* Keccak-256 哈希实现 (简化版)
|
||
* 用于从公钥派生 EVM 地址
|
||
*/
|
||
async function keccak256(data: Uint8Array): Promise<Uint8Array> {
|
||
// 使用 SubtleCrypto 的 SHA-256 作为备选
|
||
// 注意: 真实场景需要使用 keccak-256,这里使用简化实现
|
||
// 在生产环境中应该使用 js-sha3 或 ethers.js
|
||
|
||
// 简单的 Keccak-256 实现常量
|
||
const RC = [
|
||
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an,
|
||
0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n,
|
||
0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an,
|
||
0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
||
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n,
|
||
0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n,
|
||
0x000000000000800an, 0x800000008000000an, 0x8000000080008081n,
|
||
0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
||
];
|
||
|
||
const ROTC = [
|
||
[0, 36, 3, 41, 18],
|
||
[1, 44, 10, 45, 2],
|
||
[62, 6, 43, 15, 61],
|
||
[28, 55, 25, 21, 56],
|
||
[27, 20, 39, 8, 14],
|
||
];
|
||
|
||
function rotl64(x: bigint, n: number): bigint {
|
||
return ((x << BigInt(n)) | (x >> BigInt(64 - n))) & 0xffffffffffffffffn;
|
||
}
|
||
|
||
function keccakF(state: bigint[][]): void {
|
||
for (let round = 0; round < 24; round++) {
|
||
// Theta
|
||
const C: bigint[] = [];
|
||
for (let x = 0; x < 5; x++) {
|
||
C[x] = state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4];
|
||
}
|
||
const D: bigint[] = [];
|
||
for (let x = 0; x < 5; x++) {
|
||
D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1);
|
||
}
|
||
for (let x = 0; x < 5; x++) {
|
||
for (let y = 0; y < 5; y++) {
|
||
state[x][y] ^= D[x];
|
||
}
|
||
}
|
||
|
||
// Rho and Pi
|
||
const B: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n));
|
||
for (let x = 0; x < 5; x++) {
|
||
for (let y = 0; y < 5; y++) {
|
||
B[y][(2 * x + 3 * y) % 5] = rotl64(state[x][y], ROTC[x][y]);
|
||
}
|
||
}
|
||
|
||
// Chi
|
||
for (let x = 0; x < 5; x++) {
|
||
for (let y = 0; y < 5; y++) {
|
||
state[x][y] = B[x][y] ^ (~B[(x + 1) % 5][y] & B[(x + 2) % 5][y]);
|
||
}
|
||
}
|
||
|
||
// Iota
|
||
state[0][0] ^= RC[round];
|
||
}
|
||
}
|
||
|
||
// Keccak-256: rate = 1088 bits = 136 bytes, capacity = 512 bits
|
||
const rate = 136;
|
||
const outputLen = 32;
|
||
|
||
// Initialize state
|
||
const state: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n));
|
||
|
||
// Pad message
|
||
const padded = new Uint8Array(Math.ceil((data.length + 1) / rate) * rate);
|
||
padded.set(data);
|
||
padded[data.length] = 0x01;
|
||
padded[padded.length - 1] |= 0x80;
|
||
|
||
// Absorb
|
||
for (let i = 0; i < padded.length; i += rate) {
|
||
for (let j = 0; j < rate && i + j < padded.length; j += 8) {
|
||
const x = Math.floor(j / 8) % 5;
|
||
const y = Math.floor(Math.floor(j / 8) / 5);
|
||
let lane = 0n;
|
||
for (let k = 0; k < 8 && i + j + k < padded.length; k++) {
|
||
lane |= BigInt(padded[i + j + k]) << BigInt(k * 8);
|
||
}
|
||
state[x][y] ^= lane;
|
||
}
|
||
keccakF(state);
|
||
}
|
||
|
||
// Squeeze
|
||
const output = new Uint8Array(outputLen);
|
||
for (let i = 0; i < outputLen; i += 8) {
|
||
const x = Math.floor(i / 8) % 5;
|
||
const y = Math.floor(Math.floor(i / 8) / 5);
|
||
const lane = state[x][y];
|
||
for (let k = 0; k < 8 && i + k < outputLen; k++) {
|
||
output[i + k] = Number((lane >> BigInt(k * 8)) & 0xffn);
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/**
|
||
* 从 ECDSA 压缩/未压缩公钥派生 EVM 地址
|
||
*
|
||
* @param publicKey - 公钥的十六进制字符串 (压缩或未压缩格式)
|
||
* @returns EVM 地址 (带 0x 前缀)
|
||
*/
|
||
export async function deriveEvmAddress(publicKey: string): Promise<string> {
|
||
const pubKeyBytes = hexToBytes(publicKey);
|
||
|
||
// EVM 地址派生使用未压缩公钥的 x,y 坐标 (去掉 04 前缀)
|
||
// 如果是压缩公钥 (33 bytes, 02/03 前缀), 需要先解压
|
||
let uncompressedPubKey: Uint8Array;
|
||
|
||
if (pubKeyBytes.length === 65 && pubKeyBytes[0] === 0x04) {
|
||
// 已经是未压缩格式,去掉 04 前缀
|
||
uncompressedPubKey = pubKeyBytes.slice(1);
|
||
} else if (pubKeyBytes.length === 64) {
|
||
// 无前缀的未压缩格式
|
||
uncompressedPubKey = pubKeyBytes;
|
||
} else if (pubKeyBytes.length === 33 && (pubKeyBytes[0] === 0x02 || pubKeyBytes[0] === 0x03)) {
|
||
// 压缩格式,解压为 64 字节的 x,y 坐标
|
||
uncompressedPubKey = decompressPublicKey(pubKeyBytes);
|
||
} else {
|
||
throw new Error(`无效的公钥格式: length=${pubKeyBytes.length}`);
|
||
}
|
||
|
||
// 对 64 字节的公钥数据进行 Keccak-256 哈希
|
||
const hash = await keccak256(uncompressedPubKey);
|
||
|
||
// 取最后 20 字节作为地址
|
||
const addressBytes = hash.slice(-20);
|
||
|
||
return '0x' + bytesToHex(addressBytes);
|
||
}
|
||
|
||
/**
|
||
* 验证 EVM 地址格式
|
||
*
|
||
* @param address - 要验证的地址
|
||
* @returns 是否是有效的 EVM 地址
|
||
*/
|
||
export function isValidEvmAddress(address: string): boolean {
|
||
if (!address) return false;
|
||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||
}
|
||
|
||
/**
|
||
* 格式化地址显示 (缩短)
|
||
*
|
||
* @param address - 完整地址
|
||
* @param prefixLen - 前缀长度 (默认 6)
|
||
* @param suffixLen - 后缀长度 (默认 4)
|
||
* @returns 缩短的地址
|
||
*/
|
||
export function formatAddress(address: string, prefixLen = 6, suffixLen = 4): string {
|
||
if (!address || address.length < prefixLen + suffixLen + 2) return address;
|
||
return `${address.slice(0, prefixLen + 2)}...${address.slice(-suffixLen)}`;
|
||
}
|
||
|
||
/**
|
||
* 生成 Kava 区块浏览器 URL
|
||
*
|
||
* @param address - EVM 地址
|
||
* @param isTestnet - 是否是测试网
|
||
* @returns 区块浏览器 URL
|
||
*/
|
||
export function getKavaExplorerUrl(address: string, isTestnet = true): string {
|
||
const baseUrl = isTestnet
|
||
? 'https://testnet.kavascan.com'
|
||
: 'https://kavascan.com';
|
||
return `${baseUrl}/address/${address}`;
|
||
}
|