diff --git a/.gitignore b/.gitignore index 56ca43742..72638cda5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,5 @@ yarn-error.log* *.ntvs* *.njsproj *.sln - +.vscode .env diff --git a/env.js b/env.js index 8fd798203..ebefb698a 100644 --- a/env.js +++ b/env.js @@ -9,6 +9,7 @@ const sharedEnv = { IMGUR_CLIENT_ID: 'b6f46df9d1da9d9', EVM_CONTRACT: 'eosio.evm', PROJECT_ID: '2392473d6d98499c7138cd2d705a791f', + GOOGLE_APP_ID: '639241197544-kcubenhmti6u7ef3uj360n2lcl5cmn8c.apps.googleusercontent.com', // Viter's client id }; const TESTNET = { @@ -22,8 +23,9 @@ const TESTNET = { HYPERION_ENDPOINT: 'https://testnet.telos.net', NETWORK_EXPLORER: 'https://explorer-test.telos.net', CHAIN_NAME: 'telos-testnet', - OREID_APP_ID: 't_75a4d9233ec441d18c4221e92b379197', - OREID_APP_ID_NATIVE: 't_a61e9926d5204387a9ac113dfce7cbc5', + DEFAULT_NETWORK: 'telos-evm-testnet', + METAKEEP_APP_ID_NATIVE: 'ad5e05fb-280a-41ae-b186-5a2654567b92', // Viter's app id + METAKEEP_APP_ID_EVM: 'd190c88f-1bb5-4e16-bc48-96dbf33b77e0', // Viter's app id }; const MAINNET = { @@ -37,8 +39,9 @@ const MAINNET = { HYPERION_ENDPOINT: 'https://mainnet.telos.net', NETWORK_EXPLORER: 'https://explorer.telos.net', CHAIN_NAME: 'telos', - OREID_APP_ID: 'p_e5b81fcc20a04339993b0cc80df7e3fd', - OREID_APP_ID_NATIVE: 'p_751f87258d5b40998b55c626d612fd4e', + DEFAULT_NETWORK: 'telos-evm', + METAKEEP_APP_ID_NATIVE: 'ad5e05fb-280a-41ae-b186-5a2654567b92', // Viter's app id - TODO: WE NEED TO CHANGE THIS + METAKEEP_APP_ID_EVM: 'd190c88f-1bb5-4e16-bc48-96dbf33b77e0', // Viter's app id - TODO: WE NEED TO CHANGE THIS }; const env = process.env.NETWORK === 'mainnet' ? MAINNET : TESTNET; diff --git a/package.json b/package.json index a0a810652..44b581ff6 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,12 @@ "eosjs": "^21.0.3", "eosjs-ecc": "^4.0.7", "erc-20-abi": "^1.0.0", - "ethers": "^5.5.1", + "ethers": "5.7.0", "jdenticon": "^3.1.1", + "metakeep": "^2.2.0", "mitt": "^3.0.0", "node-polyfill-webpack-plugin": "^2.0.1", "numeral": "^2.0.6", - "oreid-js": "^4.7.1", - "oreid-webpopup": "^2.4.0", "pinia": "^2.0.33", "ptokens": "^0.14.0", "qrcanvas-vue": "^3.0.0", @@ -46,7 +45,6 @@ "quasar": "2", "rxjs": "^7.8.0", "ual-anchor": "^1.1.2", - "ual-oreid": "^1.0.0", "ual-wombat": "^0.3.3", "universal-authenticator-library": "^0.3.0", "vue": "3", diff --git a/quasar.conf.js b/quasar.conf.js index deef1ec28..2fc41ccc1 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -30,7 +30,7 @@ module.exports = function(/* ctx */) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://quasar.dev/quasar-cli/boot-files - boot: ['ual', 'hyperion', 'i18n', 'fuel', 'api', 'errorHandling', 'helpers', 'mixin', 'emitter', 'telosApi', 'wagmi', 'antelope'], + boot: ['ual', 'hyperion', 'i18n', 'fuel', 'api', 'errorHandling', 'helpers', 'mixin', 'emitter', 'telosApi', 'wagmi', 'antelope', 'telosCloudJs'], // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css css: ['index.scss'], diff --git a/src/App.vue b/src/App.vue index dc13a9894..3ffd699a5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,10 @@ diff --git a/src/antelope/chains/NativeChainSettings.ts b/src/antelope/chains/NativeChainSettings.ts index 8c71c31ab..653eb4b28 100644 --- a/src/antelope/chains/NativeChainSettings.ts +++ b/src/antelope/chains/NativeChainSettings.ts @@ -411,6 +411,16 @@ export default abstract class NativeChainSettings implements ChainSettings { return this.eosioCore.v1.chain.get_account(address); } + async isAccountNameAvailable(accountName: string): Promise { + return new Promise((resolve) => { + this.eosioCore.v1.chain.get_account(accountName).then(() => { + resolve(false); + }).catch(() => { + resolve(true); + }); + }); + } + async getKeyAccounts(key: string): Promise { return (await this.eosioCore.v1.history.get_key_accounts(key)) as unknown as KeyAccounts; } diff --git a/src/antelope/chains/chain-constants.ts b/src/antelope/chains/chain-constants.ts index 53f0cd569..0e3eafd28 100644 --- a/src/antelope/chains/chain-constants.ts +++ b/src/antelope/chains/chain-constants.ts @@ -10,8 +10,6 @@ export const TELOS_ANALYTICS_EVENT_NAMES = { loginFailedMetamask: 'Login Failed - Metamask', loginSuccessfulSafepal: 'Login Successful - Safepal', loginFailedSafepal: 'Login Failed - Safepal', - loginSuccessfulOreId: 'Login Successful - OreId', - loginFailedOreId: 'Login Failed - OreId', loginFailedWalletConnect: 'Login Failed - WalletConnect', loginSuccessfulWalletConnect: 'Login Successful - WalletConnect', loginSuccessfulBrave: 'Login Successful - Brave', diff --git a/src/antelope/config/AntelopeConfig.ts b/src/antelope/config/AntelopeConfig.ts index 1ad035979..2455a6d26 100644 --- a/src/antelope/config/AntelopeConfig.ts +++ b/src/antelope/config/AntelopeConfig.ts @@ -10,11 +10,6 @@ export interface ComplexMessage { text: string, } -export const chainNetworkNames: Record = { - telos: 'telos-evm', - 'telos-testnet': 'telos-evm-testnet', -}; - export const errorToString = (error: unknown) => getAntelope().config.errorToStringHandler(error); diff --git a/src/antelope/index.ts b/src/antelope/index.ts index 8c399e3f0..df37b094b 100644 --- a/src/antelope/index.ts +++ b/src/antelope/index.ts @@ -3,7 +3,7 @@ import { App, toRaw } from 'vue'; import { BehaviorSubject, Subject } from 'rxjs'; import { Store } from 'pinia'; -import { AntelopeConfig, AntelopeDebug, chainNetworkNames } from 'src/antelope/config'; +import { AntelopeConfig, AntelopeDebug } from 'src/antelope/config'; import installPinia from 'src/antelope/stores'; import { ChainModel } from 'src/antelope/stores/chain'; @@ -75,17 +75,6 @@ export class Antelope { } }); - const chainStore = useChainStore(); - - if (!chainStore.currentChain) { - if (!process.env.CHAIN_NAME) { - console.error('No chain name specified in environment config; the application will not run correctly'); - } else { - const network: string = chainNetworkNames[process.env.CHAIN_NAME]; - chainStore.setChain(CURRENT_CONTEXT, network); - } - } - // Initializing store stores.user.loadUsers(); } diff --git a/src/antelope/stores/account.ts b/src/antelope/stores/account.ts index d34c9bf10..4fb9b509b 100644 --- a/src/antelope/stores/account.ts +++ b/src/antelope/stores/account.ts @@ -20,7 +20,6 @@ import { initFuelUserWrapper } from 'src/api/fuel'; import { createTraceFunction, errorToString } from 'src/antelope/config'; import NativeChainSettings from 'src/antelope/chains/NativeChainSettings'; import { - Action, Label, NativeTransactionResponse, addressString, @@ -29,7 +28,6 @@ import { EVMAuthenticator } from 'src/antelope/wallets'; import { truncateAddress } from 'src/antelope/stores/utils/text-utils'; import { toRaw } from 'vue'; import { getAddress } from 'ethers/lib/utils'; -import { OreIdAuthenticator } from 'ual-oreid'; // dependencies -- import { @@ -40,7 +38,7 @@ import { } from 'src/antelope'; -export interface LoginNativeActionData { +export interface loginZeroActionData { authenticator: Authenticator, network: string, } @@ -112,15 +110,17 @@ export const useAccountStore = defineStore(store_name, { }, actions: { trace: createTraceFunction(store_name), - async loginNative({ authenticator, network }: LoginNativeActionData): Promise { - this.trace('loginNative', authenticator, network); + async loginZero({ authenticator, network }: loginZeroActionData): Promise { + this.trace('loginZero', authenticator, network); let success = false; try { + this.trace('loginZero', 'authenticator.init()...'); await authenticator.init(); + this.trace('loginZero', 'authenticator.login()...'); const ualUsers = await authenticator.login(); if (ualUsers?.length) { - // OreId has it's own authorization service, only init fuel service for other ual users - const ualUser = ualUsers[0] instanceof OreIdAuthenticator ? ualUsers[0] : await initFuelUserWrapper(ualUsers[0]); + this.trace('loginZero', 'authenticator.login() OK! ualUsers:', ualUsers); + const ualUser = await initFuelUserWrapper(ualUsers[0]); const permission = (ualUser as unknown as { requestPermission: string }) .requestPermission ?? 'active'; const account = await ualUser.getAccountName(); @@ -244,8 +244,10 @@ export const useAccountStore = defineStore(store_name, { const account = localStorage.getItem('account'); const isNative = localStorage.getItem('isNative') === 'true'; const autoLogin = localStorage.getItem('autoLogin'); - this.trace('autoLogin', account, isNative, autoLogin); + this.trace('autoLogin', account, network, autoLogin, isNative, this.__accounts[label]); if (account && network && autoLogin && !this.__accounts[label]) { + // Ensure we are working with the correct network + useChainStore().setChain(label, network); if (isNative) { const authenticators = getAntelope().config.authenticatorsGetter(); const authenticator = authenticators.find( @@ -255,7 +257,7 @@ export const useAccountStore = defineStore(store_name, { console.error(authenticators.map(a => a.getName()).join(', ')); throw new Error('antelope.account.error_auto_login'); } - this.loginNative({ + this.loginZero({ authenticator, network, }); @@ -270,6 +272,8 @@ export const useAccountStore = defineStore(store_name, { network, }); } + } else { + this.trace('autoLogin', 'canceled!', account, network, autoLogin, !this.__accounts[label]); } } catch (error) { console.error('Error: ', errorToString(error)); @@ -297,7 +301,6 @@ export const useAccountStore = defineStore(store_name, { this.trace('sendAction', account, data, name, actor, permission); try { useFeedbackStore().setLoading('account.sendAction'); - console.error('Account.sendAction() not implemented', account, data, name, actor, permission); return Promise.resolve({ hash: '0x0' } as NativeTransactionResponse); } catch (error) { console.error('Error: ', errorToString(error)); @@ -307,16 +310,6 @@ export const useAccountStore = defineStore(store_name, { } }, - async sendTransaction(actions: Action[]) { - this.trace('sendTransaction', actions); - try { - useFeedbackStore().setLoading('account.sendTransaction'); - console.error('Account.sendTransaction() not implemented', actions); - } catch (error) { - console.error('Error: ', errorToString(error)); - } - }, - async fetchAccountDataFor(label: string, account: AccountModel) { this.trace('fetchAccountDataFor', account); try { diff --git a/src/antelope/stores/allowances.ts b/src/antelope/stores/allowances.ts index 0f53bce9c..b6548afee 100644 --- a/src/antelope/stores/allowances.ts +++ b/src/antelope/stores/allowances.ts @@ -239,6 +239,11 @@ export const useAllowancesStore = defineStore(store_name, { const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + if (chainSettings.isNative()) { + this.trace('fetchAllowancesForAccount', 'Native chain does not have allowances'); + return; + } + const erc20AllowancesPromise = chainSettings.fetchErc20Allowances(account, { limit: ALLOWANCES_LIMIT }); const erc721AllowancesPromise = chainSettings.fetchErc721Allowances(account, { limit: ALLOWANCES_LIMIT }); const erc1155AllowancesPromise = chainSettings.fetchErc1155Allowances(account, { limit: ALLOWANCES_LIMIT }); diff --git a/src/antelope/stores/chain.ts b/src/antelope/stores/chain.ts index 177d18121..b303d01b4 100644 --- a/src/antelope/stores/chain.ts +++ b/src/antelope/stores/chain.ts @@ -67,12 +67,14 @@ export const settings: { [key: string]: ChainSettings } = { }; export interface ChainModel { + lastUpdate: number; apy: string; settings: ChainSettings; tokens: TokenClass[]; } export interface EvmChainModel { + lastUpdate: number; apy: string; stakeRatio: ethers.BigNumber; unstakeRatio: ethers.BigNumber; @@ -82,6 +84,7 @@ export interface EvmChainModel { } export interface NativeChainModel { + lastUpdate: number; apy: string; settings: NativeChainSettings; tokens: TokenClass[]; @@ -89,6 +92,7 @@ export interface NativeChainModel { const newChainModel = (network: string, isNative: boolean): ChainModel => { const model = { + lastUpdate: 0, apy: '', stakeRatio: ethers.constants.Zero, unstakeRatio: ethers.constants.Zero, @@ -116,7 +120,7 @@ export const useChainStore = defineStore(store_name, { loggedChain: state => state.__chains[CURRENT_CONTEXT], currentChain: state => state.__chains[CURRENT_CONTEXT], loggedEvmChain: state => state.__chains[CURRENT_CONTEXT].settings.isNative() ? undefined : state.__chains[CURRENT_CONTEXT] as EvmChainModel, - currentEvmChain: state => state.__chains[CURRENT_CONTEXT].settings.isNative() ? undefined : state.__chains[CURRENT_CONTEXT] as EvmChainModel, + currentEvmChain: state => state.__chains[CURRENT_CONTEXT]?.settings.isNative() ? undefined : state.__chains[CURRENT_CONTEXT] as EvmChainModel, loggedNativeChain: state => state.__chains[CURRENT_CONTEXT].settings.isNative() ? state.__chains[CURRENT_CONTEXT] as NativeChainModel : undefined, currentNativeChain: state => state.__chains[CURRENT_CONTEXT].settings.isNative() ? state.__chains[CURRENT_CONTEXT] as NativeChainModel : undefined, getChain: state => (label: string) => state.__chains[label], @@ -136,12 +140,22 @@ export const useChainStore = defineStore(store_name, { this.trace('updateChainData'); useFeedbackStore().setLoading('updateChainData'); try { - await Promise.all([ - this.updateSettings(label), - this.updateApy(label), - this.updateGasPrice(label), - this.updateStakedRatio(label), - ]); + const chain = this.getChain(label); + const now = Date.now(); + const tolerance = 1000 * 10; // 10 seconds + const isUpToDate = now - chain.lastUpdate < tolerance; + if (isUpToDate) { + // This avoid to update the chain data if the user switches from one chain to another and back + this.trace('updateChainData', label, '-> already up to date'); + } else { + this.setChainLastUpdate(label, Date.now()); + await Promise.all([ + this.updateSettings(label), + this.updateApy(label), + this.updateGasPrice(label), + this.updateStakedRatio(label), + ]); + } } catch (error) { console.error(error); throw new Error('antelope.chain.error_update_data'); @@ -180,23 +194,31 @@ export const useChainStore = defineStore(store_name, { }, async updateStakedRatio(label: string): Promise { // first we need the contract instance to be able to execute queries - this.trace('actualUpdateStakedRatio', label); - useFeedbackStore().setLoading('actualUpdateStakedRatio'); - const chain_settings = useChainStore().getChain(label).settings as EVMChainSettings; - const sysToken = chain_settings.getSystemToken(); - const stkToken = chain_settings.getStakedSystemToken(); + this.trace('updateStakedRatio', label); + const chain = this.getChain(label); + try { + useFeedbackStore().setLoading('updateStakedRatio'); + if (!chain.settings.isNative()) { + const chain_settings = chain.settings as EVMChainSettings; + const sysToken = chain_settings.getSystemToken(); + const stkToken = chain_settings.getStakedSystemToken(); - const abi = [stlosAbiPreviewDeposit[0], stlosAbiPreviewRedeem[0]]; - const provider = await getAntelope().wallets.getWeb3Provider(); - const contractInstance = new ethers.Contract(stkToken.address, abi, provider); - // Now we preview a deposit of 1 SYS to get the ratio - const oneSys = ethers.utils.parseUnits('1.0', sysToken.decimals); - const stakedRatio = await contractInstance.previewDeposit(oneSys.toString()); - const unstakedRatio:ethers.BigNumber = await contractInstance.previewRedeem(oneSys); - // Finally we update the store - this.setStakedRatio(label, stakedRatio); - this.setUnstakedRatio(label, unstakedRatio); - useFeedbackStore().unsetLoading('actualUpdateStakedRatio'); + const abi = [stlosAbiPreviewDeposit[0], stlosAbiPreviewRedeem[0]]; + const provider = await getAntelope().wallets.getWeb3Provider(); + const contractInstance = new ethers.Contract(stkToken.address, abi, provider); + // Now we preview a deposit of 1 SYS to get the ratio + const oneSys = ethers.utils.parseUnits('1.0', sysToken.decimals); + const stakedRatio = await contractInstance.previewDeposit(oneSys.toString()); + const unstakedRatio:ethers.BigNumber = await contractInstance.previewRedeem(oneSys); + // Finally we update the store + this.setStakedRatio(label, stakedRatio); + this.setUnstakedRatio(label, unstakedRatio); + } + } catch (error) { + console.error(error); + } finally { + useFeedbackStore().unsetLoading('updateStakedRatio'); + } }, async updateGasPrice(label: string): Promise { useFeedbackStore().setLoading('updateGasPrice'); @@ -206,6 +228,8 @@ export const useChainStore = defineStore(store_name, { if (!chain.settings.isNative()) { const wei = await (chain.settings as EVMChainSettings).getGasPrice(); (chain as EvmChainModel).gasPrice = wei; + } else { + this.trace('updateGasPrice', label, 'Native chain has no gas costs'); } } catch (error) { console.error(error); @@ -237,7 +261,6 @@ export const useChainStore = defineStore(store_name, { // make the change only if they are different if (network !== this.__chains[label]?.settings.getNetwork()) { this.__chains[label] = newChainModel(network, settings[network].isNative()); - this.trace('setChain', label, network, '--> void this.updateChainData(label);'); void this.updateChainData(label); getAntelope().events.onNetworkChanged.next( { label, chain: this.__chains[label] }, @@ -248,16 +271,47 @@ export const useChainStore = defineStore(store_name, { } }, setStakedRatio(label: string, ratio: ethers.BigNumber) { - const decimals = (this.getChain(label).settings as EVMChainSettings).getStakedSystemToken().decimals; - const ratioNumber = parseFloat(ethers.utils.formatUnits(ratio, decimals)); - this.trace('setStakedRatio', label, ratio.toString(), ratioNumber); - (this.__chains[label] as EvmChainModel).stakeRatio = ratio; + this.trace('setStakedRatio', label, ratio.toString()); + const chain = this.getChain(label); + try { + if (!chain.settings.isNative()) { + const decimals = (this.getChain(label).settings as EVMChainSettings).getStakedSystemToken().decimals; + const ratioNumber = parseFloat(ethers.utils.formatUnits(ratio, decimals)); + this.trace('setStakedRatio', label, ratio.toString(), ratioNumber); + (this.__chains[label] as EvmChainModel).stakeRatio = ratio; + } else { + this.trace('setStakedRatio', label, 'Native chain has no staked ratio'); + } + } catch (error) { + console.error(error); + throw new Error('antelope.chain.error_token_list'); + } finally { + useFeedbackStore().unsetLoading('updateTokenList'); + } + }, setUnstakedRatio(label: string, ratio: ethers.BigNumber) { - const decimals = (this.getChain(label).settings as EVMChainSettings).getStakedSystemToken().decimals; - const ratioNumber = parseFloat(ethers.utils.formatUnits(ratio, decimals)); - this.trace('setUnstakedRatio', label, ratio.toString(), ratioNumber); - (this.__chains[label] as EvmChainModel).unstakeRatio = ratio; + this.trace('setUnstakedRatio', label, ratio.toString()); + const chain = this.getChain(label); + try { + if (!chain.settings.isNative()) { + const decimals = (this.getChain(label).settings as EVMChainSettings).getStakedSystemToken().decimals; + const ratioNumber = parseFloat(ethers.utils.formatUnits(ratio, decimals)); + this.trace('setUnstakedRatio', label, ratio.toString(), ratioNumber); + (this.__chains[label] as EvmChainModel).unstakeRatio = ratio; + } else { + this.trace('setUnstakedRatio', label, 'Native chain has no unstaked ratio'); + } + } catch (error) { + console.error(error); + throw new Error('antelope.chain.error_token_list'); + } finally { + useFeedbackStore().unsetLoading('updateTokenList'); + } + }, + setChainLastUpdate(label: string, timestamp: number) { + this.trace('setChainLastUpdate', label, timestamp); + this.__chains[label].lastUpdate = timestamp; }, }, }); diff --git a/src/antelope/stores/history.ts b/src/antelope/stores/history.ts index 8abb115f5..0a5365973 100644 --- a/src/antelope/stores/history.ts +++ b/src/antelope/stores/history.ts @@ -190,11 +190,14 @@ export const useHistoryStore = defineStore(store_name, { const contractStore = useContractStore(); this.trace('fetchEvmNftTransfersForAccount', label); - - feedbackStore.setLoading('history.fetchEvmNftTransfersForAccount'); - const chainSettings = useChainStore().getChain(label).settings as EVMChainSettings; + if (chainSettings.isNative()) { + this.trace('fetchEvmNftTransfersForAccount', 'Native networks not supported yet'); + return; + } + + feedbackStore.setLoading('history.fetchEvmNftTransfersForAccount'); try { // get all erc721 and erc1155 transfers for the given account const [ diff --git a/src/antelope/stores/rex.ts b/src/antelope/stores/rex.ts index e79ce986a..974f28ade 100644 --- a/src/antelope/stores/rex.ts +++ b/src/antelope/stores/rex.ts @@ -116,15 +116,27 @@ export const useRexStore = defineStore(store_name, { const address = (useChainStore().getChain(label).settings as EVMChainSettings).getEscrowContractAddress(); return this.getContractInstance(label, address); }, + /** + * This method should be called to check if the REX system is available for a given context. + * @param label identifies the context (network on this case) for the data + * @returns true if the REX system is available for the given context + */ + isNetworkEVM(label: string) { + return !useChainStore().getChain(label).settings.isNative(); + }, /** * This method queries the total amount of staked tokens in the system and maintains it in the store. * @param label identifies the context (network on this case) for the data */ async updateTotalStaking(label: string) { this.trace('updateTotalStaking', label); - const contract = await this.getStakedSystemContractInstance(label); - const totalStaking = await contract.totalAssets(); - this.setTotalStaking(label, totalStaking); + if (this.isNetworkEVM(label)) { + const contract = await this.getStakedSystemContractInstance(label); + const totalStaking = await contract.totalAssets(); + this.setTotalStaking(label, totalStaking); + } else { + this.trace('updateTotalStaking', label, 'not supported for native chains yet'); + } }, /** @@ -133,9 +145,13 @@ export const useRexStore = defineStore(store_name, { */ async updateUnstakingPeriod(label: string) { this.trace('updateUnstakingPeriod', label); - const contract = await this.getEscrowContractInstance(label); - const period = await contract.lockDuration(); - this.setUnstakingPeriod(label, period.toNumber()); + if (this.isNetworkEVM(label)) { + const contract = await this.getEscrowContractInstance(label); + const period = await contract.lockDuration(); + this.setUnstakingPeriod(label, period.toNumber()); + } else { + this.trace('updateUnstakingPeriod', label, 'not supported for native chains yet'); + } }, /** @@ -178,10 +194,14 @@ export const useRexStore = defineStore(store_name, { */ async updateWithdrawable(label: string) { this.trace('updateWithdrawable', label); - const contract = await this.getEscrowContractInstance(label); - const address = useAccountStore().getAccount(label).account; - const withdrawable = await contract.maxWithdraw(address); - this.setWithdrawable(label, withdrawable); + if (this.isNetworkEVM(label)) { + const contract = await this.getEscrowContractInstance(label); + const address = useAccountStore().getAccount(label).account; + const withdrawable = await contract.maxWithdraw(address); + this.setWithdrawable(label, withdrawable); + } else { + this.trace('updateWithdrawable', label, 'not supported for native chains yet'); + } }, /** * This method queries the deposits for a given account and maintains it in the store. @@ -191,10 +211,14 @@ export const useRexStore = defineStore(store_name, { */ async updateDeposits(label: string) { this.trace('updateDeposits', label); - const contract = await this.getEscrowContractInstance(label); - const address = useAccountStore().getAccount(label).account; - const deposits = await contract.depositsOf(address); - this.setDeposits(label, deposits); + if (this.isNetworkEVM(label)) { + const contract = await this.getEscrowContractInstance(label); + const address = useAccountStore().getAccount(label).account; + const deposits = await contract.depositsOf(address); + this.setDeposits(label, deposits); + } else { + console.error('updateDeposits', label, 'not supported for native chains yet'); + } }, /** * This method queries the balance for a given account and maintains it in the store. @@ -203,10 +227,14 @@ export const useRexStore = defineStore(store_name, { */ async updateBalance(label: string) { this.trace('updateBalance', label); - const contract = await this.getEscrowContractInstance(label); - const address = useAccountStore().getAccount(label).account; - const balance = await contract.balanceOf(address); - this.setBalance(label, balance); + if (this.isNetworkEVM(label)) { + const contract = await this.getEscrowContractInstance(label); + const address = useAccountStore().getAccount(label).account; + const balance = await contract.balanceOf(address); + this.setBalance(label, balance); + } else { + console.error('updateBalance', label, 'not supported for native chains yet'); + } }, /** * utility function to get the number of decimals for the staked system token diff --git a/src/antelope/types/ual-oreid.d.ts b/src/antelope/types/ual-oreid.d.ts deleted file mode 100644 index 9b60e1900..000000000 --- a/src/antelope/types/ual-oreid.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'ual-oreid'; diff --git a/src/antelope/wallets/AntelopeWallets.ts b/src/antelope/wallets/AntelopeWallets.ts index 26fca5477..53727932d 100644 --- a/src/antelope/wallets/AntelopeWallets.ts +++ b/src/antelope/wallets/AntelopeWallets.ts @@ -42,28 +42,19 @@ export class AntelopeWallets { // we try first the best solution which is taking the provider from the current authenticator const authenticator = account.authenticator as EVMAuthenticator; const provider = authenticator.web3Provider(); + this.trace('getWeb3Provider authenticator.web3Provider() Success! (account.authenticator)', provider); return provider; } catch(e1) { this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e1); } - // we try to build a web3 provider from a local injected provider it it exists - try { - if (window.ethereum) { - const web3Provider = new ethers.providers.Web3Provider(window.ethereum); - await web3Provider.ready; - return web3Provider; - } - } catch(e2) { - this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e2); - } - try { const p:RpcEndpoint = this.getChainSettings(CURRENT_CONTEXT).getRPCEndpoint(); const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; const jsonRpcProvider = new ethers.providers.JsonRpcProvider(url); await jsonRpcProvider.ready; const web3Provider = jsonRpcProvider as ethers.providers.Web3Provider; + this.trace('getWeb3Provider authenticator.web3Provider() Success! (jsonRpcProvider)', web3Provider); return web3Provider; } catch (e3) { this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e3); diff --git a/src/antelope/wallets/authenticators/MetaKeepAuth.ts b/src/antelope/wallets/authenticators/MetaKeepAuth.ts new file mode 100644 index 000000000..25d388684 --- /dev/null +++ b/src/antelope/wallets/authenticators/MetaKeepAuth.ts @@ -0,0 +1,173 @@ + +import { EthereumProvider } from 'src/antelope/types/Providers'; +import { EVMAuthenticator } from 'src/antelope/wallets/authenticators/EVMAuthenticator'; +import { InjectedProviderAuth } from 'src/antelope/wallets/authenticators/InjectedProviderAuth'; +import { MetaKeep } from 'metakeep'; +import { AntelopeError, addressString } from 'src/antelope/types'; + +import { ethers } from 'ethers'; +import { useFeedbackStore } from 'src/antelope/stores/feedback'; +import { metakeepCache } from 'src/antelope/wallets/ual/utils/metakeep-cache'; +import { useChainStore } from 'src/antelope/stores/chain'; +import { CURRENT_CONTEXT } from 'src/antelope'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; + + +export interface MetakeepOptions { + appId: string; + appName: string; + reasonCallback?: (transaction: never) => string; +} + +let metakeep: MetaKeep | null = null; +let web3Provider: ethers.providers.Web3Provider | null = null; + +const name = 'MetaKeep'; +export const MetaKeepAuthName = name; +export class MetaKeepAuth extends InjectedProviderAuth { + + accountAddress = ''; + accountEmail = ''; + appId = ''; + appName = ''; + reasonCallback?: (transaction: never) => string; + + // this is just a dummy label to identify the authenticator base class + constructor(options: MetakeepOptions, label = name) { + super(label); + if (!options?.appId) { + throw new AntelopeError('antelope.evm.error_metakeep_app_id'); + } + this.appId = options.appId; + this.appName = options.appName; + this.reasonCallback = options.reasonCallback; + this.accountEmail = metakeepCache.getLogged() ?? ''; + } + + // InjectedProviderAuth API ------------------------------------------------------ + + getProvider(): EthereumProvider | null { + return window.ethereum as unknown as EthereumProvider ?? null; + } + + // EVMAuthenticator API ---------------------------------------------------------- + + getName(): string { + return name; + } + + setEmail(email: string): void { + this.accountEmail = email; + metakeepCache.setLogged(email); + } + + // this is the important instance creation where we define a label to assign to this instance of the authenticator + newInstance(label: string): EVMAuthenticator { + this.trace('newInstance', label); + const auth = new MetaKeepAuth({ + appId: this.appId, + appName: this.appName, + reasonCallback: this.reasonCallback, + }, label); + auth.setEmail(this.accountEmail); + return auth; + } + + async assertMetakeepSDKReady(): Promise { + this.trace('assertMetakeepSDKReady', metakeep); + if (!metakeep) { + const chainSettings = (useChainStore().getChain(CURRENT_CONTEXT).settings as EVMChainSettings); + const endpoint = chainSettings.getRPCEndpoint(); + const url = `${endpoint.protocol}://${endpoint.host}:${endpoint.port}${endpoint.path ?? ''}`; + + const chainId: number = parseInt(chainSettings.getChainId()); + + + const rpcNodeUrls = { + [chainId]: url, + } as unknown as Map; + + metakeep = new MetaKeep({ + // App id to configure UI + appId: this.appId, + // Default chain to use + chainId, + // RPC node urls map + rpcNodeUrls, + // Signed in user's email address + user: { + email: this.accountEmail, + }, + }); + + const provider = await metakeep.ethereum; + await provider.enable(); + web3Provider = new ethers.providers.Web3Provider(provider); + } + } + + async login(network: string): Promise { + this.trace('login', network); + useFeedbackStore().setLoading(`${this.getName()}.login`); + + await this.assertMetakeepSDKReady(); + + // metakeepCache + const accountAddress = metakeepCache.getEthAddress(this.accountEmail); + if (accountAddress) { + this.accountAddress = accountAddress; + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + return this.accountAddress as addressString; + } + + + const provider = await this.web3Provider(); + this.accountAddress = await provider.getSigner().getAddress(); + + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + + + const credentials = await metakeep?.getWallet(); + metakeepCache.addCredentials(this.accountEmail, credentials.wallet); + + return this.accountAddress as addressString; + } + + async logout(): Promise { + this.trace('logout'); + metakeepCache.setLogged(''); + return Promise.resolve(); + } + + async isConnectedTo(chainId: string): Promise { + this.trace('isConnectedTo', chainId); + return true; + } + + async web3Provider(): Promise { + this.trace('web3Provider'); + await this.assertMetakeepSDKReady(); + if (web3Provider) { + return web3Provider; + } else { + console.error('web3Provider not ready'); + throw new AntelopeError('antelope.evm.error_metakeep_web3_provider'); + } + } + + // returns the associated account address according to the label + getAccountAddress(): addressString { + return this.accountAddress as addressString; + } + + handleCatchError(error: Error): AntelopeError { + this.trace('handleCatchError', error.message); + if ( + (error as unknown as {status:string}).status === 'USER_REQUEST_DENIED' + ) { + return new AntelopeError('antelope.evm.error_transaction_canceled'); + } else { + return new AntelopeError('antelope.evm.error_send_transaction', { error }); + } + } +} diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts deleted file mode 100644 index d09552d52..000000000 --- a/src/antelope/wallets/authenticators/OreIdAuth.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { AuthProvider, ChainNetwork, OreId, OreIdOptions, JSONObject, UserChainAccount } from 'oreid-js'; -import { BigNumber, ethers } from 'ethers'; -import { WebPopup } from 'oreid-webpopup'; -import { - EvmABI, - EvmFunctionParam, -} from 'src/antelope/types'; -import { EVMAuthenticator } from 'src/antelope/wallets'; -import { - AntelopeError, - addressString, - EvmTransactionResponse, -} from 'src/antelope/types'; -import { useFeedbackStore } from 'src/antelope'; -import { useChainStore } from 'src/antelope/stores/chain'; -import { RpcEndpoint } from 'universal-authenticator-library'; -import { TELOS_ANALYTICS_EVENT_NAMES } from 'src/antelope/chains/chain-constants'; - -const name = 'OreId'; -export const OreIdAuthName = name; - -// This instance needs to be placed outside to avoid watch function to crash -let oreId: OreId | null = null; - -export interface AuthOreIdOptions extends OreIdOptions { - provider?: string; -} - -export class OreIdAuth extends EVMAuthenticator { - - options: AuthOreIdOptions; - userChainAccount: UserChainAccount | null = null; - // this is just a dummy label to identify the authenticator base class - constructor(options: OreIdOptions, label = name) { - super(label); - this.options = options; - } - - getNetworkNameFromChainNet(chainNetwork: ChainNetwork): string { - this.trace('getNetworkNameFromChainNet', chainNetwork); - switch (chainNetwork) { - case ChainNetwork.TelosEvmTest: - return 'telos-evm-testnet'; - case ChainNetwork.TelosEvmMain: - return 'telos-evm'; - default: - throw new AntelopeError('antelope.evm.error_invalid_chain_network'); - } - } - - getChainNetwork(network: string): ChainNetwork { - this.trace('getChainNetwork', network); - switch (network) { - case 'telos-evm-testnet': - return ChainNetwork.TelosEvmTest; - case 'telos-evm': - return ChainNetwork.TelosEvmMain; - default: - throw new AntelopeError('antelope.evm.error_invalid_chain_network'); - } - } - - async login(network: string): Promise { - this.trace('login', network); - const chainSettings = this.getChainSettings(); - const trackSuccessfulLogin = () => { - this.trace('login', 'trackAnalyticsEvent -> generic login succeeded', TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); - this.trace('login', 'trackAnalyticsEvent -> login succeeded', this.getName(), TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulOreId); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulOreId); - }; - - useFeedbackStore().setLoading(`${this.getName()}.login`); - const oreIdOptions: OreIdOptions = { - plugins: { popup: WebPopup() }, - ... this.options, - }; - - oreId = new OreId(oreIdOptions); - await oreId.init(); - - if ( - localStorage.getItem('autoLogin') === this.getName() && - typeof localStorage.getItem('rawAddress') === 'string' - ) { - // auto login without the popup - const chainAccount = localStorage.getItem('rawAddress') as addressString; - this.userChainAccount = { chainAccount } as UserChainAccount; - this.trace('login', 'userChainAccount', this.userChainAccount); - // track the login start for auto-login procceess - this.trace('login', 'trackAnalyticsEvent -> login started'); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); - // then track the successful login - trackSuccessfulLogin(); - return chainAccount; - } - - this.trace('login', 'trackAnalyticsEvent -> login started'); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); - - // launch the login flow - await oreId.popup.auth({ provider: this.provider as AuthProvider }); - const userData = await oreId.auth.user.getData(); - this.trace('login', 'userData', userData); - - this.userChainAccount = userData.chainAccounts.find( - (account: UserChainAccount) => this.getChainNetwork(network) === account.chainNetwork) ?? null; - - if (!this.userChainAccount) { - const appName = this.options.appName; - const networkName = useChainStore().getNetworkSettings(network).getDisplay(); - - this.trace('login', 'trackAnalyticsEvent -> login failed', this.getName(), TELOS_ANALYTICS_EVENT_NAMES.loginFailedOreId); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginFailedOreId); - - throw new AntelopeError('antelope.wallets.error_oreid_no_chain_account', { - networkName, - appName, - }); - } - - const address = (this.userChainAccount?.chainAccount as addressString) ?? null; - this.trace('login', 'userChainAccount', this.userChainAccount); - trackSuccessfulLogin(); - - // now we set autoLogin to this.getName() and rawAddress to the address - // to avoid the auto-login to be triggered again - localStorage.setItem('autoLogin', this.getName()); - localStorage.setItem('rawAddress', address); - - useFeedbackStore().unsetLoading(`${this.getName()}.login`); - return address; - } - - async logout(): Promise { - this.trace('logout'); - if (oreId) { - await oreId.logout(); - } - return Promise.resolve(); - } - - getName(): string { - return name; - } - - // this is the important instance creation where we define a label to assign to this instance of the authenticator - newInstance(label: string): EVMAuthenticator { - this.trace('newInstance', label); - return new OreIdAuth(this.options, label); - } - - get provider(): string { - return this.options.provider ?? ''; - } - - setProvider(provider: string): void { - this.trace('setProvider', provider); - this.options.provider = provider; - } - - - async isConnectedTo(chainId: string): Promise { - this.trace('isConnectedTo', chainId); - return true; - } - - async externalProvider(): Promise { - this.trace('externalProvider'); - return new Promise((resolve) => { - resolve(null as unknown as ethers.providers.ExternalProvider); - }); - } - - async web3Provider(): Promise { - this.trace('web3Provider'); - try { - const p:RpcEndpoint = this.getChainSettings().getRPCEndpoint(); - const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; - const jsonRpcProvider = new ethers.providers.JsonRpcProvider(url); - await jsonRpcProvider.ready; - const web3Provider = jsonRpcProvider as ethers.providers.Web3Provider; - return web3Provider; - } catch (e) { - console.error('web3Provider', e); - throw e; - } - } - - // returns the associated account address acording to the label - getAccountAddress(): addressString { - return this.userChainAccount?.chainAccount as addressString; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handleCatchError(error: Error): AntelopeError { - this.trace('handleCatchError', error.message); - if ( - error.message === 'Closed by user' || - error.message === 'sign_transaction_cancelled_by_user' - ) { - return new AntelopeError('antelope.evm.error_transaction_canceled'); - } else { - return new AntelopeError('antelope.evm.error_send_transaction', { error }); - } - } - - /** - * utility function to check if the user has a valid chain account and the oreId instance is initialized - */ - checkIntegrity(): boolean { - if (!this.userChainAccount) { - console.error('Inconsistency error: userChainAccount is null'); - throw new AntelopeError('antelope.evm.error_no_provider'); - } - - if (!oreId) { - console.error('Inconsistency error: oreId is null'); - throw new AntelopeError('antelope.evm.error_no_provider'); - } - - return true; - } - - async performOreIdTransaction(from: addressString, json: JSONObject): Promise { - this.trace('performOreIdTransaction', from, json); - const oreIdInstance = oreId as OreId; - - // sign a blockchain transaction - const transaction = await oreIdInstance.createTransaction({ - transaction: json, - chainAccount: from, - chainNetwork: this.getChainNetwork(this.getChainSettings().getNetwork()), - signOptions: { - broadcast: true, - returnSignedTransaction: true, - }, - }); - - // have the user approve signature - const { transactionId } = await oreIdInstance.popup.sign({ transaction }); - - return { - hash: transactionId, - wait: async () => Promise.resolve({} as ethers.providers.TransactionReceipt), - } as EvmTransactionResponse; - } - - async sendSystemToken(to: string, amount: ethers.BigNumber): Promise { - this.trace('sendSystemToken', to, amount.toString()); - const from = this.getAccountAddress(); - const value = amount.toHexString(); - - // Send the transaction - return this.performOreIdTransaction(from, { - from, - to, - value, - }).then( - (transaction: ethers.providers.TransactionResponse) => transaction, - ).catch((error) => { - throw this.handleCatchError(error); - }); - } - - async signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise { - this.trace('signCustomTransaction', contract, [abi], parameters, value?.toString()); - this.checkIntegrity(); - - const from = this.getAccountAddress(); - const method = abi[0].name; - - // if the developer is passing more than one function in the abi - // we must warn we asume the first one is the one to be called - if (abi.length > 1) { - console.warn( - `signCustomTransaction: abi contains more than one function, - we asume the first one (${method}) is the one to be called`, - ); - } - - // transaction body: wrap system token - const transactionBody = { - from, - to: contract, - 'contract': { - abi, - parameters, - 'method': abi[0].name, - }, - } as unknown as JSONObject; - - if (value) { - transactionBody.value = value.toHexString(); - } - - return this.performOreIdTransaction(from, transactionBody); - } -} diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts index 3f42a76ec..b3852591c 100644 --- a/src/antelope/wallets/index.ts +++ b/src/antelope/wallets/index.ts @@ -1,8 +1,8 @@ export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; export * from 'src/antelope/wallets/authenticators/InjectedProviderAuth'; export * from 'src/antelope/wallets/authenticators/MetamaskAuth'; -export * from 'src/antelope/wallets/authenticators/OreIdAuth'; export * from 'src/antelope/wallets/authenticators/WalletConnectAuth'; export * from 'src/antelope/wallets/authenticators/SafePalAuth'; +export * from 'src/antelope/wallets/authenticators/MetaKeepAuth'; export * from 'src/antelope/wallets/authenticators/BraveAuth'; export * from 'src/antelope/wallets/AntelopeWallets'; diff --git a/src/antelope/wallets/ual/MetakeepUAL.ts b/src/antelope/wallets/ual/MetakeepUAL.ts new file mode 100644 index 000000000..8d4f894bc --- /dev/null +++ b/src/antelope/wallets/ual/MetakeepUAL.ts @@ -0,0 +1,547 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Authenticator, + Chain, + UALError, + UALErrorType, + User, +} from 'universal-authenticator-library'; +import { JsonRpc } from 'eosjs'; +import { SignTransactionResponse } from 'universal-authenticator-library/dist/interfaces'; +import { MetaKeep } from 'metakeep'; +import axios from 'axios'; +import { APIClient, NameType, PackedTransaction, Serializer, Transaction } from '@greymass/eosio'; +import { metakeepCache } from 'src/antelope/wallets/ual/utils/metakeep-cache'; + +export interface UserCredentials { + email: string; + jwt: string; + keys?: string[]; +} + +export interface MetakeepUALOptions { + appId: string; + appName: string; + rpc?: JsonRpc; + accountCreateAPI: string; + reasonCallback?: (transaction: any) => string; +} +let metakeep: MetaKeep | null = null; +const metakeep_name = 'metakeep.ual'; + +// This interface is used to store the data in the local cache +export interface MetakeepData { + [email:string]: { + [chainId:string]: { + accounts: string[]; + wallet: { + eosAddress: string; + solAddress: string; + ethAddress: string; + } + } + } +} + +const metakeepDefaultAccountSelector: MetakeepAccountSelector = { + selectAccount: (accounts: string[]) => Promise.resolve(accounts[0]), +}; + +const metakeepDefaultAccountNameSelector: MetakeepNameAccountSelector = { + selectAccountName: () => Promise.resolve(''), +}; + +export interface MetakeepAccountSelector { + selectAccount: (accounts: string[]) => Promise; +} + +export interface MetakeepNameAccountSelector { + selectAccountName: () => Promise; +} + +export class MetakeepAuthenticator extends Authenticator { + private chainId: string; + private rpc: JsonRpc; + private accountCreateAPI: string; + private appId: string; + private loading = false; + private userCredentials: UserCredentials = { email: '', jwt: '', keys: [] }; + + private accountSelector: MetakeepAccountSelector = metakeepDefaultAccountSelector; + private accountNameSelector: MetakeepNameAccountSelector = metakeepDefaultAccountNameSelector; + + constructor(chains: Chain[], options: MetakeepUALOptions) { + super(chains, options); + this.chainId = chains[0].chainId; + const [chain] = chains; + const [rpc] = chain.rpcEndpoints; + + if (options && options.rpc) { + this.rpc = options.rpc; + } else { + this.rpc = new JsonRpc(`${rpc.protocol}://${rpc.host}:${rpc.port}`); + } + if (!options?.appId) { + throw new Error('MetakeepAuthenticator: Missing appId'); + } + this.appId = options.appId; + this.accountCreateAPI = options.accountCreateAPI; + this.chains = chains; + this.userCredentials = { + email: metakeepCache.getLogged() ?? '', + jwt: '', + keys: [], + }; + } + + getEmail() { + return this.userCredentials.email; + } + + getKeys() { + return this.userCredentials.keys; + } + + resetAccountSelector() { + this.accountSelector = metakeepDefaultAccountSelector; + } + + setAccountSelector(accountSelector: MetakeepAccountSelector) { + this.accountSelector = accountSelector; + } + + setAccountNameSelector(accountNameSelector: MetakeepNameAccountSelector) { + this.accountNameSelector = accountNameSelector; + } + + saveCache() { + metakeepCache.saveCache(); + } + + async init() { + // + } + + setUserCredentials(credentials: UserCredentials): void { + this.userCredentials = credentials; + metakeepCache.setLogged(credentials.email); + } + + /** + * Resets the authenticator to its initial, default state then calls init method + */ + reset() { + this.init(); + } + + /** + * Returns true if the authenticator has errored while initializing. + */ + isErrored() { + return false; + } + + getName() { + return metakeep_name; + } + + /** + * Returns a URL where the user can download and install the underlying authenticator + * if it is not found by the UAL Authenticator. + */ + getOnboardingLink() { + return ''; + } + + /** + * Returns error (if available) if the authenticator has errored while initializing. + */ + getError(): UALError | null { + return null; + } + + /** + * Returns true if the authenticator is loading while initializing its internal state. + */ + isLoading() { + return this.loading; + } + + /** + * Returns the style of the Button that will be rendered. + */ + getStyle() { + return { + // An icon displayed to app users when selecting their authentication method + icon: 'no-icon', + // Name displayed to app users + text: metakeep_name, + // Background color displayed to app users who select your authenticator + background: '#030238', + // Color of text used on top the `backgound` property above + textColor: '#FFFFFF', + }; + } + + /** + * Returns whether or not the button should render based on the operating environment and other factors. + * ie. If your Authenticator App does not support mobile, it returns false when running in a mobile browser. + */ + shouldRender() { + return true; + } + + /** + * Returns whether or not the dapp should attempt to auto login with the Authenticator app. + * Auto login will only occur when there is only one Authenticator that returns shouldRender() true and + * shouldAutoLogin() true. + */ + shouldAutoLogin() { + return true; + } + + /** + * Returns whether or not the button should show an account name input field. + * This is for Authenticators that do not have a concept of account names. + */ + async shouldRequestAccountName() { + return false; + } + + async createAccount(publicKey: string): Promise { + const suggestedName = await this.accountNameSelector.selectAccountName(); + return axios.post(this.accountCreateAPI, { + ownerKey: publicKey, + activeKey: publicKey, + jwt: this.userCredentials.jwt, + suggestedName: suggestedName, + }).then(response => response.data.accountName); + } + + resolveAccountName() { + return new Promise(async (resolve, reject) => { + let accountName = ''; + if (!metakeep) { + return reject(new Error('metakeep is not initialized')); + } + if (this.userCredentials.email === '') { + return reject(new Error('No account email')); + } + + // we check if we have the account name in the cache + const accountNames = metakeepCache.getAccountNames(this.userCredentials.email, this.chainId); + if (accountNames.length > 0) { + const publicKey = metakeepCache.getEosAddress(this.userCredentials.email); + this.userCredentials.keys = [publicKey]; + if (accountNames.length > 1) { + // if we have more than one account, we ask the user to select one using this callback + const selectedAccount = await this.accountSelector.selectAccount(accountNames); + this.resetAccountSelector(); + metakeepCache.setSelectedAccountName(this.userCredentials.email, this.chainId, selectedAccount); + return resolve(selectedAccount); + } else { + return resolve(accountNames[0]); + } + } + + // if not, we fetch all the accounts for the email + const credentials = await metakeep.getWallet(); + const publicKey = credentials.wallet.eosAddress; + + this.userCredentials.keys = [publicKey]; + metakeepCache.addCredentials(this.userCredentials.email, credentials.wallet); + + try { + // we try to get the account name from the public key + const response = await axios.post(`${this.rpc.endpoint}/v1/history/get_key_accounts`, { + public_key: publicKey, + }); + const accountExists = response?.data?.account_names.length>0; + let names:string[] = []; + + if (accountExists) { + names = response.data.account_names; + names.forEach(name => metakeepCache.addAccountName(this.userCredentials.email, this.chainId, name)); + if (names.length > 1) { + // if we have more than one account, we ask the user to select one using this callback + accountName = await this.accountSelector.selectAccount(names); + this.resetAccountSelector(); + } else { + accountName = names[0]; + } + metakeepCache.setSelectedAccountName(this.userCredentials.email, this.chainId, accountName); + } else { + accountName = await this.createAccount(publicKey); + metakeepCache.addAccountName(this.userCredentials.email, this.chainId, accountName); + names = [accountName]; + } + + this.saveCache(); + return resolve(accountName); + } catch (error) { + console.error('error', error); + throw new Error('Error getting account name'); + } + }); + } + + /** + * Login using the Authenticator App. This can return one or more users depending on multiple chain support. + * + * @param accountName The account name of the user for Authenticators that do not store accounts (optional) + */ + login: () => Promise<[User]> = async () => { + if (this.userCredentials.email === '') { + throw new Error('No account email'); + } + + this.loading = true; + + metakeep = new MetaKeep({ + // App id to configure UI + appId: this.appId, + // Signed in user's email address + user: { + email: this.userCredentials.email, + }, + }); + + const accountName = await this.resolveAccountName(); + const publicKey = metakeepCache.getEosAddress(this.userCredentials.email); + + try { + const permission = 'active'; + this.loading = false; + const userInstance = new MetakeepUser({ + accountName, + permission, + publicKey, + chainId: this.chainId, + rpc: this.rpc, + accountCreateAPI: this.accountCreateAPI, + }); + + return [userInstance]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + this.loading = false; + throw new UALError(err.messsage, UALErrorType.Login, err, 'MetakeepAuthenticator'); + } + }; + + /** + * Logs the user out of the dapp. This will be strongly dependent on each + * Authenticator app's patterns. + */ + logout = async (): Promise => { + metakeepCache.setLogged(null); + return; + }; + + /** + * Returns true if user confirmation is required for `getKeys` + */ + requiresGetKeyConfirmation() { + return false; + } +} + +// ------------------------------------------------------ + + +class MetakeepUser extends User { + private keys: string[]; + private accountName: string; + private permission: string; + private chainId: string; + private reasonCallback?: (transaction: any) => string; + + rpc: JsonRpc; + protected eosioCore: APIClient; + protected accountCreateAPI: string; + constructor({ + accountName, + permission, + publicKey, + chainId, + rpc, + accountCreateAPI, + }: { + accountName: string, + permission: string, + publicKey: string, + chainId: string, + rpc: JsonRpc, + accountCreateAPI: string, + }) { + super(); + this.keys = [publicKey]; + this.accountName = accountName; + this.permission = permission; + this.chainId = chainId; + this.rpc = rpc; + this.accountCreateAPI = accountCreateAPI; + this.eosioCore = new APIClient({ url: rpc.endpoint }); + } + + setReasonCallback(callback: (transaction: any) => string) { + this.reasonCallback = callback; + } + + handleCatchError(error: any): Error { + if ( + (error as unknown as {status:string}).status === 'USER_REQUEST_DENIED' + ) { + return new Error('antelope.evm.error_transaction_canceled'); + } else { + return new Error('antelope.evm.error_send_transaction'); + } + } + + /** + * @param transaction The transaction to be signed (a object that matches the RpcAPI structure). + */ + signTransaction = async (originalTransaction: any, options: any = {}): Promise => { + if (!metakeep) { + throw new Error('metakeep is not initialized'); + } + + try { + // expire time in seconds + const expireSeconds = 120; + + // Retrieve transaction headers + const info = await this.eosioCore.v1.chain.get_info(); + const header = info.getTransactionHeader(expireSeconds); + + // collect all contract abis + const abi_promises = originalTransaction.actions.map((a: { account: NameType; }) => + this.eosioCore.v1.chain.get_abi(a.account), + ); + const responses = await Promise.all(abi_promises); + const abis = responses.map(x => x.abi); + const abis_and_names = originalTransaction.actions.map((x: { account: any; }, i: number) => ({ + contract: x.account, + abi: abis[i], + })); + + // create complete well formed transaction + const transaction = Transaction.from( + { + ...header, + actions: originalTransaction.actions, + }, + abis_and_names, + ); + + const transaction_extensions = originalTransaction.transaction_extensions ?? [] as string[]; + const context_free_actions = originalTransaction.context_free_actions ?? [] as string[]; + const delay_sec = originalTransaction.delay_sec ?? 0; + const max_cpu_usage_ms = originalTransaction.max_cpu_usage_ms ?? 0; + const max_net_usage_words = originalTransaction.max_net_usage_words ?? 0; + const expiration = originalTransaction.expiration ?? transaction.expiration.toString(); + const ref_block_num = originalTransaction.ref_block_num ?? transaction.ref_block_num.toNumber(); + const ref_block_prefix = originalTransaction.ref_block_prefix ?? transaction.ref_block_prefix.toNumber(); + + // convert actions to JSON + const actions = transaction.actions.map(a => ({ + account: a.account.toJSON(), + name: a.name.toJSON(), + authorization: a.authorization.map((x: { actor: any; permission: any; }) => ({ + actor: x.actor.toJSON(), + permission: x.permission.toJSON(), + })), + data: a.data.toJSON(), + })); + + // compose the complete transaction + const complete_transaction = { + rawTransaction: { + expiration, + ref_block_num, + ref_block_prefix, + max_net_usage_words, + max_cpu_usage_ms, + delay_sec, + context_free_actions, + actions, + transaction_extensions, + }, + extraSigningData: { + chainId: this.chainId, + }, + }; + + // sign the transaction with metakeep + const reason = this.reasonCallback ? this.reasonCallback(originalTransaction) : 'sign this transaction'; + const response = await metakeep.signTransaction(complete_transaction, reason); + const signature = response.signature; + + + // Pack the transaction for transport + const packedTransaction = PackedTransaction.from({ + signatures: [signature], + packed_context_free_data: '', + packed_trx: Serializer.encode({ object: transaction }), + }); + + if (options.broadcast === false) { + return Promise.resolve({ + wasBroadcast: false, + transactionId: '', + status: '', + transaction: packedTransaction, + }); + } + // Broadcast the signed transaction to the blockchain + const pushResponse = await this.eosioCore.v1.chain.push_transaction( + packedTransaction, + ); + + // we compose the final response + const finalResponse/*: SignTransactionResponse*/ = { + wasBroadcast: true, + transactionId: pushResponse.transaction_id, + status: pushResponse.processed.receipt.status, + transaction: packedTransaction, + }; + + return Promise.resolve(finalResponse); + + } catch (e: any) { + throw this.handleCatchError(e); + } + } + + /** + * Note: this method is not implemented yet + * + * @param publicKey The public key to use for signing. + * @param data The data to be signed. + * @param helpText Help text to explain the need for arbitrary data to be signed. + * + * @returns The signature + */ + signArbitrary = async (): Promise => { + throw new Error('MetakeepUAL: signArbitrary not supported (yet)'); + }; + + /** + * @param challenge Challenge text sent to the authenticator. + * + * @returns Whether the user owns the private keys corresponding with provided public keys. + */ + async verifyKeyOwnership() { + return true; + } + + getAccountName = async (): Promise => this.accountName; + + getAccountPermission = async (): Promise => this.permission; + + getChainId = async (): Promise => this.chainId; + + getKeys = async (): Promise => this.keys; +} + + diff --git a/src/antelope/wallets/ual/utils/metakeep-cache.ts b/src/antelope/wallets/ual/utils/metakeep-cache.ts new file mode 100644 index 000000000..99097864a --- /dev/null +++ b/src/antelope/wallets/ual/utils/metakeep-cache.ts @@ -0,0 +1,141 @@ +// utils/metakeep-cache.ts + +export interface MetakeepCacheData { + [email: string]: { + wallet: MetakeepWallets; + chains: { + [chainId: string]: { + accounts: string[]; + } + }; + } +} + +export interface MetakeepWallets { + eosAddress: string; + solAddress: string; + ethAddress: string; +} + +const LOCAL_STORAGE_KEY_DATA = 'metakeep.data'; +const LOCAL_STORAGE_KEY_LOGGED = 'metakeep.logged'; + +class MetakeepCache { + private cache: MetakeepCacheData = {}; + private logged: string | null = null; + public mails: string[] = []; + + constructor() { + this.loadCache(); + } + + public loadCache() { + try { + const cachedData = window.localStorage.getItem(LOCAL_STORAGE_KEY_DATA); + if (cachedData) { + this.cache = JSON.parse(cachedData); + } + this.logged = window.localStorage.getItem(LOCAL_STORAGE_KEY_LOGGED); + this.mails = Object.keys(this.cache); + } catch (error) { + console.error('Error loading Metakeep cache:', error); + } + } + + public saveCache() { + try { + window.localStorage.setItem(LOCAL_STORAGE_KEY_DATA, JSON.stringify(this.cache)); + if (this.logged) { + window.localStorage.setItem(LOCAL_STORAGE_KEY_LOGGED, this.logged); + } else { + window.localStorage.removeItem(LOCAL_STORAGE_KEY_LOGGED); + } + } catch (error) { + console.error('Error saving Metakeep cache:', error); + } + } + + private assertCache(email: string, chainId?: string) { + if (!this.cache[email]) { + this.cache[email] = { + wallet: { + eosAddress: '', + solAddress: '', + ethAddress: '', + }, + chains: {}, + }; + } + if (chainId) { + if (!this.cache[email].chains[chainId]) { + this.cache[email].chains[chainId] = { + accounts: [], + }; + } + } + } + + public getMails(): string[] { + return Object.keys(this.cache); + } + + public getEosAddress(email: string): string { + this.assertCache(email); + return this.cache[email]?.wallet?.eosAddress ?? ''; + } + + public getSolAddress(email: string): string { + this.assertCache(email); + return this.cache[email]?.wallet?.solAddress ?? ''; + } + + public getEthAddress(email: string): string { + this.assertCache(email); + return this.cache[email]?.wallet?.ethAddress ?? ''; + } + + public getAccountNames(email: string, chainId: string): string[] { + this.assertCache(email, chainId); + return this.cache[email]?.chains[chainId]?.accounts ?? []; + } + + public getLogged(): string | null { + return this.logged; + } + + // setters -------------- + public setSelectedAccountName(email: string, chainId: string, accountName: string) { + this.assertCache(email, chainId); + const index = this.cache[email].chains[chainId].accounts.indexOf(accountName); + if (index !== -1) { + this.cache[email].chains[chainId].accounts.splice(index, 1); + } + this.cache[email].chains[chainId].accounts.unshift(accountName); + this.saveCache(); + } + + public addAccountName(email: string, chainId: string, accountName: string) { + this.assertCache(email, chainId); + if (!this.cache[email].chains[chainId].accounts.includes(accountName)) { + this.cache[email].chains[chainId].accounts.push(accountName); + } + this.saveCache(); + } + + public addCredentials(email: string, wallet: MetakeepWallets) { + this.assertCache(email); + this.cache[email].wallet = wallet; + this.saveCache(); + } + + public setLogged(email: string | null) { + if (email) { + this.assertCache(email); + } + this.logged = email; + this.saveCache(); + } +} + +export const metakeepCache = new MetakeepCache(); + diff --git a/src/api/fuel.js b/src/api/fuel.js index 708479765..d6fb0b119 100644 --- a/src/api/fuel.js +++ b/src/api/fuel.js @@ -41,190 +41,191 @@ const resourceProviderEndpoint = `${fuelrpc}/v1/resource_provider/request_transa // Wrapper for the user to intersect the signTransaction call // Use initFuelUserWrapper() method to initialize an instance of the class class FuelUserWrapper extends User { - user = null; - fuelServiceEnabled = false; + user = null; + fuelServiceEnabled = false; - constructor(user/*: User*/) { - super(); - this.user = user; - } + constructor(user/*: User*/) { + super(); + this.user = user; + } - // called immediately after class instantiation in initFuelUserWrapper() - async setAvailability() { - if (!fuelrpc){ - return; - }; - try { - // verify fuel service is available - this.fuelServiceEnabled = (await fetch(fuelrpc)).status === 200; - } catch(e) { - console.error(e); - } - } + // called immediately after class instantiation in initFuelUserWrapper() + async setAvailability() { + if (!fuelrpc){ + return; + }; + try { + // verify fuel service is available + this.fuelServiceEnabled = (await fetch(fuelrpc)).status === 200; + } catch(e) { + console.error(e); + } + } - async signTransaction( - originalTransaction/*: AnyTransaction*/, - originalconfig, /*: SignTransactionConfig*/ - )/*: Promise*/ { - try { - // if fuel service disabled, send tx using generic ual user method - if (!this.fuelServiceEnabled) { - return this.user.signTransaction(originalTransaction, originalconfig); - } + async signTransaction( + originalTransaction/*: AnyTransaction*/, + originalconfig, /*: SignTransactionConfig*/ + )/*: Promise*/ { + try { + // if fuel service disabled, send tx using generic ual user method + if (!this.fuelServiceEnabled) { + return this.user.signTransaction(originalTransaction, originalconfig); + } - // Retrieve transaction headers - const info = await client.v1.chain.get_info(); - const header = info.getTransactionHeader(expireSeconds); - - // collect all contract abis - const abi_promises = originalTransaction.actions.map(a => - client.v1.chain.get_abi(a.account), - ); - const responses = await Promise.all(abi_promises); - const abis = responses.map(x => x.abi); - const abis_and_names = originalTransaction.actions.map((x, i) => ({ - contract: x.account, - abi: abis[i], - })); - - // create complete well formed transaction - const transaction = Transaction.from( - { - ...header, - actions: originalTransaction.actions, - }, - abis_and_names, - ); - - // Pack the transaction for transport - const packedTransaction = PackedTransaction.from({ - signatures: [], - packed_context_free_data: '', - packed_trx: Serializer.encode({ object: transaction }), - }); - - const signer = PermissionLevel.from({ - actor: (await this.user.getAccountName()), - permission: this.requestPermission, - }); - - // Submit the transaction to the resource provider endpoint - const cosigned = await fetch(resourceProviderEndpoint, { - body: JSON.stringify({ - signer, - packedTransaction, - }), - method: 'POST', - }); - - // Interpret the resulting JSON - const rpResponse = await cosigned.json(); /*as ResourceProviderResponse*/ - - - switch (rpResponse.code) { - case 402: { - // Resource Provider provided signature in exchange for a fee - // is ok to treat them with the same logic of code = 200? - // Yes acording to this: https://gist.github.com/aaroncox/d74a73b3d9fbc20836c32ea9deda5d70#file-fuel-core-presign-js-L128-L159 - // Aron rightly suggests that we should show and confirm the fee costs for this service: - // https://github.com/telosnetwork/open-block-explorer/pull/477#discussion_r1053417964 - } - case 200: { - // Resource Provider provided signature for free - - const { data } = rpResponse; - const [, returnedTransaction] = data.request; - const modifiedTransaction/*: SignedTransaction*/ = returnedTransaction; - - // Ensure the modifed transaction is what the application expects - // These validation methods will throw an exception if invalid data exists - const fees/*: string | null*/ = validateTransaction( - signer, - modifiedTransaction, - transaction, - data.costs, - ); - - // validate with the user whether to use the service at all - try { - await confirmWithUser(this.user, fees); - } catch (e) { - // The user refuseed to use the service - break; - } - - modifiedTransaction.signatures = [...data.signatures]; - // Sign the modified transaction - const locallySigned/*: SignedTransactionResponse*/ = - await this.user.signTransaction( - modifiedTransaction, - Object.assign({ broadcast: false }, originalconfig), - ); /* as SignedTransactionResponse*/ - - // When using CleosAuthenticator the transaction returns empty - if (!locallySigned.transaction.signatures) { - return Promise.reject( - 'The transaction was not broadcasted because no signatures were obtained', - ); - } - - // Merge signatures from the user and the cosigned responsetab - modifiedTransaction.signatures = [ - ...locallySigned.transaction.signatures, - ...data.signatures, - ]; - - // Broadcast the signed transaction to the blockchain - const pushResponse = await client.v1.chain.push_transaction( - modifiedTransaction, - ); - - // we compose the final response - const finalResponse/*: SignTransactionResponse*/ = { - wasBroadcast: true, - transactionId: pushResponse.transaction_id, - status: pushResponse.processed.receipt.status, - transaction: modifiedTransaction, - }; - - return Promise.resolve(finalResponse); - } - case 400: { - // Resource Provider refused to sign the transaction, aborting - break; - } - default: - throw ( - 'Code ' + - (+rpResponse.code).toString() + - ' not expected from resource provider endpoint: ' + - resourceProviderEndpoint - ); - } + // Retrieve transaction headers + const info = await client.v1.chain.get_info(); + const header = info.getTransactionHeader(expireSeconds); - // If we got here it means the resource provider will not participate in this transaction - return this.user.signTransaction(originalTransaction, originalconfig); - } catch (e) { - throw e; - } - } + // collect all contract abis + const abi_promises = originalTransaction.actions.map(a => + client.v1.chain.get_abi(a.account), + ); + const responses = await Promise.all(abi_promises); + const abis = responses.map(x => x.abi); + const abis_and_names = originalTransaction.actions.map((x, i) => ({ + contract: x.account, + abi: abis[i], + })); + + // create complete well formed transaction + const transaction = Transaction.from( + { + ...header, + actions: originalTransaction.actions, + }, + abis_and_names, + ); - // since this is a wrapper is also wraps the posible requestPermission hidden property - get requestPermission() { - return this.user.requestPermission || 'active'; - } + // Pack the transaction for transport + const packedTransaction = PackedTransaction.from({ + signatures: [], + packed_context_free_data: '', + packed_trx: Serializer.encode({ object: transaction }), + }); + + const signer = PermissionLevel.from({ + actor: (await this.user.getAccountName()), + permission: this.requestPermission, + }); + + // Submit the transaction to the resource provider endpoint + const cosigned = await fetch(resourceProviderEndpoint, { + body: JSON.stringify({ + signer, + packedTransaction, + }), + method: 'POST', + }); + + // Interpret the resulting JSON + const rpResponse = await cosigned.json(); /*as ResourceProviderResponse*/ + + switch (rpResponse.code) { + case 402: { + // Resource Provider provided signature in exchange for a fee + // is ok to treat them with the same logic of code = 200? + // Yes according to this: https://gist.github.com/aaroncox/d74a73b3d9fbc20836c32ea9deda5d70#file-fuel-core-presign-js-L128-L159 + // Aron rightly suggests that we should show and confirm the fee costs for this service: + // https://github.com/telosnetwork/open-block-explorer/pull/477#discussion_r1053417964 + } + case 200: { + // Resource Provider provided signature for free + + const { data } = rpResponse; + const [, returnedTransaction] = data.request; + const modifiedTransaction/*: SignedTransaction*/ = returnedTransaction; + + const fee/*: string | null*/ = data.fee; + + // Ensure the modified transaction is what the application expects + // These validation methods will throw an exception if invalid data exists + validateTransaction( + signer, + modifiedTransaction, + transaction, + data.costs, + ); + + // validate with the user whether to use the service at all + try { + await confirmWithUser(this.user, fee); + } catch (e) { + // The user refused to use the service + break; + } + + modifiedTransaction.signatures = [...data.signatures]; + // Sign the modified transaction + const locallySigned/*: SignedTransactionResponse*/ = + await this.user.signTransaction( + modifiedTransaction, + Object.assign(originalconfig, { broadcast: false }), + ); /* as SignedTransactionResponse*/ + + // When using CleosAuthenticator the transaction returns empty + if (!locallySigned.transaction.signatures) { + return Promise.reject( + 'The transaction was not broadcasted because no signatures were obtained', + ); + } + + // Merge signatures from the user and the cosigned response tab + modifiedTransaction.signatures = [ + ...locallySigned.transaction.signatures, + ...data.signatures, + ]; + + // Broadcast the signed transaction to the blockchain + const pushResponse = await client.v1.chain.push_transaction( + modifiedTransaction, + ); + + // we compose the final response + const finalResponse/*: SignTransactionResponse*/ = { + wasBroadcast: true, + transactionId: pushResponse.transaction_id, + status: pushResponse.processed.receipt.status, + transaction: modifiedTransaction, + }; + + return Promise.resolve(finalResponse); + } + case 400: { + // Resource Provider refused to sign the transaction, aborting + break; + } + default: + throw ( + 'Code ' + + (+rpResponse.code).toString() + + ' not expected from resource provider endpoint: ' + + resourceProviderEndpoint + ); + } - // These functions are just proxies - signArbitrary = async ( - publicKey/*: string*/, - data/*: string*/, - helpText, /*: string*/ - )/*: Promise*/ => this.user.signArbitrary(publicKey, data, helpText); - verifyKeyOwnership = async (challenge/*: string*/)/*: Promise*/ => - this.user.verifyKeyOwnership(challenge); - getAccountName = async ()/*: Promise*/ => this.user.getAccountName(); - getChainId = async ()/*: Promise*/ => this.user.getChainId(); - getKeys = async ()/*: Promise*/ => this.user.getKeys(); + // If we got here it means the resource provider will not participate in this transaction + return this.user.signTransaction(originalTransaction, originalconfig); + } catch (e) { + throw e; + } + } + + // since this is a wrapper is also wraps the possible requestPermission hidden property + get requestPermission() { + return this.user.requestPermission || 'active'; + } + + // These functions are just proxies + signArbitrary = async ( + publicKey/*: string*/, + data/*: string*/, + helpText, /*: string*/ + )/*: Promise*/ => this.user.signArbitrary(publicKey, data, helpText); + verifyKeyOwnership = async (challenge/*: string*/)/*: Promise*/ => + this.user.verifyKeyOwnership(challenge); + getAccountName = async ()/*: Promise*/ => this.user.getAccountName(); + getChainId = async ()/*: Promise*/ => this.user.getChainId(); + getKeys = async ()/*: Promise*/ => this.user.getKeys(); } // create an instance of FuelUserWrapper class and check fuel service availability @@ -234,7 +235,7 @@ export async function initFuelUserWrapper(user) { return fuelUserWrapper; } -// Auxiliar functions to validate with the user the use of the service +// Auxiliary functions to validate with the user the use of the service /* interface Preference { remember?: boolean; @@ -286,12 +287,11 @@ async function confirmWithUser(user/*: User*/, fees/*: string | null*/) { let mymodel/*: string[]*/ = []; mymodel = []; - return new Promise((resolve, reject) => { // Try and see if the user already answer (remembered) if ( GreymassFuelService.preferences[username] && - GreymassFuelService.preferences[username].remember + GreymassFuelService.preferences[username].remember ) { // ok, the user did. What's the answer? if (GreymassFuelService.preferences[username].approve) { @@ -314,44 +314,47 @@ async function confirmWithUser(user/*: User*/, fees/*: string | null*/) { // this are the normal texts for random wallet. const cancel/*: string | boolean*/ = GreymassFuelService.globals.$t('api.reject'); const ok = GreymassFuelService.globals.$t('api.confirm'); - let message = GreymassFuelService.globals.$t('api.greymass_fuel_message'); if (typeof fees === 'string') { - message = GreymassFuelService.globals.$t('api.greymass_fuel_message_fees', { fees }); + // Only if there's some feed to charge the user wi show the confirmation Dialog + const message = GreymassFuelService.globals.$t('api.greymass_fuel_message_fees', { fees }); + Dialog.create({ + title: GreymassFuelService.globals.$t('api.greymass_dialog_title'), + message, + html: true, + cancel, + ok, + persistent: true, + class: 'text-black', + options: { + type: 'checkbox', + model: mymodel, + isValid: (model/*: string | string[]*/) => { + GreymassFuelService.setPreferences(username, { + remember: model.length === 1, + }); + return true; + }, + items: [ + { + label: GreymassFuelService.globals.$t('api.remember_my_decision'), + value: 'remember', + color: 'primary' }, + ], + }, + }) + // all answers should save the preferences + .onOk(() => handler(true)) + .onCancel(() => handler(false)); + } else { + // otherwise we go ahead without asking + handler(true); } - Dialog.create({ - title: GreymassFuelService.globals.$t('api.greymass_dialog_title'), - message, - html: true, - cancel, - ok, - persistent: true, - class: 'text-black', - options: { - type: 'checkbox', - model: mymodel, - isValid: (model/*: string | string[]*/) => { - GreymassFuelService.setPreferences(username, { - remember: model.length === 1, - }); - return true; - }, - items: [ - { - label: GreymassFuelService.globals.$t('api.remember_my_decision'), - value: 'remember', - color: 'primary' }, - ], - }, - }) - // all answers should save the preferences - .onOk(() => handler(true)) - .onCancel(() => handler(false)); }); } -// Auxiliar functions to validate modified transaction returned by the resourse provider +// Auxiliary functions to validate modified transaction returned by the resource provider // Validate the transaction function validateTransaction( @@ -360,6 +363,7 @@ function validateTransaction( transaction/*: Transaction*/, costs, /*: CostsType | null = null*/ )/*: string | null*/ { + // Ensure the first action is the `greymassnoop:noop` validateNoop(modifiedTransaction); @@ -375,7 +379,7 @@ function validateActions( costs, /*: CostsType | null*/ )/*: string | null*/ { // Determine how many actions we expect to have been added to the transaction based on the costs - const expectedNewActions = determineExpectedActionsLength(costs); + const expectedNewActions = determineExpectedActionsLength(costs, modifiedTransaction); // Ensure the proper number of actions was returned validateActionsLength(expectedNewActions, modifiedTransaction, transaction); @@ -390,14 +394,24 @@ function validateActions( } // Validate the number of actions is the number expected -function determineExpectedActionsLength(costs/*: CostsType | null*/) { +function determineExpectedActionsLength(costs/*: CostsType | null*/, modifiedTransaction/*: Transaction*/) { // By default, 1 new action is appended (noop) let expectedNewActions = 1; - // If there are costs associated with this transaction, 1 new actions is added (the fee) + // if the second action is a ram purchase, 1 new action is added (the ram purchase) + if ( + costs === null && + modifiedTransaction.actions.length > 1 && + modifiedTransaction.actions[1].account.toString() === 'eosio' && + ['buyram', 'buyrambytes'].includes(modifiedTransaction.actions[1].name.toString()) + ) { + expectedNewActions += 1; + } + + // If there are costs associated with this transaction, 1 new actions is added (the fee transfer) as second action if (costs) { expectedNewActions += 1; - // If there is a RAM cost associated with this transaction, 1 new actio is added (the ram purchase) + // If there is a RAM cost associated with this transaction, 1 new action is added (the ram purchase) (buyrambytes or buyram) if (costs.ram !== '0.0000 TLOS') { expectedNewActions += 1; } @@ -420,26 +434,21 @@ function validateActionsContent( transaction, ); - // If a fee has been added, ensure the fee is set properly - if (expectedNewActions > 1) { - let totalFee/*: null | number*/ = null; - totalFee = validateActionsFeeContent(signer, modifiedTransaction); - // If a ram purchase has been added, ensure the purchase was set properly - if (expectedNewActions > 2) { - validateActionsRamContent(signer, modifiedTransaction); - } - return `${new Number(totalFee).toFixed(4)} TLOS`; - } else { - return null; - } + // If a ram purchase was expected, ensure it is valid + // if (expectedNewActions > 1) { + // validateActionsRamContent(signer, modifiedTransaction); + // } + return null; } /* interface AuxTransactionData { [key: string]: string; } */ -function descerialize(data/*: unknown*/)/*: AuxTransactionData*/ { - return data/* as AuxTransactionData*/; +function descerialize(data/*: string*/)/*: AuxTransactionData*/ { + // we use the Serializer to decode the data string + const deserialized = Serializer.decode(data); + return deserialized; } // Ensure the transaction fee transfer is valid @@ -455,8 +464,8 @@ function validateActionsFeeContent( } if ( feeAction.account.toString() !== 'eosio.token' || - feeAction.name.toString() !== 'transfer' || - data.to.toString() !== 'fuel.gm' + feeAction.name.toString() !== 'transfer' || + data.to.toString() !== 'fuel.gm' ) { throw new Error('Fee action was deemed invalid.'); } @@ -468,15 +477,15 @@ function validateActionsRamContent( signer/*: PermissionLevel*/, modifiedTransaction, /*: Transaction*/ )/*: number*/ { - const ramAction = modifiedTransaction.actions[2]; + const ramAction = modifiedTransaction.actions[1]; const data = descerialize(ramAction.data); const amount = parseFloat(data.quant?.split(' ')[0]); if ( ramAction.account.toString() !== 'eosio' || - !['buyram', 'buyrambytes'].includes(String(ramAction.name)) || - data.payer.toString() !== 'greymassfuel' || - data.receiver.toString() !== signer.actor.toString() + !['buyram', 'buyrambytes'].includes(String(ramAction.name)) || + data.payer.toString() !== 'greymassfuel' || + data.receiver.toString() !== signer.actor.toString() ) { throw new Error('RAM action was deemed invalid.'); } @@ -490,7 +499,7 @@ function validateActionsOriginalContent( transaction, /*: Transaction*/ ) { for (const [i] of modifiedTransaction.actions.entries()) { - // Skip the expected new actions + // Skip the expected new actions if (i < expectedNewActions) { continue; } @@ -498,25 +507,25 @@ function validateActionsOriginalContent( const original = transaction.actions[i - expectedNewActions]; const action = modifiedTransaction.actions[i]; const matchesAccount = - action.account.toString() === original.account.toString(); + action.account.toString() === original.account.toString(); const matchesAction = action.name.toString() === original.name.toString(); const matchesLength = - action.authorization.length === original.authorization.length; + action.authorization.length === original.authorization.length; const matchesActor = - action.authorization[0].actor.toString() === - original.authorization[0].actor.toString(); + action.authorization[0].actor.toString() === + original.authorization[0].actor.toString(); const matchesPermission = - action.authorization[0].permission.toString() === - original.authorization[0].permission.toString(); + action.authorization[0].permission.toString() === + original.authorization[0].permission.toString(); const matchesData = action.data.toString() === original.data.toString(); if ( !action || - !matchesAccount || - !matchesAction || - !matchesLength || - !matchesActor || - !matchesPermission || - !matchesData + !matchesAccount || + !matchesAction || + !matchesLength || + !matchesActor || + !matchesPermission || + !matchesData ) { const { account, name } = original; throw new Error( @@ -534,7 +543,7 @@ function validateActionsLength( ) { if ( modifiedTransaction.actions.length !== - transaction.actions.length + expectedNewActions + transaction.actions.length + expectedNewActions ) { throw new Error('Transaction returned contains additional actions.'); } @@ -551,13 +560,13 @@ function validateNoop(modifiedTransaction/*: Transaction*/) { const [firstAuthorization] = firstAction.authorization; if ( firstAction.account.toString() !== expectedCosignerContract.toString() || - firstAction.name.toString() !== expectedCosignerAction.toString() || - firstAuthorization.actor.toString() !== - expectedCosignerAccountName.toString() || - firstAuthorization.permission.toString() !== - expectedCosignerAccountPermission.toString() || - (JSON.stringify(firstAction.data) !== '""' && - JSON.stringify(firstAction.data) !== '{}') + firstAction.name.toString() !== expectedCosignerAction.toString() || + firstAuthorization.actor.toString() !== + expectedCosignerAccountName.toString() || + firstAuthorization.permission.toString() !== + expectedCosignerAccountPermission.toString() || + (JSON.stringify(firstAction.data) !== '""' && + JSON.stringify(firstAction.data) !== '{}') ) { throw new Error( `First action within transaction response is not valid noop (${expectedCosignerContract.toString()}:${expectedCosignerAction.toString()} signed by ${expectedCosignerAccountName.toString()}:${expectedCosignerAccountPermission.toString()}).`, diff --git a/src/boot/antelope.ts b/src/boot/antelope.ts index aa968eff0..578532f39 100644 --- a/src/boot/antelope.ts +++ b/src/boot/antelope.ts @@ -1,16 +1,18 @@ import { EthereumClient } from '@web3modal/ethereum'; import { Web3ModalConfig } from '@web3modal/html'; -import { OreIdOptions } from 'oreid-js'; import { boot } from 'quasar/wrappers'; import { CURRENT_CONTEXT, installAntelope } from 'src/antelope'; +import { AccountModel } from 'src/antelope/stores/account'; import { AntelopeError } from 'src/antelope/types'; import { MetamaskAuth, WalletConnectAuth, - OreIdAuth, SafePalAuth, + MetaKeepAuth, + MetakeepOptions, } from 'src/antelope/wallets'; import { BraveAuth } from 'src/antelope/wallets/authenticators/BraveAuth'; +import { googleCtrl } from 'src/pages/home/GoogleOneTap'; import { App } from 'vue'; import { Router } from 'vue-router'; @@ -43,10 +45,19 @@ export default boot(({ app }) => { // we need to wait 1000 milisec to ensure app.config.globalProperties?.$router is not null ant.events.onLoggedIn.subscribe({ - next: async () => { - if (window.location.pathname === '/') { - (await getRouter(app)).push({ path: '/evm/wallet?tab=balance' }); + next: async (account: AccountModel) => { + if (account.isNative) { + if (!window.location.pathname.startsWith('/zero')) { + const router = await getRouter(app); + router.push({ path: '/zero/balance' }); + } + } else { + if (!window.location.pathname.startsWith('/evm')) { + const router = await getRouter(app); + router.push({ path: '/evm/wallet?tab=balance' }); + } } + }, }); ant.events.onLoggedOut.subscribe({ @@ -54,6 +65,8 @@ export default boot(({ app }) => { if (window.location.pathname !== '/') { (await getRouter(app)).push({ path: '/' }); } + // we also need to clear Google One Tap Controller + googleCtrl.logout(); }, }); @@ -84,11 +97,11 @@ export default boot(({ app }) => { ant.wallets.addEVMAuthenticator(new MetamaskAuth()); ant.wallets.addEVMAuthenticator(new SafePalAuth()); ant.wallets.addEVMAuthenticator(new BraveAuth()); - const oreIdOptions: OreIdOptions = { - appName: process.env.APP_NAME, - appId: process.env.OREID_APP_ID as string, + const metakeepOptions: MetakeepOptions = { + appName: process.env.APP_NAME as string, + appId: process.env.METAKEEP_APP_ID_EVM as string, }; - ant.wallets.addEVMAuthenticator(new OreIdAuth(oreIdOptions)); + ant.wallets.addEVMAuthenticator(new MetaKeepAuth(metakeepOptions)); // autologin -- ant.stores.account.autoLogin(); @@ -108,6 +121,10 @@ export default boot(({ app }) => { const network = new URLSearchParams(window.location.search).get('network'); if (network) { ant.stores.chain.setChain(CURRENT_CONTEXT, network); + } else if (typeof process.env.DEFAULT_NETWORK === 'string') { + // if we have a default network, we connect to it (this can be changed dynamically later on) + const defaultNetwork = process.env.DEFAULT_NETWORK; + ant.stores.chain.setChain(CURRENT_CONTEXT, defaultNetwork); } }); diff --git a/src/boot/telosCloudJs.ts b/src/boot/telosCloudJs.ts new file mode 100644 index 000000000..f9f6c59d1 --- /dev/null +++ b/src/boot/telosCloudJs.ts @@ -0,0 +1,114 @@ +// Redirect or iFrame ----------------------------------------------------------- +// if redirect or iframe is set, it means the user is coming from an external source +// and wants to use the wallet to login and come back to the external source +import { + ComponentCustomProperties, + ref, +} from 'vue'; +import { boot } from 'quasar/wrappers'; + +import { + getAntelope, + useAccountStore, +} from 'src/antelope'; +import { MetakeepAuthenticator } from 'src/antelope/wallets/ual/MetakeepUAL'; +import { createTraceFunction } from 'src/antelope/config'; + +const url = new URLSearchParams(window.location.search); +export const redirectParam = url.get('redirect'); +export const iframeParam = url.get('iframe'); +export const logoutParam = url.get('logout'); +export const redirect = ref<{url:string, hostname:string} | null>(null); +export const redirectShow = ref(false); +export const iframeShow = ref(false); + +const trace = createTraceFunction('telosCloudJs'); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let globalProps = {} as ComponentCustomProperties & Record; + +export const telosCloudResponse = async () => { + const ant = getAntelope(); + const accountStore = useAccountStore(); + + trace('telosCloudResponse'); + // if the redirect parameter is present, show a confirm notification to the user + + if (redirectShow.value && redirect.value) { + const hostname = redirect.value.hostname; + const message = globalProps.$t('home.redirect_notification_message', { hostname }); + globalProps.$notifyWarningWithAction(message, { + label: ant.config.localizationHandler('home.redirect_me'), + handler: () => { + // we redirect the user to the url + if (redirect.value) { + // we need to generate a new url based on redirect.value?.url adding the accountStore.loggedNativeAccount?.account + // and the email of the user if it's a metakeep authenticator + const url = new URL(redirect.value.url); + trace('telosCloudResponse', 'url', url.toString()); + url.searchParams.set('account', accountStore.loggedNativeAccount?.account || ''); + trace('telosCloudResponse', 'adding the account...', url.toString()); + const authenticator = accountStore.loggedNativeAccount.authenticator; + trace('telosCloudResponse', 'adding the email...', url.toString()); + const auth = authenticator as never as MetakeepAuthenticator; + url.searchParams.set('email', auth.getEmail()); + trace('telosCloudResponse', 'redirecting to', url.toString()); + window.location.href = url.toString(); + } + }, + }); + } else if (iframeShow.value) { + // if the iframe parameter is present, we send the credentials to the parent window + const authenticator = accountStore.loggedNativeAccount.authenticator; + const auth = authenticator as never as MetakeepAuthenticator; + const credentials: {account: string, email: string, keys: string[] } = { + account: accountStore.loggedNativeAccount?.account || '', + email: auth.getEmail(), + keys: auth.getKeys() ?? [], + }; + const str = JSON.stringify(credentials); + trace('telosCloudResponse', { credentials, str, logoutParam, href: window.location.href }); + if (logoutParam) { + accountStore.logout(); + } + window.parent.postMessage(str, '*'); + } +}; + +export default boot(async ({ app }) => { + const ant = getAntelope(); + globalProps = app.config.globalProperties; + + if (redirectParam) { + const isValid = new RegExp('^(http|https)://', 'i').test(redirectParam); + if (isValid) { + redirect.value = { + url: redirectParam, + hostname: new URL(redirectParam).hostname, + }; + redirectShow.value = true; + } + } + + if (iframeParam) { + const isValid = new RegExp('^(http|https)://', 'i').test(iframeParam); + if (isValid) { + redirect.value = { + url: iframeParam, + hostname: new URL(iframeParam).hostname, + }; + iframeShow.value = true; + } + } + + const subscription = ant.events.onLoggedIn.subscribe({ + next: () => { + telosCloudResponse(); + subscription.unsubscribe(); + }, + }); + + +}); + +console.log('TelosCloudJs-0.9.19 supported'); diff --git a/src/boot/ual.js b/src/boot/ual.js index 3f988b301..7552bbba4 100644 --- a/src/boot/ual.js +++ b/src/boot/ual.js @@ -3,9 +3,8 @@ import { UAL } from 'universal-authenticator-library'; import { Anchor } from 'ual-anchor'; import { Wombat } from 'ual-wombat'; import { CleosAuthenticator } from '@telosnetwork/ual-cleos'; -import { WebPopup } from 'oreid-webpopup'; -import { OreIdAuthenticator, AuthProvider } from 'ual-oreid'; import { Dialog, Notify, copyToClipboard } from 'quasar'; +import { MetakeepAuthenticator } from 'src/antelope/wallets/ual/MetakeepUAL'; export default boot(async ({ app, store }) => { @@ -51,7 +50,7 @@ export default boot(async ({ app, store }) => { }).catch((e) => { throw e; }); - if (store.state.account.justViewer) { + if (store.state.account.justViewer || localStorage.getItem('justViewer')) { permission = 'active'; } else { await new Promise((resolve) => { @@ -151,6 +150,11 @@ export default boot(async ({ app, store }) => { loginHandler, signHandler, }), + new MetakeepAuthenticator([chain], { + appName: process.env.APP_NAME, + appId: process.env.METAKEEP_APP_ID_NATIVE, + accountCreateAPI: `${process.env.TELOS_API_ENDPOINT}/accounts/create4google`, + }), ]; const ual = new UAL([chain], 'ual', authenticators); diff --git a/src/components/evm/AppNav.vue b/src/components/evm/AppNav.vue index 64dc2e8bb..d9bd8e74b 100644 --- a/src/components/evm/AppNav.vue +++ b/src/components/evm/AppNav.vue @@ -4,7 +4,7 @@ import InlineSvg from 'vue-inline-svg'; import UserInfo from 'components/evm/UserInfo.vue'; import { getAntelope, useChainStore } from 'src/antelope'; -import EVMLoginButtons from 'pages/home/EVMLoginButtons.vue'; +import LoginButtons from 'pages/home/LoginButtons.vue'; import { getShortenedHash } from 'src/antelope/stores/utils'; const ant = getAntelope(); @@ -14,7 +14,7 @@ const chainStore = useChainStore(); export default defineComponent({ name: 'AppNav', components: { - EVMLoginButtons, + LoginButtons, UserInfo, InlineSvg, }, @@ -413,7 +413,7 @@ export default defineComponent({ @click="closeLoginMenu()" /> - + diff --git a/src/css/components/_notification.scss b/src/css/components/_notification.scss index 60f037a79..54d773c73 100644 --- a/src/css/components/_notification.scss +++ b/src/css/components/_notification.scss @@ -105,6 +105,15 @@ font-weight: bold; margin-bottom: 1rem; } + &--bold { + font-weight: bold; + } + &--center { + text-align: center; + } + &--paragraph { + margin-bottom: 1rem; + } } &__action-btn { color: var(--text-color, #000); diff --git a/src/css/quasar.variables.sass b/src/css/quasar.variables.sass index 7deecbe1a..9a0725cb4 100644 --- a/src/css/quasar.variables.sass +++ b/src/css/quasar.variables.sass @@ -13,7 +13,7 @@ // Tip: Use the "Theme Builder" on Quasar's documentation website. $primary : #571aff -$secondary : #dfdfed +$secondary : #1a85ff $accent : #ffd75e $dark : #130C3F diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index 14d804bb6..f3b1c0001 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -30,12 +30,14 @@ export default { wallet_logo_alt: 'Telos Wallet logo', view_any_account: 'View Any Account', connect_with_wallet: 'Connect Your Wallet', - login_with_social_media: 'Telos Cloud Wallet', + telos_cloud_wallet: 'Telos Cloud Wallet', + telos_cloud_login: 'Telos Cloud Login', + available_accounts: 'Available Accounts', sign_with_google: 'Sign with Google', sign_with_facebook: 'Sign with Facebook', sign_with_x: 'Sign with X', sign_with_email: 'Sign in with Email', - coming_soon: 'Coming soon', + login_with_social_media: 'Login with Social Media', create_new_account: 'Create a New Account', logged_as: 'Connected as {account}', view_wallet: 'View Wallet', @@ -58,6 +60,14 @@ export default { oauth_facebook: 'Facebook', oauth_twitter: 'Twitter', oauth_email: 'Email', + random: 'Random', + continue: 'Continue', + account_name: 'Account Name', + name_selection_text: 'Account name needs to be twelve characters long and can only consist of lowercase letters and numerical characters from 1 to 5.', + account_selection_text: 'Choose one of your accounts to log in', + redirect_me: 'Redirect me', + redirect_warning: 'After you log in, you will be redirected to the following site:', + redirect_notification_message: 'Do you approve to be redirected to
{hostname}?

only proceed if you trust the site.', }, nav: { copy_address: 'Copy address to clipboard', @@ -364,6 +374,10 @@ export default { resources_low: 'Your resources are low', recommend_bying: 'We recommend you buy more for 1 TLOS', proceed_q: 'Proceed?', + account_name_feedback_no_dots: 'Account name cannot contain dots', + account_name_feedback_invalid_character: 'invalid character \'{char}\'', + account_name_feedback_invalid_length: '{length} of 12 characters', + account_name_feedback_taken: 'Account name is taken', }, streaming: { title: 'Live Streaming Example', @@ -593,7 +607,7 @@ export default { }, evm: { error_support_provider_request: 'Provider does not support request method', - error_login: 'Error in login proccess', + error_login: 'Error in login process', error_add_chain_rejected: 'User has rejected the request to add the chain', error_connect_rejected: 'User has rejected the request to connect to the chain', error_add_chain: 'Error in adding chain', @@ -626,6 +640,8 @@ export default { error_withdraw_failed: 'An unknown error occurred when withdrawing tokens', error_fetching_token_price: 'An unknown error occurred when fetching token price data', error_transfer_nft: 'An error occurred while transferring collectible', + error_metakeep_web3_provider: 'An error occurred while initializing Metakeep Web3 provider', + error_metakeep_app_id: 'App ID not provided for Metakeep', error_updating_allowance: 'An error occurred while updating allowance', }, history: { @@ -646,6 +662,7 @@ export default { error_login_native: 'An error has occurred trying to login to the native chain', error_login_evm: 'An error has occurred trying to login to the EVM chain', error_auto_login: 'An error has occurred trying to auto login the user', + logging_in_as: 'Logging in as {account}', }, utils: { error_parsing_transaction: 'Failed to parse transaction data', @@ -662,7 +679,6 @@ export default { wallets: { error_system_token_transfer_config: 'Error getting Wagmi system token transfer config', error_token_transfer_config: 'Error getting Wagmi token transfer config', - error_oreid_no_chain_account: 'The app {appName} does not have a chain account for the chain {networkName}', network_switch_success: 'Network switched successfully', }, wrap: { @@ -685,9 +701,4 @@ export default { years: 'years', }, }, - temporal: { - telos_cloud_discontinued_title: 'Important', - telos_cloud_discontinued_message_title: 'Attention Users: Telos Cloud Wallet account option will be discontinued.', - telos_cloud_discontinued_message_body:'The Telos Cloud Wallet (ORE ID via Google) account option to connect and sign transactions will be discontinued after December 31st. If you use the Telos Cloud Wallet to access your account, please transfer your assets to another wallet before this deadline. This change does not impact users accessing their accounts via Metamask, WalletConnect, Anchor, or other sign-in methods.', - }, }; diff --git a/src/layouts/NativeLayout.vue b/src/layouts/NativeLayout.vue index 1324074d4..352a9e550 100644 --- a/src/layouts/NativeLayout.vue +++ b/src/layouts/NativeLayout.vue @@ -2,8 +2,9 @@ import { mapGetters, mapActions } from 'vuex'; import navBar from 'components/native/NavBar.vue'; -import NativeLoginButton from 'pages/home/NativeLoginButton.vue'; +import LoginButtons from 'pages/home/LoginButtons.vue'; import { getAntelope } from 'src/antelope'; +import { googleCtrl } from 'src/pages/home/GoogleOneTap'; const pagesData = [ { @@ -31,7 +32,7 @@ const pagesData = [ export default { name: 'NativeLayout', - components: { NavBar: navBar, NativeLoginButton }, + components: { NavBar: navBar, LoginButtons }, data() { return { avatar: null, @@ -111,8 +112,9 @@ export default { ); this.accountHistory = actionHistory.data.actions || []; }, - logOut() { + performLogOut() { this.resetTokens(); + googleCtrl.logout(); this.logout(true); }, resetTokens() { @@ -121,16 +123,17 @@ export default { }, }, async mounted() { - await this.memoryAutoLogin(); + if (!this.isUserAuthenticated) { + await this.memoryAutoLogin(); + } this.loadUserProfile(); - this.checkPath(); }, };