diff --git a/libs/evm-protocols/src/common-protocol/chainConfig.ts b/libs/evm-protocols/src/common-protocol/chainConfig.ts index d0b8049df45..587767e2617 100644 --- a/libs/evm-protocols/src/common-protocol/chainConfig.ts +++ b/libs/evm-protocols/src/common-protocol/chainConfig.ts @@ -112,6 +112,9 @@ export const factoryContracts: FactoryContractsType = { ReferralFeeManager: '0x9d3BE262bed6F3A0AAb4E97c0232071EF730632f', TokenLaunchpad: '0xACD353A1Bf569662E2e0e5Dd127FeCbee505986c', TokenBondingCurve: '0xC815d94E05dCAa6D84a740291A03A1e73BF48C1f', + BinaryVault: '0x9F43B07FF2B63F895930017D727c3dfA58fB838c', + FutarchyRouter: '0xFE985509910e5C1f667C2E8AD86349c50f35f234', + FutarchyGovernor: '0xD454743008A7c127136bDc0DFA1A24524F3Daf66', chainId: 8453, }, [ValidChains.Linea]: { diff --git a/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarket.ts b/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarket.ts index 33b6984ed47..e9d75a8b348 100644 --- a/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarket.ts +++ b/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarket.ts @@ -99,6 +99,32 @@ function formatRevertReason(err: unknown): string | null { } } +function normalizeAddress( + web3Like: { utils: { toChecksumAddress: (value: string) => string } }, + value: string, + fieldName: string, +): `0x${string}` { + try { + return web3Like.utils.toChecksumAddress(value) as `0x${string}`; + } catch { + throw new Error(`Invalid ${fieldName} address: ${value}`); + } +} + +/** ERC-20 allowance cap for uint256-sized values passed to approve(). */ +const MAX_UINT256 = 2n ** 256n - 1n; + +/** + * Allowance to request before propose(): 2× initial liquidity (capped at MAX_UINT256). + * Gives headroom for protocols that pull collateral in more than one transferFrom, + * without granting unlimited spend. + */ +function allowanceForPropose(initialLiquidityWei: bigint): bigint { + if (initialLiquidityWei <= 0n) return 0n; + if (initialLiquidityWei > MAX_UINT256 / 2n) return MAX_UINT256; + return initialLiquidityWei * 2n; +} + class PredictionMarket extends ContractBase { constructor(governorAddress: string, rpc: string) { super(governorAddress, FutarchyGovernorAbi as unknown as AbiItem[], rpc); @@ -151,25 +177,36 @@ class PredictionMarket extends ContractBase { logs?: Array<{ address?: string; data?: string; topics?: string[] }>; }> { this.isInitialized(); + const normalizedCollateralAddress = normalizeAddress( + this.web3, + collateralAddress, + 'collateral', + ); + const normalizedFromAddress = normalizeAddress( + this.web3, + fromAddress, + 'wallet', + ); // Approve governor to spend collateral before propose (required when initialLiquidity > 0). // Matches common-protocol prediction_market_helpers_frontend: approve then propose. if (initialLiquidityWei > 0n) { const collateralToken = new this.web3.eth.Contract( erc20Abi as unknown as AbiItem[], - collateralAddress, + normalizedCollateralAddress, ); const spender = this.contractAddress; const currentAllowance = BigInt( (await collateralToken.methods - .allowance(fromAddress, spender) + .allowance(normalizedFromAddress, spender) .call()) as string, ); - if (currentAllowance < initialLiquidityWei) { + const targetAllowance = allowanceForPropose(initialLiquidityWei); + if (currentAllowance < targetAllowance) { try { await collateralToken.methods - .approve(spender, initialLiquidityWei) - .send({ from: fromAddress }); + .approve(spender, targetAllowance.toString()) + .send({ from: normalizedFromAddress }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (/user rejected|denied|reject/i.test(msg)) { @@ -182,10 +219,10 @@ class PredictionMarket extends ContractBase { } } const currentBalance = BigInt( - (await collateralToken.methods.balanceOf(fromAddress).call()) as string, + (await collateralToken.methods + .balanceOf(normalizedFromAddress) + .call()) as string, ); - console.log('currentBalance', currentBalance); - console.log('initialLiquidityWei', initialLiquidityWei); if (currentBalance < initialLiquidityWei) { throw new Error( 'Insufficient collateral balance for initial liquidity.', @@ -196,15 +233,15 @@ class PredictionMarket extends ContractBase { const tx = this.contract.methods.propose( proposalId, marketId, - collateralAddress, + normalizedCollateralAddress, durationSeconds, resolutionThreshold, initialLiquidityWei, ); try { - const gas = await tx.estimateGas({ from: fromAddress }); + const gas = await tx.estimateGas({ from: normalizedFromAddress }); return await tx.send({ - from: fromAddress, + from: normalizedFromAddress, gas: String(BigInt(gas.toString()) + 100000n), }); } catch (err) { @@ -227,8 +264,17 @@ class PredictionMarket extends ContractBase { * Propose a new market, decode events, and return the payload for deployPredictionMarket mutation. */ async deploy(params: DeployParams): Promise { - console.log('params => ', params); this.isInitialized(); + const normalizedCollateralAddress = normalizeAddress( + this.web3, + params.collateral_address, + 'collateral', + ); + const normalizedUserAddress = normalizeAddress( + this.web3, + params.user_address, + 'wallet', + ); const proposalId = randomBytes32(); const marketId = randomBytes32(); const durationDays = Math.max(1, Math.floor(params.duration_days || 1)); @@ -258,7 +304,7 @@ class PredictionMarket extends ContractBase { if (liquidityInput && liquidityInput !== '0') { const collateralToken = new this.web3.eth.Contract( erc20Abi as unknown as AbiItem[], - params.collateral_address, + normalizedCollateralAddress, ); const decimals = Number( (await collateralToken.methods.decimals().call()) as string | number, @@ -279,11 +325,11 @@ class PredictionMarket extends ContractBase { const rawReceipt = await this.propose( proposalId, marketId, - params.collateral_address, + normalizedCollateralAddress, durationSeconds, resolutionThresholdWei, initialLiquidityWei, - params.user_address, + normalizedUserAddress, ); const logs: Array<{ address: string; data: string; topics: string[] }> = ( diff --git a/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarketTrade.ts b/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarketTrade.ts index 932a623d4d9..f2973e33e51 100644 --- a/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarketTrade.ts +++ b/packages/commonwealth/client/scripts/features/blockchain/contractHelpers/predictionMarketTrade.ts @@ -295,6 +295,141 @@ export function applySlippage(amount: bigint, slippageBps: number): bigint { return (amount * BigInt(10000 - slippageBps)) / 10000n; } +const GAS_PRICE_MULTIPLIER_NUMERATOR = 2n; +const GAS_PRICE_MULTIPLIER_DENOMINATOR = 1n; +const MIN_GAS_PRICE_WEI = 5_000_000_000n; // 5 gwei floor for faster inclusion on congested mempools. + +async function getAggressiveGasPriceWei(web3: Web3): Promise { + const networkGasPrice = BigInt(await web3.eth.getGasPrice()); + const boostedGasPrice = + (networkGasPrice * GAS_PRICE_MULTIPLIER_NUMERATOR) / + GAS_PRICE_MULTIPLIER_DENOMINATOR; + const finalGasPriceWei = + boostedGasPrice > MIN_GAS_PRICE_WEI ? boostedGasPrice : MIN_GAS_PRICE_WEI; + return finalGasPriceWei.toString(10); +} + +function extractTransactionHash(input: string): string | null { + const match = input.match(/0x[a-fA-F0-9]{64}/); + return match ? match[0] : null; +} + +function extractTransactionHashFromUnknown(input: unknown): string | null { + if (!input) return null; + if (typeof input === 'string') return extractTransactionHash(input); + if (typeof input !== 'object') return null; + + const candidate = input as { + transactionHash?: unknown; + receipt?: unknown; + data?: unknown; + cause?: unknown; + error?: unknown; + message?: unknown; + }; + + if (typeof candidate.transactionHash === 'string') { + const parsed = extractTransactionHash(candidate.transactionHash); + if (parsed) return parsed; + } + if (typeof candidate.message === 'string') { + const parsed = extractTransactionHash(candidate.message); + if (parsed) return parsed; + } + + return ( + extractTransactionHashFromUnknown(candidate.receipt) ?? + extractTransactionHashFromUnknown(candidate.data) ?? + extractTransactionHashFromUnknown(candidate.cause) ?? + extractTransactionHashFromUnknown(candidate.error) + ); +} + +function extractReceiptStatus(input: unknown): boolean | null { + if (!input || typeof input !== 'object') return null; + const candidate = input as { + status?: unknown; + receipt?: unknown; + data?: unknown; + cause?: unknown; + error?: unknown; + }; + + if (typeof candidate.status === 'boolean') return candidate.status; + if (typeof candidate.status === 'string') { + if (candidate.status === '0x1' || candidate.status === '1') return true; + if (candidate.status === '0x0' || candidate.status === '0') return false; + } + + return ( + extractReceiptStatus(candidate.receipt) ?? + extractReceiptStatus(candidate.data) ?? + extractReceiptStatus(candidate.cause) ?? + extractReceiptStatus(candidate.error) + ); +} + +async function getReceiptStatusIfAvailable( + web3: Web3, + txHash: string | null, +): Promise { + if (!txHash) return null; + try { + const receipt = await web3.eth.getTransactionReceipt(txHash); + if (!receipt) return null; + return Boolean(receipt.status); + } catch { + return null; + } +} + +function mapTransactionError(err: unknown): string { + const raw = err instanceof Error ? err.message : String(err ?? ''); + const lowered = raw.toLowerCase(); + const txHash = extractTransactionHash(raw); + const txHint = txHash ? ` Tx hash: ${txHash}` : ''; + + if ( + /user denied|user rejected|rejected the transaction|denied transaction/i.test( + raw, + ) + ) { + return 'Transaction was rejected in your wallet.'; + } + if (/insufficient funds|not enough funds|exceeds balance/i.test(raw)) { + return 'Insufficient balance to cover the transaction and network fee.'; + } + if ( + lowered.includes('not mined within') || + lowered.includes('transaction was not mined') || + lowered.includes('transaction still pending') + ) { + return ( + 'Transaction was submitted but not mined in time. ' + + 'This usually means gas fee is too low or an older pending nonce is blocking this account. ' + + `Check wallet activity, speed up/cancel the oldest pending tx, then retry.${txHint}` + ); + } + if ( + lowered.includes('nonce too low') || + lowered.includes('replacement transaction underpriced') || + lowered.includes('already known') + ) { + return ( + 'A pending transaction with this account nonce is blocking or conflicting with this one. ' + + `Speed up/cancel the pending tx in your wallet and retry.${txHint}` + ); + } + if ( + lowered.includes('max fee per gas less than block base fee') || + lowered.includes('fee cap less than block base fee') || + lowered.includes('underpriced') + ) { + return 'Network fee is too low for current conditions. Increase gas fee in wallet settings and retry.'; + } + return raw || 'Transaction failed.'; +} + export type SwapQuoteParams = { chain_rpc: string; eth_chain_id: number; @@ -389,28 +524,66 @@ async function approveToken( ); if (currentAllowance < amount) { const tx = token.methods.approve(spender, amount); - const gas = await tx.estimateGas({ from: fromAddress }); - await tx.send({ - from: fromAddress, - gas: String(BigInt(gas.toString()) + 50000n), - }); + try { + const gas = await tx.estimateGas({ from: fromAddress }); + const gasPrice = await getAggressiveGasPriceWei(web3); + await tx.send({ + from: fromAddress, + gas: String(BigInt(gas.toString()) + 100000n), + gasPrice, + }); + } catch (err) { + const rawMessage = err instanceof Error ? err.message : String(err ?? ''); + const txHash = + extractTransactionHashFromUnknown(err) ?? + extractTransactionHash(rawMessage); + const receiptStatus = extractReceiptStatus(err); + if (receiptStatus === true) return; + const status = await getReceiptStatusIfAvailable(web3, txHash); + if (status === true) return; + throw new Error(mapTransactionError(err)); + } } } /** Send tx with estimated gas + buffer (matches deploy flow to avoid inflated provider estimates). */ async function sendWithEstimatedGas( + web3: Web3, tx: { estimateGas: (opts: { from: string }) => Promise; send: (opts: { from: string; gas: string; + gasPrice: string; }) => Promise<{ transactionHash: string }>; }, fromAddress: string, ): Promise<{ transactionHash: string }> { - const gas = await tx.estimateGas({ from: fromAddress }); - const gasLimit = BigInt(gas as unknown as string) + 100000n; - return tx.send({ from: fromAddress, gas: String(gasLimit) }); + try { + const gas = await tx.estimateGas({ from: fromAddress }); + const gasPrice = await getAggressiveGasPriceWei(web3); + // Slightly over-estimate to reduce risk of borderline out-of-gas / network variance. + const gasLimit = BigInt(gas as unknown as string) + 200000n; + return await tx.send({ + from: fromAddress, + gas: String(gasLimit), + gasPrice, + }); + } catch (err) { + const rawMessage = err instanceof Error ? err.message : String(err ?? ''); + const txHash = + extractTransactionHashFromUnknown(err) ?? + extractTransactionHash(rawMessage); + const receiptStatus = extractReceiptStatus(err); + if (receiptStatus === true) { + return { transactionHash: txHash ?? '' }; + } + const status = await getReceiptStatusIfAvailable(web3, txHash); + if (status === true && txHash) { + return { transactionHash: txHash }; + } + throw new Error(mapTransactionError(err)); + } } class BinaryVaultHelper extends ContractBase { @@ -436,7 +609,7 @@ class BinaryVaultHelper extends ContractBase { marketIdBytes, amountWei.toString(10), ); - return sendWithEstimatedGas(tx, fromAddress); + return sendWithEstimatedGas(this.web3, tx, fromAddress); } async merge( @@ -465,7 +638,7 @@ class BinaryVaultHelper extends ContractBase { marketIdBytes, amountWei.toString(10), ); - return sendWithEstimatedGas(tx, fromAddress); + return sendWithEstimatedGas(this.web3, tx, fromAddress); } async redeem( @@ -486,7 +659,7 @@ class BinaryVaultHelper extends ContractBase { marketIdBytes, amountWei.toString(10), ); - return sendWithEstimatedGas(tx, fromAddress); + return sendWithEstimatedGas(this.web3, tx, fromAddress); } } @@ -544,7 +717,7 @@ class FutarchyRouterHelper extends ContractBase { amountInWei.toString(10), minAmountOutWei.toString(10), ); - return sendWithEstimatedGas(tx, fromAddress); + return sendWithEstimatedGas(this.web3, tx, fromAddress); } } diff --git a/packages/commonwealth/client/scripts/views/components/PredictionMarket/useCollateralMeta.ts b/packages/commonwealth/client/scripts/views/components/PredictionMarket/useCollateralMeta.ts index 35eede71d27..cf8b9b6e652 100644 --- a/packages/commonwealth/client/scripts/views/components/PredictionMarket/useCollateralMeta.ts +++ b/packages/commonwealth/client/scripts/views/components/PredictionMarket/useCollateralMeta.ts @@ -13,16 +13,28 @@ const DEFAULT_COLLATERAL_META: CollateralMeta = { decimals: 18, }; -const KNOWN_COLLATERAL_META: Record = { +const BASE_MAINNET_CHAIN_ID = 8453; +const BASE_SEPOLIA_CHAIN_ID = 84532; +const EMPTY_COLLATERAL_META: Record = {}; + +const BASE_SEPOLIA_COLLATERAL_META: Record = { // Base Sepolia USDC '0x036cbd53842c5426634e7929541ec2318f3dcf7e': { symbol: 'USDC', decimals: 6 }, - // Base (and Base Sepolia) WETH + // Base Sepolia WETH + '0x4200000000000000000000000000000000000006': { + symbol: 'WETH', + decimals: 18, + }, +}; + +const BASE_MAINNET_COLLATERAL_META: Record = { + // Base Mainnet USDC + '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': { symbol: 'USDC', decimals: 6 }, + // Base Mainnet WETH '0x4200000000000000000000000000000000000006': { symbol: 'WETH', decimals: 18, }, - // Ethereum Mainnet USDC - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { symbol: 'USDC', decimals: 6 }, }; type UseCollateralMetaProps = { @@ -48,6 +60,15 @@ export const useCollateralMeta = ({ const chainRpc = (community as { ChainNode?: { url?: string } } | undefined)?.ChainNode ?.url ?? ''; + const ethChainId = + (community as { ChainNode?: { eth_chain_id?: number } } | undefined) + ?.ChainNode?.eth_chain_id ?? 0; + const knownCollateralMetaByChain = + ethChainId === BASE_MAINNET_CHAIN_ID + ? BASE_MAINNET_COLLATERAL_META + : ethChainId === BASE_SEPOLIA_CHAIN_ID + ? BASE_SEPOLIA_COLLATERAL_META + : EMPTY_COLLATERAL_META; useEffect(() => { const addr = collateralAddress?.trim() ?? ''; @@ -57,7 +78,7 @@ export const useCollateralMeta = ({ return; } const lower = addr.toLowerCase(); - const known = KNOWN_COLLATERAL_META[lower]; + const known = knownCollateralMetaByChain[lower]; if (known) { setCollateralMeta(known); // Known addresses are deterministic enough for display and avoid RPC dependency. @@ -75,7 +96,7 @@ export const useCollateralMeta = ({ return () => { cancelled = true; }; - }, [chainRpc, collateralAddress, readerAddress]); + }, [chainRpc, collateralAddress, knownCollateralMetaByChain, readerAddress]); return collateralMeta; }; diff --git a/packages/commonwealth/client/scripts/views/components/ThreadPredictionMarketTag/ThreadPredictionMarketTag.tsx b/packages/commonwealth/client/scripts/views/components/ThreadPredictionMarketTag/ThreadPredictionMarketTag.tsx index bab34595fef..ef3a8e4dac7 100644 --- a/packages/commonwealth/client/scripts/views/components/ThreadPredictionMarketTag/ThreadPredictionMarketTag.tsx +++ b/packages/commonwealth/client/scripts/views/components/ThreadPredictionMarketTag/ThreadPredictionMarketTag.tsx @@ -8,10 +8,7 @@ import CWPopover, { usePopover, } from 'client/scripts/views/components/component_kit/new_designs/CWPopover'; import { CWTag } from 'client/scripts/views/components/component_kit/new_designs/CWTag'; -import { - sumWeiValues, - weiToDisplayNumber, -} from 'client/scripts/views/pages/view_thread/predictionMarketUtils'; +import { predictionMarketTotalMintedDisplayNumber } from 'client/scripts/views/pages/view_thread/predictionMarketUtils'; import './ThreadPredictionMarketTag.scss'; @@ -54,8 +51,10 @@ const ThreadPredictionMarketTag = ({ const isPassLeading = passPct >= 50; const label = isPassLeading ? `PASS ${passPct}%` : `FAIL ${failPct}%`; const tagType = isPassLeading ? 'passed' : 'failed'; - const lockedDisplay = weiToDisplayNumber( - sumWeiValues(market.total_collateral, market.initial_liquidity), + const lockedDisplay = predictionMarketTotalMintedDisplayNumber( + market.status, + market.total_collateral, + market.initial_liquidity, collateralMeta.decimals, ); diff --git a/packages/commonwealth/client/scripts/views/modals/PredictionMarket/PredictionMarketEditorModal.tsx b/packages/commonwealth/client/scripts/views/modals/PredictionMarket/PredictionMarketEditorModal.tsx index ccca80f17c8..aa80f8a9603 100644 --- a/packages/commonwealth/client/scripts/views/modals/PredictionMarket/PredictionMarketEditorModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/PredictionMarket/PredictionMarketEditorModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { notifyError, @@ -43,8 +43,10 @@ import { } from './predictionMarketEditorValidation'; import { SyncPromptData } from './SyncPromptData'; -// Base Sepolia placeholder addresses; replace with chain config when available -const COLLATERAL_OPTIONS = [ +const BASE_MAINNET_CHAIN_ID = 8453; +const BASE_SEPOLIA_CHAIN_ID = 84532; + +const BASE_SEPOLIA_COLLATERAL_OPTIONS = [ { value: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', label: 'USDC', @@ -53,7 +55,17 @@ const COLLATERAL_OPTIONS = [ value: '0x4200000000000000000000000000000000000006', label: 'WETH', }, - { value: 'custom', label: 'Custom ERC20' }, +]; + +const BASE_MAINNET_COLLATERAL_OPTIONS = [ + { + value: '0x833589fCD6EDb6E08f4c7C32D4f71b54bDa02913', + label: 'USDC', + }, + { + value: '0x4200000000000000000000000000000000000006', + label: 'WETH', + }, ]; type Phase = 'form' | 'creating' | 'deploying' | 'success' | 'error'; @@ -70,15 +82,6 @@ type PredictionMarketEditorModalProps = { onGeneratePrompt?: () => void; }; -const INITIAL_FORM_VALUES = { - prompt: '', - collateralOption: COLLATERAL_OPTIONS[0], - customCollateralAddress: '', - durationDays: 14, - resolutionThreshold: THRESHOLD_DEFAULT, - initialLiquidity: '1', -}; - export const PredictionMarketEditorModal = ({ onModalClose, thread, @@ -104,6 +107,28 @@ export const PredictionMarketEditorModal = ({ const ethChainId = (community as { ChainNode?: { eth_chain_id?: number } } | undefined) ?.ChainNode?.eth_chain_id ?? 0; + const collateralOptions = useMemo( + () => [ + ...(ethChainId === BASE_MAINNET_CHAIN_ID + ? BASE_MAINNET_COLLATERAL_OPTIONS + : ethChainId === BASE_SEPOLIA_CHAIN_ID + ? BASE_SEPOLIA_COLLATERAL_OPTIONS + : BASE_SEPOLIA_COLLATERAL_OPTIONS), + { value: 'custom', label: 'Custom ERC20' }, + ], + [ethChainId], + ); + const initialFormValues = useMemo( + () => ({ + prompt: '', + collateralOption: collateralOptions[0], + customCollateralAddress: '', + durationDays: 14, + resolutionThreshold: THRESHOLD_DEFAULT, + initialLiquidity: '1', + }), + [collateralOptions], + ); const createMutation = useCreatePredictionMarketMutation(); const deployMutation = useDeployPredictionMarketMutation(); @@ -272,7 +297,7 @@ export const PredictionMarketEditorModal = ({ console.log(values)} > @@ -320,7 +345,7 @@ export const PredictionMarketEditorModal = ({ hookToForm label="Collateral token" isSearchable={false} - options={COLLATERAL_OPTIONS} + options={collateralOptions} placeholder="Select collateral" /> {watch('collateralOption')?.value === 'custom' && ( diff --git a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss index 99afd244279..745e88121bb 100644 --- a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss +++ b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.scss @@ -345,6 +345,28 @@ .alert-icon + * { color: colors.$rorange-700; } + + .balance-copy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + margin-left: 4px; + border: none; + background: none; + cursor: pointer; + color: colors.$rorange-700; + + &:hover { + color: colors.$rorange-500; + } + + &:focus-visible { + outline: 2px solid colors.$rorange-500; + outline-offset: 1px; + border-radius: 3px; + } + } } .loading-row { diff --git a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx index 4c9836170c0..93cb6548621 100644 --- a/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/PredictionMarketTradeModal/PredictionMarketTradeModal.tsx @@ -44,6 +44,10 @@ import CWTabsRow from '../../components/component_kit/new_designs/CWTabs/CWTabsR import { CWTextInput } from '../../components/component_kit/new_designs/CWTextInput'; import { CWTooltip } from '../../components/component_kit/new_designs/CWTooltip'; import FractionalValue from '../../components/FractionalValue'; +import { + PREDICTION_MARKET_LEDGER_DECIMALS, + weiToDisplayNumber, +} from '../../pages/view_thread/predictionMarketUtils'; import { CustomAddressOption, CustomAddressOptionElement, @@ -64,15 +68,28 @@ function formatTokenDisplay(wei: bigint, decimals = 18): string { return fractionalTrimmed ? `${whole}.${fractionalTrimmed}` : whole.toString(); } -function weiToDisplayNumber(wei: bigint, decimals = 18): number { - if (wei <= 0n) return 0; - const safeDecimals = Math.max(0, decimals); - const raw = wei.toString(); - if (safeDecimals === 0) return Number(raw); - const padded = raw.padStart(safeDecimals + 1, '0'); - const whole = padded.slice(0, -safeDecimals); - const frac = padded.slice(-safeDecimals).replace(/0+$/, ''); - return Number(frac ? `${whole}.${frac}` : whole); +function extractTxHash(input: string): string | null { + const match = input.match(/0x[a-fA-F0-9]{64}/); + return match ? match[0] : null; +} + +function formatTxHashShort(hash: string): string { + if (!hash.startsWith('0x') || hash.length < 14) return hash; + return `${hash.slice(0, 8)}...${hash.slice(-6)}`; +} + +function parseErrorForDisplay(message: string): { + userMessage: string; + txHash: string | null; +} { + const txHash = extractTxHash(message); + if (!txHash) return { userMessage: message, txHash: null }; + const userMessage = message + .replace(`Tx hash: ${txHash}`, '') + .replace(txHash, '') + .replace(/\s+/g, ' ') + .trim(); + return { userMessage, txHash }; } type Market = { @@ -190,6 +207,7 @@ export const PredictionMarketTradeModal = ({ const swapQuoteDebounceRef = useRef | null>( null, ); + const parsedError = errorMessage ? parseErrorForDisplay(errorMessage) : null; const { data: community } = useGetCommunityByIdQuery({ id: threadCommunityId, @@ -367,8 +385,10 @@ export const PredictionMarketTradeModal = ({ setSwapQuoteLoading(false); return; } - const swapDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountInWei = parseTokenAmount(swapAmount, swapDecimals); + const amountInWei = parseTokenAmount( + swapAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const chainIdForQuote = effectiveMarket.eth_chain_id ?? ethChainId; if ( amountInWei <= 0n || @@ -441,7 +461,6 @@ export const PredictionMarketTradeModal = ({ effectiveMarket.f_token_address, effectiveMarket.eth_chain_id, ethChainId, - collateralInfo?.decimals, ]); const isResolved = market.status === 'resolved'; @@ -451,10 +470,16 @@ export const PredictionMarketTradeModal = ({ const mintDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; const totalMintedDisplay = weiToDisplayNumber( marketCollateralOnChain ?? BigInt(market.total_collateral ?? '0'), - mintDecimals, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); + const pBalanceDisplay = weiToDisplayNumber( + pTokenBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); + const fBalanceDisplay = weiToDisplayNumber( + fTokenBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, ); - const pBalanceDisplay = weiToDisplayNumber(pTokenBalance, mintDecimals); - const fBalanceDisplay = weiToDisplayNumber(fTokenBalance, mintDecimals); const handleMint = async () => { const amountWei = parseTokenAmount(mintAmount, mintDecimals); @@ -522,8 +547,10 @@ export const PredictionMarketTradeModal = ({ setErrorMessage('Connect a wallet to swap.'); return; } - const swapDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountInWei = parseTokenAmount(swapAmount, swapDecimals); + const amountInWei = parseTokenAmount( + swapAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); if (amountInWei <= 0n) { setErrorMessage('Enter a valid amount.'); return; @@ -531,8 +558,12 @@ export const PredictionMarketTradeModal = ({ const sellBalance = swapBuyPass ? fTokenBalance : pTokenBalance; if (amountInWei > sellBalance) { const tokenName = swapBuyPass ? 'FAIL' : 'PASS'; + const bal = formatTokenDisplay( + sellBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); setErrorMessage( - `Insufficient ${tokenName} tokens. You have ${formatTokenDisplay(sellBalance, swapDecimals)} ${tokenName}.`, + `Insufficient ${tokenName} tokens. You have ${bal} ${tokenName}.`, ); return; } @@ -611,14 +642,19 @@ export const PredictionMarketTradeModal = ({ }; const handleMerge = async () => { - const mergeDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountWei = parseTokenAmount(mergeAmount, mergeDecimals); + const amountWei = parseTokenAmount( + mergeAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); if (amountWei <= 0n) { setErrorMessage('Enter a valid amount.'); return; } if (amountWei > minBalanceForMerge) { - const maxDisplay = formatTokenDisplay(minBalanceForMerge, mergeDecimals); + const maxDisplay = formatTokenDisplay( + minBalanceForMerge, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); setErrorMessage( `Insufficient balance. You can merge at most ${maxDisplay} (limited by your PASS/FAIL balance).`, ); @@ -684,12 +720,14 @@ export const PredictionMarketTradeModal = ({ setErrorMessage('Market has no winner yet.'); return; } - const redeemDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountWei = parseTokenAmount(redeemAmount, redeemDecimals); + const amountWei = parseTokenAmount( + redeemAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const maxRedeem = winner === 1 ? pTokenBalance : fTokenBalance; if (amountWei <= 0n || amountWei > maxRedeem) { setErrorMessage( - `Enter a valid amount (max ${formatTokenDisplay(maxRedeem, redeemDecimals)}).`, + `Enter a valid amount (max ${formatTokenDisplay(maxRedeem, PREDICTION_MARKET_LEDGER_DECIMALS)}).`, ); return; } @@ -789,8 +827,10 @@ export const PredictionMarketTradeModal = ({ ); } if (activeTab === 'swap') { - const swapDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountInWei = parseTokenAmount(swapAmount, swapDecimals); + const amountInWei = parseTokenAmount( + swapAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const minOutAfterSlippageWei = swapQuoteOutWei === null ? 0n @@ -830,7 +870,11 @@ export const PredictionMarketTradeModal = ({ You Sell - Balance: {formatTokenDisplay(sellBalance, swapDecimals)} + Balance:{' '} + {formatTokenDisplay( + sellBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, + )}
@@ -852,7 +896,10 @@ export const PredictionMarketTradeModal = ({ className="max-link" onClick={() => setSwapAmount( - formatTokenDisplay(sellBalance, swapDecimals), + formatTokenDisplay( + sellBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, + ), ) } > @@ -875,7 +922,11 @@ export const PredictionMarketTradeModal = ({ You Buy - Balance: {formatTokenDisplay(buyBalance, swapDecimals)} + Balance:{' '} + {formatTokenDisplay( + buyBalance, + PREDICTION_MARKET_LEDGER_DECIMALS, + )}
@@ -892,13 +943,19 @@ export const PredictionMarketTradeModal = ({ : swapQuoteError ? '—' : swapQuoteOutWei !== null && amountInWei > 0n - ? formatTokenDisplay(swapQuoteOutWei, swapDecimals) + ? formatTokenDisplay( + swapQuoteOutWei, + PREDICTION_MARKET_LEDGER_DECIMALS, + ) : '—'} {minOutAfterSlippageWei > 0n && swapQuoteOutWei !== null && ( Min. received (≤{DEFAULT_SLIPPAGE_BPS / 100}% slippage):{' '} - {formatTokenDisplay(minOutAfterSlippageWei, swapDecimals)} + {formatTokenDisplay( + minOutAfterSlippageWei, + PREDICTION_MARKET_LEDGER_DECIMALS, + )} )} {swapQuoteError && ( @@ -920,8 +977,10 @@ export const PredictionMarketTradeModal = ({ ); } if (activeTab === 'merge') { - const mergeDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountWei = parseTokenAmount(mergeAmount, mergeDecimals); + const amountWei = parseTokenAmount( + mergeAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const validMerge = amountWei > 0n && amountWei <= minBalanceForMerge; const limitedByPass = pTokenBalance <= fTokenBalance; const mergeDisplay = validMerge ? mergeAmount || '0' : '0'; @@ -933,7 +992,11 @@ export const PredictionMarketTradeModal = ({ Amount to merge - Available: {formatTokenDisplay(minBalanceForMerge, mergeDecimals)}{' '} + Available:{' '} + {formatTokenDisplay( + minBalanceForMerge, + PREDICTION_MARKET_LEDGER_DECIMALS, + )}{' '} (Limited by  {limitedByPass ? 'PASS' : 'FAIL'}) @@ -956,7 +1019,10 @@ export const PredictionMarketTradeModal = ({ buttonWidth="narrow" onClick={() => setMergeAmount( - formatTokenDisplay(minBalanceForMerge, mergeDecimals), + formatTokenDisplay( + minBalanceForMerge, + PREDICTION_MARKET_LEDGER_DECIMALS, + ), ) } /> @@ -1003,9 +1069,11 @@ export const PredictionMarketTradeModal = ({ } // redeem const canRedeem = winner === 1 || winner === 2; - const redeemDecimals = collateralInfo?.decimals ?? COLLATERAL_DECIMALS; const redeemCollateralSymbol = collateralInfo?.symbol ?? 'ETH'; - const amountWei = parseTokenAmount(redeemAmount, redeemDecimals); + const amountWei = parseTokenAmount( + redeemAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const maxRedeem = winner === 1 ? pTokenBalance : fTokenBalance; const winningToken = winner === 1 ? 'PASS' : 'FAIL'; const validRedeem = canRedeem && amountWei > 0n && amountWei <= maxRedeem; @@ -1025,7 +1093,11 @@ export const PredictionMarketTradeModal = ({ Winning token amount ({winningToken}) - Available: {formatTokenDisplay(maxRedeem, redeemDecimals)}{' '} + Available:{' '} + {formatTokenDisplay( + maxRedeem, + PREDICTION_MARKET_LEDGER_DECIMALS, + )}{' '} {winningToken}
@@ -1046,7 +1118,12 @@ export const PredictionMarketTradeModal = ({ buttonHeight="sm" buttonWidth="narrow" onClick={() => - setRedeemAmount(formatTokenDisplay(maxRedeem, redeemDecimals)) + setRedeemAmount( + formatTokenDisplay( + maxRedeem, + PREDICTION_MARKET_LEDGER_DECIMALS, + ), + ) } /> @@ -1130,9 +1207,10 @@ export const PredictionMarketTradeModal = ({ collateralInfo.balanceWei) : activeTab === 'swap' ? (() => { - const swapDecimals = - collateralInfo?.decimals ?? COLLATERAL_DECIMALS; - const amountWei = parseTokenAmount(swapAmount, swapDecimals); + const amountWei = parseTokenAmount( + swapAmount, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const sellBalance = swapBuyPass ? fTokenBalance : pTokenBalance; const hasBalanceData = userPosition != null || onChainBalances != null; @@ -1146,25 +1224,17 @@ export const PredictionMarketTradeModal = ({ : activeTab === 'merge' ? !activeAddress || !mergeAmount || - parseTokenAmount( - mergeAmount, - collateralInfo?.decimals ?? COLLATERAL_DECIMALS, - ) <= 0n || - parseTokenAmount( - mergeAmount, - collateralInfo?.decimals ?? COLLATERAL_DECIMALS, - ) > minBalanceForMerge + parseTokenAmount(mergeAmount, PREDICTION_MARKET_LEDGER_DECIMALS) <= + 0n || + parseTokenAmount(mergeAmount, PREDICTION_MARKET_LEDGER_DECIMALS) > + minBalanceForMerge : !activeAddress || (winner !== 1 && winner !== 2) || !redeemAmount || - parseTokenAmount( - redeemAmount, - collateralInfo?.decimals ?? COLLATERAL_DECIMALS, - ) <= 0n || - parseTokenAmount( - redeemAmount, - collateralInfo?.decimals ?? COLLATERAL_DECIMALS, - ) > (winner === 1 ? pTokenBalance : fTokenBalance); + parseTokenAmount(redeemAmount, PREDICTION_MARKET_LEDGER_DECIMALS) <= + 0n || + parseTokenAmount(redeemAmount, PREDICTION_MARKET_LEDGER_DECIMALS) > + (winner === 1 ? pTokenBalance : fTokenBalance); return (
@@ -1566,7 +1636,33 @@ export const PredictionMarketTradeModal = ({ iconSize="small" className="alert-icon" /> - {errorMessage} +
+ + {parsedError?.userMessage ?? errorMessage} + + {parsedError?.txHash && ( + + Tx hash: {formatTxHashShort(parsedError.txHash)}{' '} + + + )} +
)} {isLoading && ( diff --git a/packages/commonwealth/client/scripts/views/pages/ExplorePage/PredictionMarketsList/ExplorePredictionMarketCard.tsx b/packages/commonwealth/client/scripts/views/pages/ExplorePage/PredictionMarketsList/ExplorePredictionMarketCard.tsx index 13c9a8dc994..2fa066a018c 100644 --- a/packages/commonwealth/client/scripts/views/pages/ExplorePage/PredictionMarketsList/ExplorePredictionMarketCard.tsx +++ b/packages/commonwealth/client/scripts/views/pages/ExplorePage/PredictionMarketsList/ExplorePredictionMarketCard.tsx @@ -1,7 +1,8 @@ import { getThreadUrl } from '@hicommonwealth/shared'; import { useCollateralMeta } from 'client/scripts/views/components/PredictionMarket/useCollateralMeta'; import { - sumWeiValues, + PREDICTION_MARKET_LEDGER_DECIMALS, + predictionMarketTotalMintedDisplayNumber, weiToDisplayNumber, } from 'client/scripts/views/pages/view_thread/predictionMarketUtils'; import moment from 'moment'; @@ -87,17 +88,17 @@ export const ExplorePredictionMarketCard = ({ }); const timeDisplay = formatTimeLeft(market.end_time, market.status); - const totalMintedWei = sumWeiValues( + const totalMinted = predictionMarketTotalMintedDisplayNumber( + market.status, market.total_collateral, market.initial_liquidity, - ); - const totalMinted = weiToDisplayNumber( - totalMintedWei, collateralMeta.decimals, ); + // Backend mixes collateral (collateral decimals) with swap legs (18-decimal outcome tokens). + // Single scale is approximate; 18 matches outcome-token-dominant activity. const volume = weiToDisplayNumber( market.market_volume ?? '0', - collateralMeta.decimals, + PREDICTION_MARKET_LEDGER_DECIMALS, ); const handleClick = (e: React.MouseEvent) => { diff --git a/packages/commonwealth/client/scripts/views/pages/HomePage/ActivePredictionMarketList/ActivePredictionMarketList.tsx b/packages/commonwealth/client/scripts/views/pages/HomePage/ActivePredictionMarketList/ActivePredictionMarketList.tsx index 802fe2494e3..a34fdeace80 100644 --- a/packages/commonwealth/client/scripts/views/pages/HomePage/ActivePredictionMarketList/ActivePredictionMarketList.tsx +++ b/packages/commonwealth/client/scripts/views/pages/HomePage/ActivePredictionMarketList/ActivePredictionMarketList.tsx @@ -5,10 +5,7 @@ import { CWButton } from 'client/scripts/views/components/component_kit/new_desi import FractionalValue from 'client/scripts/views/components/FractionalValue'; import { useCollateralMeta } from 'client/scripts/views/components/PredictionMarket/useCollateralMeta'; import { Skeleton } from 'client/scripts/views/components/Skeleton'; -import { - sumWeiValues, - weiToDisplayNumber, -} from 'client/scripts/views/pages/view_thread/predictionMarketUtils'; +import { predictionMarketTotalMintedDisplayNumber } from 'client/scripts/views/pages/view_thread/predictionMarketUtils'; import moment from 'moment'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; @@ -68,12 +65,10 @@ const PredictionMarketCardCompact = ({ collateralAddress: market.collateral_address, }); - const totalMintedWei = sumWeiValues( + const totalMinted = predictionMarketTotalMintedDisplayNumber( + market.status, market.total_collateral, market.initial_liquidity, - ); - const totalMinted = weiToDisplayNumber( - totalMintedWei, collateralMeta.decimals, ); const navigate = useCommonNavigate(); diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/CollateralMetaDecimalsDevPanel.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/CollateralMetaDecimalsDevPanel.tsx new file mode 100644 index 00000000000..06313baa885 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/CollateralMetaDecimalsDevPanel.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { CWText } from '../../components/component_kit/cw_text'; +import { useCollateralMeta } from '../../components/PredictionMarket/useCollateralMeta'; + +/** Dev-only: one hook per row (Rules of Hooks). */ +const CollateralMetaDevRow = ({ + communityId, + label, + tokenAddress, +}: { + communityId: string; + label: string; + tokenAddress: string; +}) => { + const meta = useCollateralMeta({ + communityId, + collateralAddress: tokenAddress, + }); + return ( +
+ + {label} + + + {meta.symbol} · {meta.decimals} decimals + + + {tokenAddress} + +
+ ); +}; + +/** Dev-only smoke test for `useCollateralMeta` (known map vs RPC). */ +export const CollateralMetaDecimalsDevPanel = ({ + communityId, +}: { + communityId: string; +}) => { + return ( +
+ + useCollateralMeta (dev): USDC → 6, WETH → 18, DAI → 18 on Base (DAI + usually via RPC). Other communities use that chain's RPC. + + + + +
+ ); +}; diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx index 76d7221d491..7de735b1b8a 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/ThreadPredictionMarketCard.tsx @@ -41,7 +41,11 @@ import { DeployDraftPredictionMarketModal } from '../../modals/PredictionMarket/ import { PredictionMarketResolveModal } from '../../modals/PredictionMarket/PredictionMarketResolveModal'; import { PredictionMarketTradeModal } from '../../modals/PredictionMarketTradeModal'; import './poll_cards.scss'; -import { weiToDisplayNumber } from './predictionMarketUtils'; +import { + PREDICTION_MARKET_LEDGER_DECIMALS, + predictionMarketTotalMintedDisplayNumber, + weiToDisplayNumber, +} from './predictionMarketUtils'; import './ThreadPredictionMarketCard.scss'; function formatCollateralBalance(wei: bigint, decimals: number): string { @@ -52,20 +56,13 @@ function formatCollateralBalance(wei: bigint, decimals: number): string { return `${whole}.${frac.toString().padStart(2, '0').slice(0, 2)}`; } -function parseWeiString(value?: string | null): bigint { - try { - return BigInt(value ?? '0'); - } catch { - return 0n; - } -} - export type PredictionMarketResult = { id: number; thread_id: number; prompt: string; status: string; total_collateral?: string; + initial_liquidity?: string | null; current_probability?: number; duration?: number; resolution_threshold?: number; @@ -159,10 +156,6 @@ export const ThreadPredictionMarketCard = ({ const [isTradeModalOpen, setIsTradeModalOpen] = useState(false); const [tradeRefreshNonce, setTradeRefreshNonce] = useState(0); const [timeDisplay, setTimeDisplay] = useState(null); - const [onChainPassFailBalances, setOnChainPassFailBalances] = useState<{ - p: bigint; - f: bigint; - } | null>(null); const user = useUserStore(); const uniqueAddresses = getUniqueUserAddresses({ forChain: ChainBase.Ethereum }) ?? []; @@ -292,48 +285,6 @@ export const ThreadPredictionMarketCard = ({ return () => clearInterval(interval); }, [market?.end_time, market]); - // Match trade modal: if API has no position row, read PASS/FAIL balances from chain - useEffect(() => { - if (userPosition) { - setOnChainPassFailBalances(null); - return; - } - if ( - !chainRpc || - !selectedAddress || - !market?.p_token_address || - !market?.f_token_address - ) { - return; - } - let cancelled = false; - getPredictionMarketBalancesFromChain( - chainRpc, - selectedAddress, - market.p_token_address, - market.f_token_address, - ) - .then(({ pTokenBalanceWei, fTokenBalanceWei }) => { - if (!cancelled) - setOnChainPassFailBalances({ - p: pTokenBalanceWei, - f: fTokenBalanceWei, - }); - }) - .catch(() => { - if (!cancelled) setOnChainPassFailBalances(null); - }); - return () => { - cancelled = true; - }; - }, [ - chainRpc, - selectedAddress, - market?.p_token_address, - market?.f_token_address, - userPosition, - ]); - useEffect(() => { const addr = market?.collateral_address; const isZero = !addr || addr.toLowerCase() === ZERO_ADDRESS.toLowerCase(); @@ -513,31 +464,27 @@ export const ThreadPredictionMarketCard = ({ (userPosition?.f_token_balance ? BigInt(String(userPosition.f_token_balance)) : 0n); - const pBalanceDisplay = weiToDisplayNumber(pBalanceWei, decimals); - const fBalanceDisplay = weiToDisplayNumber(fBalanceWei, decimals); - const pWei = userPosition - ? BigInt( - String( - (userPosition as { p_token_balance: string }).p_token_balance ?? '0', - ), - ) - : (onChainPassFailBalances?.p ?? 0n); - const fWei = userPosition - ? BigInt( - String( - (userPosition as { f_token_balance: string }).f_token_balance ?? '0', - ), - ) - : (onChainPassFailBalances?.f ?? 0n); - const pBalance = formatCollateralBalance(pWei, decimals); - const fBalance = formatCollateralBalance(fWei, decimals); + const pBalanceDisplay = weiToDisplayNumber( + pBalanceWei, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); + const fBalanceDisplay = weiToDisplayNumber( + fBalanceWei, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); const passProbability = market?.current_probability ?? 0.5; const failProbability = 1 - passProbability; - const totalMintedWei = - marketCollateralOnChain ?? parseWeiString(market?.total_collateral); - const totalMintedDisplay = weiToDisplayNumber(totalMintedWei, decimals); - const marketVolumeDisplay = weiToDisplayNumber(marketVolume, decimals); + const totalMintedDisplay = predictionMarketTotalMintedDisplayNumber( + market?.status, + marketCollateralOnChain ?? market?.total_collateral, + market?.initial_liquidity, + decimals, + ); + const marketVolumeDisplay = weiToDisplayNumber( + marketVolume, + PREDICTION_MARKET_LEDGER_DECIMALS, + ); if (marketProp === undefined && isLoading) { return ( diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/ViewThreadPage.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/ViewThreadPage.tsx index 497afba6359..3b6e9f7744f 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/ViewThreadPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/ViewThreadPage.tsx @@ -93,6 +93,7 @@ import { SnapshotPollCardContainer } from '../Snapshots/ViewSnapshotProposal/Sna import { CommentTree } from '../discussions/CommentTree'; import { StreamingReplyInstance } from '../discussions/CommentTree/TreeHierarchy'; import { clearEditingLocalStorage } from '../discussions/CommentTree/helpers'; +import { CollateralMetaDecimalsDevPanel } from './CollateralMetaDecimalsDevPanel'; import { LinkedUrlCard } from './LinkedUrlCard'; import { ThreadPollCard } from './ThreadPollCard'; import { ThreadPollEditorCard } from './ThreadPollEditorCard'; @@ -753,6 +754,14 @@ const ViewThreadPage = ({ identifier }: ViewThreadPageProps) => { }, ] : []), + ...(communityId + ? [ + { + label: 'Dev: PM collateral decimals', + item: , + }, + ] + : []), ]; const governanceType = proposal diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/predictionMarketUtils.ts b/packages/commonwealth/client/scripts/views/pages/view_thread/predictionMarketUtils.ts index 9b07979e87b..8bac9843c14 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/predictionMarketUtils.ts +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/predictionMarketUtils.ts @@ -1,3 +1,53 @@ +/** + * PASS/FAIL outcome token amounts (balances, swaps) use this fixed 1e18 scale. + * + * **Total minted / locked collateral from the indexer or vault logs** (`total_collateral`, + * `getMarketCollateralBalanceFromLogs`) is stored in **protocol 1e18 fixed-point** (same scale + * as mint event `collateral_amount` in tests), not necessarily the collateral ERC-20’s + * `decimals()`. + * + * **`initial_liquidity` on a draft** is stored in **native ERC-20** smallest units (from + * `convertInitialLiquidityToWei`). Use `collateralTokenDecimals` only for that field. + * + * Aggregates that mix collateral legs with swap notionals (e.g. `market_volume` SQL) are not + * a single decimal scale; display uses 18 as an approximation where noted. + */ +export const PREDICTION_MARKET_LEDGER_DECIMALS = 18; + +export function parseMarketAmountBigint( + value?: string | bigint | null, +): bigint { + try { + if (typeof value === 'bigint') return value; + return BigInt(value ?? '0'); + } catch { + return 0n; + } +} + +/** + * Human-readable “total minted” for PM UI: draft uses `initial_liquidity` + token decimals; + * live markets use ledger totals + {@link PREDICTION_MARKET_LEDGER_DECIMALS}. + */ +export function predictionMarketTotalMintedDisplayNumber( + status: string | undefined, + ledgerTotalWei: string | bigint | null | undefined, + initialLiquidity: string | null | undefined, + collateralTokenDecimals: number, +): number { + const draft = (status ?? '').toLowerCase() === 'draft'; + if (draft) { + return weiToDisplayNumber( + parseMarketAmountBigint(initialLiquidity), + collateralTokenDecimals, + ); + } + return weiToDisplayNumber( + parseMarketAmountBigint(ledgerTotalWei), + PREDICTION_MARKET_LEDGER_DECIMALS, + ); +} + /** * Format collateral amount from wei/smallest unit to human-readable string. * Assumes 18 decimals. Returns e.g. "1.50K", "2.00M", or "0.00" on error.