diff --git a/components/pages/ecosystem/widget/useTransferWidget.ts b/components/pages/ecosystem/widget/useTransferWidget.ts index 25c565a5..aa42863a 100644 --- a/components/pages/ecosystem/widget/useTransferWidget.ts +++ b/components/pages/ecosystem/widget/useTransferWidget.ts @@ -1,10 +1,12 @@ -import { useReducer, useRef, useEffect } from 'react'; -import { useWeb3Context } from './web3-context'; +import { BN } from '@polkadot/util'; import { validateEVMAddress, validateSubstrateAddress, validateTokenAmount } from 'lib/crypto'; import { processEVMDeposit } from 'lib/crypto/deposit'; import { processEVMWithdrawal } from 'lib/crypto/withdrawal'; +import { useCallback, useEffect, useReducer, useRef } from 'react'; +import { useWeb3Context } from './web3-context'; // Shared types +export type Network = 'mainnet' | 'testnet'; type FormState = 'initial' | 'ready' | 'in-progress' | 'success' | 'error'; type TargetTransferType = 'deposit' | 'withdrawal'; type FormErrorsState = { @@ -17,43 +19,57 @@ type FormErrorsState = { // State type State = { formState: FormState; + network: Network; ethereumConnected: boolean; polkadotConnected: boolean; targetTransferType: TargetTransferType; selectedPolkadotAccount: string | undefined; selectedEthereumAccount: string | undefined; errors: FormErrorsState; + message: string | null; tx: string | null; + block: string | null; }; // Initial state const initialState: State = { formState: 'initial', + network: 'mainnet', ethereumConnected: false, polkadotConnected: false, targetTransferType: 'deposit', selectedPolkadotAccount: undefined, selectedEthereumAccount: undefined, errors: {}, + message: null, tx: null, + block: null, }; // Action types type Action = | { type: 'SET_FORM_STATE'; payload: FormState } + | { type: 'SET_NETWORK'; payload: Network } | { type: 'SET_ETHEREUM_CONNECTED'; payload: boolean } | { type: 'SET_POLKADOT_CONNECTED'; payload: boolean } | { type: 'SET_TARGET_TRANSFER_TYPE'; payload: TargetTransferType } | { type: 'SET_SELECTED_POLKADOT_ACCOUNT'; payload: string | undefined } | { type: 'SET_SELECTED_ETHEREUM_ACCOUNT'; payload: string | undefined } | { type: 'SET_ERRORS'; payload: FormErrorsState } - | { type: 'SET_TX'; payload: string | null }; + | { type: 'SET_MESSAGE'; payload: string | null } + | { type: 'SET_TX'; payload: string | null } + | { type: 'SET_BLOCK'; payload: string | null }; // Reducer const reducer = (state: State, action: Action): State => { switch (action.type) { case 'SET_FORM_STATE': return { ...state, formState: action.payload }; + case 'SET_NETWORK': + return { + ...state, + network: action.payload, + }; case 'SET_ETHEREUM_CONNECTED': return { ...state, ethereumConnected: action.payload }; case 'SET_POLKADOT_CONNECTED': @@ -66,8 +82,12 @@ const reducer = (state: State, action: Action): State => { return { ...state, selectedEthereumAccount: action.payload }; case 'SET_ERRORS': return { ...state, errors: action.payload }; + case 'SET_MESSAGE': + return { ...state, message: action.payload }; case 'SET_TX': return { ...state, tx: action.payload }; + case 'SET_BLOCK': + return { ...state, block: action.payload }; default: return state; } @@ -76,12 +96,20 @@ const reducer = (state: State, action: Action): State => { export const useTransferWidget = () => { const [state, dispatch] = useReducer(reducer, initialState); const amountInputRef = useRef(null); - const { connectToEthereum, ethereumAccounts, connectToPolkadot, polkadotAccounts } = - useWeb3Context(); + const { + connectToEthereum, + disconnectFromEthereum, + ethereumAccounts, + connectToPolkadot, + disconnectFromPolkadot, + polkadotAccounts, + updateBalances, + } = useWeb3Context(); // Actions const setFormState = (formState: FormState) => dispatch({ type: 'SET_FORM_STATE', payload: formState }); + const setNetwork = (network: Network) => dispatch({ type: 'SET_NETWORK', payload: network }); const setEthereumConnected = (connected: boolean) => dispatch({ type: 'SET_ETHEREUM_CONNECTED', payload: connected }); const setPolkadotConnected = (connected: boolean) => @@ -93,7 +121,10 @@ export const useTransferWidget = () => { const setSelectedEthereumAccount = (account: string | undefined) => dispatch({ type: 'SET_SELECTED_ETHEREUM_ACCOUNT', payload: account }); const setErrors = (errors: FormErrorsState) => dispatch({ type: 'SET_ERRORS', payload: errors }); + const setMessage = (message: string | null) => + dispatch({ type: 'SET_MESSAGE', payload: message }); const setTx = (tx: string | null) => dispatch({ type: 'SET_TX', payload: tx }); + const setBlock = (block: string | null) => dispatch({ type: 'SET_BLOCK', payload: block }); // Logic functions const handleDepositSubmit = () => { @@ -136,15 +167,35 @@ export const useTransferWidget = () => { return; } + // validate balance + const balance = polkadotAccounts.find((a) => a.address === substrateAddress).balance; + const transferBn = new BN(amount).mul(new BN(10).pow(new BN(18))); + const sufficientBallance = balance?.amount.gt(transferBn); + + if (!sufficientBallance) { + setErrors({ + amount: 'Insufficient balance', + }); + + return; + } + // submit form and continue with processing setFormState('in-progress'); async function process() { try { - const result = await processEVMDeposit(evmAddress, substrateAddress, amount); + const result = await processEVMDeposit(evmAddress, substrateAddress, amount, state.network); if (result.success) { setFormState('success'); + setMessage(result?.message); setTx(result?.data?.tx); + setBlock(result?.data?.block); + } else { + setErrors({ + global: result.message, + }); + setFormState('error'); } } catch (error) { setErrors({ @@ -198,15 +249,41 @@ export const useTransferWidget = () => { return; } + // validate balance + const balance = ethereumAccounts.find((a) => a.address === evmAddress).balance; + const transferBn = new BN(amount).mul(new BN(10).pow(new BN(18))); + const sufficientBallance = balance?.amount.gt(transferBn); + + if (!sufficientBallance) { + setErrors({ + amount: 'Insufficient balance', + }); + + return; + } + // submit form and continue with processing setFormState('in-progress'); async function process() { try { - const result = await processEVMWithdrawal(evmAddress, substrateAddress, amount); + const result = await processEVMWithdrawal( + evmAddress, + substrateAddress, + amount, + state.network + ); + if (result.success) { - setFormState('success'); setTx(result?.data?.tx); + setMessage(result?.message); + setBlock(result?.data?.block); + setFormState('success'); + } else { + setErrors({ + global: result.message, + }); + setFormState('error'); } } catch (error) { console.error(error); @@ -228,6 +305,8 @@ export const useTransferWidget = () => { } else { handleWithdrawalSubmit(); } + + handleBalanceUpdate(); }; const handleReset = (event: React.MouseEvent) => { @@ -237,21 +316,24 @@ export const useTransferWidget = () => { } else { setFormState('initial'); } - setTargetTransferType('deposit'); setErrors({}); + setTx(null); + setBlock(null); + setMessage(null); amountInputRef.current.value = ''; }; const handleConnect = (type: 'polkadot' | 'ethereum') => { - const isConnected = type === 'polkadot' ? state.polkadotConnected : state.ethereumConnected; - - if (isConnected) { + if ( + (type === 'polkadot' && state.polkadotConnected) || + (type === 'ethereum' && state.ethereumConnected) + ) { return; } async function connect() { if (type === 'polkadot') { - const result = await connectToPolkadot(); + const result = await connectToPolkadot(state.network); if (result) { setPolkadotConnected(true); if (state.ethereumConnected) { @@ -260,7 +342,7 @@ export const useTransferWidget = () => { } } else { try { - const result = await connectToEthereum(); + const result = await connectToEthereum(state.network); if (result) { setEthereumConnected(true); if (state.polkadotConnected) { @@ -285,30 +367,67 @@ export const useTransferWidget = () => { return; } - if (type === 'polkadot') { - setPolkadotConnected(false); - setSelectedPolkadotAccount(undefined); - setFormState('initial'); - } + async function disconnect() { + if (type === 'polkadot') { + await disconnectFromPolkadot(); + setPolkadotConnected(false); + setSelectedPolkadotAccount(undefined); + setFormState('initial'); + } - if (type === 'ethereum') { - setEthereumConnected(false); - setSelectedEthereumAccount(undefined); - setFormState('initial'); + if (type === 'ethereum') { + await disconnectFromEthereum(); + setEthereumConnected(false); + setSelectedEthereumAccount(undefined); + setFormState('initial'); + } } + + disconnect(); }; + const handleNetworkChange = (network: Network) => { + // reset state to ready and clear errors + setFormState('initial'); + setPolkadotConnected(false); + setEthereumConnected(false); + setSelectedPolkadotAccount(undefined); + setSelectedEthereumAccount(undefined); + setErrors({}); + setNetwork(network); + }; + + const handleBalanceUpdate = useCallback(() => { + // Balance will update by just connecting to the network again + async function update() { + console.log('Periodically updating balances'); + await updateBalances(state.network); + } + + window.setTimeout(update, 10_000); + }, [state.network, updateBalances]); + // useEffect hooks to synchronize hook state with useWeb3Context state useEffect(() => { if (state.polkadotConnected && polkadotAccounts && polkadotAccounts.length > 0) { - setSelectedPolkadotAccount(polkadotAccounts[0].address); + if (!state.selectedPolkadotAccount) { + setSelectedPolkadotAccount(polkadotAccounts[0].address); + } } if (state.ethereumConnected && ethereumAccounts && ethereumAccounts.length > 0) { - setSelectedEthereumAccount(ethereumAccounts[0].address); + if (!state.selectedEthereumAccount) { + setSelectedEthereumAccount(ethereumAccounts[0].address); + } } }, [state, polkadotAccounts, ethereumAccounts]); + useEffect(() => { + if (state.formState === 'success') { + handleBalanceUpdate(); + } + }, [state.formState, handleBalanceUpdate]); + return { state, polkadotAccounts, @@ -322,9 +441,11 @@ export const useTransferWidget = () => { setSelectedEthereumAccount, setErrors, setTx, + setBlock, handleSubmit, handleReset, handleConnect, handleDisconnect, + handleNetworkChange, }; }; diff --git a/components/pages/ecosystem/widget/web3-context.tsx b/components/pages/ecosystem/widget/web3-context.tsx index 28519f50..57b50e44 100644 --- a/components/pages/ecosystem/widget/web3-context.tsx +++ b/components/pages/ecosystem/widget/web3-context.tsx @@ -1,30 +1,71 @@ -import React, { createContext, useState } from 'react'; -import Web3 from 'web3'; import { web3Accounts, web3Enable } from '@polkadot/extension-dapp'; +import { AccountInfo } from '@polkadot/types/interfaces'; +import { BN, formatBalance } from '@polkadot/util'; import { initPolkadotAPI } from 'lib/crypto/polkadot'; +import React, { createContext, useState } from 'react'; +import Web3 from 'web3'; export type Account = { address: string; networkAddress?: string; label: string; + balance: { + amount: BN; + formatted: string; + }; }; +type Network = 'mainnet' | 'testnet'; + type Web3ContextType = { - connectToEthereum: () => Promise; - connectToPolkadot: () => Promise; + connectToEthereum: (network: Network) => Promise; + disconnectFromEthereum: () => Promise; + connectToPolkadot: (network: Network) => Promise; + disconnectFromPolkadot: () => void; + updateBalances: (network: Network) => Promise; ethereumAccounts: Account[]; polkadotAccounts: Account[]; }; const Web3Context = createContext(null); -const EDG_EVM_NETWORK_ID = 2021; +const EDG_EVM_NETWORK_ID: Record = { + mainnet: 2021, + testnet: 2022, +}; + +const EDG_EVM_NETWORKS_EXTENSION: Record = { + mainnet: { + chainId: '0x7E5', + chainName: 'Edgeware EdgeEVM', + rpcUrls: ['https://edgeware-evm.jelliedowl.net/'], + nativeCurrency: { + name: 'Edgeware', + symbol: 'EDG', + decimals: 18, + }, + blockExplorerUrls: ['https://edgscan.live/'], + }, + testnet: { + chainId: '0x7E6', + chainName: 'Edgeware EdgeEVM Testnet', + rpcUrls: ['https://beresheet-evm.jelliedowl.net/'], + nativeCurrency: { + name: 'Edgeware', + symbol: 'tEDG', + decimals: 18, + }, + blockExplorerUrls: ['https://beresheet.edgscan.live/'], + }, +}; export const Web3ContextProvider = ({ children }) => { const [ethereumAccounts, setEthereumAccounts] = useState([]); const [polkadotAccounts, setPolkadotAccounts] = useState([]); - const connectToEthereum = async () => { + const connectToEthereum = async (network: Network) => { + console.log(`Attempting to connect with EVM wallet (${network})`); + try { if ('ethereum' in window) { await (window as any).ethereum?.enable(); @@ -33,18 +74,78 @@ export const Web3ContextProvider = ({ children }) => { // check for network const networkId = await web3.eth.net.getId(); - if (networkId !== EDG_EVM_NETWORK_ID) { - alert('Please select the Edgeware EVM network in Metamask'); - throw new Error('Please select the Edgeware EVM network in Metamask'); + if (networkId !== EDG_EVM_NETWORK_ID[network]) { + await tryEthereumNetworkSwitch(network); } - const accounts = await web3.eth.getAccounts(); - setEthereumAccounts( - accounts.map((a, index) => ({ - address: a, - label: `Metamask account #${index + 1}`, - })) + await updateEthereumBalances(network); + return true; + } else { + throw new Error('No Ethereum wallet detected'); + } + } catch (err) { + console.error(err); + } + }; + + const disconnectFromEthereum = async () => { + console.log('Disconnecting from EVM wallet'); + try { + const web3 = new Web3(Web3.givenProvider); + } catch (err) { + console.error(err); + } + }; + + const connectToPolkadot = async (network: Network) => { + console.log(`Attempting to connect with Polkadot wallet (${network})`); + + try { + if ((window as any).injectedWeb3) { + return await updatePolkadotBalances(network); + } + } catch (err) { + console.error(err); + } + }; + + const disconnectFromPolkadot = async () => { + console.log('Disconnecting from Polkadot wallet'); + try { + console.log('Done!'); + } catch (err) { + console.error(err); + } + }; + + const updateEthereumBalances = async (network: Network) => { + console.log(`Attempting to update with EVM wallets (${network})`); + + try { + if ('ethereum' in window) { + await (window as any).ethereum?.enable(); + const web3 = new Web3(Web3.givenProvider); + + const allAccounts = await web3.eth.getAccounts(); + + // get balances for accounts + const accountsWithBalance = await Promise.all( + allAccounts.map(async (a, index) => { + const balanceInWei = await web3.eth.getBalance(a); + const amount = new BN(balanceInWei); + + return { + address: a, + label: `Metamask account #${index + 1}`, + balance: { + amount, + formatted: formatBalance(amount), + }, + }; + }) ); + + setEthereumAccounts(accountsWithBalance); return true; } else { throw new Error('No Ethereum wallet detected'); @@ -54,7 +155,8 @@ export const Web3ContextProvider = ({ children }) => { } }; - const connectToPolkadot = async () => { + const updatePolkadotBalances = async (network: Network) => { + console.log(`Attempting to connect with Polkadot wallet (${network})`); try { if ((window as any).injectedWeb3) { const extensions = await web3Enable('Polkadot-JS Apps'); @@ -62,17 +164,32 @@ export const Web3ContextProvider = ({ children }) => { throw new Error('No Polkadot wallet detected'); } - const api = await initPolkadotAPI(); + const api = await initPolkadotAPI(network); const allAccounts = await web3Accounts(); - const accounts = allAccounts - .map((a) => ({ - address: a.address, - networkAddress: api.createType('Address', a.address).toString(), - label: a.meta.name, - })) - .reverse(); - - setPolkadotAccounts(accounts); + + formatBalance.setDefaults({ + decimals: 18, // adjust this to match your network + unit: 'EDG', + }); + + const accounts = await Promise.all( + allAccounts.map(async (a) => { + const accountInfo = (await api.query.system.account(a.address)) as AccountInfo; + const amount = accountInfo.data.free; + + return { + address: a.address, + networkAddress: api.createType('Address', a.address).toString(), + label: a.meta.name, + balance: { + amount, + formatted: formatBalance(amount), + }, + }; + }) + ); + + setPolkadotAccounts(accounts.reverse()); return true; } } catch (err) { @@ -80,9 +197,27 @@ export const Web3ContextProvider = ({ children }) => { } }; + const updateBalances = async (network: Network) => { + await Promise.all([updateEthereumBalances(network), updatePolkadotBalances(network)]); + }; + + const tryEthereumNetworkSwitch = async (network: Network) => { + try { + await (window as any).ethereum.request({ + method: 'wallet_addEthereumChain', + params: [EDG_EVM_NETWORKS_EXTENSION[network]], + }); + } catch (err) { + console.error(err); + } + }; + const contextValue = { connectToEthereum, + disconnectFromEthereum, connectToPolkadot, + disconnectFromPolkadot, + updateBalances, ethereumAccounts, polkadotAccounts, }; diff --git a/components/pages/ecosystem/widget/widget-form.tsx b/components/pages/ecosystem/widget/widget-form.tsx index 7862bd5d..13108555 100644 --- a/components/pages/ecosystem/widget/widget-form.tsx +++ b/components/pages/ecosystem/widget/widget-form.tsx @@ -1,6 +1,9 @@ import { useTransferWidget } from './useTransferWidget'; +import { WidgetNetworkSelector } from './widget-network-selector'; +import { WidgetSuccess } from './widget-success'; import { WidgetTransferSelector } from './widget-transfer-selector'; import { WidgetWalletSelector } from './widget-wallet-selector'; +import IconFunds from 'remixicon/icons/Finance/exchange-funds-line.svg'; export const WidgetForm = () => { const { @@ -9,6 +12,7 @@ export const WidgetForm = () => { setTargetTransferType, handleConnect, handleDisconnect, + handleNetworkChange, handleSubmit, handleReset, polkadotAccounts, @@ -18,13 +22,17 @@ export const WidgetForm = () => { } = useTransferWidget(); return ( -
+
+
+ +
+
{ onClick={handleSubmit} disabled={state.formState !== 'ready'} > - Transfer + {state.targetTransferType === 'deposit' ? 'Deposit' : 'Withdraw'}
{state.errors.amount && {state.errors.amount}} {state.formState === 'error' ? ( -

- {state.errors.global || 'Something went wrong. Please try again.'} -

+ <> +

+ {state.errors.global || 'Something went wrong. Please try again.'} +

+ + ) : null} {state.formState === 'success' && state.tx && ( -
- Transfer submitted! -
- - View Extrinsic on Subscan - - -
-
+ <> + + )} {state.formState === 'in-progress' && ( -

Processing request...

+

+ + Processing request... Please wait! +

)}
); diff --git a/components/pages/ecosystem/widget/widget-network-selector.tsx b/components/pages/ecosystem/widget/widget-network-selector.tsx new file mode 100644 index 00000000..e2bf00d3 --- /dev/null +++ b/components/pages/ecosystem/widget/widget-network-selector.tsx @@ -0,0 +1,24 @@ +type WidgetNetworkSelectorProps = { + network: string; + onNetworkChange: (network: string) => void; +}; + +export const WidgetNetworkSelector = ({ network, onNetworkChange }: WidgetNetworkSelectorProps) => { + const networks = ['mainnet', 'testnet']; + + return ( +
+ {networks.map((n) => ( + + ))} +
+ ); +}; diff --git a/components/pages/ecosystem/widget/widget-success.tsx b/components/pages/ecosystem/widget/widget-success.tsx new file mode 100644 index 00000000..b475c3eb --- /dev/null +++ b/components/pages/ecosystem/widget/widget-success.tsx @@ -0,0 +1,54 @@ +import type { Network } from './useTransferWidget'; + +type WidgetSuccessProps = { + tx: string; + block: string; + message: string; + handleReset: (e: any) => void; + network: Network; +}; + +export const WidgetSuccess = ({ tx, block, message, handleReset, network }: WidgetSuccessProps) => { + const rpcByNetwork = { + mainnet: 'wss://edgeware.jelliedowl.net', + testnet: 'wss://beresheet.jelliedowl.net', + }; + + const rpc = encodeURI(rpcByNetwork[network]); + + return ( +
+ {message &&

{message}

} +
+ {network === 'mainnet' && ( + + View Extrinsic on Subscan + + )} + + {block && ( + + View Block details on EdgeApps + + )} + + +
+
+ ); +}; diff --git a/components/pages/ecosystem/widget/widget-transfer-selector.tsx b/components/pages/ecosystem/widget/widget-transfer-selector.tsx index 46527a53..999205ac 100644 --- a/components/pages/ecosystem/widget/widget-transfer-selector.tsx +++ b/components/pages/ecosystem/widget/widget-transfer-selector.tsx @@ -36,11 +36,18 @@ export const WidgetTransferSelector = ({ return (
-
EdgeWASM
+
EdgeWASM
+ + {messageText} + - - {messageText} -
-
EdgeEVM
+
EdgeEVM
); }; diff --git a/components/pages/ecosystem/widget/widget-wallet-selector.tsx b/components/pages/ecosystem/widget/widget-wallet-selector.tsx index e8572cb8..f8b9c00c 100644 --- a/components/pages/ecosystem/widget/widget-wallet-selector.tsx +++ b/components/pages/ecosystem/widget/widget-wallet-selector.tsx @@ -64,7 +64,14 @@ export const WidgetWalletSelector = ({ - {fullSelectedAccount.label} + + {fullSelectedAccount.label} + {fullSelectedAccount.balance && ( + + {fullSelectedAccount.balance.formatted} + + )} + {fullSelectedAccount.networkAddress || fullSelectedAccount.address} diff --git a/lib/crypto/deposit.ts b/lib/crypto/deposit.ts index eedfd91e..77b2057b 100644 --- a/lib/crypto/deposit.ts +++ b/lib/crypto/deposit.ts @@ -7,10 +7,13 @@ type DepositResult = { data?: any; }; +type Network = 'mainnet' | 'testnet'; + export const processEVMDeposit = async ( targetAddress: string, sourceAddress: string, - inputAmount: string + inputAmount: string, + network: Network ): Promise => { const mainnetAddress = evmConvert(targetAddress); @@ -30,37 +33,48 @@ export const processEVMDeposit = async ( const amount = Number(inputAmount); // Initialize API - const api = await initPolkadotAPI(); + const api = await initPolkadotAPI(network); if (!api) { return { success: false, - message: 'Failed to connect to the network. Please try again later.', + message: 'Failed to connect to the network.', }; } // Request transfer try { - const tx = await requestTransfer(api, mainnetAddress, sourceAddress, amount); + const { txHash, blockHash, error } = await requestTransfer( + api, + mainnetAddress, + sourceAddress, + amount + ); + + console.log({ + txHash, + blockHash, + }); - if (tx) { + if (txHash) { return { success: true, - message: `Successfully sent ${amount} EDG to ${mainnetAddress}.`, + message: `Successfully sent ${amount} EDG to ${targetAddress}.`, data: { - tx, + tx: txHash, + block: blockHash, }, }; } else { return { success: false, - message: 'Failed to send transaction. Please try again later.', + message: 'Failed to send transaction.', }; } } catch (error) { console.error(error); return { success: false, - message: error?.message || 'Failed to request transfer. Please try again later.', + message: error?.message || 'Failed to request transfer.', }; } }; diff --git a/lib/crypto/polkadot.ts b/lib/crypto/polkadot.ts index fed9a1f3..2c5e7220 100644 --- a/lib/crypto/polkadot.ts +++ b/lib/crypto/polkadot.ts @@ -1,21 +1,26 @@ -import { spec } from '@edgeware/node-types'; import { ApiPromise, WsProvider } from '@polkadot/api'; import { web3Accounts, web3Enable, web3FromAddress } from '@polkadot/extension-dapp'; import { TypeRegistry } from '@polkadot/types'; import BigNumber from 'bignumber.js'; -export const initPolkadotAPI = async () => { +type Network = 'mainnet' | 'testnet'; + +const getNetworkUrl = (network: Network) => { + return network === 'testnet' ? 'wss://beresheet.jelliedowl.net' : 'wss://edgeware.jelliedowl.net'; +}; + +export const initPolkadotAPI = async (network: Network) => { await web3Enable('Polkadot-JS Apps'); - const polkadotUrl = 'wss://edgeware.jelliedowl.net'; + const polkadotUrl = getNetworkUrl(network); const registry = new TypeRegistry(); - const api = await new ApiPromise({ + const api = await ApiPromise.create({ provider: new WsProvider(polkadotUrl), registry, - ...spec, - }).isReady; + }); + await api.isReady; return api; }; @@ -38,7 +43,7 @@ export const requestTransfer = async ( } // convert amount to big number - const decimals = api.registry.chainDecimals[0]; + const decimals = api.registry.chainDecimals[0] || 18; const bnAmount = getBigNumberAmount(amount, decimals); // prepare transfer tx @@ -47,9 +52,48 @@ export const requestTransfer = async ( // get signer const injector = await web3FromAddress(senderAccount.address); - // sign and send tx - const hash = await transfer.signAndSend(senderAccount.address, { signer: injector.signer }); - return hash; + const { promise, resolve } = (() => { + let _resolve: any; + const promise = new Promise((resolve) => { + _resolve = resolve; + }); + return { promise, resolve: _resolve }; + })(); + + transfer + .signAndSend( + senderAccount.address, + { signer: injector.signer }, + ({ status, events, dispatchError }) => { + console.log(`Transaction status: ${status.type}`); + + if (status.isInBlock) { + console.log(`Included in block with hash ${status.asInBlock}`); + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + const { name, section } = decoded; + console.log(`${section}.${name}}`); + } else { + console.log(dispatchError.toString()); + } + } else { + for (const { + event: { data, method, section }, + } of events) { + console.log(`\t'${section}.${method}':: ${data}`); + } + } + resolve({ blockHash: status.asInBlock, txHash: transfer.hash }); + } + } + ) + .catch((error: Error) => { + console.log(':( transaction failed', error); + resolve({ blockHash: '', txHash: '', error: error.message }); + }); + + return promise; }; export const requestEVMWithdrawal = async ( @@ -81,12 +125,52 @@ export const requestEVMWithdrawal = async ( const withdrawal = api.tx.evm.withdraw(`0x${evmAddress}`, bnAmount); // sign and send tx - const hash = await withdrawal.signAndSend(senderAccount.address, { signer: injector.signer }); - return hash; + const { promise, resolve } = (() => { + let _resolve: any; + const promise = new Promise((resolve) => { + _resolve = resolve; + }); + return { promise, resolve: _resolve }; + })(); + + withdrawal + .signAndSend( + senderAccount.address, + { signer: injector.signer }, + ({ status, events, dispatchError }) => { + console.log(`Transaction status: ${status.type}`); + + if (status.isInBlock) { + console.log(`Included in block with hash ${status.asInBlock}`); + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + const { name, section } = decoded; + console.log(`${section}.${name}}`); + } else { + console.log(dispatchError.toString()); + } + } else { + for (const { + event: { data, method, section }, + } of events) { + console.log(`\t'${section}.${method}':: ${data}`); + } + } + resolve({ blockHash: status.asInBlock, txHash: withdrawal.hash }); + } + } + ) + .catch((error: Error) => { + console.log(':( transaction failed', error); + resolve({ blockHash: '', txHash: '', error: error.message }); + }); + + return promise; }; export const getBigNumberAmount = (amount: number, chainDecimals: number) => { const bn = new BigNumber(amount).shiftedBy(chainDecimals); - return bn.toString(); + return bn.toFixed(); }; diff --git a/lib/crypto/withdrawal.ts b/lib/crypto/withdrawal.ts index 371b2761..a05dd8ee 100644 --- a/lib/crypto/withdrawal.ts +++ b/lib/crypto/withdrawal.ts @@ -1,8 +1,9 @@ import { decodeAddress } from '@polkadot/util-crypto'; import Web3 from 'web3'; - import { initPolkadotAPI, requestEVMWithdrawal } from './polkadot'; +type Network = 'mainnet' | 'testnet'; + type WithdrawalResult = { success: boolean; message?: string; @@ -12,7 +13,8 @@ type WithdrawalResult = { export const processEVMWithdrawal = async ( sourceAddress: string, substrateAddress: string, - inputAmount: string + inputAmount: string, + network: Network ): Promise => { // 1. discover intermediary EVM withdrawal address const intermediaryEVMAddress = Buffer.from( @@ -37,9 +39,16 @@ export const processEVMWithdrawal = async ( const receipt = await web3.eth.getTransactionReceipt(result.transactionHash); console.log(receipt); + // 3a. Check ballance on intermediary EVM withdrawal address + const balanceInWei = await web3.eth.getBalance(intermediaryEVMAddress); + const availableBalance = parseFloat(web3.utils.fromWei(balanceInWei, 'ether')); + + console.log('IntermediaryEVMAddress', intermediaryEVMAddress); + console.log('Found additional EDG balance in intermediary account:', availableBalance); + // 4. sign withdrawal transaction command on substrate // Initialize API - const api = await initPolkadotAPI(); + const api = await initPolkadotAPI(network); if (!api) { return { success: false, @@ -47,31 +56,54 @@ export const processEVMWithdrawal = async ( }; } - // Request withdrawal + // Calculate amounts + + // Reserve 1 EDG as a buffer for fees etc + const BUFFER = 1; const amount = Number(inputAmount); + const withdrawExtra = availableBalance - BUFFER > amount; + const actualAmount = withdrawExtra ? availableBalance - BUFFER : amount; + const difference = Math.round(actualAmount - amount); + // Request withdrawal: try { - const tx = await requestEVMWithdrawal(api, intermediaryEVMAddress, substrateAddress, amount); + const { txHash, blockHash } = await requestEVMWithdrawal( + api, + intermediaryEVMAddress, + substrateAddress, + actualAmount + ); + + console.log({ + txHash, + blockHash, + }); + + if (txHash) { + let message = `Successfully withdrawn ${amount} EDG`; + if (withdrawExtra) { + message += ` (${inputAmount} EDG requested + ${difference} EDG recovered from intermediary account)`; + } - if (tx) { return { success: true, - message: `Successfully withdrawn ${amount} EDG to ${substrateAddress}.`, + message: message, data: { - tx, + tx: txHash, + block: blockHash, }, }; } else { return { success: false, - message: 'Failed to send transaction. Please try again later.', + message: 'Failed to send transaction.', }; } } catch (error) { console.error(error); return { success: false, - message: error?.message || 'Failed to request transfer. Please try again later.', + message: error?.message || 'Failed to request transfer.', }; } }; diff --git a/pages/ecosystem.tsx b/pages/ecosystem.tsx index d19e7f18..db7240ea 100644 --- a/pages/ecosystem.tsx +++ b/pages/ecosystem.tsx @@ -3,10 +3,8 @@ import { GetStaticProps, NextPage } from 'next'; import { EcosystemHero } from 'components/pages/ecosystem/ecosystem-hero'; import { EcosystemPartnersList } from 'components/pages/ecosystem/ecosystem-list'; -import { EcosystemWidget } from 'components/pages/ecosystem/widget/ecosystem-widget'; import { AllPartnersData, getAllPartners } from 'lib/api'; -import { Suspense } from 'react'; const DynamicEcosystemWidget = dynamic( () => import('../components/pages/ecosystem/widget/ecosystem-widget'), diff --git a/public/sitemap.xml b/public/sitemap.xml index 37e8cf77..20de2b2e 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,16 +1,16 @@ -https://www.edgeware.iodaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/binancedaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/builddaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/collectivesdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/contributedaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/developersdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/ecosystemdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/get-starteddaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/toolsdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/partnersdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/pressdaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/privacydaily0.72022-08-31T16:05:42.664Z -https://www.edgeware.io/societydaily0.72022-08-31T16:05:42.664Z - +https://www.edgeware.iodaily0.72023-06-16T14:55:11.505Z +https://www.edgeware.io/binancedaily0.72023-06-16T14:55:11.505Z +https://www.edgeware.io/builddaily0.72023-06-16T14:55:11.505Z +https://www.edgeware.io/collectivesdaily0.72023-06-16T14:55:11.505Z +https://www.edgeware.io/contributedaily0.72023-06-16T14:55:11.505Z +https://www.edgeware.io/developersdaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/ecosystemdaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/get-starteddaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/partnersdaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/pressdaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/privacydaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/societydaily0.72023-06-16T14:55:11.506Z +https://www.edgeware.io/toolsdaily0.72023-06-16T14:55:11.506Z + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9e6652f6..624eeeb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3115,20 +3115,10 @@ camelcase@^6.2.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001219: - version "1.0.30001237" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz" - integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw== - -caniuse-lite@^1.0.30001317: - version "1.0.30001332" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz" - integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== - -caniuse-lite@^1.0.30001332: - version "1.0.30001344" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz" - integrity sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g== +caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332: + version "1.0.30001509" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz" + integrity sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA== caseless@~0.12.0: version "0.12.0"