diff --git a/package.json b/package.json index 33e296b..947db74 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "react-tooltip": "^3.11.1", "redux": "^4.0.4", "redux-form-validators": "^3.3.2", - "rimble-ui": "^0.9.8", + "rimble-ui": "^0.10.0", "styled-components": "^4.3.2", "styled-spinkit": "^0.7.4", "typescript": "3.5.2", diff --git a/src/App.tsx b/src/App.tsx index 4e0c944..855fd7e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,12 @@ import MyLoan from './components/pages/myLoan' import { AppPath } from './constant/appPath' import { Provider } from 'react-redux' import { store } from './store' +import { setToastProvider } from './store/toastProvider' +import { ToastMessage } from 'rimble-ui' + +const dispatchToastProvider = node => { + store.dispatch(setToastProvider(node)) +} const App: React.FC = () => { return ( @@ -40,6 +46,7 @@ const App: React.FC = () => { + dispatchToastProvider(node)} /> ) } diff --git a/src/components/pages/faucet/component.tsx b/src/components/pages/faucet/component.tsx index 41ef283..a57d261 100644 --- a/src/components/pages/faucet/component.tsx +++ b/src/components/pages/faucet/component.tsx @@ -2,12 +2,14 @@ import React from 'react' import { ChasingDots } from 'styled-spinkit' import walletIcon from '../../../images/icons/wallet.svg' import { RouteComponentProps, withRouter } from 'react-router-dom' +import { store } from '../../../store' import { Container } from '../../../styles/bases' import { Margin, Padding } from '../../../styles/utils' import { Row, Col, Button, Spinner } from '../../lib' import { FaucetActionMobile, FaucetBox, FaucetWrapper } from './styled' import RenderConnectWallet from '../renderConnectWallet' import contractAddresses from '../../../config/ines.fund.js' +import { networksExplorer } from '../../../utils/getWeb3' import { connectToWallet, prepBigNumber } from '../../../utils/web3Utils' import { getDeployedFromConfig } from '../../../utils/getDeployed' import { request } from '../../../utils/tokenFaucet' @@ -37,6 +39,31 @@ class Faucet extends React.Component { loaded: false, } + get txEvents() { + const { networkId, toastProvider } = store.getState() + return { + onTransactionHash: hash => + toastProvider.addMessage('Processing request...', { + secondaryMessage: 'Check progress on Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${hash}`, + actionText: 'Check', + variant: 'processing', + }), + onReceipt: receipt => + toastProvider.addMessage('Request successful...', { + secondaryMessage: 'View transaction Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${receipt.transactionHash}`, + actionText: 'View', + variant: 'success', + }), + onError: error => + toastProvider.addMessage('Request failed...', { + secondaryMessage: `${error.message || error}`, + variant: 'failure', + }), + } + } + onRequest = async () => { // const {history} = this.props; const { @@ -52,7 +79,9 @@ class Faucet extends React.Component { try { this.setState({ transacting: true }) - const tx = await request(faucetInstance, address) + const tx = await request(faucetInstance, address, { + txEvents: this.txEvents, + }) console.log(tx) this.setState({ transacting: false }) diff --git a/src/components/pages/loanOffer/checkout/index.tsx b/src/components/pages/loanOffer/checkout/index.tsx index b59894f..2049415 100644 --- a/src/components/pages/loanOffer/checkout/index.tsx +++ b/src/components/pages/loanOffer/checkout/index.tsx @@ -6,6 +6,7 @@ import { RouteComponentProps, withRouter } from 'react-router-dom' import { AppPath } from '../../../../constant/appPath' import PatternImage from '../../../../images/pattern.png' import { connect } from 'react-redux' +import { store } from '../../../../store' import { Spinner, Row, @@ -53,6 +54,7 @@ import { getLoanPeriod, } from '../../../../utils/metadata' import contractAddresses from '../../../../config/ines.fund' +import { networksExplorer } from '../../../../utils/getWeb3' import { BN, connectToWallet, @@ -108,6 +110,55 @@ class Checkout extends React.Component { const { history, networkId } = this.props const { name, email } = values const { crowdloanInstance, investmentAmount } = this.state + + const { toastProvider } = store.getState() + + const localError = error => + toastProvider.addMessage('Error occured', { + secondaryMessage: `${error.message || error}`, + variant: 'failure', + }) + + const logDetails = async tx => { + try { + const resp = await axios.post(INES_FUND_POST_URL, { + sender: tx.from, + transactionHash: tx.transactionHash, + network: networkId.toString(), + amount: tx.events.Fund.returnValues.amount, + email: email, + name: name, + gasUsed: tx.gasUsed.toString(), + cumulativeGasUsed: tx.cumulativeGasUsed.toString(), + }) + console.log(resp) + } catch (error) { + localError(error) + console.error(error) + } + } + + const txEvents = { + onTransactionHash: hash => + toastProvider.addMessage('Processing investment...', { + secondaryMessage: 'Check progress on Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${hash}`, + actionText: 'Check', + variant: 'processing', + }), + onReceipt: receipt => + toastProvider.addMessage('Investment completed...', { + secondaryMessage: 'View transaction Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${receipt.transactionHash}`, + actionText: 'View', + variant: 'success', + }), + onError: error => + toastProvider.addMessage('Investment failed...', { + secondaryMessage: `${error.message || error}`, + variant: 'failure', + }), + } if (!+investmentAmount) { return console.error('Can not contribute Zero(0)') } @@ -161,6 +212,7 @@ class Checkout extends React.Component { transacting: false, txError, }) + txEvents.onError(txError) return console.error(txError) } @@ -172,14 +224,16 @@ class Checkout extends React.Component { let tx if (BN(approvedBalance).lt(BN(valueInERC20))) { - tx = await approveAndFund( + tx = approveAndFund( paymentTokenInstance, crowdloanInstance, - valueInERC20 + valueInERC20, + { txEvents } ) } else { - tx = await fund(crowdloanInstance, valueInERC20) + tx = fund(crowdloanInstance, valueInERC20, { txEvents }) } + // console.log(tx) // console.log(` @@ -197,27 +251,16 @@ class Checkout extends React.Component { // CumulativeGasUsed: ${tx.cumulativeGasUsed} // `) - try { - const resp = await axios.post(INES_FUND_POST_URL, { - sender: tx.from, - transactionHash: tx.transactionHash, - network: networkId.toString(), - amount: tx.events.Fund.returnValues.amount, - email: email, - name: name, - gasUsed: tx.gasUsed.toString(), - cumulativeGasUsed: tx.cumulativeGasUsed.toString(), - }) - console.log(resp) - } catch (error) { - console.error(error) - } + tx = await tx + console.log(tx) + await logDetails(tx) this.setState({ transacting: false }) history.push(AppPath.LoanOfferThankYou) return } catch (e) { + txEvents.onError(e) this.setState({ transacting: false }) return console.error(e) } diff --git a/src/components/pages/myLoan/component.tsx b/src/components/pages/myLoan/component.tsx index dd2b55f..345d921 100644 --- a/src/components/pages/myLoan/component.tsx +++ b/src/components/pages/myLoan/component.tsx @@ -1,11 +1,13 @@ import React from 'react' import { RouteComponentProps, withRouter } from 'react-router-dom' import { ChasingDots } from 'styled-spinkit' +import { store } from '../../../store' import RenderBorrowerLoan from './renderBorrowerLoan' import RenderLenderLoan from './renderLenderLoan' import RenderConnectWallet from '../renderConnectWallet' import contractAddresses from '../../../config/ines.fund.js' import { LoanStatuses, MILLISECONDS, ZERO } from '../../../config/constants.js' +import { networksExplorer } from '../../../utils/getWeb3' import { BN, connectToWallet, @@ -92,6 +94,31 @@ class MyLoan extends React.Component { }, } + get txEvents() { + const { networkId, toastProvider } = store.getState() + return { + onTransactionHash: hash => + toastProvider.addMessage('Processing transaction...', { + secondaryMessage: 'Check progress on Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${hash}`, + actionText: 'Check', + variant: 'processing', + }), + onReceipt: receipt => + toastProvider.addMessage('Transaction completed...', { + secondaryMessage: 'View transaction Etherscan', + actionHref: `${networksExplorer[networkId]}/tx/${receipt.transactionHash}`, + actionText: 'View', + variant: 'success', + }), + onError: error => + toastProvider.addMessage('Transaction failed...', { + secondaryMessage: `${error.message || error}`, + variant: 'failure', + }), + } + } + onWithdraw = async () => { // const {history} = this.props; const { releaseAllowance, crowdloanInstance } = this.state @@ -102,7 +129,9 @@ class MyLoan extends React.Component { try { this.setState({ transacting: true }) - const tx = await withdrawRepayment(crowdloanInstance) + const tx = await withdrawRepayment(crowdloanInstance, { + txEvents: this.txEvents, + }) console.log(tx) this.setState({ transacting: false, loaded: false }, () => @@ -126,7 +155,9 @@ class MyLoan extends React.Component { return console.error('Crowdfund already started') } - const tx = await startCrowdfund(crowdloanInstance) + const tx = await startCrowdfund(crowdloanInstance, { + txEvents: this.txEvents, + }) console.log(tx) this.setState({ transacting: false, loaded: false }, () => @@ -152,7 +183,10 @@ class MyLoan extends React.Component { const tx = await withdrawPrincipal( crowdloanInstance, - prepBigNumber(amount, paymentToken.decimals) + prepBigNumber(amount, paymentToken.decimals), + { + txEvents: this.txEvents, + } ) console.log(tx) @@ -207,10 +241,15 @@ class MyLoan extends React.Component { tx = await approveAndPay( paymentTokenInstance, crowdloanInstance, - amountInERC20 + amountInERC20, + { + txEvents: this.txEvents, + } ) } else { - tx = await repay(crowdloanInstance, amountInERC20) + tx = await repay(crowdloanInstance, amountInERC20, { + txEvents: this.txEvents, + }) } console.log(tx) diff --git a/src/store/index.js b/src/store/index.js index 5b473fa..d08cbd6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,9 +1,11 @@ import { combineReducers, createStore } from 'redux' import { composeWithDevTools } from 'redux-devtools-extension' import { networkId } from './networkId' +import { toastProvider } from './toastProvider' const rootReducer = combineReducers({ networkId, + toastProvider, }) export const store = createStore(rootReducer, composeWithDevTools()) diff --git a/src/store/toastProvider.js b/src/store/toastProvider.js new file mode 100644 index 0000000..6fdf76f --- /dev/null +++ b/src/store/toastProvider.js @@ -0,0 +1,20 @@ +// Actions +export const SET_TOAST_PROVIDER = 'SET_TOAST_PROVIDER' + +// Action creator +export const setToastProvider = provider => { + return { + type: SET_TOAST_PROVIDER, + provider, + } +} + +// Reducer +export const toastProvider = (state = '', action) => { + switch (action.type) { + case SET_TOAST_PROVIDER: + return (state = action.provider) + default: + return state + } +} diff --git a/src/utils/crowdloan.js b/src/utils/crowdloan.js index bbef8c9..468c78e 100644 --- a/src/utils/crowdloan.js +++ b/src/utils/crowdloan.js @@ -1,157 +1,165 @@ import { - contractGetEvents, - contractGetPastEvents, - contractMethodCall, - contractMethodTransaction -} from "./web3Utils"; -import { approve } from "./paymentToken"; + contractGetEvents, + contractGetPastEvents, + contractMethodCall, + contractMethodTransaction, +} from './web3Utils' +import { approve } from './paymentToken' // Read functions // Loan Terms -const getBorrower = instance => contractMethodCall(instance, "borrower"); +const getBorrower = instance => contractMethodCall(instance, 'borrower') const getLoanMetadataUrl = instance => - contractMethodCall(instance, "loanMetadataUrl"); -const getPrincipalToken = instance => contractMethodCall(instance, "token"); + contractMethodCall(instance, 'loanMetadataUrl') +const getPrincipalToken = instance => contractMethodCall(instance, 'token') const getPrincipalRequested = instance => - contractMethodCall(instance, "principalRequested"); -const getRepaymentCap = instance => contractMethodCall(instance, "repaymentCap"); + contractMethodCall(instance, 'principalRequested') +const getRepaymentCap = instance => contractMethodCall(instance, 'repaymentCap') // Crowdfund Terms const getCrowdfundStart = instance => - contractMethodCall(instance, "crowdfundStart"); + contractMethodCall(instance, 'crowdfundStart') const getCrowdfundDuration = instance => - contractMethodCall(instance, "crowdfundDuration"); -const getCrowdfundEnd = instance => - contractMethodCall(instance, "crowdfundEnd"); + contractMethodCall(instance, 'crowdfundDuration') +const getCrowdfundEnd = instance => contractMethodCall(instance, 'crowdfundEnd') // Contributions const amountContributed = (instance, account) => - contractMethodCall(instance, "amountContributed", account); + contractMethodCall(instance, 'amountContributed', account) const totalContributed = instance => - contractMethodCall(instance, "totalContributed"); + contractMethodCall(instance, 'totalContributed') const principalWithdrawn = instance => - contractMethodCall(instance, "principalWithdrawn"); + contractMethodCall(instance, 'principalWithdrawn') // Repayments const repaymentWithdrawn = (instance, account) => - contractMethodCall(instance, "repaymentWithdrawn", account); -const amountRepaid = instance => contractMethodCall(instance, "amountRepaid"); -const totalRepaymentWithdrawn = instance => contractMethodCall(instance, "totalRepaymentWithdrawn"); + contractMethodCall(instance, 'repaymentWithdrawn', account) +const amountRepaid = instance => contractMethodCall(instance, 'amountRepaid') +const totalRepaymentWithdrawn = instance => + contractMethodCall(instance, 'totalRepaymentWithdrawn') // Transactions -const startCrowdfund = instance => - contractMethodTransaction(instance, "startCrowdfund"); +const startCrowdfund = (instance, txOptions) => + contractMethodTransaction(instance, 'startCrowdfund', txOptions) const fund = (instance, amount, txOptions) => - contractMethodTransaction(instance, "fund", amount, txOptions); + contractMethodTransaction(instance, 'fund', amount, txOptions) const withdrawPrincipal = (instance, amount, txOptions) => - contractMethodTransaction(instance, "withdrawPrincipal", amount, txOptions); + contractMethodTransaction(instance, 'withdrawPrincipal', amount, txOptions) const repay = (instance, amount, txOptions) => - contractMethodTransaction(instance, "repay", amount, txOptions); + contractMethodTransaction(instance, 'repay', amount, txOptions) const withdrawRepayment = (instance, txOptions) => - contractMethodTransaction(instance, "withdrawRepayment", txOptions); + contractMethodTransaction(instance, 'withdrawRepayment', txOptions) const approveAndFund = async ( - paymentTokenInstance, - instance, - amount, - txOptions -) => { - await approve( paymentTokenInstance, - instance.options.address, + instance, amount, txOptions - ); - return fund(instance, amount, txOptions); -}; +) => { + await approve( + paymentTokenInstance, + instance.options.address, + amount, + txOptions && txOptions.approve + ) + return fund(instance, amount, txOptions) +} const approveAndPay = async ( - paymentTokenInstance, - instance, - amount, - txOptions -) => { - await approve( paymentTokenInstance, - instance.options.address, + instance, amount, txOptions - ); - return repay(instance, amount, txOptions); -}; +) => { + await approve( + paymentTokenInstance, + instance.options.address, + amount, + txOptions && txOptions.approve + ) + return repay(instance, amount, txOptions) +} /* Events */ const FundEvent = (instance, eventOptions, watch) => { - if (watch) { - return contractGetEvents(instance, "Fund", eventOptions); - } else { - return contractGetPastEvents(instance, "Fund", eventOptions); - } -}; + if (watch) { + return contractGetEvents(instance, 'Fund', eventOptions) + } else { + return contractGetPastEvents(instance, 'Fund', eventOptions) + } +} const WithdrawPrincipalEvent = (instance, eventOptions, watch) => { - if (watch) { - return contractGetEvents(instance, "WithdrawPrincipal", eventOptions); - } else { - return contractGetPastEvents(instance, "WithdrawPrincipal", eventOptions); - } -}; + if (watch) { + return contractGetEvents(instance, 'WithdrawPrincipal', eventOptions) + } else { + return contractGetPastEvents( + instance, + 'WithdrawPrincipal', + eventOptions + ) + } +} const WithdrawRepaymentEvent = (instance, eventOptions, watch) => { - if (watch) { - return contractGetEvents(instance, "WithdrawRepayment", eventOptions); - } else { - return contractGetPastEvents(instance, "WithdrawRepayment", eventOptions); - } -}; + if (watch) { + return contractGetEvents(instance, 'WithdrawRepayment', eventOptions) + } else { + return contractGetPastEvents( + instance, + 'WithdrawRepayment', + eventOptions + ) + } +} const RepayEvent = (instance, eventOptions, watch) => { - if (watch) { - return contractGetEvents(instance, "Repay", eventOptions); - } else { - return contractGetPastEvents(instance, "Repay", eventOptions); - } -}; + if (watch) { + return contractGetEvents(instance, 'Repay', eventOptions) + } else { + return contractGetPastEvents(instance, 'Repay', eventOptions) + } +} const StartCrowdfundEvent = (instance, eventOptions, watch) => { - if (watch) { - return contractGetEvents(instance, "StartCrowdfund", eventOptions); - } else { - return contractGetPastEvents(instance, "StartCrowdfund", eventOptions); - } -}; + if (watch) { + return contractGetEvents(instance, 'StartCrowdfund', eventOptions) + } else { + return contractGetPastEvents(instance, 'StartCrowdfund', eventOptions) + } +} export { - //Loan Terms - getBorrower, - getLoanMetadataUrl, - getPrincipalToken, - getPrincipalRequested, - getRepaymentCap, - //Crowdfund Terms - getCrowdfundEnd, - getCrowdfundStart, - getCrowdfundDuration, - //Contributions - amountContributed, - totalContributed, - principalWithdrawn, - //Repayments - amountRepaid, - repaymentWithdrawn, - totalRepaymentWithdrawn, - //Contract transactions - startCrowdfund, - fund, - withdrawPrincipal, - repay, - withdrawRepayment, - // Derived function - approveAndFund, - approveAndPay, - //Events - FundEvent, - WithdrawPrincipalEvent, - WithdrawRepaymentEvent, - RepayEvent, - StartCrowdfundEvent -}; + //Loan Terms + getBorrower, + getLoanMetadataUrl, + getPrincipalToken, + getPrincipalRequested, + getRepaymentCap, + //Crowdfund Terms + getCrowdfundEnd, + getCrowdfundStart, + getCrowdfundDuration, + //Contributions + amountContributed, + totalContributed, + principalWithdrawn, + //Repayments + amountRepaid, + repaymentWithdrawn, + totalRepaymentWithdrawn, + //Contract transactions + startCrowdfund, + fund, + withdrawPrincipal, + repay, + withdrawRepayment, + // Derived function + approveAndFund, + approveAndPay, + //Events + FundEvent, + WithdrawPrincipalEvent, + WithdrawRepaymentEvent, + RepayEvent, + StartCrowdfundEvent, +} diff --git a/src/utils/getWeb3.js b/src/utils/getWeb3.js index 868aaad..f98d589 100644 --- a/src/utils/getWeb3.js +++ b/src/utils/getWeb3.js @@ -77,6 +77,12 @@ const connectToWallet = async () => { } } +const networksExplorer = { + 0: '127.0.0.1:7545', + 1: 'https://etherscan.io', + 42: 'https://kovan.etherscan.io', +} + const resolveWeb3 = async () => { if (!web3) { if (gettingWeb3) { @@ -90,4 +96,4 @@ const resolveWeb3 = async () => { } export default resolveWeb3 -export { getGanacheWeb3, connectToWallet } +export { getGanacheWeb3, connectToWallet, networksExplorer } diff --git a/src/utils/web3Utils.js b/src/utils/web3Utils.js index a524b42..81b6294 100644 --- a/src/utils/web3Utils.js +++ b/src/utils/web3Utils.js @@ -60,7 +60,10 @@ const contractMethodCall = async (contract, method, ...args) => { } const contractMethodTransaction = async (contract, method, ...args) => { - let txOptions = args[args.length - 1] + let contractEvents = {} + let txOptions = {} //set default value for txOptions + + txOptions = args[args.length - 1] if (args.length > 0 && !txOptions) { //remove UNDEFINED txOptions args = args.slice(0, args.length - 1) @@ -71,17 +74,23 @@ const contractMethodTransaction = async (contract, method, ...args) => { (!txOptions.from && !txOptions.data && !txOptions.gas && - !txOptions.gasPrice) + !txOptions.gasPrice && + !txOptions.txEvents) ) { - txOptions = {} //set default value for txOptions + txOptions = {} } else { args = args.slice(0, args.length - 1) //remove txOptions from args array } await getWeb3() //Ensure Web3 is fully loaded + if (txOptions && txOptions.txEvents) { + contractEvents = txOptions.txEvents + } txOptions = await prepTransactionOptions(txOptions) try { - return await contract.methods[method](...args).send(txOptions) + const tx = contract.methods[method](...args).send(txOptions) + setTransactionEvents(tx, contractEvents) + return tx } catch (e) { console.error(method, ...args) console.error(e) @@ -89,6 +98,21 @@ const contractMethodTransaction = async (contract, method, ...args) => { } } +const setTransactionEvents = ( + tx, + { + onTransactionHash = null, + onReceipt = null, + onConfirmation = null, + onError = null, + } +) => { + onTransactionHash && tx.once('transactionHash', onTransactionHash) + onConfirmation && tx.on('confirmation', onConfirmation) + onReceipt && tx.on('receipt', onReceipt) + onError && tx.on('error', onError) +} + const contractGetEvents = ( contract, eventString = 'allEvents', @@ -163,6 +187,7 @@ export { contractGetPastEvents, contractMethodCall, contractMethodTransaction, + setTransactionEvents, getAccounts, getBlock, getInjectedAccountAddress, diff --git a/yarn.lock b/yarn.lock index a6d5b36..3468ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17190,10 +17190,10 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimble-ui@^0.9.8: - version "0.9.8" - resolved "https://registry.yarnpkg.com/rimble-ui/-/rimble-ui-0.9.8.tgz#9d4fd612e2ca24b413ffa3b7daa36c418922e8e8" - integrity sha512-brhTTh4UwpRZusqclcqrbj2N9zgejIxMty+/gIdCLT0Gsgfz1OZes0OeFDFjqVYCHK5DrRiaCjybpCG+rtfNfw== +rimble-ui@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/rimble-ui/-/rimble-ui-0.10.0.tgz#34ba861310b91648082a17d3705fcae12da4f33f" + integrity sha512-AHcEjY3Nkjj+p/fzKdNixMN3xjtjwPG3c5E30ewbSmHft0kYG5tZq0a98IT3uQ44Db8yrCeEkMzzXuMkMtv1+A== dependencies: "@d8660091/react-popper" "^1.0.4" "@svgr/rollup" "^4.2.0"