Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"start": "next start",
"lint": "next lint",
"generate-seo": "node scripts/generate-seo.mjs",
"test:unit": "node --test 'test/**/*.test.mjs'",
"build-only": "next build",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
Expand Down
111 changes: 59 additions & 52 deletions src/components/debug/hooks/useCreateProposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,54 @@ import { ethers } from 'ethers';
import { useAccount, useChainId, useSwitchChain, useWalletClient, usePublicClient } from 'wagmi';
import { CHAIN_CONFIG, getExplorerTxUrl, getExplorerAddressUrl } from '../constants/chainConfig';
import { getEthersSigner } from '../../../utils/ethersAdapters';
import { FUTARCHY_FACTORY_ABI, buildProposalParams } from '../../../features/marketCreation/proposalCalldata';
import { findCreatedProposalAddress } from '../../../features/marketCreation/receiptEvents';
import { getRpcUrls } from '../../../utils/getRpcUrl';

// Futarchy Factory ABI
const FUTARCHY_FACTORY_ABI = [
'function createProposal((string,address,address,string,string,uint256,uint32)) returns (address)',
'function proposals(uint256) view returns (address)',
'function marketsCount() view returns (uint256)'
];
// ethers v5 error codes that mean the RPC transport failed (try the next
// endpoint) rather than the call itself reverting (a real simulation result).
const TRANSPORT_ERROR_CODES = new Set(['NETWORK_ERROR', 'TIMEOUT', 'SERVER_ERROR']);

/**
* Signerless createProposal simulation via callStatic, iterating the app's
* multi-RPC list. Shares ABI + param assembly with the broadcast path so the
* two provably encode identical calldata.
* @returns {{ok:boolean, message:string, proposalAddress?:string}}
*/
export async function simulateProposal(formData) {
const chainId = formData.chainId || 100;
const chainConfig = CHAIN_CONFIG[chainId];
if (!chainConfig) return { ok: false, message: `Invalid chain: ${chainId}` };

let params;
try {
params = buildProposalParams(formData);
} catch (e) {
return { ok: false, message: e.message };
}

let lastTransportError = null;
for (const rpcUrl of getRpcUrls(chainId)) {
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const factory = new ethers.Contract(chainConfig.factoryAddress, FUTARCHY_FACTORY_ABI, provider);
try {
const predicted = await factory.callStatic.createProposal(params, {
from: '0x000000000000000000000000000000000000dEaD',
});
return { ok: true, message: `Would succeed — new proposal ${predicted}`, proposalAddress: predicted };
} catch (e) {
if (TRANSPORT_ERROR_CODES.has(e.code)) {
lastTransportError = e;
continue; // RPC problem, not a verdict on the call — try the next endpoint
}
return { ok: false, message: `Would revert: ${e.reason || e.shortMessage || e.message}` };
}
}
return {
ok: false,
message: `All RPC endpoints unreachable — try again (${lastTransportError?.message || 'no endpoints'})`,
};
}

/**
* Hook for creating Futarchy proposals with multi-chain support
Expand Down Expand Up @@ -62,7 +103,7 @@ export const useCreateProposal = () => {
if (!formData.currencyToken || !/^0x[a-fA-F0-9]{40}$/.test(formData.currencyToken)) {
return { isValid: false, error: 'Invalid currency token address' };
}
if (!formData.openingTime) {
if (!formData.openingTime && !Number.isFinite(formData.openingTimeUnix)) {
return { isValid: false, error: 'Opening time is required' };
}
return { isValid: true };
Expand Down Expand Up @@ -139,19 +180,8 @@ export const useCreateProposal = () => {
freshSigner
);

// Convert opening time to unix timestamp
const openingTimeUnix = Math.floor(new Date(formData.openingTime).getTime() / 1000);

// Prepare proposal parameters (tuple)
const params = [
formData.marketName,
formData.companyToken,
formData.currencyToken,
formData.category,
formData.language,
formData.minBond,
openingTimeUnix
];
// Shared with simulateProposal — one calldata builder for both paths.
const params = buildProposalParams(formData);

console.log('Creating proposal with params:', params);
console.log('Factory address:', chainConfig.factoryAddress);
Expand All @@ -170,43 +200,20 @@ export const useCreateProposal = () => {
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt);

// Get new proposal address
let newProposalAddress = null;

// Try to parse event logs first
try {
const iface = new ethers.utils.Interface([
'event ProposalCreated(address indexed proposal, string marketName)'
]);
for (const log of receipt.logs) {
try {
const parsed = iface.parseLog({ topics: log.topics, data: log.data });
if (parsed && parsed.name === 'ProposalCreated') {
newProposalAddress = parsed.args[0];
break;
}
} catch { }
}
} catch { }

// Fallback: get from marketsCount
if (!newProposalAddress) {
try {
const count = await factory.marketsCount();
if (count.gt(0)) {
newProposalAddress = await factory.proposals(count.sub(1));
}
} catch (e) {
console.error('Failed to get proposal address:', e);
}
}
// The factory's NewProposal event is the only deterministic result
// for this transaction. Never use the latest global proposal entry:
// another transaction may have landed before this receipt is read.
const newProposalAddress = findCreatedProposalAddress(
receipt.logs,
chainConfig.factoryAddress,
);

setProposalAddress(newProposalAddress);
setStatus({
type: 'success',
type: newProposalAddress ? 'success' : 'error',
message: newProposalAddress
? `✅ Proposal created successfully!`
: '✅ Transaction confirmed! Check explorer for proposal address.'
: 'Transaction confirmed, but the factory NewProposal event was missing; cannot safely continue.'
});

return {
Expand Down
Loading
Loading