diff --git a/env.js b/env.js index c031563f6..6d267adee 100644 --- a/env.js +++ b/env.js @@ -22,6 +22,7 @@ const TESTNET = { HYPERION_ENDPOINT: 'https://testnet.telos.net', NETWORK_EXPLORER: 'https://explorer-test.telos.net', CHAIN_NAME: 'telos-testnet', + APP_OREID_APP_ID: 't_75a4d9233ec441d18c4221e92b379197', }; const MAINNET = { @@ -35,6 +36,7 @@ const MAINNET = { HYPERION_ENDPOINT: 'https://mainnet.telos.net', NETWORK_EXPLORER: 'https://explorer.telos.net', CHAIN_NAME: 'telos', + APP_OREID_APP_ID: 'p_e5b81fcc20a04339993b0cc80df7e3fd', }; const env = process.env.NETWORK === 'mainnet' ? MAINNET : TESTNET; diff --git a/jest.config.js b/jest.config.js index 80c066ab6..227cad422 100755 --- a/jest.config.js +++ b/jest.config.js @@ -90,6 +90,7 @@ module.exports = { 'jest-transform-stub', }, transformIgnorePatterns: [`node_modules/(?!(${esModules}))`], + testPathIgnorePatterns: ['balances'], setupFiles: ['/jest.init.js', '/test/jest/setEnvVars.ts'], globalSetup: './global-jest-setup.js', snapshotSerializers: ['/node_modules/jest-serializer-vue'], diff --git a/package.json b/package.json index 980184eef..185af5180 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "2.1.0", "description": "A Web Wallet for Telos", "productName": "Telos Web Wallet", - "author": "Ryuhei Matsuda ", "private": true, "scripts": { "dev": "quasar dev", @@ -38,6 +37,8 @@ "mitt": "^3.0.0", "node-polyfill-webpack-plugin": "^2.0.1", "numeral": "^2.0.6", + "oreid-js": "^4.7.1", + "oreid-webpopup": "^2.3.0", "pinia": "^2.0.33", "ptokens": "^0.14.0", "qrcanvas-vue": "^3.0.0", diff --git a/src/App.vue b/src/App.vue index 1bfafe94e..e44c78ef0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,22 @@ - diff --git a/src/antelope/chains/EVMChainSettings.ts b/src/antelope/chains/EVMChainSettings.ts index 21ad92892..154ded128 100644 --- a/src/antelope/chains/EVMChainSettings.ts +++ b/src/antelope/chains/EVMChainSettings.ts @@ -219,6 +219,7 @@ export default abstract class EVMChainSettings implements ChainSettings { abstract getSystemTokens(): TokenClass[]; abstract getIndexerApiEndpoint(): string; abstract hasIndexerSupport(): boolean; + abstract trackAnalyticsEvent(params: Record): void; async getBalances(account: string): Promise { if (!this.hasIndexerSupport()) { diff --git a/src/antelope/chains/NativeChainSettings.ts b/src/antelope/chains/NativeChainSettings.ts index 14e4c0406..285c7d942 100644 --- a/src/antelope/chains/NativeChainSettings.ts +++ b/src/antelope/chains/NativeChainSettings.ts @@ -142,6 +142,7 @@ export default abstract class NativeChainSettings implements ChainSettings { abstract getMapDisplay(): boolean; abstract getTheme(): Theme; abstract getFiltersSupported(prop: string): boolean; + abstract trackAnalyticsEvent(params: Record): void; /** * Retrieves the list of IDs for the important tokens. diff --git a/src/antelope/chains/chain-constants.ts b/src/antelope/chains/chain-constants.ts new file mode 100644 index 000000000..6822ad20b --- /dev/null +++ b/src/antelope/chains/chain-constants.ts @@ -0,0 +1,17 @@ +import { NETWORK as telosEvmNetwork } from 'src/antelope/chains/evm/telos-evm'; +import { NETWORK as telosTestnetEvmNetwork } from 'src/antelope/chains/evm/telos-evm-testnet'; + +export const TELOS_CHAIN_IDS = ['40', '41']; +export const TELOS_NETWORK_NAMES = [telosEvmNetwork, telosTestnetEvmNetwork]; +export const TELOS_ANALYTICS_EVENT_IDS = { + loginStarted: 'JXIYBP1S', + loginSuccessful: 'HIP11SFR', + loginSuccessfulMetamask: 'ABGMND23', + loginFailedMetamask: 'ZFGE6TFS', + loginSuccessfulSafepal: 'LKCBKDU2', + loginFailedSafepal: '6PSIWGNV', + loginSuccessfulOreId: 'MSXMDDNK', + loginFailedOreId: 'UFX3JYRJ', + loginFailedWalletConnect: '9V4IV1BV', + loginSuccessfulWalletConnect: '2EG2OR3H', +}; diff --git a/src/antelope/chains/evm/telos-evm-testnet/index.ts b/src/antelope/chains/evm/telos-evm-testnet/index.ts index 49197377b..4c2ed6eef 100644 --- a/src/antelope/chains/evm/telos-evm-testnet/index.ts +++ b/src/antelope/chains/evm/telos-evm-testnet/index.ts @@ -8,7 +8,7 @@ import { getFiatPriceFromIndexer } from 'src/api/price'; const LOGO = 'https://raw.githubusercontent.com/telosnetwork/images/master/logos_2021/Symbol%202.svg'; const CHAIN_ID = '41'; -const NETWORK = 'telos-evm-testnet'; +export const NETWORK = 'telos-evm-testnet'; const DISPLAY = 'Telos EVM Testnet'; const TOKEN = new TokenClass({ name: 'Telos', @@ -58,6 +58,8 @@ const NETWORK_EVM_ENDPOINT = 'https://testnet.telos.net'; const INDEXER_ENDPOINT = 'https://api.testnet.teloscan.io'; const CONTRACTS_BUCKET = 'https://verified-evm-contracts-testnet.s3.amazonaws.com'; +declare const fathom: { trackGoal: (eventId: string, value: 0) => void }; + export default class TelosEVMTestnet extends EVMChainSettings { getNetwork(): string { return NETWORK; @@ -149,4 +151,13 @@ export default class TelosEVMTestnet extends EVMChainSettings { return true; } + trackAnalyticsEvent(params: Record): void { + if (typeof fathom === 'undefined') { + console.warn(`Failed to track event with ID ${params.id}: Fathom Analytics not loaded`); + return; + } + + const id = params.id as string; + fathom.trackGoal(id, 0); + } } diff --git a/src/antelope/chains/evm/telos-evm/index.ts b/src/antelope/chains/evm/telos-evm/index.ts index 2b6f6b97c..c4e4acdf9 100644 --- a/src/antelope/chains/evm/telos-evm/index.ts +++ b/src/antelope/chains/evm/telos-evm/index.ts @@ -8,7 +8,7 @@ import { getFiatPriceFromIndexer } from 'src/api/price'; const LOGO = 'https://raw.githubusercontent.com/telosnetwork/images/master/logos_2021/Symbol%202.svg'; const CHAIN_ID = '40'; -const NETWORK = 'telos-evm'; +export const NETWORK = 'telos-evm'; const DISPLAY = 'Telos EVM Mainnet'; const TOKEN = new TokenClass({ name: 'Telos', @@ -58,6 +58,8 @@ const NETWORK_EVM_ENDPOINT = 'https://mainnet.telos.net'; const INDEXER_ENDPOINT = 'https://api.teloscan.io'; const CONTRACTS_BUCKET = 'https://verified-evm-contracts.s3.amazonaws.com'; +declare const fathom: { trackGoal: (eventId: string, value: 0) => void }; + export default class TelosEVMTestnet extends EVMChainSettings { getNetwork(): string { return NETWORK; @@ -148,4 +150,14 @@ export default class TelosEVMTestnet extends EVMChainSettings { hasIndexerSupport(): boolean { return true; } + + trackAnalyticsEvent(params: Record): void { + if (typeof fathom === 'undefined') { + console.warn(`Failed to track event with ID ${params.id}: Fathom Analytics not loaded`); + return; + } + + const id = params.id as string; + fathom.trackGoal(id, 0); + } } diff --git a/src/antelope/chains/native/eos/index.ts b/src/antelope/chains/native/eos/index.ts index 94100d6b6..7659a743c 100644 --- a/src/antelope/chains/native/eos/index.ts +++ b/src/antelope/chains/native/eos/index.ts @@ -102,4 +102,8 @@ export default class EOS extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/chains/native/jungle/index.ts b/src/antelope/chains/native/jungle/index.ts index 5178c54b0..77917984e 100644 --- a/src/antelope/chains/native/jungle/index.ts +++ b/src/antelope/chains/native/jungle/index.ts @@ -97,4 +97,8 @@ export default class TelosTestnet extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/chains/native/telos-testnet/index.ts b/src/antelope/chains/native/telos-testnet/index.ts index 2933b6be9..dd359ae9c 100644 --- a/src/antelope/chains/native/telos-testnet/index.ts +++ b/src/antelope/chains/native/telos-testnet/index.ts @@ -116,4 +116,8 @@ export default class TelosTestnet extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/chains/native/telos/index.ts b/src/antelope/chains/native/telos/index.ts index fd9f109bd..20be122dd 100644 --- a/src/antelope/chains/native/telos/index.ts +++ b/src/antelope/chains/native/telos/index.ts @@ -123,4 +123,8 @@ export default class Telos extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/chains/native/ux/index.ts b/src/antelope/chains/native/ux/index.ts index 6bac24364..e864b20e1 100644 --- a/src/antelope/chains/native/ux/index.ts +++ b/src/antelope/chains/native/ux/index.ts @@ -97,4 +97,8 @@ export default class UX extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/chains/native/wax/index.ts b/src/antelope/chains/native/wax/index.ts index 22e2776d7..f7d530da6 100644 --- a/src/antelope/chains/native/wax/index.ts +++ b/src/antelope/chains/native/wax/index.ts @@ -97,4 +97,8 @@ export default class EOS extends NativeChainSettings { getSystemTokens(): TokenClass[] { return [TOKEN]; } + + trackAnalyticsEvent(): void { + console.warn(`trackAnalyticsEvent not implemented for ${NETWORK}`); + } } diff --git a/src/antelope/types/ChainSettings.ts b/src/antelope/types/ChainSettings.ts index 5ef44ccff..ff6ee121b 100644 --- a/src/antelope/types/ChainSettings.ts +++ b/src/antelope/types/ChainSettings.ts @@ -16,5 +16,6 @@ export interface ChainSettings { getUsdPrice(): Promise; getSystemTokens(): TokenClass[]; getNFTsInventory(address: string, filter: IndexerTransactionsFilter): Promise; - getNFTsCollection(contract: string, filter: IndexerTransactionsFilter): Promise + getNFTsCollection(contract: string, filter: IndexerTransactionsFilter): Promise; + trackAnalyticsEvent(params: Record): void; } diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts index 51fd004a1..c912e46cc 100644 --- a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts +++ b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts @@ -2,11 +2,14 @@ import { ethers } from 'ethers'; import { BehaviorSubject, filter, map } from 'rxjs'; -import { useEVMStore, useFeedbackStore } from 'src/antelope'; +import { useChainStore, useEVMStore, useFeedbackStore } from 'src/antelope'; import { AntelopeError, ERC20_TYPE, EthereumProvider, EvmTransactionResponse, TokenClass, addressString } from 'src/antelope/types'; import { EVMAuthenticator } from 'src/antelope/wallets'; import { AbiItem } from 'web3-utils'; import Web3 from 'web3'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { TELOS_NETWORK_NAMES, TELOS_ANALYTICS_EVENT_IDS } from 'src/antelope/chains/chain-constants'; +import { MetamaskAuthName, SafePalAuthName } from 'src/antelope/wallets'; export abstract class InjectedProviderAuth extends EVMAuthenticator { onReady = new BehaviorSubject(false); @@ -62,11 +65,67 @@ export abstract class InjectedProviderAuth extends EVMAuthenticator { // EVMAuthenticator API ---------------------------------------------------------- async login(network: string): Promise { + const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + const authName = this.getName(); + this.trace('login', network); useFeedbackStore().setLoading(`${this.getName()}.login`); - const response = await super.login(network); - useFeedbackStore().unsetLoading(`${this.getName()}.login`); - return response; + + this.trace('login', 'trackAnalyticsEvent -> login started'); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginStarted }, + ); + + const response = await super.login(network).then((res) => { + if (TELOS_NETWORK_NAMES.includes(network)) { + let successfulLoginEventId = ''; + + if (authName === MetamaskAuthName) { + successfulLoginEventId = TELOS_ANALYTICS_EVENT_IDS.loginSuccessfulMetamask; + } else if (authName === SafePalAuthName) { + successfulLoginEventId = TELOS_ANALYTICS_EVENT_IDS.loginSuccessfulSafepal; + } + + if (successfulLoginEventId) { + this.trace('login', 'trackAnalyticsEvent -> login succeeded', authName, successfulLoginEventId); + chainSettings.trackAnalyticsEvent( + { id: successfulLoginEventId }, + ); + } + + this.trace('login', 'trackAnalyticsEvent -> generic login succeeded', TELOS_ANALYTICS_EVENT_IDS.loginSuccessful); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginSuccessful }, + ); + } + + return res; + }).catch((error) => { + // if the user rejects the connection, we don't want to track it as an error + if ( + TELOS_NETWORK_NAMES.includes(network) && + error.message !== 'antelope.evm.error_connect_rejected' + ) { + let failedLoginEventId = ''; + + if (authName === MetamaskAuthName) { + failedLoginEventId = TELOS_ANALYTICS_EVENT_IDS.loginFailedMetamask; + } else if (authName === SafePalAuthName) { + failedLoginEventId = TELOS_ANALYTICS_EVENT_IDS.loginFailedSafepal; + } + + if (failedLoginEventId) { + this.trace('login', 'trackAnalyticsEvent -> login failed', authName, failedLoginEventId); + chainSettings.trackAnalyticsEvent( + { id: failedLoginEventId }, + ); + } + } + }).finally(() => { + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + }); + + return response ?? null; } async logout(): Promise { diff --git a/src/antelope/wallets/authenticators/MetamaskAuth.ts b/src/antelope/wallets/authenticators/MetamaskAuth.ts index 132ba81da..4b0cb3379 100644 --- a/src/antelope/wallets/authenticators/MetamaskAuth.ts +++ b/src/antelope/wallets/authenticators/MetamaskAuth.ts @@ -2,6 +2,7 @@ import { EthereumProvider } from 'src/antelope/types'; import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; const name = 'Metamask'; +export const MetamaskAuthName = name; export class MetamaskAuth extends InjectedProviderAuth { // this is just a dummy label to identify the authenticator base class diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts new file mode 100644 index 000000000..a5ff4b6ff --- /dev/null +++ b/src/antelope/wallets/authenticators/OreIdAuth.ts @@ -0,0 +1,280 @@ +import { AuthProvider, ChainNetwork, OreId, OreIdOptions, JSONObject, UserChainAccount } from 'oreid-js'; +import { ethers } from 'ethers'; +import { WebPopup } from 'oreid-webpopup'; +import { erc20Abi } from 'src/antelope/types'; +import { EVMAuthenticator } from 'src/antelope/wallets'; +import { + AntelopeError, + TokenClass, + addressString, + EvmTransactionResponse, +} from 'src/antelope/types'; +import { useFeedbackStore } from 'src/antelope/stores/feedback'; +import { useChainStore } from 'src/antelope/stores/chain'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { TELOS_ANALYTICS_EVENT_IDS } 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; + } + + get provider(): string { + return this.options.provider ?? ''; + } + + setProvider(provider: string): void { + this.trace('setProvider', provider); + this.options.provider = provider; + } + + // EVMAuthenticator API ---------------------------------------------------------- + + 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); + } + + 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 = useChainStore().currentChain.settings as EVMChainSettings; + const trackSuccessfulLogin = () => { + this.trace('login', 'trackAnalyticsEvent -> generic login succeeded', TELOS_ANALYTICS_EVENT_IDS.loginSuccessful); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginSuccessful }, + ); + this.trace('login', 'trackAnalyticsEvent -> login succeeded', this.getName(), TELOS_ANALYTICS_EVENT_IDS.loginSuccessfulOreId); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.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('account') === 'string' + ) { + // auto login without the popup + const chainAccount = localStorage.getItem('account') 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( + { id: TELOS_ANALYTICS_EVENT_IDS.loginStarted }, + ); + // then track the successful login + trackSuccessfulLogin(); + return chainAccount; + } + + this.trace('login', 'trackAnalyticsEvent -> login started'); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.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_IDS.loginFailedOreId); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.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(); + + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + return address; + } + + async logout(): Promise { + this.trace('logout'); + if (oreId) { + await oreId.logout(); + } + return Promise.resolve(); + } + + async getSystemTokenBalance(address: addressString | string): Promise { + this.trace('getSystemTokenBalance', address); + const provider = await this.web3Provider(); + if (provider) { + return provider.getBalance(address); + } else { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } + + async getERC20TokenBalance(address: addressString, token: addressString): Promise { + this.trace('getERC20TokenBalance', [address, token]); + const provider = await this.web3Provider(); + if (provider) { + const erc20Contract = new ethers.Contract(token, erc20Abi, provider); + const balance = await erc20Contract.balanceOf(address); + return balance; + } else { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } + + async transferTokens(token: TokenClass, amount: ethers.BigNumber, to: addressString): Promise { + this.trace('transferTokens', token, amount, to); + + 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'); + } + + const from = this.userChainAccount.chainAccount as addressString; + const value = amount.toHexString(); + const abi = erc20Abi; + + const systemTransfer = { + from, + to, + value, + }; + + const erc20Transfer = { + from, + to: token.address, + 'contract': { + abi, + 'parameters': [to, value], + 'method': 'transfer', + }, + } as unknown as JSONObject; + + let transactionBody = null as unknown as JSONObject; + if (token.isSystem) { + transactionBody = systemTransfer; + } else { + transactionBody = erc20Transfer; + } + + // sign a blockchain transaction + this.trace('createTransaction()'); + const transaction = await oreId.createTransaction({ + transaction: transactionBody, + chainAccount: from, + chainNetwork: this.getChainNetwork(useChainStore().getChain(this.label).settings.getNetwork()), + signOptions: { + broadcast: true, + returnSignedTransaction: true, + }, + }); + + // have the user approve signature + this.trace('Signing a transaction...', transaction); + + const { transactionId } = await oreId.popup.sign({ transaction }); + this.trace('transactionId:', transactionId); + + return { + hash: transactionId, + wait: async () => Promise.resolve({} as ethers.providers.TransactionReceipt), + } as EvmTransactionResponse; + } + + async prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { + this.trace('prepareTokenForTransfer', [token], amount, to); + } + + async isConnectedTo(chainId: string): Promise { + this.trace('isConnectedTo', chainId); + return true; + } + + async web3Provider(): Promise { + this.trace('web3Provider'); + const p:RpcEndpoint = (useChainStore().getChain(this.label).settings as EVMChainSettings).getRPCEndpoint(); + const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + const web3Provider = new ethers.providers.JsonRpcProvider(url); + await web3Provider.ready; + return web3Provider as ethers.providers.Web3Provider; + } + + async externalProvider(): Promise { + this.trace('externalProvider'); + return new Promise(async (resolve) => { + resolve(null as unknown as ethers.providers.ExternalProvider); + }); + } + +} diff --git a/src/antelope/wallets/authenticators/SafePalAuth.ts b/src/antelope/wallets/authenticators/SafePalAuth.ts index a3e03396c..4574c4367 100644 --- a/src/antelope/wallets/authenticators/SafePalAuth.ts +++ b/src/antelope/wallets/authenticators/SafePalAuth.ts @@ -2,6 +2,7 @@ import { EthereumProvider } from 'src/antelope/types'; import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; const name = 'SafePal'; +export const SafePalAuthName = name; export class SafePalAuth extends InjectedProviderAuth { // this is just a dummy label to identify the authenticator base class @@ -17,7 +18,6 @@ export class SafePalAuth extends InjectedProviderAuth { // EVMAuthenticator API ---------------------------------------------------------- - getName(): string { return name; } diff --git a/src/antelope/wallets/authenticators/WalletConnectAuth.ts b/src/antelope/wallets/authenticators/WalletConnectAuth.ts index 2e35860af..33e6dfc02 100644 --- a/src/antelope/wallets/authenticators/WalletConnectAuth.ts +++ b/src/antelope/wallets/authenticators/WalletConnectAuth.ts @@ -18,6 +18,7 @@ import { import { Web3Modal, Web3ModalConfig } from '@web3modal/html'; import { BigNumber, ethers } from 'ethers'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { TELOS_ANALYTICS_EVENT_IDS } from 'src/antelope/chains/chain-constants'; import { useChainStore } from 'src/antelope/stores/chain'; import { useEVMStore } from 'src/antelope/stores/evm'; import { useFeedbackStore } from 'src/antelope/stores/feedback'; @@ -60,6 +61,8 @@ export class WalletConnectAuth extends EVMAuthenticator { async walletConnectLogin(network: string): Promise { this.trace('walletConnectLogin'); + const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + try { this.clearAuthenticator(); const address = getAccount().address as addressString; @@ -74,10 +77,38 @@ export class WalletConnectAuth extends EVMAuthenticator { console.error(e); } + this.trace( + 'login', + 'trackAnalyticsEvent -> login successful', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_IDS.loginSuccessfulWalletConnect, + ); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginSuccessfulWalletConnect }, + ); + this.trace( + 'login', + 'trackAnalyticsEvent -> generic login successful', + TELOS_ANALYTICS_EVENT_IDS.loginSuccessful, + ); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginSuccessful }, + ); + return address; } catch (e) { // This is a non-expected error console.error(e); + this.trace( + 'walletConnectLogin', + 'trackAnalyticsEvent -> login failed', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_IDS.loginFailedWalletConnect, + ); + const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginFailedWalletConnect }, + ); throw new AntelopeError('antelope.evm.error_login'); } finally { useFeedbackStore().unsetLoading(`${this.getName()}.login`); @@ -86,19 +117,57 @@ export class WalletConnectAuth extends EVMAuthenticator { async login(network: string): Promise { this.trace('login', network); + const wagmiConnected = () => localStorage.getItem('wagmi.connected'); + const chainSettings = useChainStore().currentChain.settings as EVMChainSettings; + useFeedbackStore().setLoading(`${this.getName()}.login`); - if (localStorage.getItem('wagmi.connected')) { + if (wagmiConnected()) { + // We are in auto-login process. So log loginStarted before calling the walletConnectLogin method + this.trace( + 'login', + 'trackAnalyticsEvent -> login started', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_IDS.loginStarted, + ); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginStarted }, + ); return this.walletConnectLogin(network); } else { return new Promise(async (resolve) => { this.trace('login', 'web3Modal.openModal()'); const web3Modal = new Web3Modal(this.options, this.wagmiClient); web3Modal.subscribeModal(async (newState) => { - this.trace('login', 'web3Modal.subscribeModal ', newState, localStorage.getItem('wagmi.connected')); + this.trace('login', 'web3Modal.subscribeModal ', newState, wagmiConnected); + + if (newState.open === true) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login started', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_IDS.loginStarted, + ); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginStarted }, + ); + } + if (newState.open === false) { useFeedbackStore().unsetLoading(`${this.getName()}.login`); + + if (!wagmiConnected()) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login failed', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_IDS.loginFailedWalletConnect, + ); + chainSettings.trackAnalyticsEvent( + { id: TELOS_ANALYTICS_EVENT_IDS.loginFailedWalletConnect }, + ); + } } - if (localStorage.getItem('wagmi.connected')) { + if (wagmiConnected()) { resolve(this.walletConnectLogin(network)); } }); @@ -208,6 +277,12 @@ export class WalletConnectAuth extends EVMAuthenticator { async isConnectedTo(chainId: string): Promise { this.trace('isConnectedTo', chainId); + + if (usePlatformStore().isMobile) { + this.trace('isConnectedTo', 'mobile -> true'); + return true; + } + return new Promise(async (resolve) => { const web3Provider = await this.web3Provider(); const correct = +web3Provider.network.chainId === +chainId; diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts index b99fccadb..33b7ee216 100644 --- a/src/antelope/wallets/index.ts +++ b/src/antelope/wallets/index.ts @@ -31,5 +31,7 @@ export class AntelopeWallets { 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/SafePalAuth'; export * from 'src/antelope/wallets/authenticators/WalletConnectAuth'; +export * from 'src/antelope/wallets/authenticators/SafePalAuth'; diff --git a/src/assets/evm/icon-oauth-email.svg b/src/assets/evm/icon-oauth-email.svg new file mode 100644 index 000000000..ac708fe7b --- /dev/null +++ b/src/assets/evm/icon-oauth-email.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/evm/icon-oauth-facebook.svg b/src/assets/evm/icon-oauth-facebook.svg new file mode 100644 index 000000000..b10752a99 --- /dev/null +++ b/src/assets/evm/icon-oauth-facebook.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/evm/icon-oauth-github.svg b/src/assets/evm/icon-oauth-github.svg new file mode 100644 index 000000000..922d27069 --- /dev/null +++ b/src/assets/evm/icon-oauth-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/evm/icon-oauth-google.svg b/src/assets/evm/icon-oauth-google.svg new file mode 100644 index 000000000..a587f07fb --- /dev/null +++ b/src/assets/evm/icon-oauth-google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/evm/icon-oauth-twitter.svg b/src/assets/evm/icon-oauth-twitter.svg new file mode 100644 index 000000000..05f5c1551 --- /dev/null +++ b/src/assets/evm/icon-oauth-twitter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/evm/ore-id.svg b/src/assets/evm/ore-id.svg new file mode 100644 index 000000000..855d122df --- /dev/null +++ b/src/assets/evm/ore-id.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/logo--tlos.svg b/src/assets/logo--tlos.svg index 0b08a01ea..4aa7b3caa 100644 --- a/src/assets/logo--tlos.svg +++ b/src/assets/logo--tlos.svg @@ -1,6 +1,6 @@ - + diff --git a/src/boot/antelope.ts b/src/boot/antelope.ts index de6e4e6c8..30ce939f3 100644 --- a/src/boot/antelope.ts +++ b/src/boot/antelope.ts @@ -1,10 +1,12 @@ import { EthereumClient } from '@web3modal/ethereum'; import { Web3ModalConfig } from '@web3modal/html'; +import { OreIdOptions } from 'oreid-js'; import { boot } from 'quasar/wrappers'; import { installAntelope } from 'src/antelope'; import { MetamaskAuth, WalletConnectAuth, + OreIdAuth, SafePalAuth, } from 'src/antelope/wallets'; import { App } from 'vue'; @@ -63,6 +65,11 @@ export default boot(({ app }) => { ant.wallets.addEVMAuthenticator(new WalletConnectAuth(options, wagmiClient)); ant.wallets.addEVMAuthenticator(new MetamaskAuth()); ant.wallets.addEVMAuthenticator(new SafePalAuth()); + const oreIdOptions: OreIdOptions = { + appName: process.env.APP_NAME, + appId: process.env.APP_OREID_APP_ID as string, + }; + ant.wallets.addEVMAuthenticator(new OreIdAuth(oreIdOptions)); // autologin -- ant.stores.account.autoLogin(); diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index 76eacfd52..41b50bf24 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -30,6 +30,7 @@ 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', create_new_account: 'Create a New Account', logged_as: 'Logged in as {account}', view_wallet: 'View Wallet', @@ -44,6 +45,12 @@ export default { multiple_providers_notification_message: 'Multiple providers detected. Disable all providers except MetaMask to continue.', no_provider_action_label: 'Install {provider}', no_injected_provider_found: '{providerName}\'s provider was not found.', + sign_in_with: 'Sign in with', + oauth_google: 'Google', + oauth_github: 'GitHub', + oauth_facebook: 'Facebook', + oauth_twitter: 'Twitter', + oauth_email: 'Email', }, nav: { copy_address: 'Copy address to clipboard', @@ -465,13 +472,11 @@ export default { balances: { error_at_transfer_tokens: 'An error has occurred trying to transfer tokens', error_token_contract_not_found: 'Token contract not found for address {address}', - error_transfer_timeout: 'Timeout while waiting for transfer to complete', }, 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: 'Switched to {networkName} network', + error_oreid_no_chain_account: 'The app {appName} does not have a chain account for the chain {networkName}', }, }, }; diff --git a/src/index.template.html b/src/index.template.html index 7f6b55f93..86ad8ef0d 100644 --- a/src/index.template.html +++ b/src/index.template.html @@ -15,8 +15,6 @@ - - diff --git a/src/pages/home/ConnectWalletOptions.vue b/src/pages/home/ConnectWalletOptions.vue index 54194efa9..6862ad791 100644 --- a/src/pages/home/ConnectWalletOptions.vue +++ b/src/pages/home/ConnectWalletOptions.vue @@ -1,9 +1,10 @@