From a269e4d14b429884323404179f75bafb86eb7224 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 30 Dec 2025 04:47:49 -0800 Subject: [PATCH] =?UTF-8?q?feat(service-party-app):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=85=AC=E9=92=A5=E6=B4=BE=E7=94=9FEVM?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E5=B9=B6=E6=98=BE=E7=A4=BAKAVA=E4=BD=99?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现secp256k1公钥解压缩算法 (y² = x³ + 7 mod p) - 支持从TSS keygen的33字节压缩公钥派生EVM地址 - 在钱包主页显示KAVA代币余额 (通过Kava Testnet RPC获取) - 添加余额加载状态和样式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/pages/Home.module.css | 31 ++++++ .../service-party-app/src/pages/Home.tsx | 71 ++++++++++++- .../service-party-app/src/utils/address.ts | 100 +++++++++++++++++- 3 files changed, 198 insertions(+), 4 deletions(-) diff --git a/backend/mpc-system/services/service-party-app/src/pages/Home.module.css b/backend/mpc-system/services/service-party-app/src/pages/Home.module.css index afd0a793..8f1f352e 100644 --- a/backend/mpc-system/services/service-party-app/src/pages/Home.module.css +++ b/backend/mpc-system/services/service-party-app/src/pages/Home.module.css @@ -373,3 +373,34 @@ text-align: center; text-decoration: none; } + +/* Balance Section */ +.balanceSection { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: var(--radius-md); +} + +.balanceLabel { + font-size: 12px; + color: rgba(255, 255, 255, 0.85); + font-weight: 500; +} + +.balanceValue { + font-size: 18px; + font-weight: 700; + color: white; + font-family: monospace; +} + +.balanceLoading { + font-size: 14px; + font-weight: 400; + color: rgba(255, 255, 255, 0.7); + font-style: italic; +} diff --git a/backend/mpc-system/services/service-party-app/src/pages/Home.tsx b/backend/mpc-system/services/service-party-app/src/pages/Home.tsx index d660be06..8dc98ed1 100644 --- a/backend/mpc-system/services/service-party-app/src/pages/Home.tsx +++ b/backend/mpc-system/services/service-party-app/src/pages/Home.tsx @@ -16,6 +16,44 @@ interface ShareItem { interface ShareWithAddress extends ShareItem { evmAddress?: string; + kavaBalance?: string; + balanceLoading?: boolean; +} + +// Kava Testnet EVM RPC endpoint +const KAVA_TESTNET_RPC = 'https://evm.testnet.kava.io'; + +/** + * 获取 KAVA 代币余额 + * @param address EVM 地址 + * @returns 余额字符串 (格式化后的 KAVA 数量) + */ +async function fetchKavaBalance(address: string): Promise { + try { + const response = await fetch(KAVA_TESTNET_RPC, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_getBalance', + params: [address, 'latest'], + id: 1, + }), + }); + + const data = await response.json(); + if (data.result) { + // 将 wei 转换为 KAVA (18 位小数) + const balanceWei = BigInt(data.result); + const balanceKava = Number(balanceWei) / 1e18; + // 格式化显示,最多 6 位小数 + return balanceKava.toFixed(balanceKava < 0.000001 && balanceKava > 0 ? 8 : 6).replace(/\.?0+$/, '') || '0'; + } + return '0'; + } catch (error) { + console.error('Failed to fetch KAVA balance:', error); + return '0'; + } } export default function Home() { @@ -30,7 +68,7 @@ export default function Home() { for (const share of shareList) { try { const evmAddress = await deriveEvmAddress(share.publicKey); - sharesWithAddresses.push({ ...share, evmAddress }); + sharesWithAddresses.push({ ...share, evmAddress, balanceLoading: true }); } catch (error) { console.warn(`Failed to derive address for share ${share.id}:`, error); sharesWithAddresses.push({ ...share }); @@ -39,6 +77,20 @@ export default function Home() { return sharesWithAddresses; }, []); + // 单独获取所有钱包的余额 + const fetchAllBalances = useCallback(async (sharesWithAddrs: ShareWithAddress[]) => { + const updatedShares = await Promise.all( + sharesWithAddrs.map(async (share) => { + if (share.evmAddress) { + const kavaBalance = await fetchKavaBalance(share.evmAddress); + return { ...share, kavaBalance, balanceLoading: false }; + } + return { ...share, balanceLoading: false }; + }) + ); + setShares(updatedShares); + }, []); + useEffect(() => { loadShares(); }, []); @@ -64,6 +116,9 @@ export default function Home() { // 为每个 share 派生 EVM 地址 const sharesWithAddresses = await deriveAddresses(shareList); setShares(sharesWithAddresses); + + // 异步获取所有余额 (不阻塞 UI) + fetchAllBalances(sharesWithAddresses); } catch (error) { console.error('Failed to load shares:', error); } finally { @@ -215,6 +270,20 @@ export default function Home() { )} + {/* KAVA 余额显示 */} + {share.evmAddress && ( +
+ KAVA 余额 + + {share.balanceLoading ? ( + 加载中... + ) : ( + <>{share.kavaBalance || '0'} KAVA + )} + +
+ )} +
参与方 diff --git a/backend/mpc-system/services/service-party-app/src/utils/address.ts b/backend/mpc-system/services/service-party-app/src/utils/address.ts index e9e0f2bf..3dc1f52c 100644 --- a/backend/mpc-system/services/service-party-app/src/utils/address.ts +++ b/backend/mpc-system/services/service-party-app/src/utils/address.ts @@ -3,6 +3,101 @@ * 从 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 */ @@ -155,9 +250,8 @@ export async function deriveEvmAddress(publicKey: string): Promise { // 无前缀的未压缩格式 uncompressedPubKey = pubKeyBytes; } else if (pubKeyBytes.length === 33 && (pubKeyBytes[0] === 0x02 || pubKeyBytes[0] === 0x03)) { - // 压缩格式,需要解压 - 这里简化处理,实际需要椭圆曲线计算 - // 在真实场景中应该使用 elliptic 或 secp256k1 库 - throw new Error('压缩公钥暂不支持,请提供未压缩公钥'); + // 压缩格式,解压为 64 字节的 x,y 坐标 + uncompressedPubKey = decompressPublicKey(pubKeyBytes); } else { throw new Error(`无效的公钥格式: length=${pubKeyBytes.length}`); }