From e8016897f1922eaab26f5f871521302a89c9c28f Mon Sep 17 00:00:00 2001 From: Viterbo Date: Thu, 20 Jul 2023 01:38:02 -0300 Subject: [PATCH] adding OreId authenticator (2) --- env.js | 4 + jest.config.js | 1 + package.json | 2 + src/antelope/stores/balances.ts | 15 +- .../wallets/authenticators/OreIdAuth.ts | 257 ++++++++++++++++++ .../authenticators/WalletConnectAuth.ts | 6 + src/antelope/wallets/index.ts | 1 + src/assets/evm/icon-oauth-email.svg | 10 + src/assets/evm/icon-oauth-facebook.svg | 12 + src/assets/evm/icon-oauth-github.svg | 3 + src/assets/evm/icon-oauth-google.svg | 9 + src/assets/evm/icon-oauth-twitter.svg | 6 + src/assets/evm/ore-id.svg | 10 + src/boot/antelope.ts | 7 + src/i18n/en-us/index.js | 11 +- src/pages/evm/wallet/SendPage.vue | 1 + src/pages/home/ConnectWalletOptions.vue | 111 +++++++- src/pages/home/EVMLoginButtons.vue | 15 +- src/pages/home/HomePage.vue | 11 + .../antelope/stores/balances.spec.ts | 1 + .../pages/home/ConnectWalletOptions.spec.ts | 1 + yarn.lock | 204 ++++++++++++-- 22 files changed, 648 insertions(+), 50 deletions(-) create mode 100644 src/antelope/wallets/authenticators/OreIdAuth.ts create mode 100644 src/assets/evm/icon-oauth-email.svg create mode 100644 src/assets/evm/icon-oauth-facebook.svg create mode 100644 src/assets/evm/icon-oauth-github.svg create mode 100644 src/assets/evm/icon-oauth-google.svg create mode 100644 src/assets/evm/icon-oauth-twitter.svg create mode 100644 src/assets/evm/ore-id.svg diff --git a/env.js b/env.js index c031563f6..900fcfb67 100644 --- a/env.js +++ b/env.js @@ -22,6 +22,9 @@ const TESTNET = { HYPERION_ENDPOINT: 'https://testnet.telos.net', NETWORK_EXPLORER: 'https://explorer-test.telos.net', CHAIN_NAME: 'telos-testnet', + APP_OREID_APP_ID: 't_23991cde82994c88bb582c019a9c45e1', + // TODO: uncomment this line when the testnet app is ready + // APP_OREID_APP_ID: 't_75a4d9233ec441d18c4221e92b379197', }; const MAINNET = { @@ -35,6 +38,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 ae6a60f4c..b91200989 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,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/antelope/stores/balances.ts b/src/antelope/stores/balances.ts index f9ef0a426..6db428875 100644 --- a/src/antelope/stores/balances.ts +++ b/src/antelope/stores/balances.ts @@ -293,27 +293,14 @@ export const useBalancesStore = defineStore(store_name, { amount: BigNumber, ): Promise { this.trace('transferEVMTokens', settings, account, token, to, amount.toString()); - let timer: NodeJS.Timeout | null = null; try { useFeedbackStore().setLoading('transferEVMTokens'); - const timeoutPromise = new Promise((_resolve, reject) => { - timer = setTimeout(() => { - reject(new AntelopeError('antelope.balances.error_transfer_timeout')); - - }, getAntelope().config.transactionSecTimeout * 1000); - }); - const transferPromise = account.authenticator.transferTokens(token, amount, to); - const result = await Promise.race([timeoutPromise, transferPromise]); - console.log('result: ', result); - clearTimeout(timer ?? undefined); + const result = await account.authenticator.transferTokens(token, amount, to); return result as EvmTransactionResponse | SendTransactionResult; } catch (error) { console.error(error); throw getAntelope().config.wrapError('antelope.evm.error_transfer_failed', error); } finally { - if (timer) { - clearTimeout(timer); - } useFeedbackStore().unsetLoading('transferEVMTokens'); } }, diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts new file mode 100644 index 000000000..189d5eaa0 --- /dev/null +++ b/src/antelope/wallets/authenticators/OreIdAuth.ts @@ -0,0 +1,257 @@ +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'; + + +const name = 'OreId'; + +// This instance needs to be placed outside to avoid watch function to crash +let oreId: OreId | null = null; + + +/* +// if we define options like this +options: OreIdOptions; + +// we can not use the options to place to provider like this +this.options.provider = 'google'; + +// because the options are not defined as a class +// so we need to define our own type extending the OreIdOptions interface to add the provider property + + + +*/ + +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); + + 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); + return chainAccount; + } + + // 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(); + 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); + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + return address; + } + + + async logout(): Promise { + this.trace('logout'); + } + + 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 + console.log('createTransaction()...'); + const transaction = await oreId.createTransaction({ + transaction: transactionBody, + chainAccount: from, + chainNetwork: ChainNetwork.TelosEvmTest, + signOptions: { + broadcast: true, + returnSignedTransaction: true, + }, + }); + + // have the user approve signature + console.log('Signing a transaction...', transaction); + const { transactionId } = await oreId.popup.sign({ transaction }); + console.log('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/WalletConnectAuth.ts b/src/antelope/wallets/authenticators/WalletConnectAuth.ts index 867f63012..f3cb01ddc 100644 --- a/src/antelope/wallets/authenticators/WalletConnectAuth.ts +++ b/src/antelope/wallets/authenticators/WalletConnectAuth.ts @@ -174,6 +174,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 e6ef5fac2..18011528a 100644 --- a/src/antelope/wallets/index.ts +++ b/src/antelope/wallets/index.ts @@ -31,5 +31,6 @@ export class AntelopeWallets { export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; export * from 'src/antelope/wallets/authenticators/ExternalProviderAuth'; 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'; 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/boot/antelope.ts b/src/boot/antelope.ts index 2cc65fbb8..9c4a769da 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, usePlatformStore } 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 821bf7c3e..e6d07a2d5 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: 'Login with Social Media', create_new_account: 'Create a New Account', logged_as: 'Logged in as {account}', view_wallet: 'View Wallet', @@ -43,6 +44,12 @@ export default { wallet_introduction: 'What is a Web Wallet?', no_provider_notification_message: 'No wallet provider was detected. Make sure you have the wallet installed and enabled. If you have multiple wallets installed, you can disable the others to avoid possible conflicts.', no_provider_action_label: 'Install {provider}', + 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', @@ -464,13 +471,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/pages/evm/wallet/SendPage.vue b/src/pages/evm/wallet/SendPage.vue index 00478280a..b7fff42e1 100644 --- a/src/pages/evm/wallet/SendPage.vue +++ b/src/pages/evm/wallet/SendPage.vue @@ -246,6 +246,7 @@ export default defineComponent({ const to = this.address; if (this.isFormValid) { + ant.stores.balances.transferTokens(token, to, amount).then((trx: TransactionResponse) => { const chain_settings = ant.stores.chain.loggedEvmChain?.settings; if(chain_settings) { diff --git a/src/pages/home/ConnectWalletOptions.vue b/src/pages/home/ConnectWalletOptions.vue index 778a20509..0f83d9b2c 100644 --- a/src/pages/home/ConnectWalletOptions.vue +++ b/src/pages/home/ConnectWalletOptions.vue @@ -1,9 +1,10 @@