From c28df19a84f1d2b5daa8155f7a8ab050090b845d Mon Sep 17 00:00:00 2001 From: Daniel Fugere Date: Wed, 20 Sep 2023 15:03:31 -0700 Subject: [PATCH] Telos support in EVM bridge (#168) * fix: catching metamask error when funds are insufficient closes issue #166 * chore: adding support for Telos EVM * refactor: using transferTypes object to determine how different transfers should be processed * refactor: putting all evm transfer logic in evm specific classes * fix: getting page running again * refactor: refactored transfer page to use manager classes * fix: getting page running again * refactor: using the get method to get store values * chore: added telos evm bridge methods * fix: getting telos bridge working * fix: connecting to evm wallet for telos bridge * enhancement: handling case where enter is pressed in the transfer form * chore: getting the telos page working * style: linted * fix: using correct evm network for telos bridge * enhancement: polished evm network switching * fix: catching error when fee makes transfer exceed balance * fix: making network switching more robust --- src/abi-types.ts | 23 ++ src/auth.ts | 8 +- .../elements/form/transaction.svelte | 2 - src/config.ts | 2 +- src/lib/evm.ts | 336 ++++++++++-------- src/pages/transfer/confirm.svelte | 46 ++- src/pages/transfer/form.svelte | 115 ++++-- src/pages/transfer/index.svelte | 191 ++++------ src/pages/transfer/managers/eosEvmBridge.ts | 81 +++++ src/pages/transfer/managers/evmEosBridge.ts | 90 +++++ src/pages/transfer/managers/evmTelosBridge.ts | 65 ++++ src/pages/transfer/managers/index.ts | 12 + src/pages/transfer/managers/telosEvmBridge.ts | 103 ++++++ .../transfer/managers/transferManager.ts | 100 ++++++ src/pages/transfer/success.svelte | 51 ++- src/store.ts | 24 +- src/stores/account-provider.ts | 6 + src/stores/balances-provider.ts | 13 +- 18 files changed, 947 insertions(+), 321 deletions(-) create mode 100644 src/pages/transfer/managers/eosEvmBridge.ts create mode 100644 src/pages/transfer/managers/evmEosBridge.ts create mode 100644 src/pages/transfer/managers/evmTelosBridge.ts create mode 100644 src/pages/transfer/managers/index.ts create mode 100644 src/pages/transfer/managers/telosEvmBridge.ts create mode 100644 src/pages/transfer/managers/transferManager.ts diff --git a/src/abi-types.ts b/src/abi-types.ts index c881898e..7770a8fa 100644 --- a/src/abi-types.ts +++ b/src/abi-types.ts @@ -11,6 +11,7 @@ import { UInt16, UInt32, UInt64, + Checksum160, } from 'anchor-link' @Struct.type('buyrambytes') @@ -170,3 +171,25 @@ export class Transfer extends Struct { @Struct.field('asset') quantity!: Asset @Struct.field('string') memo!: string } + +@Struct.type('withdraw') +export class TelosEvmWithdraw extends Struct { + @Struct.field('name') to!: Name + @Struct.field('asset') quantity!: Asset +} + +@Struct.type('openwallet') +export class TelosEvmOpenWallet extends Struct { + @Struct.field('name') account!: Name + @Struct.field('checksum160') address!: Checksum160 + @Struct.field('name') actor!: Name + @Struct.field('name') permission!: Name +} + +@Struct.type('create') +export class TelosEvmCreate extends Struct { + @Struct.field('name') account!: Name + @Struct.field('string') data!: string + @Struct.field('name') actor!: Name + @Struct.field('name') permission!: Name +} diff --git a/src/auth.ts b/src/auth.ts index cc458aaa..14364a7b 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -5,7 +5,8 @@ import {get} from 'svelte/store' import {storeAccount} from './stores/account-provider' import {getClient} from './api-client' import {appId, chains} from './config' -import {activeSession, availableSessions} from './store' +import {activeEvmSession, activeSession, availableSessions} from './store' +import {startEvmSession} from './lib/evm' const transport = new Transport({ requestStatus: false, @@ -97,4 +98,9 @@ export async function activate(id: SessionLike) { throw new Error('No such session') } activeSession.set(session) + + if (get(activeEvmSession)) { + activeEvmSession.set(undefined) + startEvmSession() + } } diff --git a/src/components/elements/form/transaction.svelte b/src/components/elements/form/transaction.svelte index f5000a9b..98a67e6d 100644 --- a/src/components/elements/form/transaction.svelte +++ b/src/components/elements/form/transaction.svelte @@ -61,7 +61,6 @@ }, clear: () => { error = false - console.log('clearing') transaction_id.set(undefined) }, retryTransaction: () => { @@ -73,7 +72,6 @@ } }, setTransaction: (id: string) => { - console.log('setting') transaction_id.set(id) }, setTransactionError: (err: any) => { diff --git a/src/config.ts b/src/config.ts index 760d1074..d46f79e1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -203,7 +203,7 @@ export const chains: ChainConfig[] = [ resourceSampleAccount: 'greymassfuel', resourceSampleMilliseconds: 1000, testnet: false, - bloksUrl: 'https://telos.bloks.io', + bloksUrl: 'https://explorer.telos.net', balanceProviders: new Set([BalanceProviders.LightAPI]), }, { diff --git a/src/lib/evm.ts b/src/lib/evm.ts index e44aa197..ce0063cb 100644 --- a/src/lib/evm.ts +++ b/src/lib/evm.ts @@ -1,12 +1,21 @@ -import type {LinkSession} from 'anchor-link' -import {Asset, Name} from 'anchor-link' -import {ethers} from 'ethers' - +import {get} from 'svelte/store' import BN from 'bn.js' +import {activeBlockchain, activeEvmSession, activeSession} from '~/store' +import {wait} from '~/helpers' -import {Transfer} from '~/abi-types' +import {Asset, Name, NameType} from 'anchor-link' +import {BigNumber, ethers} from 'ethers' import {getClient} from '~/api-client' +export type AvailableEvms = 'eos-mainnet' | 'telos' +export interface EvmChainConfig { + chainId: string + chainName: string + nativeCurrency: {name: string; symbol: string; decimals: number} + rpcUrls: string[] + blockExplorerUrls: string[] +} + let evmProvider: ethers.providers.Web3Provider declare global { @@ -15,36 +24,80 @@ declare global { } } -interface EvmAccountParams { +export const evmChainConfigs: {[key: string]: EvmChainConfig} = { + eos: { + chainId: '0x4571', + chainName: 'EOS EVM Network', + nativeCurrency: {name: 'EOS', symbol: '4,EOS', decimals: 18}, + rpcUrls: ['https://api.evm.eosnetwork.com/'], + blockExplorerUrls: ['https://explorer.evm.eosnetwork.com'], + }, + telos: { + chainId: '0x28', + chainName: 'Telos EVM Mainnet', + nativeCurrency: {name: 'Telos', symbol: '4,TLOS', decimals: 18}, + rpcUrls: ['https://mainnet.telos.net/evm'], + blockExplorerUrls: ['https://teloscan.io'], + }, +} + +export interface EvmSessionParams { signer: ethers.providers.JsonRpcSigner address: string + chainName: string + nativeAccountName?: NameType } -function getProvider() { - if (evmProvider) { - return evmProvider - } - - if (window.ethereum) { - evmProvider = new ethers.providers.Web3Provider(window.ethereum) - return evmProvider - } - - throw new Error('No provider found') +export interface EvmSessionFromParams { + chainName: string + nativeAccountName?: NameType } -export class EvmAccount { +export class EvmSession { address: string signer: ethers.providers.JsonRpcSigner + chainName: string + nativeAccountName?: NameType - constructor({address, signer}: EvmAccountParams) { + constructor({address, signer, chainName, nativeAccountName}: EvmSessionParams) { this.address = address this.signer = signer + this.chainName = chainName + this.nativeAccountName = nativeAccountName } - static from(EvmAccountParams: EvmAccountParams) { - // Implement your logic here - return new EvmAccount(EvmAccountParams) + get checksumAddress() { + return this.address.replace('0x', '').toLowerCase() + } + + get chainConfig() { + return evmChainConfigs[this.chainName] + } + + static async from({chainName, nativeAccountName}: EvmSessionFromParams) { + if (window.ethereum) { + const evmChainConfig = evmChainConfigs[chainName] + const provider = getProvider() + let network = await provider.getNetwork() + if (network.chainId !== Number(evmChainConfig.chainId.replace('0x', ''))) { + await switchNetwork(evmChainConfig) + network = await provider.detectNetwork() + } + + await window.ethereum.request({method: 'eth_requestAccounts'}) + const signer = provider.getSigner() + + await window.ethereum.get_currency_balance + + return new EvmSession({ + address: await signer.getAddress(), + signer, + chainName, + nativeAccountName, + }) + } else { + throw new Error('You need to install Metamask in order to use this feature.') + } } async sendTransaction(tx: ethers.providers.TransactionRequest) { @@ -52,10 +105,68 @@ export class EvmAccount { } async getBalance() { + if (this.chainName === 'telos') { + return this.getTelosEvmBalance() + } + const wei = await this.signer.getBalance() - return formatEOS(ethers.utils.formatEther(wei)) + const tokenName = evmChainConfigs[this.chainName].nativeCurrency.name + + return Asset.from(formatToken(ethers.utils.formatEther(wei), tokenName)) } + + async getTelosEvmBalance() { + if (!this.nativeAccountName) { + throw new Error('Native account name is necessary to fetch Telos balance.') + } + + const account = await getTelosEvmAccount(this.nativeAccountName) + + if (!account) { + return Asset.from(0, this.chainConfig.nativeCurrency.symbol) + } + + const bn = BigNumber.from(`0x${account.balance}`) + + return Asset.from( + Number(ethers.utils.formatEther(bn)), + this.chainConfig.nativeCurrency.symbol + ) + } +} + +export async function getTelosEvmAccount(nativeAccountName: NameType) { + const chain = get(activeBlockchain) + const client = getClient(chain.chainId) + + if (!client) { + throw new Error('API client could not be instantiated') + } + + const {rows} = await client.v1.chain.get_table_rows({ + code: 'eosio.evm', + scope: 'eosio.evm', + table: 'account', + index_position: 'tertiary', + lower_bound: Name.from(nativeAccountName), + upper_bound: Name.from(nativeAccountName), + }) + + return rows[0] +} + +export function getProvider() { + if (evmProvider) { + return evmProvider + } + + if (window.ethereum) { + evmProvider = new ethers.providers.Web3Provider(window.ethereum, 'any') + return evmProvider + } + + throw new Error('No provider found') } export function convertToEvmAddress(eosAccountName: string): string { @@ -66,6 +177,26 @@ export function convertToEvmAddress(eosAccountName: string): string { return convertToEthAddress(eosAccountName) } +function formatToken(amount: string, tokenSymbol: string) { + return `${Number(amount).toFixed(4)} ${tokenSymbol}` +} + +export async function switchNetwork(evmChainConfig: EvmChainConfig) { + await window.ethereum + .request({ + method: 'wallet_switchEthereumChain', + params: [{chainId: evmChainConfig.chainId}], + }) + .catch(async (e: {code: number}) => { + if (e.code === 4902) { + await window.ethereum.request({ + method: 'wallet_addEthereumChain', + params: [evmChainConfig], + }) + } + }) +} + function convertToEthAddress(eosAddress: string) { try { return uint64ToAddr(strToUint64(eosAddress)) @@ -118,149 +249,44 @@ function uint64ToAddr(str: string) { return '0xbbbbbbbbbbbbbbbbbbbbbbbb' + str } -interface TransferParams { - amount: string - evmAccount: EvmAccount - nativeSession: LinkSession -} - -export async function transferNativeToEvm({nativeSession, evmAccount, amount}: TransferParams) { - const action = Transfer.from({ - from: nativeSession.auth.actor, - to: 'eosio.evm', - quantity: String(Asset.fromFloat(Number(amount), '4,EOS')), - memo: evmAccount.address, - }) - - return nativeSession.transact({ - action: { - authorization: [nativeSession.auth], - account: Name.from('eosio.token'), - name: Name.from('transfer'), - data: action, - }, - }) -} - -export async function estimateGas({nativeSession, evmAccount, amount}: TransferParams) { - const provider = getProvider() - - const targetEvmAddress = convertToEvmAddress(String(nativeSession.auth.actor)) - - const gasPrice = await provider.getGasPrice() - - // Reducing the amount by 0.005 EOS to avoid getting an error when entire balance is sent. Proper amount is calculated once the gas fee is known. - const reducedAmount = String(Number(amount) - 0.005) - - const gas = await provider.estimateGas({ - from: evmAccount.address, - to: targetEvmAddress, - value: ethers.utils.parseEther(reducedAmount), - gasPrice, - data: ethers.utils.formatBytes32String(''), - }) - - return {gas, gasPrice} -} - -export async function getNativeTransferFee({ - nativeSession, -}: { - nativeSession: LinkSession -}): Promise { - const apiClient = getClient(nativeSession.chainId) +let connectingToEvm = false - let apiResponse +export async function startEvmSession(): Promise { + let evmSession: EvmSession - try { - apiResponse = await apiClient.v1.chain.get_table_rows({ - code: 'eosio.evm', - scope: 'eosio.evm', - table: 'config', - }) - } catch (err) { - throw new Error('Failed to get config table from eosio.evm. Full error: ' + err) + if (connectingToEvm) { + await wait(5000) + return startEvmSession() } - const config = apiResponse.rows[0] + connectingToEvm = true - return Asset.from(config.ingress_bridge_fee) -} - -export async function getGasAmount({ - nativeSession, - evmAccount, - amount, -}: TransferParams): Promise { - const {gas, gasPrice} = await estimateGas({nativeSession, evmAccount, amount}) - - const eosAmount = ethers.utils.formatEther(Number(gas) * Number(gasPrice)) - - return Asset.fromFloat(Number(eosAmount), '4,EOS') -} - -export async function transferEvmToNative({nativeSession, evmAccount, amount}: TransferParams) { - const targetEvmAddress = convertToEvmAddress(String(nativeSession.auth.actor)) - - const {gas} = await estimateGas({nativeSession, evmAccount, amount}) + const blockchain = get(activeBlockchain) + const nativeSession = get(activeSession) - return evmAccount.sendTransaction({ - from: evmAccount.address, - to: targetEvmAddress, - value: ethers.utils.parseEther(amount), - gasPrice: await getProvider().getGasPrice(), - gasLimit: gas, - data: ethers.utils.formatBytes32String(''), - }) -} - -async function switchNetwork() { - await window.ethereum - .request({ - method: 'wallet_switchEthereumChain', - params: [{chainId: '0x4571'}], - }) - .catch(async (e: {code: number}) => { - if (e.code === 4902) { - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: '0x4571', - chainName: 'EOS EVM Network', - nativeCurrency: {name: 'EOS', symbol: 'EOS', decimals: 18}, - rpcUrls: ['https://api.evm.eosnetwork.com/'], - blockExplorerUrls: ['https://explorer.evm.eosnetwork.com'], - }, - ], - }) - } + try { + evmSession = await EvmSession.from({ + chainName: blockchain.id, + nativeAccountName: nativeSession?.auth.actor, }) -} - -export async function connectEthWallet(): Promise { - if (window.ethereum) { - const provider = getProvider() - let networkId = await provider.getNetwork() - if (networkId.chainId !== 17777) { - await switchNetwork() - networkId = await provider.getNetwork() + } catch (e) { + if (e.code === -32002) { + await wait(5000) + return startEvmSession() } - await window.ethereum.request({method: 'eth_requestAccounts'}) - const signer = provider.getSigner() + if (!e.message) { + connectingToEvm = false + return + } - await window.ethereum.get_currency_balance + throw new Error(`Could not connect to EVM. Error: ${e.message}`) + } - return EvmAccount.from({ - address: await signer.getAddress(), - signer, - }) - } else { - throw 'You need to install Metamask in order to use this feature.' + if (evmSession) { + activeEvmSession.set(evmSession) + connectingToEvm = false } -} -function formatEOS(amount: String) { - return `${Number(amount).toFixed(4)} EOS` + return evmSession } diff --git a/src/pages/transfer/confirm.svelte b/src/pages/transfer/confirm.svelte index d99f390d..42b3dca6 100644 --- a/src/pages/transfer/confirm.svelte +++ b/src/pages/transfer/confirm.svelte @@ -5,18 +5,36 @@ import TokenImage from '~/components/elements/image/token.svelte' import {systemTokenKey} from '~/stores/tokens' - import {valueInFiat} from '~/lib/fiat' + import type {TransferManager} from './managers/transferManager' - import {evmAccount, activeSession, activePriceTicker} from '~/store' - import type {Token} from '~/stores/tokens' - - export let from: Token - export let to: Token + export let transferManager: TransferManager export let depositAmount: Asset export let receivedAmount: Asset export let feeAmount: Asset | undefined export let handleConfirm: () => void export let handleBack: () => void + + let depositAmountInUsd: string + let receivedAmountInUsd: string + let feeAmountInUsd: string + + function getUsdValues() { + transferManager.convertToUsd(depositAmount?.value).then((usdValue) => { + depositAmountInUsd = usdValue + }) + + transferManager.convertToUsd(receivedAmount?.value).then((usdValue) => { + receivedAmountInUsd = usdValue + }) + + if (feeAmount) { + transferManager.convertToUsd(feeAmount?.value).then((usdValue) => { + feeAmountInUsd = usdValue + }) + } + } + + getUsdValues()