rwadurian/backend/mpc-system/services/service-party-app/src/utils/address.ts

305 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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}`;
}