diff --git a/package.json b/package.json index e0344d7..db41df4 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "verify:balances": "node scripts/verify-balances.mjs" }, "keywords": [ "coti", diff --git a/scripts/verify-balances.mjs b/scripts/verify-balances.mjs new file mode 100644 index 0000000..afbb3af --- /dev/null +++ b/scripts/verify-balances.mjs @@ -0,0 +1,123 @@ +/** + * Verifies on-chain balance reads match expected values for known test wallets. + * Run: npm run verify:balances + */ +import { ethers } from 'ethers'; +import { decryptUint } from '@coti-io/coti-sdk-typescript'; + +const NESTED_ABI = [ + 'function balanceOf(address account) view returns (tuple(tuple(uint256 high, uint256 low) high, tuple(uint256 high, uint256 low) low))', +]; +const FLAT_ABI = [ + 'function balanceOf(address) view returns (tuple(uint256 ciphertextHigh, uint256 ciphertextLow))', +]; +const ERC20_ABI = ['function balanceOf(address) view returns (uint256)']; + +const EXPECTED = [ + { + label: 'Sepolia', + chainId: 11155111, + rpc: 'https://ethereum-sepolia-rpc.publicnode.com', + wallet: '0xb64381b3EE1161c1fE7858Bb600fa65D9Da1f3fc', + aesKey: 'e41f8141802d93c6079c03daa0041a63', + nativeSymbol: 'ETH', + minNative: 0.01, + addresses: { + MTT: '0xd3f5c63f4D87D2235b295FbA83351d31d0eD1BeE', + 'p.MTT': '0x34727cc7233e6B20aE071Cd16A81027172b6bdbA', + 'p.ETH': '0x4667DFcbCd354c2719E129A9FcC2Bb3a98456b91', + }, + expectPrivate: { 'p.MTT': 100, 'p.ETH': 0 }, + }, + { + label: 'Fuji', + chainId: 43113, + rpc: 'https://api.avax-test.network/ext/bc/C/rpc', + wallet: '0xC93b05B38c2D3B57977335A9D3FD5Dcf6aa8E71a', + aesKey: '83e5bf3298bc803486ca5a01abec2298', + nativeSymbol: 'AVAX', + minNative: 0.01, + addresses: { + MTT: '0x328e70e1c52662cd5f19f824fcb8b463d77a6686', + 'p.MTT': '0x53a5A16f3BC408CB808B442fA69481386945f5cf', + 'p.AVAX': '0x69dF41ebdd5D5e0017c1965bd480843857158324', + }, + expectPrivate: { 'p.MTT': 500, 'p.AVAX': 0 }, + }, +]; + +function isZeroNested(r) { + const hh = r.high?.high ?? r[0]?.[0]; + const hl = r.high?.low ?? r[0]?.[1]; + const lh = r.low?.high ?? r[1]?.[0]; + const ll = r.low?.low ?? r[1]?.[1]; + return [hh, hl, lh, ll].every(v => v === 0n || v === undefined); +} + +function decryptNested(enc, aesKey) { + const d1 = decryptUint(enc.high.high, aesKey); + const d2 = decryptUint(enc.high.low, aesKey); + const d3 = decryptUint(enc.low.high, aesKey); + const d4 = decryptUint(enc.low.low, aesKey); + return (BigInt(d1) << 192n) + (BigInt(d2) << 128n) + (BigInt(d3) << 64n) + BigInt(d4); +} + +/** Mirrors usePrivateTokenBalance fetch path. */ +async function fetchPrivateBalance(provider, user, aesKey, contractAddress) { + const nested = new ethers.Contract(contractAddress, NESTED_ABI, provider); + const flat = new ethers.Contract(contractAddress, FLAT_ABI, provider); + try { + const enc = await nested.balanceOf(user); + if (enc?.high?.high !== undefined || enc?.[0]?.[0] !== undefined) { + if (isZeroNested(enc)) return 0; + return Number(ethers.formatUnits(decryptNested(enc, aesKey), 18)); + } + throw new Error('not nested'); + } catch { + const enc = await flat.balanceOf(user); + const high = enc.ciphertextHigh ?? enc[0] ?? 0n; + const low = enc.ciphertextLow ?? enc[1] ?? 0n; + if (high === 0n && low === 0n) return 0; + const d1 = decryptUint(high, aesKey); + const d2 = decryptUint(low, aesKey); + const val = (BigInt(d1) << 64n) + BigInt(d2); + return Number(ethers.formatUnits(val, 18)); + } +} + +let failed = false; + +for (const c of EXPECTED) { + console.log(`\n=== ${c.label} ===`); + const provider = new ethers.JsonRpcProvider(c.rpc, c.chainId); + const native = Number(ethers.formatEther(await provider.getBalance(c.wallet))); + console.log(`Public ${c.nativeSymbol}: ${native}`); + if (native < c.minNative) { + console.error(`FAIL: ${c.nativeSymbol} balance ${native} < ${c.minNative}`); + failed = true; + } + + const mtt = new ethers.Contract(c.addresses.MTT, ERC20_ABI, provider); + const mttBal = Number(ethers.formatUnits(await mtt.balanceOf(c.wallet), 18)); + console.log(`Public MTT: ${mttBal}`); + if (mttBal <= 0) { + console.error('FAIL: public MTT should be > 0'); + failed = true; + } + + for (const [sym, addr] of Object.entries(c.addresses).filter(([k]) => k.startsWith('p.'))) { + const bal = await fetchPrivateBalance(provider, c.wallet, c.aesKey, addr); + const expected = c.expectPrivate[sym]; + console.log(`${sym}: ${bal} (expected ${expected})`); + if (bal !== expected) { + console.error(`FAIL: ${sym} balance ${bal} !== expected ${expected}`); + failed = true; + } + } +} + +if (failed) { + console.error('\n❌ Balance verification FAILED'); + process.exit(1); +} +console.log('\n✅ Balance verification PASSED'); diff --git a/src/chains/avalancheFuji.ts b/src/chains/avalancheFuji.ts index b819633..c4c56a5 100644 --- a/src/chains/avalancheFuji.ts +++ b/src/chains/avalancheFuji.ts @@ -1,196 +1,5 @@ -import type { ChainConfig } from "./types"; +import { getConfiguredChain } from "./config"; export const AVALANCHE_FUJI_CHAIN_ID = 43113; -const AVALANCHE_FUJI_RPC_URL = - "https://api.avax-test.network/ext/bc/C/rpc"; - -const AVALANCHE_FUJI_INBOX = "0xB4A53FE02401fDFA8DAc00450dA3FfF8D01502F8"; - -/** Underlying ERC-20s from PrivacyPortalConfig.json (Avalanche Fuji). */ -const MTT = "0x328e70e1c52662cd5f19f824fcb8b463d77a6686"; -const USDC = "0x5425890298aed601595a70AB815c96711a31Bc65"; -const WAVAX = "0xd00ae08403B9bbb9124bB305C09058E32C39A48c"; - -/** Deployed PoD portal pairs from pod-mpc-lib deployConfig.json (Fuji). */ -const P_AVAX = "0x69dF41ebdd5D5e0017c1965bd480843857158324"; -const P_USDC = "0x0291d4DCE114161bfE692AB31A479AF533630f28"; -const PORTAL_AVAX = "0xdaa65aB142Fb148e210103649536FcD29Ed8025f"; -const PORTAL_USDC = "0xe191FdfbA64c99C489D9846f2C0cEa495eA35974"; - -export const avalancheFujiChain: ChainConfig = { - id: AVALANCHE_FUJI_CHAIN_ID, - hexId: "0xa869", - name: "Avalanche Fuji", - rpcUrl: AVALANCHE_FUJI_RPC_URL, - explorerBaseUrl: "https://testnet.snowtrace.io", - podInboxAddress: AVALANCHE_FUJI_INBOX, - unlockStrategy: "manual-aes-key", - portalStrategy: "pod-privacy-portal", - addresses: { - MTT, - USDC, - WAVAX, - "p.MTT": "0x53a5A16f3BC408CB808B442fA69481386945f5cf", - "p.USDC": P_USDC, - "p.AVAX": P_AVAX, - PrivacyPortalMTT: "0x248DF7c9f68c6d8aEFa88Ec218c53f0E6Da6dC81", - PrivacyPortalUSDC: PORTAL_USDC, - PrivacyPortalAVAX: PORTAL_AVAX, - }, - tokens: [ - { - symbol: "MTT", - name: "MyTestToken", - icon: "/icons/coti.svg", - decimals: 18, - isPrivate: false, - addressKey: "MTT", - bridgeAddressKey: "PrivacyPortalMTT", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - { - symbol: "p.MTT", - name: "Private MyTestToken", - icon: "/icons/coti.svg", - decimals: 18, - isPrivate: true, - addressKey: "p.MTT", - bridgeAddressKey: "PrivacyPortalMTT", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - { - symbol: "USDC", - name: "USD Coin", - icon: "/icons/USDC.svg", - decimals: 6, - isPrivate: false, - addressKey: "USDC", - bridgeAddressKey: "PrivacyPortalUSDC", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - { - symbol: "p.USDC", - name: "Private USDC", - icon: "/icons/USDC.svg", - decimals: 6, - isPrivate: true, - addressKey: "p.USDC", - bridgeAddressKey: "PrivacyPortalUSDC", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - { - symbol: "AVAX", - name: "Avalanche", - icon: "/icons/avalanche.svg", - decimals: 18, - isPrivate: false, - isNative: true, - addressKey: "WAVAX", - bridgeAddressKey: "PrivacyPortalAVAX", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - { - symbol: "p.AVAX", - name: "Private WAVAX", - icon: "/icons/avalanche.svg", - decimals: 18, - isPrivate: true, - addressKey: "p.AVAX", - bridgeAddressKey: "PrivacyPortalAVAX", - supportedChainIds: [AVALANCHE_FUJI_CHAIN_ID], - }, - ], - walletNetwork: { - chainId: "0xa869", - chainName: "Avalanche Fuji Testnet", - rpcUrls: [AVALANCHE_FUJI_RPC_URL], - nativeCurrency: { name: "Avalanche", symbol: "AVAX", decimals: 18 }, - blockExplorerUrls: ["https://testnet.snowtrace.io"], - }, - getBridgeDataOverride: addresses => [ - { - bridgeName: "MTT PoD Portal", - bridgeAddress: addresses.PrivacyPortalMTT, - publicToken: "MTT", - publicTokenIcon: "/icons/coti.svg", - privateToken: "p.MTT", - privateTokenIcon: "/icons/coti.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 18, - isLoading: false, - error: null, - }, - { - bridgeName: "USDC PoD Portal", - bridgeAddress: addresses.PrivacyPortalUSDC, - publicToken: "USDC", - publicTokenIcon: "/icons/USDC.svg", - privateToken: "p.USDC", - privateTokenIcon: "/icons/USDC.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 6, - isLoading: false, - error: null, - }, - { - bridgeName: "AVAX PoD Portal", - bridgeAddress: addresses.PrivacyPortalAVAX, - publicToken: "AVAX", - publicTokenIcon: "/icons/avalanche.svg", - privateToken: "p.AVAX", - privateTokenIcon: "/icons/avalanche.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 18, - isLoading: false, - error: null, - }, - ], - indexPage: { - showPodRequestTracker: true, - amountModalGasLabel: "Estimated Gas and PoD fee", - amountModalGasSymbol: "native", - }, -}; +export const avalancheFujiChain = getConfiguredChain(AVALANCHE_FUJI_CHAIN_ID); diff --git a/src/chains/config.json b/src/chains/config.json new file mode 100644 index 0000000..d1ec955 --- /dev/null +++ b/src/chains/config.json @@ -0,0 +1,633 @@ +{ + "chains": [ + { + "id": 11155111, + "hexId": "0xaa36a7", + "name": "Sepolia", + "rpcUrl": "https://ethereum-sepolia-rpc.publicnode.com", + "explorerBaseUrl": "https://sepolia.etherscan.io", + "unlockStrategy": "manual-aes-key", + "portalStrategy": "pod-privacy-portal", + "podInboxAddress": "0xAb625bE229F603f6BBF964474AFf6d5487e364De", + "addresses": { + "MTT": "0xd3f5c63f4D87D2235b295FbA83351d31d0eD1BeE", + "WETH": "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "USDC": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + "p.MTT": "0xeA8DaEbfFAA7AcC5121b27eBd1aeDb309A92D24d", + "p.USDC": "0x81bF0837b6A80e7432919Cde3AA9334b1345cDc4", + "p.ETH": "0x2AD91b05f7d4b1eF471Fe79343d45bCFDd6a209e", + "PrivacyPortalMTT": "0x9b0abd7CB33F7B851179AEa39Cc4Ab077d924FC7", + "PrivacyPortalUSDC": "0x87d26C89DA25Df89B1549EF03FC04816825F6830", + "PrivacyPortalETH": "0x83d7Eb9E7a7D27d3bEf6Df16F9553704983b689A" + }, + "tokens": [ + { + "symbol": "MTT", + "name": "MyTestToken", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "MTT", + "bridgeAddressKey": "PrivacyPortalMTT" + }, + { + "symbol": "p.MTT", + "name": "Private MyTestToken", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.MTT", + "bridgeAddressKey": "PrivacyPortalMTT" + }, + { + "symbol": "USDC", + "name": "USD Coin", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDC", + "bridgeAddressKey": "PrivacyPortalUSDC" + }, + { + "symbol": "p.USDC", + "name": "Private USDC", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDC", + "bridgeAddressKey": "PrivacyPortalUSDC" + }, + { + "symbol": "ETH", + "name": "Ether", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": false, + "isNative": true, + "addressKey": "WETH", + "bridgeAddressKey": "PrivacyPortalETH" + }, + { + "symbol": "p.ETH", + "name": "Private WETH", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.ETH", + "bridgeAddressKey": "PrivacyPortalETH" + } + ], + "walletNetwork": { + "chainId": "0xaa36a7", + "chainName": "Sepolia", + "rpcUrls": ["https://ethereum-sepolia-rpc.publicnode.com"], + "nativeCurrency": { "name": "Sepolia Ether", "symbol": "ETH", "decimals": 18 }, + "blockExplorerUrls": ["https://sepolia.etherscan.io"] + }, + "indexPage": { + "showPodRequestTracker": true, + "amountModalGasLabel": "Estimated Gas and PoD fee", + "amountModalGasSymbol": "native" + } + }, + { + "id": 43113, + "hexId": "0xa869", + "name": "Avalanche Fuji", + "rpcUrl": "https://api.avax-test.network/ext/bc/C/rpc", + "explorerBaseUrl": "https://testnet.snowtrace.io", + "unlockStrategy": "manual-aes-key", + "portalStrategy": "pod-privacy-portal", + "podInboxAddress": "0xAb625bE229F603f6BBF964474AFf6d5487e364De", + "addresses": { + "MTT": "0x328e70e1c52662cd5f19f824fcb8b463d77a6686", + "USDC": "0x5425890298aed601595a70AB815c96711a31Bc65", + "WAVAX": "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", + "p.MTT": "0x648C730B59efc6ebde56B112EaB995385d6Dd949", + "p.USDC": "0xe1Db680bd93B5f3c161CB85646FbCb57C3E7046a", + "p.AVAX": "0x04e9a58eFb8B8198b4c9F55ec4AaD87073FcF801", + "PrivacyPortalMTT": "0x93BbcAD50D3fA5Ae85ac92F091EC82998f33eB0B", + "PrivacyPortalUSDC": "0x1bB80107989Aec9f6c574AaAfAe39AdBEc144a70", + "PrivacyPortalAVAX": "0xbF1c1272edFcb686b395eAdBfcb26156Aea5cB7b" + }, + "tokens": [ + { + "symbol": "MTT", + "name": "MyTestToken", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "MTT", + "bridgeAddressKey": "PrivacyPortalMTT" + }, + { + "symbol": "p.MTT", + "name": "Private MyTestToken", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.MTT", + "bridgeAddressKey": "PrivacyPortalMTT" + }, + { + "symbol": "USDC", + "name": "USD Coin", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDC", + "bridgeAddressKey": "PrivacyPortalUSDC" + }, + { + "symbol": "p.USDC", + "name": "Private USDC", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDC", + "bridgeAddressKey": "PrivacyPortalUSDC" + }, + { + "symbol": "AVAX", + "name": "Avalanche", + "icon": "/icons/avalanche.svg", + "decimals": 18, + "isPrivate": false, + "isNative": true, + "addressKey": "WAVAX", + "bridgeAddressKey": "PrivacyPortalAVAX" + }, + { + "symbol": "p.AVAX", + "name": "Private WAVAX", + "icon": "/icons/avalanche.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.AVAX", + "bridgeAddressKey": "PrivacyPortalAVAX" + } + ], + "walletNetwork": { + "chainId": "0xa869", + "chainName": "Avalanche Fuji Testnet", + "rpcUrls": ["https://api.avax-test.network/ext/bc/C/rpc"], + "nativeCurrency": { "name": "Avalanche", "symbol": "AVAX", "decimals": 18 }, + "blockExplorerUrls": ["https://testnet.snowtrace.io"] + }, + "indexPage": { + "showPodRequestTracker": true, + "amountModalGasLabel": "Estimated Gas and PoD fee", + "amountModalGasSymbol": "native" + } + }, + { + "id": 7082400, + "hexId": "0x6c11a0", + "name": "COTI Testnet", + "rpcUrl": "https://testnet.coti.io/rpc", + "explorerBaseUrl": "https://testnet.cotiscan.io", + "unlockStrategy": "snap", + "portalStrategy": "coti-bridge", + "podInboxAddress": "0xAb625bE229F603f6BBF964474AFf6d5487e364De", + "addresses": { + "PrivateCoti": "0x6cE8907414986E73De9e7D28d62Ea2080F8E88E1", + "PrivacyBridgeCotiNative": "0x96117882328407B13A5f378de25764C4eD8477eA", + "CotiPriceConsumer": "0xD5EeD24e909AdE249b688671e32dcc013B236B74", + "WETH": "0x8bca4e6bbE402DB4aD189A316137aD08206154FB", + "WBTC": "0x5dBDb2E5D51c3FFab5D6B862Caa11FCe1D83F492", + "USDT": "0x9e961430053cd5AbB3b060544cEcCec848693Cf0", + "USDC_E": "0x63f3D2Cc8F5608F57ce6E5Aa3590A2Beb428D19C", + "WADA": "0xe3E2cd3Abf412c73a404b9b8227B71dE3CfE829D", + "gCOTI": "0x878a42D3cB737DEC9E6c7e7774d973F46fd8ed4C", + "MTT": "", + "PrivacyPortalMTT": "0xF5Da19E1c8FB39Fdd11bB029882fc69E86650beB", + "p.WETH": "0xF009BADb181d471995a1CFF406C3Db7B180F64eA", + "p.WBTC": "0xB50F1680a4C69145ABc09A2A71c8D5b8051578cF", + "p.USDT": "0xcEF137E96eDF68EE99D4CdEa7085f154d74895cD", + "p.USDC_E": "0x37f78dcCd15876F74391EF1F01b76557D9FF1dea", + "p.WADA": "0x1245f50a3E9129A219b4bf66D10fEaEA47467B69", + "p.gCOTI": "0x1503b02a4Aa27812306c65116FD23b733603F142", + "p.MTT": "0xb681D264a250715977450212d370b05d98761422", + "PrivacyBridgeWETH": "0x6f1628D7F1D5d5aDF39b924a28acb10B5C9df283", + "PrivacyBridgeWBTC": "0x9EBEbC8F5Cff021459a56A681719dF1C0A7435cc", + "PrivacyBridgeUSDT": "0x549634daBE8237EBda7473d7aA2bB17459826781", + "PrivacyBridgeUSDCe": "0xaa63D41890D576F93d79272988e212d8B46CdD26", + "PrivacyBridgeWADA": "0x4AFe67D4aD7AF0a70ACe6f79FbfAB66C72211c9F", + "PrivacyBridgegCOTI": "0xDB54B4B1B8F9614567bd673A17a5cD31A3DD6033" + }, + "tokens": [ + { + "symbol": "COTI", + "name": "COTI", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "bridgeAddressKey": "PrivacyBridgeCotiNative", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WETH", + "name": "Wrapped Ether", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "WETH", + "bridgeAddressKey": "PrivacyBridgeWETH", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WBTC", + "name": "Wrapped BTC", + "icon": "/icons/wBTC.svg", + "decimals": 8, + "isPrivate": false, + "addressKey": "WBTC", + "bridgeAddressKey": "PrivacyBridgeWBTC", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "USDT", + "name": "Tether USD", + "icon": "/icons/usdt.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDT", + "bridgeAddressKey": "PrivacyBridgeUSDT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "USDC.e", + "name": "Bridged USDC", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDC_E", + "bridgeAddressKey": "PrivacyBridgeUSDCe", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WADA", + "name": "Wrapped ADA", + "icon": "/icons/wADA.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "WADA", + "bridgeAddressKey": "PrivacyBridgeWADA", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "gCOTI", + "name": "gCOTI", + "icon": "/icons/gcoti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "gCOTI", + "bridgeAddressKey": "PrivacyBridgegCOTI", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "MTT", + "name": "MTT", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "MTT", + "bridgeAddressKey": "PrivacyPortalMTT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.COTI", + "name": "p.COTI", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "PrivateCoti", + "bridgeAddressKey": "PrivacyBridgeCotiNative", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WETH", + "name": "p.WETH", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.WETH", + "bridgeAddressKey": "PrivacyBridgeWETH", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WBTC", + "name": "p.WBTC", + "icon": "/icons/wBTC.svg", + "decimals": 8, + "isPrivate": true, + "addressKey": "p.WBTC", + "bridgeAddressKey": "PrivacyBridgeWBTC", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.USDT", + "name": "p.USDT", + "icon": "/icons/usdt.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDT", + "bridgeAddressKey": "PrivacyBridgeUSDT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.USDC.e", + "name": "p.USDC.e", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDC_E", + "bridgeAddressKey": "PrivacyBridgeUSDCe", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WADA", + "name": "p.WADA", + "icon": "/icons/wADA.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.WADA", + "bridgeAddressKey": "PrivacyBridgeWADA", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.gCOTI", + "name": "p.gCOTI", + "icon": "/icons/gcoti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.gCOTI", + "bridgeAddressKey": "PrivacyBridgegCOTI", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.MTT", + "name": "p.MTT", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.MTT", + "bridgeAddressKey": "PrivacyPortalMTT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + } + ], + "walletNetwork": { + "chainId": "0x6c11a0", + "chainName": "COTI Testnet", + "rpcUrls": ["https://testnet.coti.io/rpc"], + "nativeCurrency": { "name": "COTI", "symbol": "COTI", "decimals": 18 }, + "blockExplorerUrls": ["https://testnet.cotiscan.io"] + }, + "indexPage": { + "showPodRequestTracker": false, + "amountModalGasLabel": "Estimated Gas Fee", + "amountModalGasSymbol": "COTI" + } + }, + { + "id": 2632500, + "hexId": "0x282b34", + "name": "COTI Mainnet", + "rpcUrl": "https://mainnet.coti.io/rpc", + "explorerBaseUrl": "https://mainnet.cotiscan.io", + "unlockStrategy": "snap", + "portalStrategy": "coti-bridge", + "addresses": { + "PrivateCoti": "", + "PrivacyBridgeCotiNative": "", + "CotiPriceConsumer": "0x830c5112E677459648C1aa7Bc5Dd65A36d71Aa4D", + "WETH": "0x639aCc80569c5FC83c6FBf2319A6Cc38bBfe26d1", + "WBTC": "0x8C39B1fD0e6260fdf20652Fc436d25026832bfEA", + "USDT": "0xfA6f73446b17A97a56e464256DA54AD43c2Cbc3E", + "USDC_E": "0xf1Feebc4376c68B7003450ae66343Ae59AB37D3C", + "WADA": "0xe757Ca19d2c237AA52eBb1d2E8E4368eeA3eb331", + "gCOTI": "0x7637C7838EC4Ec6b85080F28A678F8E234bB83D1", + "MTT": "", + "PrivacyPortalMTT": "", + "p.WETH": "", + "p.WBTC": "", + "p.USDT": "", + "p.USDC_E": "", + "p.WADA": "", + "p.gCOTI": "", + "p.MTT": "", + "PrivacyBridgeWETH": "", + "PrivacyBridgeWBTC": "", + "PrivacyBridgeUSDT": "", + "PrivacyBridgeUSDCe": "", + "PrivacyBridgeWADA": "", + "PrivacyBridgegCOTI": "" + }, + "tokens": [ + { + "symbol": "COTI", + "name": "COTI", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "bridgeAddressKey": "PrivacyBridgeCotiNative", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WETH", + "name": "Wrapped Ether", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "WETH", + "bridgeAddressKey": "PrivacyBridgeWETH", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WBTC", + "name": "Wrapped BTC", + "icon": "/icons/wBTC.svg", + "decimals": 8, + "isPrivate": false, + "addressKey": "WBTC", + "bridgeAddressKey": "PrivacyBridgeWBTC", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "USDT", + "name": "Tether USD", + "icon": "/icons/usdt.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDT", + "bridgeAddressKey": "PrivacyBridgeUSDT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "USDC.e", + "name": "Bridged USDC", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "USDC_E", + "bridgeAddressKey": "PrivacyBridgeUSDCe", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "WADA", + "name": "Wrapped ADA", + "icon": "/icons/wADA.svg", + "decimals": 6, + "isPrivate": false, + "addressKey": "WADA", + "bridgeAddressKey": "PrivacyBridgeWADA", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "gCOTI", + "name": "gCOTI", + "icon": "/icons/gcoti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "gCOTI", + "bridgeAddressKey": "PrivacyBridgegCOTI", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "MTT", + "name": "MTT", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": false, + "addressKey": "MTT", + "bridgeAddressKey": "PrivacyPortalMTT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.COTI", + "name": "p.COTI", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "PrivateCoti", + "bridgeAddressKey": "PrivacyBridgeCotiNative", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WETH", + "name": "p.WETH", + "icon": "/icons/wETH.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.WETH", + "bridgeAddressKey": "PrivacyBridgeWETH", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WBTC", + "name": "p.WBTC", + "icon": "/icons/wBTC.svg", + "decimals": 8, + "isPrivate": true, + "addressKey": "p.WBTC", + "bridgeAddressKey": "PrivacyBridgeWBTC", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.USDT", + "name": "p.USDT", + "icon": "/icons/usdt.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDT", + "bridgeAddressKey": "PrivacyBridgeUSDT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.USDC.e", + "name": "p.USDC.e", + "icon": "/icons/USDC.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.USDC_E", + "bridgeAddressKey": "PrivacyBridgeUSDCe", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.WADA", + "name": "p.WADA", + "icon": "/icons/wADA.svg", + "decimals": 6, + "isPrivate": true, + "addressKey": "p.WADA", + "bridgeAddressKey": "PrivacyBridgeWADA", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.gCOTI", + "name": "p.gCOTI", + "icon": "/icons/gcoti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.gCOTI", + "bridgeAddressKey": "PrivacyBridgegCOTI", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + }, + { + "symbol": "p.MTT", + "name": "p.MTT", + "icon": "/icons/coti.svg", + "decimals": 18, + "isPrivate": true, + "addressKey": "p.MTT", + "bridgeAddressKey": "PrivacyPortalMTT", + "timeout": 1800, + "supportedChainIds": [7082400, 2632500] + } + ], + "walletNetwork": { + "chainId": "0x282b34", + "chainName": "COTI Mainnet", + "rpcUrls": ["https://mainnet.coti.io/rpc"], + "nativeCurrency": { "name": "COTI", "symbol": "COTI", "decimals": 18 }, + "blockExplorerUrls": ["https://mainnet.cotiscan.io"] + }, + "indexPage": { + "showPodRequestTracker": false, + "amountModalGasLabel": "Estimated Gas Fee", + "amountModalGasSymbol": "COTI" + } + } + ] +} diff --git a/src/chains/config.ts b/src/chains/config.ts new file mode 100644 index 0000000..fa0ebbc --- /dev/null +++ b/src/chains/config.ts @@ -0,0 +1,103 @@ +import rawConfig from "./config.json"; +import type { BridgeData } from "../hooks/useBridgeData"; +import type { ChainConfig, TokenConfig } from "./types"; + +type RawTokenConfig = Omit & { + supportedChainIds?: number[]; +}; + +type RawChainConfig = Omit & { + tokens: RawTokenConfig[]; +}; + +const emptyBridgeData: Omit< + BridgeData, + | "bridgeName" + | "bridgeAddress" + | "publicToken" + | "publicTokenIcon" + | "privateToken" + | "privateTokenIcon" + | "tokenDecimals" +> = { + depositFixedFee: "0", + depositPercentageBps: "0", + depositMaxFee: "0", + withdrawFixedFee: "0", + withdrawPercentageBps: "0", + withdrawMaxFee: "0", + minDepositAmount: "0", + maxDepositAmount: "0", + minWithdrawAmount: "0", + maxWithdrawAmount: "0", + accumulatedFees: "0", + accumulatedCotiFees: "0", + nativeCotiFee: "0", + bridgeBalance: "0", + isPaused: false, + isLoading: false, + error: null, +}; + +const makePodBridgeDataOverride = + (tokens: TokenConfig[]) => + (addresses: Record): BridgeData[] => { + const privateTokensByBridge = tokens.reduce>((acc, token) => { + if (token.isPrivate && token.bridgeAddressKey) { + acc.set(token.bridgeAddressKey, token); + } + return acc; + }, new Map()); + + const bridges: BridgeData[] = []; + + for (const publicToken of tokens) { + const bridgeAddressKey = publicToken.bridgeAddressKey; + if (publicToken.isPrivate || !bridgeAddressKey?.startsWith("PrivacyPortal")) continue; + + const privateToken = privateTokensByBridge.get(bridgeAddressKey); + const bridgeAddress = addresses[bridgeAddressKey]; + if (!privateToken || !bridgeAddress) continue; + + bridges.push({ + ...emptyBridgeData, + bridgeName: `${publicToken.symbol} PoD Portal`, + bridgeAddress, + publicToken: publicToken.symbol, + publicTokenIcon: publicToken.icon, + privateToken: privateToken.symbol, + privateTokenIcon: privateToken.icon, + tokenDecimals: publicToken.decimals, + }); + } + + return bridges; + }; + +const buildChainConfig = (rawChain: RawChainConfig): ChainConfig => { + const tokens = rawChain.tokens.map(token => ({ + ...token, + supportedChainIds: token.supportedChainIds ?? [rawChain.id], + })); + + return { + ...rawChain, + tokens, + getBridgeDataOverride: + rawChain.portalStrategy === "pod-privacy-portal" ? makePodBridgeDataOverride(tokens) : undefined, + }; +}; + +export const CHAIN_CONFIG_LIST: ChainConfig[] = (rawConfig.chains as unknown as RawChainConfig[]).map(buildChainConfig); + +export const CHAIN_CONFIGS: Record = Object.fromEntries( + CHAIN_CONFIG_LIST.map(chain => [chain.id, chain]) +); + +export const getConfiguredChain = (chainId: number): ChainConfig => { + const chain = CHAIN_CONFIGS[chainId]; + if (!chain) { + throw new Error(`Missing chain config for ${chainId}`); + } + return chain; +}; diff --git a/src/chains/coti.ts b/src/chains/coti.ts index bed9621..a10441b 100644 --- a/src/chains/coti.ts +++ b/src/chains/coti.ts @@ -1,125 +1,7 @@ -import type { ChainConfig, TokenConfig } from "./types"; +import { getConfiguredChain } from "./config"; export const COTI_TESTNET_CHAIN_ID = 7082400; export const COTI_MAINNET_CHAIN_ID = 2632500; -const cotiTokenConfigs = (supportedChainIds: number[]): TokenConfig[] => [ - { symbol: "COTI", name: "COTI", icon: "/icons/coti.svg", decimals: 18, isPrivate: false, bridgeAddressKey: "PrivacyBridgeCotiNative", timeout: 1800, supportedChainIds }, - { symbol: "WETH", name: "Wrapped Ether", icon: "/icons/wETH.svg", decimals: 18, isPrivate: false, addressKey: "WETH", bridgeAddressKey: "PrivacyBridgeWETH", timeout: 1800, supportedChainIds }, - { symbol: "WBTC", name: "Wrapped BTC", icon: "/icons/wBTC.svg", decimals: 8, isPrivate: false, addressKey: "WBTC", bridgeAddressKey: "PrivacyBridgeWBTC", timeout: 1800, supportedChainIds }, - { symbol: "USDT", name: "Tether USD", icon: "/icons/usdt.svg", decimals: 6, isPrivate: false, addressKey: "USDT", bridgeAddressKey: "PrivacyBridgeUSDT", timeout: 1800, supportedChainIds }, - { symbol: "USDC.e", name: "Bridged USDC", icon: "/icons/USDC.svg", decimals: 6, isPrivate: false, addressKey: "USDC_E", bridgeAddressKey: "PrivacyBridgeUSDCe", timeout: 1800, supportedChainIds }, - { symbol: "WADA", name: "Wrapped ADA", icon: "/icons/wADA.svg", decimals: 6, isPrivate: false, addressKey: "WADA", bridgeAddressKey: "PrivacyBridgeWADA", timeout: 1800, supportedChainIds }, - { symbol: "gCOTI", name: "gCOTI", icon: "/icons/gcoti.svg", decimals: 18, isPrivate: false, addressKey: "gCOTI", bridgeAddressKey: "PrivacyBridgegCOTI", timeout: 1800, supportedChainIds }, - { symbol: "MTT", name: "MTT", icon: "/icons/coti.svg", decimals: 18, isPrivate: false, addressKey: "MTT", bridgeAddressKey: "PrivacyPortalMTT", timeout: 1800, supportedChainIds }, - { symbol: "p.COTI", name: "p.COTI", icon: "/icons/coti.svg", decimals: 18, isPrivate: true, addressKey: "PrivateCoti", bridgeAddressKey: "PrivacyBridgeCotiNative", timeout: 1800, supportedChainIds }, - { symbol: "p.WETH", name: "p.WETH", icon: "/icons/wETH.svg", decimals: 18, isPrivate: true, addressKey: "p.WETH", bridgeAddressKey: "PrivacyBridgeWETH", timeout: 1800, supportedChainIds }, - { symbol: "p.WBTC", name: "p.WBTC", icon: "/icons/wBTC.svg", decimals: 8, isPrivate: true, addressKey: "p.WBTC", bridgeAddressKey: "PrivacyBridgeWBTC", timeout: 1800, supportedChainIds }, - { symbol: "p.USDT", name: "p.USDT", icon: "/icons/usdt.svg", decimals: 6, isPrivate: true, addressKey: "p.USDT", bridgeAddressKey: "PrivacyBridgeUSDT", timeout: 1800, supportedChainIds }, - { symbol: "p.USDC.e", name: "p.USDC.e", icon: "/icons/USDC.svg", decimals: 6, isPrivate: true, addressKey: "p.USDC_E", bridgeAddressKey: "PrivacyBridgeUSDCe", timeout: 1800, supportedChainIds }, - { symbol: "p.WADA", name: "p.WADA", icon: "/icons/wADA.svg", decimals: 6, isPrivate: true, addressKey: "p.WADA", bridgeAddressKey: "PrivacyBridgeWADA", timeout: 1800, supportedChainIds }, - { symbol: "p.gCOTI", name: "p.gCOTI", icon: "/icons/gcoti.svg", decimals: 18, isPrivate: true, addressKey: "p.gCOTI", bridgeAddressKey: "PrivacyBridgegCOTI", timeout: 1800, supportedChainIds }, - { symbol: "p.MTT", name: "p.MTT", icon: "/icons/coti.svg", decimals: 18, isPrivate: true, addressKey: "p.MTT", bridgeAddressKey: "PrivacyPortalMTT", timeout: 1800, supportedChainIds }, -]; - -/** PoD inbox on COTI testnet (same deployment as Sepolia/Fuji PoD portal). */ -const COTI_TESTNET_POD_INBOX = "0xB4A53FE02401fDFA8DAc00450dA3FfF8D01502F8"; - -export const cotiTestnetChain: ChainConfig = { - id: COTI_TESTNET_CHAIN_ID, - hexId: "0x6c11a0", - name: "COTI Testnet", - rpcUrl: "https://testnet.coti.io/rpc", - explorerBaseUrl: "https://testnet.cotiscan.io", - unlockStrategy: "snap", - portalStrategy: "coti-bridge", - podInboxAddress: COTI_TESTNET_POD_INBOX, - addresses: { - PrivateCoti: "0x6cE8907414986E73De9e7D28d62Ea2080F8E88E1", - PrivacyBridgeCotiNative: "0x96117882328407B13A5f378de25764C4eD8477eA", - CotiPriceConsumer: "0xD5EeD24e909AdE249b688671e32dcc013B236B74", - WETH: "0x8bca4e6bbE402DB4aD189A316137aD08206154FB", - WBTC: "0x5dBDb2E5D51c3FFab5D6B862Caa11FCe1D83F492", - USDT: "0x9e961430053cd5AbB3b060544cEcCec848693Cf0", - USDC_E: "0x63f3D2Cc8F5608F57ce6E5Aa3590A2Beb428D19C", - WADA: "0xe3E2cd3Abf412c73a404b9b8227B71dE3CfE829D", - gCOTI: "0x878a42D3cB737DEC9E6c7e7774d973F46fd8ed4C", - MTT: "", - PrivacyPortalMTT: "0xF5Da19E1c8FB39Fdd11bB029882fc69E86650beB", - "p.WETH": "0xF009BADb181d471995a1CFF406C3Db7B180F64eA", - "p.WBTC": "0xB50F1680a4C69145ABc09A2A71c8D5b8051578cF", - "p.USDT": "0xcEF137E96eDF68EE99D4CdEa7085f154d74895cD", - "p.USDC_E": "0x37f78dcCd15876F74391EF1F01b76557D9FF1dea", - "p.WADA": "0x1245f50a3E9129A219b4bf66D10fEaEA47467B69", - "p.gCOTI": "0x1503b02a4Aa27812306c65116FD23b733603F142", - "p.MTT": "0xb681D264a250715977450212d370b05d98761422", - PrivacyBridgeWETH: "0x6f1628D7F1D5d5aDF39b924a28acb10B5C9df283", - PrivacyBridgeWBTC: "0x9EBEbC8F5Cff021459a56A681719dF1C0A7435cc", - PrivacyBridgeUSDT: "0x549634daBE8237EBda7473d7aA2bB17459826781", - PrivacyBridgeUSDCe: "0xaa63D41890D576F93d79272988e212d8B46CdD26", - PrivacyBridgeWADA: "0x4AFe67D4aD7AF0a70ACe6f79FbfAB66C72211c9F", - PrivacyBridgegCOTI: "0xDB54B4B1B8F9614567bd673A17a5cD31A3DD6033", - }, - tokens: cotiTokenConfigs([COTI_TESTNET_CHAIN_ID, COTI_MAINNET_CHAIN_ID]), - walletNetwork: { - chainId: "0x6c11a0", - chainName: "COTI Testnet", - rpcUrls: ["https://testnet.coti.io/rpc"], - nativeCurrency: { name: "COTI", symbol: "COTI", decimals: 18 }, - blockExplorerUrls: ["https://testnet.cotiscan.io"], - }, - indexPage: { - showPodRequestTracker: false, - amountModalGasLabel: "Estimated Gas Fee", - amountModalGasSymbol: "COTI", - }, -}; - -export const cotiMainnetChain: ChainConfig = { - id: COTI_MAINNET_CHAIN_ID, - hexId: "0x282b34", - name: "COTI Mainnet", - rpcUrl: "https://mainnet.coti.io/rpc", - explorerBaseUrl: "https://mainnet.cotiscan.io", - unlockStrategy: "snap", - portalStrategy: "coti-bridge", - addresses: { - PrivateCoti: "", - PrivacyBridgeCotiNative: "", - CotiPriceConsumer: "0x830c5112E677459648C1aa7Bc5Dd65A36d71Aa4D", - WETH: "0x639aCc80569c5FC83c6FBf2319A6Cc38bBfe26d1", - WBTC: "0x8C39B1fD0e6260fdf20652Fc436d25026832bfEA", - USDT: "0xfA6f73446b17A97a56e464256DA54AD43c2Cbc3E", - USDC_E: "0xf1Feebc4376c68B7003450ae66343Ae59AB37D3C", - WADA: "0xe757Ca19d2c237AA52eBb1d2E8E4368eeA3eb331", - gCOTI: "0x7637C7838EC4Ec6b85080F28A678F8E234bB83D1", - MTT: "", - PrivacyPortalMTT: "", - "p.WETH": "", - "p.WBTC": "", - "p.USDT": "", - "p.USDC_E": "", - "p.WADA": "", - "p.gCOTI": "", - "p.MTT": "", - PrivacyBridgeWETH: "", - PrivacyBridgeWBTC: "", - PrivacyBridgeUSDT: "", - PrivacyBridgeUSDCe: "", - PrivacyBridgeWADA: "", - PrivacyBridgegCOTI: "", - }, - tokens: cotiTokenConfigs([COTI_TESTNET_CHAIN_ID, COTI_MAINNET_CHAIN_ID]), - walletNetwork: { - chainId: "0x282b34", - chainName: "COTI Mainnet", - rpcUrls: ["https://mainnet.coti.io/rpc"], - nativeCurrency: { name: "COTI", symbol: "COTI", decimals: 18 }, - blockExplorerUrls: ["https://mainnet.cotiscan.io"], - }, - indexPage: { - showPodRequestTracker: false, - amountModalGasLabel: "Estimated Gas Fee", - amountModalGasSymbol: "COTI", - }, -}; +export const cotiTestnetChain = getConfiguredChain(COTI_TESTNET_CHAIN_ID); +export const cotiMainnetChain = getConfiguredChain(COTI_MAINNET_CHAIN_ID); diff --git a/src/chains/index.ts b/src/chains/index.ts index 932d57b..5659b3a 100644 --- a/src/chains/index.ts +++ b/src/chains/index.ts @@ -1,6 +1,7 @@ import { cotiMainnetChain, cotiTestnetChain, COTI_MAINNET_CHAIN_ID, COTI_TESTNET_CHAIN_ID } from "./coti"; import { sepoliaChain, SEPOLIA_CHAIN_ID } from "./sepolia"; import { avalancheFujiChain, AVALANCHE_FUJI_CHAIN_ID } from "./avalancheFuji"; +import { CHAIN_CONFIGS } from "./config"; import type { ChainConfig, ResolvedIndexPageUi, @@ -11,13 +12,7 @@ import type { export type { ChainConfig, ResolvedIndexPageUi, TokenConfig, UnlockStrategy, WalletNetworkConfig }; export { COTI_MAINNET_CHAIN_ID, COTI_TESTNET_CHAIN_ID, SEPOLIA_CHAIN_ID, AVALANCHE_FUJI_CHAIN_ID }; - -export const CHAIN_CONFIGS: Record = { - [sepoliaChain.id]: sepoliaChain, - [avalancheFujiChain.id]: avalancheFujiChain, - [cotiTestnetChain.id]: cotiTestnetChain, - [cotiMainnetChain.id]: cotiMainnetChain, -}; +export { CHAIN_CONFIGS }; export const DEFAULT_CHAIN_ID = COTI_TESTNET_CHAIN_ID; diff --git a/src/chains/portal/executePodPortalTransaction.ts b/src/chains/portal/executePodPortalTransaction.ts index 77b3693..2369e8c 100644 --- a/src/chains/portal/executePodPortalTransaction.ts +++ b/src/chains/portal/executePodPortalTransaction.ts @@ -132,7 +132,16 @@ export const quotePortalPodRequest = async ( }; }; -/** Native / WETH-style pTokens expose plain uint256 balances in status helpers. */ +/** Newer MpcCore pTokens expose flat ctUint256 balances in status helpers. */ +const POD_PTOKEN_FLAT_STATE_ABI = [ + "function balanceWithState(address account) view returns (tuple(uint256 ciphertextHigh,uint256 ciphertextLow) balance,bool pending,bool callbackErrored)", +] as const; + +const POD_PTOKEN_FLAT_STATUS_ABI = [ + "function balanceOfWithStatus(address account) view returns (tuple(uint256 ciphertextHigh,uint256 ciphertextLow),bool)", +] as const; + +/** Native / WETH-style pTokens may expose plain uint256 balances in status helpers. */ const POD_PTOKEN_PLAIN_STATUS_ABI = [ "function balanceOfWithStatus(address account) view returns (uint256,bool)", ] as const; @@ -144,6 +153,17 @@ const readPodPTokenPendingState = async ( try { const [, pending, callbackErrored] = await pToken.balanceWithState(account); return { pending: Boolean(pending), callbackErrored: Boolean(callbackErrored) }; + } catch { + /* fall through to flat balanceWithState */ + } + + const pTokenAddress = await pToken.getAddress(); + const runner = pToken.runner as ethers.ContractRunner; + + try { + const flatStateToken = new ethers.Contract(pTokenAddress, POD_PTOKEN_FLAT_STATE_ABI, runner); + const [, pending, callbackErrored] = await flatStateToken.balanceWithState(account); + return { pending: Boolean(pending), callbackErrored: Boolean(callbackErrored) }; } catch { /* fall through to balanceOfWithStatus variants */ } @@ -151,15 +171,19 @@ const readPodPTokenPendingState = async ( try { const [, pending] = await pToken.balanceOfWithStatus(account); return { pending: Boolean(pending), callbackErrored: false }; + } catch { + /* fall through to flat status ABI */ + } + + try { + const flatStatusToken = new ethers.Contract(pTokenAddress, POD_PTOKEN_FLAT_STATUS_ABI, runner); + const [, pending] = await flatStatusToken.balanceOfWithStatus(account); + return { pending: Boolean(pending), callbackErrored: false }; } catch { /* fall through to plain pToken ABI */ } - const plainToken = new ethers.Contract( - await pToken.getAddress(), - POD_PTOKEN_PLAIN_STATUS_ABI, - pToken.runner as ethers.ContractRunner, - ); + const plainToken = new ethers.Contract(pTokenAddress, POD_PTOKEN_PLAIN_STATUS_ABI, runner); const [, pending] = await plainToken.balanceOfWithStatus(account); return { pending: Boolean(pending), callbackErrored: false }; }; diff --git a/src/chains/portal/podRequestStatus.ts b/src/chains/portal/podRequestStatus.ts index ef4ed2d..10896da 100644 --- a/src/chains/portal/podRequestStatus.ts +++ b/src/chains/portal/podRequestStatus.ts @@ -12,22 +12,22 @@ const hasPodExecutionError = (execution: RequestTrackingResponse["execution"]) = return BigInt(errorCode) !== 0n; }; +const normalizeRequestId = (requestId: string) => + requestId.startsWith("0x") ? requestId : `0x${requestId}`; + const getFailedRequestHex = async ( provider: ethers.Provider, - request: PodPortalRequest, + requestId: string, pTokenAddress: string, ) => { - if (!request.requestId) { - /* v8 ignore next -- unreachable: resolvePodRequestStatus guards requestId before calling */ - return "0x"; - } const pToken = new ethers.Contract(pTokenAddress, POD_PTOKEN_ABI, provider); - const failedRaw = await pToken.failedRequests(request.requestId).catch(() => "0x"); + const failedRaw = await pToken.failedRequests(requestId).catch(() => "0x"); return typeof failedRaw === "string" ? failedRaw : ethers.hexlify(failedRaw); }; export async function resolvePodRequestStatus(request: PodPortalRequest) { if (!request.requestId) return null; + const requestId = normalizeRequestId(request.requestId); const chainId = request.chainId; const addresses = CONTRACT_ADDRESSES[chainId]; @@ -42,7 +42,7 @@ export async function resolvePodRequestStatus(request: PodPortalRequest) { const portalAddress = pubCfg?.bridgeAddressKey ? addresses[pubCfg.bridgeAddressKey] : undefined; if (!pTokenAddress || !portalAddress) return null; - const failedHex = await getFailedRequestHex(provider, request, pTokenAddress); + const failedHex = await getFailedRequestHex(provider, requestId, pTokenAddress); if (failedHex !== "0x") { return { status: "callback-errored" as const, @@ -52,7 +52,7 @@ export async function resolvePodRequestStatus(request: PodPortalRequest) { } const tracker = new PodRequest(getPodSdkConfig()); - const tracking = await tracker.trackRequest(chainId, request.requestId); + const tracking = await tracker.trackRequest(chainId, requestId); if (hasPodExecutionError(tracking.execution)) { return { diff --git a/src/chains/sepolia.ts b/src/chains/sepolia.ts index a19e34f..d518f49 100644 --- a/src/chains/sepolia.ts +++ b/src/chains/sepolia.ts @@ -1,191 +1,5 @@ -import type { ChainConfig } from "./types"; +import { getConfiguredChain } from "./config"; export const SEPOLIA_CHAIN_ID = 11155111; -/** Underlying ERC-20s from PrivacyPortalConfig.json (Sepolia). */ -const WETH = "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9"; -const USDC = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; -const MTT = "0xd3f5c63f4D87D2235b295FbA83351d31d0eD1BeE"; - -/** Deployed PoD portal pairs from pod-mpc-lib deployConfig.json (Sepolia). */ -const P_ETH = "0x4667DFcbCd354c2719E129A9FcC2Bb3a98456b91"; -const P_USDC = "0x4926c63B42ab17Bb72C6F8b3818A3A947C2aE2f9"; -const PORTAL_ETH = "0xF0b8d0A7bd03157D72c3364BC6d30d8F061DBD44"; -const PORTAL_USDC = "0x75E7D308b11066d6C5F5d65a94F562ce18a5976C"; - -export const sepoliaChain: ChainConfig = { - id: SEPOLIA_CHAIN_ID, - hexId: "0xaa36a7", - name: "Sepolia", - rpcUrl: "https://ethereum-sepolia-rpc.publicnode.com", - explorerBaseUrl: "https://sepolia.etherscan.io", - unlockStrategy: "manual-aes-key", - portalStrategy: "pod-privacy-portal", - podInboxAddress: "0xB4A53FE02401fDFA8DAc00450dA3FfF8D01502F8", - addresses: { - MTT, - WETH, - USDC, - "p.MTT": "0x34727cc7233e6B20aE071Cd16A81027172b6bdbA", - "p.USDC": P_USDC, - "p.ETH": P_ETH, - PrivacyPortalMTT: "0xffb8770353AcCF2492F6e75f9B5610CE4af8fe89", - PrivacyPortalUSDC: PORTAL_USDC, - PrivacyPortalETH: PORTAL_ETH, - }, - tokens: [ - { - symbol: "MTT", - name: "MyTestToken", - icon: "/icons/coti.svg", - decimals: 18, - isPrivate: false, - addressKey: "MTT", - bridgeAddressKey: "PrivacyPortalMTT", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - { - symbol: "p.MTT", - name: "Private MyTestToken", - icon: "/icons/coti.svg", - decimals: 18, - isPrivate: true, - addressKey: "p.MTT", - bridgeAddressKey: "PrivacyPortalMTT", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - { - symbol: "USDC", - name: "USD Coin", - icon: "/icons/USDC.svg", - decimals: 6, - isPrivate: false, - addressKey: "USDC", - bridgeAddressKey: "PrivacyPortalUSDC", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - { - symbol: "p.USDC", - name: "Private USDC", - icon: "/icons/USDC.svg", - decimals: 6, - isPrivate: true, - addressKey: "p.USDC", - bridgeAddressKey: "PrivacyPortalUSDC", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - { - symbol: "ETH", - name: "Ether", - icon: "/icons/wETH.svg", - decimals: 18, - isPrivate: false, - isNative: true, - addressKey: "WETH", - bridgeAddressKey: "PrivacyPortalETH", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - { - symbol: "p.ETH", - name: "Private WETH", - icon: "/icons/wETH.svg", - decimals: 18, - isPrivate: true, - addressKey: "p.ETH", - bridgeAddressKey: "PrivacyPortalETH", - supportedChainIds: [SEPOLIA_CHAIN_ID], - }, - ], - walletNetwork: { - chainId: "0xaa36a7", - chainName: "Sepolia", - rpcUrls: ["https://ethereum-sepolia-rpc.publicnode.com"], - nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 }, - blockExplorerUrls: ["https://sepolia.etherscan.io"], - }, - getBridgeDataOverride: addresses => [ - { - bridgeName: "MTT PoD Portal", - bridgeAddress: addresses.PrivacyPortalMTT, - publicToken: "MTT", - publicTokenIcon: "/icons/coti.svg", - privateToken: "p.MTT", - privateTokenIcon: "/icons/coti.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 18, - isLoading: false, - error: null, - }, - { - bridgeName: "USDC PoD Portal", - bridgeAddress: addresses.PrivacyPortalUSDC, - publicToken: "USDC", - publicTokenIcon: "/icons/USDC.svg", - privateToken: "p.USDC", - privateTokenIcon: "/icons/USDC.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 6, - isLoading: false, - error: null, - }, - { - bridgeName: "ETH PoD Portal", - bridgeAddress: addresses.PrivacyPortalETH, - publicToken: "ETH", - publicTokenIcon: "/icons/wETH.svg", - privateToken: "p.ETH", - privateTokenIcon: "/icons/wETH.svg", - depositFixedFee: "0", - depositPercentageBps: "0", - depositMaxFee: "0", - withdrawFixedFee: "0", - withdrawPercentageBps: "0", - withdrawMaxFee: "0", - minDepositAmount: "0", - maxDepositAmount: "0", - minWithdrawAmount: "0", - maxWithdrawAmount: "0", - accumulatedFees: "0", - accumulatedCotiFees: "0", - nativeCotiFee: "0", - bridgeBalance: "0", - isPaused: false, - tokenDecimals: 18, - isLoading: false, - error: null, - }, - ], - indexPage: { - showPodRequestTracker: true, - amountModalGasLabel: "Estimated Gas and PoD fee", - amountModalGasSymbol: "native", - }, -}; +export const sepoliaChain = getConfiguredChain(SEPOLIA_CHAIN_ID); diff --git a/src/context/privacyBridge/sessionShared.ts b/src/context/privacyBridge/sessionShared.ts index 9bdc48e..02ff638 100644 --- a/src/context/privacyBridge/sessionShared.ts +++ b/src/context/privacyBridge/sessionShared.ts @@ -51,7 +51,6 @@ export interface PrivacyBridgeSessionCore { version: 64 | 256, decimals?: number, readChainId?: number, - isPlainBalance?: boolean, ) => Promise; getAesKeyFromProvider: (accountAddress: string) => Promise; } diff --git a/src/context/privacyBridge/usePrivacyBridgeUnlockSession.ts b/src/context/privacyBridge/usePrivacyBridgeUnlockSession.ts index ac9e54d..899fc1b 100644 --- a/src/context/privacyBridge/usePrivacyBridgeUnlockSession.ts +++ b/src/context/privacyBridge/usePrivacyBridgeUnlockSession.ts @@ -1,5 +1,9 @@ import { useCallback } from 'react'; -import { saveAesKeyLocally, unlockCachedAesKey as unlockCachedAesKeyFromVault } from '../../crypto/localAesKeyVault'; +import { + clearCachedAesKey, + saveAesKeyLocally, + unlockCachedAesKey as unlockCachedAesKeyFromVault, +} from '../../crypto/localAesKeyVault'; import { logger } from '../../lib/logger'; import { getInitialPrivateTokens, @@ -52,8 +56,14 @@ export const usePrivacyBridgeUnlockSession = ({ setSnapError(null); const chainOverride = wagmiSyncRef.current ? wagmiChainId : undefined; - const success = await updateAccountState(walletAddress, true, true, key, chainOverride); - if (success) setArePrivateBalancesHidden(false); + try { + const success = await updateAccountState(walletAddress, true, true, key, chainOverride); + if (success) setArePrivateBalancesHidden(false); + } catch (err) { + setSessionAesKey(null); + clearCachedAesKey(walletAddress); + throw err; + } }; const unlockCachedAesKey = async () => { @@ -117,6 +127,7 @@ export const usePrivacyBridgeUnlockSession = ({ if (err.message?.includes('AES key') || err.message?.includes('onboarding')) { setSessionAesKey(null); + clearCachedAesKey(walletAddress); clearSnapCache(); setArePrivateBalancesHidden(true); const mismatchError = new Error('AES_KEY_MISMATCH'); diff --git a/src/context/privacyBridge/usePrivacyBridgeWagmiSync.ts b/src/context/privacyBridge/usePrivacyBridgeWagmiSync.ts index dcdfb67..10d525c 100644 --- a/src/context/privacyBridge/usePrivacyBridgeWagmiSync.ts +++ b/src/context/privacyBridge/usePrivacyBridgeWagmiSync.ts @@ -50,7 +50,7 @@ export const usePrivacyBridgeWagmiSync = ({ chainId: wagmiChainId, }); wagmiSyncRef.current = true; - updateAccountState(wagmiAddress, false, true, undefined, wagmiChainId); + updateAccountState(wagmiAddress, false, false, undefined, wagmiChainId); const isMetaMask = mapConnectorIdToWalletType(wagmiConnector?.id) === 'metamask'; if (isMetaMask) { @@ -80,7 +80,7 @@ export const usePrivacyBridgeWagmiSync = ({ logger.log('RainbowKit account switched', truncateAddress(wagmiAddress)); setSessionAesKey(null); clearSnapCache(); - updateAccountState(wagmiAddress, false, true, undefined, wagmiChainId); + updateAccountState(wagmiAddress, false, false, undefined, wagmiChainId); } }, [ wagmiConnected, @@ -138,7 +138,7 @@ export const usePrivacyBridgeWagmiSync = ({ from: prevWagmiChainIdRef.current, to: wagmiChainId, }); - updateAccountState(wagmiAddress, false, true, undefined, wagmiChainId); + updateAccountState(wagmiAddress, false, false, undefined, wagmiChainId); } prevWagmiChainIdRef.current = wagmiChainId; } diff --git a/src/hooks/useBalanceUpdater.ts b/src/hooks/useBalanceUpdater.ts index e643811..f9bbabe 100644 --- a/src/hooks/useBalanceUpdater.ts +++ b/src/hooks/useBalanceUpdater.ts @@ -15,7 +15,7 @@ interface UseBalanceUpdaterProps { setPrivateTokens: React.Dispatch>; checkNetwork: (provider: ethers.BrowserProvider) => Promise; getAESKeyFromSnap: (accountAddress: string) => Promise; - fetchPrivateBalance: (userAddress: string, aesKey: string, contractAddress: string, version: 64 | 256, decimals?: number, readChainId?: number, isPlainBalance?: boolean) => Promise; + fetchPrivateBalance: (userAddress: string, aesKey: string, contractAddress: string, version: 64 | 256, decimals?: number, readChainId?: number) => Promise; sessionAesKey?: string | null; setSessionAesKey: (key: string | null, keyWallet?: string) => void; } @@ -59,8 +59,8 @@ export const useBalanceUpdater = ({ if (window.ethereum || hasChainOverride) { const browserProvider = window.ethereum ? new ethers.BrowserProvider(window.ethereum) : null; - // Ensure network name is updated immediately when MetaMask is available. - if (browserProvider) { + // Wallet network check is only needed when reads go through the browser provider. + if (browserProvider && !hasChainOverride) { await checkNetwork(browserProvider); if (isStale()) return false; } @@ -130,19 +130,10 @@ export const useBalanceUpdater = ({ } const privateTokenConfigs = getPrivateTokensForChain(currentChainId); - const publicTokenConfigs = getPublicTokensForChain(currentChainId); - const hasPlainPrivateTokens = privateTokenConfigs.some(token => { - const publicSymbol = token.symbol.replace(/^p\./, ''); - return !!publicTokenConfigs.find(t => t.symbol === publicSymbol)?.isNative; - }); - - if (!aesKey && !hasPlainPrivateTokens) { - logger.log('ℹ️ Snap available but keys missing/rejected.'); - return false; - } - - if (aesKey || hasPlainPrivateTokens) { - if (aesKey && !sessionAesKey) { + if (!aesKey) { + logger.log('ℹ️ Private balances require an AES key — public balances updated.'); + } else { + if (!sessionAesKey) { if (isStale()) return false; setHasSnap(true); } @@ -154,21 +145,14 @@ export const useBalanceUpdater = ({ if (!tokenAddress) { return { symbol: token.symbol, value: '0', isMismatch: false }; } - const publicSymbol = token.symbol.replace(/^p\./, ''); - const pubCfg = publicTokenConfigs.find(t => t.symbol === publicSymbol); - const isPlainBalance = !!pubCfg?.isNative; - if (!aesKey && !isPlainBalance) { - return { symbol: token.symbol, value: '0', isMismatch: false }; - } try { const value = await fetchPrivateBalance( account, - aesKey ?? '', + aesKey, tokenAddress, 256, token.decimals, currentChainId, - isPlainBalance, ); return { symbol: token.symbol, value, isMismatch: false }; } catch (e: any) { @@ -192,7 +176,11 @@ export const useBalanceUpdater = ({ if (mismatchCount > 0) { logger.warn( - `⚠️ AES key mismatch for ${mismatchCount} private token(s); showing 0 for those tokens.`, + `⚠️ AES key mismatch for ${mismatchCount} private token(s); locking private balances.`, + ); + throw new CotiPluginError( + CotiErrorCode.AES_KEY_MISMATCH, + 'AES key mismatch: unable to decrypt one or more non-zero private token balances.', ); } @@ -211,7 +199,6 @@ export const useBalanceUpdater = ({ supportedChainIds: token.supportedChainIds, }; })); - return true; } } catch (privateError: any) { if (isStale()) return false; diff --git a/src/hooks/usePrivateTokenBalance.ts b/src/hooks/usePrivateTokenBalance.ts index 8aa001b..7d44954 100644 --- a/src/hooks/usePrivateTokenBalance.ts +++ b/src/hooks/usePrivateTokenBalance.ts @@ -7,23 +7,14 @@ import { CotiPluginError, CotiErrorCode } from '../errors'; import { logger } from '../lib/logger'; /** - * ABI for native PoD pTokens (p.ETH, p.AVAX): balance is a plain uint256 on-chain. - */ -const PLAIN_BALANCE_ABI = [ - "function balanceOf(address account) view returns (uint256)", -]; - -/** - * ABI for the nested 4-part ciphertext format used by PoD pTokens (e.g., Sepolia p.MTT). - * Returns: tuple(tuple(uint256 high, uint256 low) high, tuple(uint256 high, uint256 low) low) + * ABI for PoD pTokens (PodERC20): ctUint256 is a nested tuple in the contract ABI. */ const NESTED_BALANCE_ABI = [ "function balanceOf(address account) view returns (tuple(tuple(uint256 high, uint256 low) high, tuple(uint256 high, uint256 low) low))" ]; /** - * ABI for the flat 2-part ciphertext format used by COTI native privacy tokens. - * Returns: tuple(uint256 ciphertextHigh, uint256 ciphertextLow) + * ABI for ctUint256 as flat ciphertextHigh/ciphertextLow (zero balances and some native pTokens). */ const FLAT_BALANCE_ABI = [ "function balanceOf(address) view returns (tuple(uint256 ciphertextHigh, uint256 ciphertextLow))" @@ -83,12 +74,8 @@ export const usePrivateTokenBalance = () => { version: 64 | 256, decimals: number = 18, _readChainId?: number, - isPlainBalance: boolean = false, ): Promise => { - if (!contractAddress) { - return '0.00'; - } - if (!isPlainBalance && !aesKey) { + if (!contractAddress || !aesKey) { return '0.00'; } if (_readChainId == null && !window.ethereum) { @@ -116,12 +103,6 @@ export const usePrivateTokenBalance = () => { return '0.00'; } - if (isPlainBalance) { - const plainContract = new ethers.Contract(contractAddress, PLAIN_BALANCE_ABI, runner); - const balance = await plainContract.balanceOf(userAddress); - return ethers.formatUnits(balance, decimals); - } - if (version === 64) { // 64-bit Native token legacy ABI return const contract = new ethers.Contract(contractAddress, [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 757af1b..4550dea 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,10 @@ export const TOKEN_BALANCE_DISPLAY_DECIMALS: Record = { COTI: 4, "p.COTI": 4, + ETH: 6, + "p.ETH": 6, + AVAX: 6, + "p.AVAX": 6, WETH: 6, "p.WETH": 6, USDC: 4, diff --git a/tests/hooks/useBalanceUpdater.test.ts b/tests/hooks/useBalanceUpdater.test.ts index 57b2807..0f059b9 100644 --- a/tests/hooks/useBalanceUpdater.test.ts +++ b/tests/hooks/useBalanceUpdater.test.ts @@ -166,7 +166,7 @@ describe('useBalanceUpdater', () => { expect(props.setPrivateTokens).toHaveBeenCalledTimes(1); }); - it('returns false when no AES key can be obtained for private balances', async () => { + it('returns true with public balances when no AES key is available for private fetch', async () => { const props = makeProps({ getAESKeyFromSnap: vi.fn().mockResolvedValue(null), }); @@ -174,7 +174,8 @@ describe('useBalanceUpdater', () => { const ok = await result.current.updateAccountState(ACCOUNT, true, true, undefined, COTI_TESTNET); - expect(ok).toBe(false); + expect(ok).toBe(true); + expect(props.setPublicTokens).toHaveBeenCalledTimes(1); expect(props.setPrivateTokens).not.toHaveBeenCalled(); }); @@ -192,11 +193,12 @@ describe('useBalanceUpdater', () => { it('fetches private balances when fetchPrivate is true even if checkSnap is false', async () => { const props = makeProps({ + sessionAesKey: 'a'.repeat(64), fetchPrivateBalance: vi.fn().mockResolvedValue('1.5'), }); const { result } = renderHook(() => useBalanceUpdater(props)); - const ok = await result.current.updateAccountState(ACCOUNT, false, true, undefined, SEPOLIA); + const ok = await result.current.updateAccountState(ACCOUNT, false, true, 'a'.repeat(64), SEPOLIA); expect(ok).toBe(true); expect(props.fetchPrivateBalance).toHaveBeenCalled(); expect(props.setPrivateTokens).toHaveBeenCalledTimes(1); diff --git a/tests/hooks/usePrivateTokenBalance.contract.test.ts b/tests/hooks/usePrivateTokenBalance.contract.test.ts index 2488232..d4e097e 100644 --- a/tests/hooks/usePrivateTokenBalance.contract.test.ts +++ b/tests/hooks/usePrivateTokenBalance.contract.test.ts @@ -81,22 +81,23 @@ describe('usePrivateTokenBalance (contract paths)', () => { ).rejects.toMatchObject({ code: CotiErrorCode.AES_KEY_MISMATCH }); }); - it('returns a plain uint256 balance for native PoD pTokens', async () => { - h.balanceOf.mockResolvedValueOnce(2500000000000000000n); + it('decrypts a flat ctUint256 balance when nested shape is unavailable', async () => { + h.balanceOf + .mockRejectedValueOnce(new Error('Not nested format')) + .mockResolvedValueOnce({ ciphertextHigh: 5n, ciphertextLow: 6n }); + (decryptCtUint256 as any).mockReturnValue(2500000000000000000n); const { result } = renderHook(() => usePrivateTokenBalance()); const bal = await result.current.fetchPrivateBalance( USER, - '', + 'a'.repeat(64), CONTRACT, 256, 18, - undefined, - true, ); expect(bal).toBe('formatted:2500000000000000000'); - expect(decryptCtUint256).not.toHaveBeenCalled(); + expect(decryptCtUint256).toHaveBeenCalled(); }); it('decrypts a 256-bit nested ciphertext', async () => { @@ -152,22 +153,23 @@ describe('usePrivateTokenBalance (contract paths)', () => { ).rejects.toMatchObject({ code: CotiErrorCode.AES_KEY_MISMATCH }); }); - it('reads a plain balance via RPC when readChainId is provided', async () => { - h.balanceOf.mockResolvedValueOnce(1000000000000000000n); + it('reads a flat balance via RPC when readChainId is provided', async () => { + h.balanceOf + .mockRejectedValueOnce(new Error('Not nested format')) + .mockResolvedValueOnce({ ciphertextHigh: 0n, ciphertextLow: 0n }); (window as any).ethereum = undefined; const { result } = renderHook(() => usePrivateTokenBalance()); const bal = await result.current.fetchPrivateBalance( USER, - '', + 'a'.repeat(64), CONTRACT, 256, 18, 11155111, - true, ); - expect(bal).toBe('formatted:1000000000000000000'); + expect(bal).toBe('0.00'); expect(h.getSigner).not.toHaveBeenCalled(); });