import { pick } from 'lodash'; import Web3 from 'web3'; import { TransactionObject } from 'web3/eth/types'; import EthContract from 'web3/eth/contract'; import { TApp } from './store'; import CrowdFundFactory from 'contracts/contracts/CrowdFundFactory.json'; import CrowdFund from 'contracts/contracts/CrowdFund.json'; import { Proposal, Contract, INITIAL_CONTRACT, ContractMilestone, ContractContributor, ContractMethodInput, INITIAL_CONTRACT_CONTRIBUTOR, INITIAL_CONTRACT_MILESTONE, } from './types'; type Web3Method = (index: number) => TransactionObject; export async function initializeWeb3(app: TApp): Promise { let web3 = (window as any).web3; if (web3) { app.web3Type = 'injected'; web3 = new Web3(web3.currentProvider); } else if (process.env.NODE_ENV !== 'production') { const localProviderString = 'http://localhost:8545'; const provider = new Web3.providers.HttpProvider(localProviderString); web3 = new Web3(provider); app.web3Type = 'local - ' + localProviderString; } else { console.error('No web3 detected!'); app.web3Enabled = false; return null; } try { app.ethNetId = await web3.eth.net.getId(); getAccount(app, web3); window.setInterval(() => getAccount(app, web3), 10000); checkCrowdFundFactory(app, web3); app.web3Enabled = true; return web3; } catch (e) { if (e.message && e.message.startsWith('Invalid JSON RPC response:')) { console.warn('Unable to interact with web3. Web3 will be disabled.'); } else { console.error('There was a problem interacting with web3.', e); } app.web3Enabled = false; return null; } } async function getAccount(app: TApp, web3: Web3) { await web3.eth.getAccounts((_, accounts) => { app.ethAccount = (accounts.length && accounts[0]) || ''; }); } function checkCrowdFundFactory(app: TApp, web3: Web3) { web3.eth.net.getId((_, netId) => { const networks = Object.keys((CrowdFundFactory as any).networks).join(', '); if (!(CrowdFundFactory as any).networks[netId]) { app.crowdFundFactoryDefinitionStatus = `network mismatch (has ${networks})`; } else { app.crowdFundFactoryDefinitionStatus = `loaded, has correct network (${networks})`; } }); } export async function proposalContractSend( app: TApp, web3: Web3, proposalId: number, methodName: keyof Contract, inputs: ContractMethodInput[], args: any[], ) { const storeProposal = app.proposals.find(p => p.proposalId === proposalId); if (storeProposal) { const { proposalAddress } = storeProposal; await getAccount(app, web3); const storeMethod = storeProposal.contract[methodName]; const contract = new web3.eth.Contract(CrowdFund.abi, proposalAddress); app.crowdFundGeneralStatus = `calling (${storeProposal.title}).${methodName}...`; try { console.log(args); storeMethod.status = 'loading'; storeMethod.error = ''; if (inputs.length === 1 && inputs[0].name === 'value') { await contract.methods[methodName]() .send({ from: app.ethAccount, value: args[0], }) .once('transactionHash', () => { storeMethod.status = 'waiting'; }) .once('confirmation', () => { storeMethod.status = 'loaded'; }); } else { await contract.methods[methodName](...args) .send({ from: app.ethAccount }) .once('transactionHash', () => { storeMethod.status = 'waiting'; }) .once('confirmation', () => { storeMethod.status = 'loaded'; }); } } catch (e) { console.error(e); storeMethod.error = e.message || e.toString(); storeMethod.status = 'error'; } app.crowdFundGeneralStatus = `idle`; } } export async function populateProposalContract( app: TApp, web3: Web3, proposalId: number, ) { const storeProposal = app.proposals.find(p => p.proposalId === proposalId); if (storeProposal) { const { proposalAddress } = storeProposal; const contract = new web3.eth.Contract(CrowdFund.abi, proposalAddress); storeProposal.contractStatus = 'loading...'; const methods = Object.keys(INITIAL_CONTRACT).map(k => k as keyof Contract); for (const method of methods) { const methodType = INITIAL_CONTRACT[method].type; if (methodType !== 'deep' && methodType !== 'send') { app.crowdFundGeneralStatus = `calling (${storeProposal.title}).${method}...`; const storeMethod = storeProposal.contract[method]; const contractMethod = contract.methods[method]; try { storeMethod.status = 'loading'; if (methodType === 'eth' && method === 'getBalance') { storeMethod.value = (await web3.eth.getBalance(proposalAddress)) + ''; } else if (methodType === 'array') { const result = await collectArrayElements(contractMethod, app.ethAccount); if (method === 'milestones') { storeMethod.value = result.map(r => // clean-up incoming object before attaching to store cleanClone(INITIAL_CONTRACT_MILESTONE, r), ); } else { storeMethod.value = result.map(r => r + ''); } } else { storeMethod.value = (await contractMethod().call({ from: app.ethAccount })) + ''; } storeMethod.status = 'loaded'; } catch (e) { console.error(proposalId, method, e); storeMethod.status = 'error'; } } } await populateProposalContractDeep(storeProposal.contract, contract, app.ethAccount); storeProposal.contractStatus = 'updated @ ' + new Date().toISOString(); app.crowdFundGeneralStatus = `idle`; } } async function populateProposalContractDeep( storeContract: Proposal['contract'], contract: EthContract, fromAcct: string, ) { storeContract.contributors.status = 'loading'; const milestones = storeContract.milestones.value as ContractMilestone[]; const contributorList = storeContract.contributorList.value as string[]; const contributors = await Promise.all( contributorList.map(async addr => { const contributor = await contract.methods .contributors(addr) .call({ from: fromAcct }); contributor.address = addr; contributor.milestoneNoVotes = await Promise.all( milestones.map( async (_, idx) => await contract.methods .getContributorMilestoneVote(addr, idx) .call({ from: fromAcct }), ), ); contributor.contributionAmount = await contract.methods .getContributorContributionAmount(addr) .call({ from: fromAcct }); // clean-up incoming object before attaching to store return cleanClone(INITIAL_CONTRACT_CONTRIBUTOR, contributor); }), ); storeContract.contributors.value = contributors as ContractContributor[]; storeContract.contributors.status = 'loaded'; } // clone and filter keys by keySource object's keys export function cleanClone(keySource: T, target: Partial) { const sourceKeys = Object.keys(keySource); const fullClone = { ...(target as object) }; const clone = pick(fullClone, sourceKeys); return clone as T; } export async function collectArrayElements( method: Web3Method, account: string, ): Promise { const arrayElements = []; let noError = true; let index = 0; while (noError) { try { arrayElements.push(await method(index).call({ from: account })); index += 1; } catch (e) { noError = false; } } return arrayElements; }