From 644119f77882de1ff2e7f5d713649a8cce404e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viterbo=20Rodr=C3=ADguez?= Date: Wed, 22 May 2024 11:47:41 -0300 Subject: [PATCH 01/26] saving wip --- src/antelope/index.ts | 7 - src/antelope/mocks/AccountStore.ts | 182 ------- src/antelope/mocks/AntelopeConfig.ts | 337 ------------ src/antelope/mocks/ChainStore.ts | 95 ---- src/antelope/mocks/ContractStore.ts | 17 - src/antelope/mocks/EVMStore.ts | 171 ------ src/antelope/mocks/FeedbackStore.ts | 40 -- src/antelope/mocks/PlatformStore.ts | 23 - src/antelope/mocks/chain-constants.ts | 16 - src/antelope/mocks/index.ts | 9 - src/antelope/stores/utils/media-utils.ts | 74 --- src/antelope/stores/utils/nft-utils.ts | 160 ------ src/antelope/types/ABIv1.ts | 33 -- src/antelope/types/Actions.ts | 248 --------- src/antelope/types/AntelopeError.ts | 17 - src/antelope/types/Api.ts | 113 ---- src/antelope/types/Basic.ts | 3 - src/antelope/types/ChainInfo.ts | 19 - src/antelope/types/ChainSettings.ts | 26 - src/antelope/types/EvmBlockData.ts | 24 - src/antelope/types/EvmContractData.ts | 109 ---- src/antelope/types/EvmLog.ts | 26 - src/antelope/types/EvmRexDeposit.ts | 10 - src/antelope/types/EvmTransaction.ts | 161 ------ src/antelope/types/ExceptionError.ts | 5 - src/antelope/types/Filters.ts | 43 -- src/antelope/types/IndexerTypes.ts | 259 --------- src/antelope/types/KeyAccounts.ts | 1 - src/antelope/types/NFTClass.ts | 394 -------------- src/antelope/types/OpenSeaTypes.ts | 14 - src/antelope/types/PriceData.ts | 31 -- src/antelope/types/Producers.ts | 27 - src/antelope/types/Proposals.ts | 82 --- src/antelope/types/Providers.ts | 24 - src/antelope/types/Theme.ts | 17 - src/antelope/types/TokenClass.ts | 337 ------------ src/antelope/types/TransactionV1.ts | 21 - src/antelope/types/index.ts | 33 -- src/antelope/types/ual-oreid.d.ts | 1 - .../wallets/authenticators/BraveAuth.ts | 30 - .../authenticators/EVMAuthenticator.ts | 137 ----- .../authenticators/InjectedProviderAuth.ts | 331 ----------- .../wallets/authenticators/MetamaskAuth.ts | 32 -- .../wallets/authenticators/OreIdAuth.ts | 426 --------------- .../wallets/authenticators/SafePalAuth.ts | 30 - .../authenticators/WalletConnectAuth.ts | 512 ------------------ src/antelope/wallets/index.ts | 9 - src/antelope/wallets/init.ts | 90 --- src/antelope/wallets/utils/abi/erc1155.ts | 133 ----- src/antelope/wallets/utils/abi/erc20.ts | 226 -------- src/antelope/wallets/utils/abi/erc721.ts | 356 ------------ .../wallets/utils/abi/erc721Metadata.ts | 9 - src/antelope/wallets/utils/abi/escrowAbi.ts | 11 - src/antelope/wallets/utils/abi/index.ts | 43 -- .../utils/abi/signature/events_signatures.ts | 26 - .../abi/signature/functions_signatures.ts | 28 - .../wallets/utils/abi/signature/index.ts | 3 - .../abi/signature/transfer_signatures.ts | 2 - src/antelope/wallets/utils/abi/stlosAbi.ts | 49 -- .../wallets/utils/abi/supportsInterface.ts | 11 - src/antelope/wallets/utils/abi/wrapAbi.ts | 32 -- .../wallets/utils/contracts/EvmContract.ts | 236 -------- .../utils/contracts/EvmContractFactory.ts | 63 --- src/antelope/wallets/utils/currency-utils.ts | 387 ------------- src/antelope/wallets/utils/date-utils.ts | 66 --- src/antelope/wallets/utils/index.ts | 307 ----------- src/antelope/wallets/utils/text-utils.ts | 48 -- src/antelope/wallets/utils/trx-utils.ts | 36 -- 68 files changed, 6878 deletions(-) delete mode 100644 src/antelope/index.ts delete mode 100644 src/antelope/mocks/AccountStore.ts delete mode 100644 src/antelope/mocks/AntelopeConfig.ts delete mode 100644 src/antelope/mocks/ChainStore.ts delete mode 100644 src/antelope/mocks/ContractStore.ts delete mode 100644 src/antelope/mocks/EVMStore.ts delete mode 100644 src/antelope/mocks/FeedbackStore.ts delete mode 100644 src/antelope/mocks/PlatformStore.ts delete mode 100644 src/antelope/mocks/chain-constants.ts delete mode 100644 src/antelope/mocks/index.ts delete mode 100644 src/antelope/stores/utils/media-utils.ts delete mode 100644 src/antelope/stores/utils/nft-utils.ts delete mode 100644 src/antelope/types/ABIv1.ts delete mode 100644 src/antelope/types/Actions.ts delete mode 100644 src/antelope/types/AntelopeError.ts delete mode 100644 src/antelope/types/Api.ts delete mode 100644 src/antelope/types/Basic.ts delete mode 100644 src/antelope/types/ChainInfo.ts delete mode 100644 src/antelope/types/ChainSettings.ts delete mode 100644 src/antelope/types/EvmBlockData.ts delete mode 100644 src/antelope/types/EvmContractData.ts delete mode 100644 src/antelope/types/EvmLog.ts delete mode 100644 src/antelope/types/EvmRexDeposit.ts delete mode 100644 src/antelope/types/EvmTransaction.ts delete mode 100644 src/antelope/types/ExceptionError.ts delete mode 100644 src/antelope/types/Filters.ts delete mode 100644 src/antelope/types/IndexerTypes.ts delete mode 100644 src/antelope/types/KeyAccounts.ts delete mode 100644 src/antelope/types/NFTClass.ts delete mode 100644 src/antelope/types/OpenSeaTypes.ts delete mode 100644 src/antelope/types/PriceData.ts delete mode 100644 src/antelope/types/Producers.ts delete mode 100644 src/antelope/types/Proposals.ts delete mode 100644 src/antelope/types/Providers.ts delete mode 100644 src/antelope/types/Theme.ts delete mode 100644 src/antelope/types/TokenClass.ts delete mode 100644 src/antelope/types/TransactionV1.ts delete mode 100644 src/antelope/types/index.ts delete mode 100644 src/antelope/types/ual-oreid.d.ts delete mode 100644 src/antelope/wallets/authenticators/BraveAuth.ts delete mode 100644 src/antelope/wallets/authenticators/EVMAuthenticator.ts delete mode 100644 src/antelope/wallets/authenticators/InjectedProviderAuth.ts delete mode 100644 src/antelope/wallets/authenticators/MetamaskAuth.ts delete mode 100644 src/antelope/wallets/authenticators/OreIdAuth.ts delete mode 100644 src/antelope/wallets/authenticators/SafePalAuth.ts delete mode 100644 src/antelope/wallets/authenticators/WalletConnectAuth.ts delete mode 100644 src/antelope/wallets/index.ts delete mode 100644 src/antelope/wallets/init.ts delete mode 100644 src/antelope/wallets/utils/abi/erc1155.ts delete mode 100644 src/antelope/wallets/utils/abi/erc20.ts delete mode 100644 src/antelope/wallets/utils/abi/erc721.ts delete mode 100644 src/antelope/wallets/utils/abi/erc721Metadata.ts delete mode 100644 src/antelope/wallets/utils/abi/escrowAbi.ts delete mode 100644 src/antelope/wallets/utils/abi/index.ts delete mode 100644 src/antelope/wallets/utils/abi/signature/events_signatures.ts delete mode 100644 src/antelope/wallets/utils/abi/signature/functions_signatures.ts delete mode 100644 src/antelope/wallets/utils/abi/signature/index.ts delete mode 100644 src/antelope/wallets/utils/abi/signature/transfer_signatures.ts delete mode 100644 src/antelope/wallets/utils/abi/stlosAbi.ts delete mode 100644 src/antelope/wallets/utils/abi/supportsInterface.ts delete mode 100644 src/antelope/wallets/utils/abi/wrapAbi.ts delete mode 100644 src/antelope/wallets/utils/contracts/EvmContract.ts delete mode 100644 src/antelope/wallets/utils/contracts/EvmContractFactory.ts delete mode 100644 src/antelope/wallets/utils/currency-utils.ts delete mode 100644 src/antelope/wallets/utils/date-utils.ts delete mode 100644 src/antelope/wallets/utils/index.ts delete mode 100644 src/antelope/wallets/utils/text-utils.ts delete mode 100644 src/antelope/wallets/utils/trx-utils.ts diff --git a/src/antelope/index.ts b/src/antelope/index.ts deleted file mode 100644 index a1cfac13e..000000000 --- a/src/antelope/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from 'src/antelope/mocks/AccountStore'; -export * from 'src/antelope/mocks/AntelopeConfig'; -export * from 'src/antelope/mocks/ChainStore'; -export * from 'src/antelope/mocks/ContractStore'; -export * from 'src/antelope/mocks/EVMStore'; -export * from 'src/antelope/mocks/FeedbackStore'; -export * from 'src/antelope/mocks/PlatformStore'; diff --git a/src/antelope/mocks/AccountStore.ts b/src/antelope/mocks/AccountStore.ts deleted file mode 100644 index 62690433e..000000000 --- a/src/antelope/mocks/AccountStore.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* eslint-disable max-len */ -/* eslint-disable no-unused-vars */ -// Mocking AccountStore ----------------------------------- -// useAccountStore().getAccount(this.label).account as addressString; -import { EVMAuthenticator } from 'src/antelope/wallets'; -import { AntelopeError, addressString } from 'src/antelope/types'; -import { CURRENT_CONTEXT, EVMChainSettings, createTraceFunction, useChainStore } from 'src/antelope/mocks'; -import { BigNumber } from 'ethers'; 'src/antelope/mocks/FeedbackStore'; -import { getAntelope } from 'src/antelope/mocks/AntelopeConfig'; -import { useFeedbackStore } from 'src/antelope/mocks'; -import { EvmABI, EvmFunctionParam, Label, TransactionResponse } from 'src/antelope/types'; -import { subscribeForTransactionReceipt } from 'src/antelope/wallets/utils/trx-utils'; - -export interface AccountModel { - label: typeof CURRENT_CONTEXT; - isNative: boolean; - authenticator: EVMAuthenticator; - account: addressString; -} - -export interface EvmAccountModel extends AccountModel { - address: addressString; - displayAddress: string; - isNative: false; - associatedNative: string; - authenticator: EVMAuthenticator; -} - -let currentAuthenticator = {} as EVMAuthenticator; -let currentAccount = null as addressString | null; - -interface LoginEVMActionData { - authenticator: EVMAuthenticator - network: string, - autoLogAccount?: string, -} - -class AccountStore { - - trace: (action: string, ...args: unknown[]) => void; - - constructor() { - this.trace = createTraceFunction('EVMStore'); - } - - getAccount(label: string) { - return { - label, - isNative: false, - authenticator: currentAuthenticator, - account: currentAccount, - } as AccountModel; - } - - async loginEVM({ authenticator, network, autoLogAccount }: LoginEVMActionData, trackAnalyticsEvents: boolean): Promise { - currentAuthenticator = authenticator; - currentAccount = autoLogAccount ? await authenticator.autoLogin(network, autoLogAccount, trackAnalyticsEvents) : await authenticator.login(network, trackAnalyticsEvents); - - const account = useAccountStore().getAccount(authenticator.label); - getAntelope().events.onLoggedIn.next(account); - return true; - } - - logout() { - currentAuthenticator.logout(); - currentAuthenticator = {} as EVMAuthenticator; - currentAccount = null; - getAntelope().events.onLoggedOut.next(); - } - - get loggedAccount() { - return this.getAccount(CURRENT_CONTEXT); - } - - get currentAccount() { - return this.getAccount(CURRENT_CONTEXT); - } - - async subscribeForTransactionReceipt(account: EvmAccountModel, response: TransactionResponse): Promise { - this.trace('subscribeForTransactionReceipt', account.account, response.hash); - return subscribeForTransactionReceipt(account, response).then(({ newResponse, receipt }) => { - newResponse.wait().then(() => { - this.trace('subscribeForTransactionReceipt', newResponse.hash, 'receipt:', receipt.status, receipt); - }); - return newResponse; - }); - } - - async signCustomTransaction( - label: Label, - actionMessage: string, - actionError: string, - contract: string, - abi: EvmABI, - parameters: EvmFunctionParam[], - value?: BigNumber, - ): Promise{ - const ant = getAntelope(); - const funcname = 'signCustomTransaction'; - this.trace(funcname, label, contract, abi, parameters, value?.toString()); - - if (! await this.assertNetworkConnection(label)) { - throw new AntelopeError('antelope.evm.error_switch_chain_rejected'); - } - - try { - useFeedbackStore().setLoading(funcname); - const account = this.loggedAccount as EvmAccountModel; - const authenticator = this.loggedAccount.authenticator as EVMAuthenticator; - const chainSettings = useChainStore().loggedChain.settings as unknown as EVMChainSettings; - - const tx = await authenticator.signCustomTransaction(contract, abi, parameters, value) - .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); - - // we create tne neutral notification - const dismiss = ant.config.notifyNeutralMessageHandler(actionMessage); - - tx.wait().then(() => { - ant.config.notifySuccessfulTrxHandler( - `${chainSettings.getExplorerUrl()}/tx/${tx.hash}`, - ); - }).catch((err) => { - console.error(err); - }).finally(() => { - dismiss(); - }); - - return tx; - } catch (error) { - const trxError = ant.config.transactionError(actionError, error); - ant.config.transactionErrorHandler(trxError, funcname); - throw trxError; - } finally { - useFeedbackStore().unsetLoading(funcname); - } - } - - async isConnectedToCorrectNetwork(label: string): Promise { - this.trace('isConnectedToCorrectNetwork', label); - try { - useFeedbackStore().setLoading('account.isConnectedToCorrectNetwork'); - const authenticator = useAccountStore().getAccount(label)?.authenticator as EVMAuthenticator; - return authenticator.isConnectedToCorrectChain(); - } catch (error) { - console.error('Error: ', error); - return Promise.resolve(false); - } finally { - useFeedbackStore().unsetLoading('account.isConnectedToCorrectNetwork'); - } - } - - async assertNetworkConnection(label: string): Promise { - if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve) => { - const ant = getAntelope(); - const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; - try { - await authenticator.ensureCorrectChain(); - if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { - resolve(false); - } else { - resolve(true); - } - } catch (error) { - const message = (error as Error).message; - if (message === 'antelope.evm.error_switch_chain_rejected') { - ant.config.notifyNeutralMessageHandler(message); - } - resolve(false); - } - }); - } else { - return true; - } - } - -} - -const accountStore = new AccountStore(); - -export const useAccountStore = () => accountStore; diff --git a/src/antelope/mocks/AntelopeConfig.ts b/src/antelope/mocks/AntelopeConfig.ts deleted file mode 100644 index da5ea3190..000000000 --- a/src/antelope/mocks/AntelopeConfig.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// Mocking Antelope and Config ----------------------------------- -import { EVMAuthenticator } from 'src/antelope/wallets/authenticators/EVMAuthenticator'; -import { AntelopeError, AntelopeErrorPayload } from 'src/antelope/types'; -import { App } from 'vue'; -import { Authenticator } from 'universal-authenticator-library'; -import { Subject } from 'rxjs'; -import { AccountModel } from 'src/antelope/mocks/AccountStore'; - -export interface ComplexMessage { - tag: string, - class: string, - text: string, -} - -export class AntelopeWallets { - private authenticators: Map = new Map(); - init() { - // dummie function - } - addEVMAuthenticator(authenticator: EVMAuthenticator) { - this.authenticators.set(authenticator.getName(), authenticator); - } - getAuthenticator(name: string) { - return this.authenticators.get(name); - } -} - -export class AntelopeConfig { - transactionError(description: string, error: unknown): AntelopeError { - if (error instanceof AntelopeError) { - return error as AntelopeError; - } - const msgOrObject = this.errorMessageExtractor(error); - // if it matches antelope.*.error_* - if (typeof msgOrObject === 'string') { - return new AntelopeError(msgOrObject, { error }); - } else { - return new AntelopeError(description, { error: msgOrObject }); - } - } - - // indexer health threshold -- - private __indexer_health_threshold = 10; // 10 seconds - - // indexer health check interval -- - private __indexer_health_check_interval = 1000 * 60 * 5; // 5 minutes expressed in milliseconds - - // notifucation handlers -- - private __notify_error_handler: (message: string) => void = m => alert(`Error: ${m}`); - private __notify_success_handler: (message: string) => void = alert; - private __notify_warning_handler: (message: string) => void = alert; - - // notification handlers -- - private __notify_successful_trx_handler: (link: string) => void = alert; - private __notify_success_message_handler: (message: string, payload?: never) => void = alert; - private __notify_success_copy_handler: () => void = alert; - private __notify_failure_message_handler: (message: string, payload?: AntelopeErrorPayload) => void = alert; - private __notify_failure_action_handler: (message: string, payload?: AntelopeErrorPayload) => void = alert; - private __notify_disconnected_handler: () => void = alert; - private __notify_neutral_message_handler: (message: string) => (() => void) = () => (() => void 0); - private __notify_remember_info_handler: (title: string, message: string | ComplexMessage[], - payload: string, key: string) => (() => void) = () => (() => void 0); - - // ual authenticators list getter -- - private __authenticators_getter: () => Authenticator[] = () => []; - - // localization handler -- - private __localization_handler: (key: string, payload?: Record) => string = (key: string) => key; - - // transaction error handler -- - private __transaction_error_handler: (err: AntelopeError, trxFailed: string) => void = () => void 0; - - // error to string handler -- - private __error_message_extractor: (error: unknown) => object | string | null = (error: unknown) => { - try { - type EVMError = {code:string}; - const evmErr = error as EVMError; - - // high priority generic errors - switch (evmErr.code) { - case 'CALL_EXCEPTION': return 'antelope.evm.error_call_exception'; - case 'INSUFFICIENT_FUNDS': return 'antelope.evm.error_insufficient_funds'; - case 'MISSING_NEW': return 'antelope.evm.error_missing_new'; - case 'NONCE_EXPIRED': return 'antelope.evm.error_nonce_expired'; - case 'NUMERIC_FAULT': return 'antelope.evm.error_numeric_fault'; - case 'REPLACEMENT_UNDERPRICED': return 'antelope.evm.error_replacement_underpriced'; - case 'TRANSACTION_REPLACED': return 'antelope.evm.error_transaction_replaced'; - case 'USER_REJECTED': return 'antelope.evm.error_user_rejected'; - case 'ACTION_REJECTED': return 'antelope.evm.error_transaction_canceled'; - } - - if (typeof error === 'object') { - const candidates = ['message', 'reason']; - - const extractDeepestErrorMessage = (error: unknown): string => { - if (typeof error !== 'object' || error === null) { - return 'unknown'; // We return 'unknown' if it is not an object or is null - } - const queue: {node: unknown, depth: number}[] = [{ node: error, depth: 0 }]; - let deepestMessage = 'unknown'; - let maxDepth = -1; - - while (queue.length > 0) { - const { node, depth } = queue.shift()!; // Sacamos el primer elemento de la cola - - const nodeKeys = Object.keys(node as Record); - for (const key of nodeKeys) { - const value = (node as Record)[key]; - if (candidates.includes(key) && typeof value === 'string') { - // If we find a message in a deeper level, we update it - if (depth > maxDepth) { - deepestMessage = value; - maxDepth = depth; - } - } else if (typeof value === 'object' && value !== null) { - // If the value is an object, we add it to the queue to explore its children - queue.push({ node: value, depth: depth + 1 }); - } - } - } - - return deepestMessage; - }; - const messageFound = extractDeepestErrorMessage(error as Record); - if (messageFound !== 'unknown') { - return messageFound; - } - } - - // low priority generic errors - switch (evmErr.code) { - case 'UNPREDICTABLE_GAS_LIMIT': return 'antelope.evm.error_unpredictable_gas_limit'; - } - - if (typeof error === 'string') { - return { text: error }; - } - if (typeof error === 'number') { - return { number: error.toString() }; - } - if (typeof error === 'boolean') { - return { boolean: error.toString() }; - } - if (typeof error === 'undefined') { - return { value: 'undefined' }; - } - if (typeof error === 'object') { - return error; - } - return { }; - } catch (er) { - return { }; - } - }; - - // Vue.App holder -- - private __app: App | null = null; - - constructor() { - // - } - - init(app: App) { - this.__app = app; - } - - get app() { - return this.__app; - } - - get indexerHealthThresholdSeconds() { - return this.__indexer_health_threshold; - } - - get indexerHealthCheckInterval() { - return this.__indexer_health_check_interval; - } - - get notifyErrorHandler() { - return this.__notify_error_handler; - } - - get notifySuccessHandler() { - return this.__notify_success_handler; - } - - get notifyWarningHandler() { - return this.__notify_warning_handler; - } - - get notifySuccessfulTrxHandler() { - return this.__notify_successful_trx_handler; - } - - get notifySuccessMessageHandler() { - return this.__notify_success_message_handler; - } - - get notifySuccessCopyHandler() { - return this.__notify_success_copy_handler; - } - - get notifyFailureMessage() { - return this.__notify_failure_message_handler; - } - - get notifyFailureWithAction() { - return this.__notify_failure_action_handler; - } - - get notifyDisconnectedHandler() { - return this.__notify_disconnected_handler; - } - - get notifyNeutralMessageHandler() { - return this.__notify_neutral_message_handler; - } - - get notifyRememberInfoHandler() { - return this.__notify_remember_info_handler; - } - - get authenticatorsGetter() { - return this.__authenticators_getter; - } - - get localizationHandler() { - return this.__localization_handler; - } - - get transactionErrorHandler() { - return this.__transaction_error_handler; - } - - get errorMessageExtractor() { - return this.__error_message_extractor; - } - - // setting indexer constants -- - public setIndexerHealthThresholdSeconds(threshold: number) { - this.__indexer_health_threshold = threshold; - } - - public setIndexerHealthCheckInterval(interval: number) { - this.__indexer_health_check_interval = interval; - } - - // setting notification handlers -- - public setNotifyErrorHandler(handler: (message: string) => void) { - this.__notify_error_handler = handler; - } - - public setNotifySuccessHandler(handler: (message: string) => void) { - this.__notify_success_handler = handler; - } - - public setNotifyWarningHandler(handler: (message: string) => void) { - this.__notify_warning_handler = handler; - } - - public setNotifySuccessfulTrxHandler(handler: (link: string) => void) { - this.__notify_successful_trx_handler = handler; - } - - public setNotifySuccessMessageHandler(handler: (message: string, payload?: never) => void) { - this.__notify_success_message_handler = handler; - } - - public setNotifySuccessCopyHandler(handler: () => void) { - this.__notify_success_copy_handler = handler; - } - - public setNotifyFailureMessage(handler: (message: string, payload?: AntelopeErrorPayload) => void) { - this.__notify_failure_message_handler = handler; - } - - public setNotifyFailureWithAction(handler: (message: string, payload?: AntelopeErrorPayload) => void) { - this.__notify_failure_action_handler = handler; - } - - public setNotifyDisconnectedHandler(handler: () => void) { - this.__notify_disconnected_handler = handler; - } - - public setNotifyNeutralMessageHandler(handler: (message: string) => (() => void)) { - this.__notify_neutral_message_handler = handler; - } - - public setNotifyRememberInfoHandler(handler: ( - title: string, - message: string | ComplexMessage[], - payload: string, - key: string, - ) => (() => void)) { - this.__notify_remember_info_handler = handler; - } - - // setting authenticators getter -- - public setAuthenticatorsGetter(getter: () => Authenticator[]) { - this.__authenticators_getter = getter; - } - - // setting translation handler -- - public setLocalizationHandler(handler: (key: string, payload?: Record) => string) { - this.__localization_handler = handler; - } - - // setting transaction error handler -- - public setTransactionErrorHandler(handler: (err: AntelopeError, trxFailed: string) => void) { - this.__transaction_error_handler = handler; - } - - // setting error to string handler -- - public setErrorMessageExtractor(handler: (catched: unknown) => object | string | null) { - this.__error_message_extractor = handler; - } - -} - -const config = new AntelopeConfig(); -const wallets = new AntelopeWallets(); -const events = { - onLoggedIn: new Subject(), - onLoggedOut: new Subject(), -}; -const Antelope = { - config, - wallets, - events, -}; - -export const getAntelope = () => Antelope; -// ---------------------------------------------------------------- - diff --git a/src/antelope/mocks/ChainStore.ts b/src/antelope/mocks/ChainStore.ts deleted file mode 100644 index 2afe7b82d..000000000 --- a/src/antelope/mocks/ChainStore.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// Mocking ChainStore ----------------------------------- -declare const fathom: { trackEvent: (eventName: string) => void }; - -import { RpcEndpoint } from 'universal-authenticator-library'; -import { NativeCurrencyAddress, TokenClass } from 'src/antelope/types'; - -export interface EVMChainSettings { - getStakedSystemToken(): TokenClass; - getWrappedSystemToken: () => TokenClass; - getChainId: () => string; - getDisplay: () => string; - trackAnalyticsEvent: (name: string) => void; - getRPCEndpoint: () => RpcEndpoint; - getEscrowContractAddress: () => string; - getNetwork: () => string; - getSystemToken: () => TokenClass; - getExplorerUrl: () => string; - getSmallLogoPath: () => string; - getLargeLogoPath: () => string; -} - -const settings = { - getChainId: () => process.env.NETWORK_EVM_CHAIN_ID, - getDisplay: () => process.env.NETWORK_EVM_DISPLAY, - trackAnalyticsEvent(eventName: string): void { - if (typeof fathom === 'undefined') { - console.warn(`Failed to track event with name ${eventName}: Fathom Analytics not loaded`); - return; - } - - fathom.trackEvent(eventName); - }, - getRPCEndpoint: () => { - // extract the url parts - const regex = /^(https?):\/\/([^:/]+)(?::(\d+))?(\/.*)?$/; - const match = (process.env.NETWORK_EVM_RPC as string).match(regex); - if (!match) { - throw new Error('Invalid RPC endpoint'); - } - // We destructure the result of the match to get each component - const [, protocol, host, port, path] = match; - return { - protocol, - host, - port: port ? parseInt(port, 10) : 443, - path: path || '/', - }; - }, - getEscrowContractAddress: () => process.env.TELOS_ESCROW_CONTRACT_ADDRESS, - getStakedSystemToken: () => ({ - address: process.env.STAKED_TLOS_CONTRACT_ADDRESS, - decimals: 18, - symbol: 'STLOS', - } as TokenClass), - getWrappedSystemToken: () => ({ - address: process.env.WRAPPED_TLOS_CONTRACT_ADDRESS, - decimals: 18, - symbol: 'WTLOS', - } as TokenClass), - getSystemToken: () => ({ - name: 'Telos', - address: NativeCurrencyAddress, - decimals: 18, - symbol: 'TLOS', - } as TokenClass), - getNetwork: () => process.env.NETWORK_EVM_NAME, - getExplorerUrl: () => window.location.origin, - getSmallLogoPath: () => 'small-icon-url', - getLargeLogoPath: () => 'large-icon-url', -} as EVMChainSettings; - -const currentChain = { - settings, -}; - -const loggedChain = { - settings, -}; - -const loggedEvmChain = { - settings, -}; - -const ChainStore = { - currentChain, - loggedChain, - loggedEvmChain, - getNetworkSettings: (network: string) => settings, - getChain: (label: string) => currentChain, - setChain: (context: string, network: string) => void 0, -}; - -export const useChainStore = () => ChainStore; diff --git a/src/antelope/mocks/ContractStore.ts b/src/antelope/mocks/ContractStore.ts deleted file mode 100644 index 863b0f8a5..000000000 --- a/src/antelope/mocks/ContractStore.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable max-len */ - -import { EvmABI, erc1155Abi, erc20Abi, erc721Abi } from 'src/antelope/types'; - -// Mocking ContractStore ----------------------------------- -const ContractStore = { - getTokenABI(type:string): EvmABI { - if(type === 'erc721'){ - return erc721Abi; - } else if(type === 'erc1155'){ - return erc1155Abi; - } - return erc20Abi; - }, -}; - -export const useContractStore = () => ContractStore; diff --git a/src/antelope/mocks/EVMStore.ts b/src/antelope/mocks/EVMStore.ts deleted file mode 100644 index 582582cf8..000000000 --- a/src/antelope/mocks/EVMStore.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -// Mocking EVMStore ----------------------------------- -import { ethers } from 'ethers'; -import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; -import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; -import { EVMChainSettings, useChainStore, useFeedbackStore, useAccountStore } from 'src/antelope/mocks'; -import { AntelopeError, EthereumProvider, ExceptionError } from 'src/antelope/types'; -import { RpcEndpoint } from 'universal-authenticator-library'; - -class EVMStore { - trace: (action: string, ...args: unknown[]) => void; - - constructor() { - this.trace = createTraceFunction('EVMStore'); - } - - // actions --- - async initInjectedProvider(authenticator: InjectedProviderAuth): Promise { - this.trace('initInjectedProvider', authenticator.getName(), [authenticator.getProvider()]); - const provider: EthereumProvider | null = authenticator.getProvider(); - const evm = useEVMStore(); - - if (provider && !provider.__initialized) { - this.trace('initInjectedProvider', authenticator.getName(), 'initializing provider'); - // ensure this provider actually has the correct methods - // Check consistency of the provider - const methods = ['request', 'on']; - const candidate = provider as unknown as Record; - for (const method of methods) { - if (typeof candidate[method] !== 'function') { - console.warn(`MetamaskAuth.getProvider: method ${method} not found`); - throw new AntelopeError('antelope.evm.error_invalid_provider'); - } - } - - provider.on('accountsChanged', async (value) => { - const accounts = value as string[]; - const network = useChainStore().currentChain.settings.getNetwork(); - evm.trace('provider.accountsChanged', ...accounts); - - if (accounts.length > 0) { - // If we are here one of two possible things had happened: - // 1. The user has just logged in to the wallet - // 2. The user has switched the account in the wallet - - // if we are in case 1, then we are in the middle of the login process and we don't need to do anything - // We can tell because the account store has no logged account - - // But if we are in case 2 and have a logged account, we need to re-login the account using the same authenticator - // overwriting the previous logged account, which in turn will trigger all account data to be reloaded - if (useAccountStore().loggedAccount) { - // if the user is already authenticated we try to re login the account using the same authenticator - const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; - if (!authenticator) { - console.error('Inconsistency: logged account authenticator is null', authenticator); - } else { - useAccountStore().loginEVM({ authenticator, network }, false); - } - } - } else { - // the user has disconnected the all the accounts from the wallet so we logout - useAccountStore().logout(); - } - }); - - // This initialized property is not part of the standard provider, it's just a flag to know if we already initialized the provider - provider.__initialized = true; - evm.addInjectedProvider(authenticator); - } - authenticator.onReady.next(true); - } - - addInjectedProvider(authenticator: InjectedProviderAuth) { - this.trace('addInjectedProvider', authenticator.getName()); - } - - async switchChainInjected(InjectedProvider: ethers.providers.ExternalProvider): Promise { - this.trace('switchChainInjected', [InjectedProvider]); - useFeedbackStore().setLoading('evm.switchChainInjected'); - const provider = InjectedProvider; - if (provider) { - const chainSettings = useChainStore().loggedChain.settings as unknown as EVMChainSettings; - const chainId = parseInt(chainSettings.getChainId(), 10); - const chainIdParam = `0x${chainId.toString(16)}`; - if (!provider.request) { - useFeedbackStore().unsetLoading('evm.switchChainInjected'); - throw new AntelopeError('antelope.evm.error_support_provider_request'); - } - try { - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: chainIdParam }], - }); - return true; - } catch (error) { - const chainNotAddedCodes = [ - 4902, - -32603, // https://github.com/MetaMask/metamask-mobile/issues/2944 - ]; - - if (chainNotAddedCodes.includes((error as unknown as ExceptionError).code)) { // 'Chain hasn't been added' - const p:RpcEndpoint = chainSettings.getRPCEndpoint(); - const rpcUrl = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; - try { - if (!provider.request) { - throw new AntelopeError('antelope.evm.error_support_provider_request'); - } - const payload = { - method: 'wallet_addEthereumChain', - params: [{ - chainId: chainIdParam, - chainName: chainSettings.getDisplay(), - nativeCurrency: { - name: chainSettings.getSystemToken().name, - symbol: chainSettings.getSystemToken().symbol, - decimals: chainSettings.getSystemToken().decimals, - }, - rpcUrls: [rpcUrl], - blockExplorerUrls: [chainSettings.getExplorerUrl()], - iconUrls: [chainSettings.getSmallLogoPath(), chainSettings.getLargeLogoPath()], - }], - }; - await provider.request(payload); - return true; - } catch (e) { - if ((e as unknown as ExceptionError).code === 4001) { - throw new AntelopeError('antelope.evm.error_add_chain_rejected'); - } else { - console.error('Error:', e); - throw new AntelopeError('antelope.evm.error_add_chain'); - } - } - } else if ((error as unknown as ExceptionError).code === 4001) { - throw new AntelopeError('antelope.evm.error_switch_chain_rejected'); - } else { - console.error('Error:', error); - throw new AntelopeError('antelope.evm.error_switch_chain'); - } - } finally { - useFeedbackStore().unsetLoading('evm.switchChainInjected'); - } - } else { - useFeedbackStore().unsetLoading('evm.switchChainInjected'); - throw new AntelopeError('antelope.evm.error_no_provider'); - } - } - - async isProviderOnTheCorrectChain(provider: ethers.providers.Web3Provider, correctChainId: string): Promise { - const { chainId } = await provider.getNetwork(); - const response = +chainId === +correctChainId; - this.trace('isProviderOnTheCorrectChain', provider, ' -> ', response); - return response; - } - - async ensureCorrectChain(authenticator: EVMAuthenticator): Promise { - this.trace('ensureCorrectChain', authenticator); - const checkProvider = await authenticator.web3Provider(); - let response = checkProvider; - const correctChainId = useChainStore().currentChain.settings.getChainId(); - if (!await this.isProviderOnTheCorrectChain(checkProvider, correctChainId)) { - const provider = await authenticator.externalProvider(); - await this.switchChainInjected(provider); - response = new ethers.providers.Web3Provider(provider); - } - return response; - } - -} - -export const useEVMStore = () => new EVMStore(); diff --git a/src/antelope/mocks/FeedbackStore.ts b/src/antelope/mocks/FeedbackStore.ts deleted file mode 100644 index 08116bd57..000000000 --- a/src/antelope/mocks/FeedbackStore.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -// Mocking FeedbackStore ----------------------------------- -// auxiliary tracing functions -export const createTraceFunction = (store_name: string) => function(action: string, ...args: unknown[]) { - if (trace) { - const titlecase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); - const eventName = `${titlecase(store_name)}.${action}()`; - console.debug(eventName, [...args]); - } -}; - - -// only if we are NOT in production mode search in the url for the trace flag -// to turn on the Antelope trace mode -let trace = false; -if (process.env.NODE_ENV !== 'production') { - const urlParams = new URLSearchParams(window.location.search); - trace = urlParams.get('trace') === 'true'; -} -export const isTracingAll = () => trace; -export const createInitFunction = () => function() { - // dummie function -}; - -const FeedBackStoreMock = { - loading: [] as string[], - unsetLoading(key: string) { - this.loading = this.loading.filter((k: string) => k !== key); - }, - setLoading(key: string) { - this.loading.push(key); - }, - setDebug(name: string, value: boolean) { - // dummie function - }, -}; - -export const useFeedbackStore = () => FeedBackStoreMock; diff --git a/src/antelope/mocks/PlatformStore.ts b/src/antelope/mocks/PlatformStore.ts deleted file mode 100644 index f983bfe1f..000000000 --- a/src/antelope/mocks/PlatformStore.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable max-len */ -// Mocking PlatformStore ----------------------------------- -const PlatformStore = { - isBrowser: false, - isBraveBrowser: false, - isIOSMobile: false, - isMobile: false, -}; - -// detect brave browser -const type_navegator = navigator as unknown as { brave?:{isBrave:()=>Promise} }; -if (type_navegator.brave) { - type_navegator.brave.isBrave().then((isBrave) => { - PlatformStore.isBraveBrowser = isBrave; - }); -} - -// detect mobile -const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i; -PlatformStore.isMobile = (mobileRegex.test(navigator.userAgent)); -PlatformStore.isIOSMobile = ((/iPhone|iPad|iPod/i).test(navigator.userAgent)); - -export const usePlatformStore = () => PlatformStore; diff --git a/src/antelope/mocks/chain-constants.ts b/src/antelope/mocks/chain-constants.ts deleted file mode 100644 index 0fee6a1da..000000000 --- a/src/antelope/mocks/chain-constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const TELOS_CHAIN_IDS = ['40', '41']; -export const TELOS_NETWORK_NAMES = ['telos-evm', 'telos-evm-testnet']; -export const TELOS_ANALYTICS_EVENT_NAMES = { - loginStarted: 'Login Started', - loginSuccessful: 'Login Successful', - loginSuccessfulMetamask: 'Login Successful - Metamask', - 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', - loginFailedBrave: 'Login Failed - Brave', -}; diff --git a/src/antelope/mocks/index.ts b/src/antelope/mocks/index.ts deleted file mode 100644 index a9ab5b7e7..000000000 --- a/src/antelope/mocks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from 'src/antelope/mocks/FeedbackStore'; -export * from 'src/antelope/mocks/AccountStore'; -export * from 'src/antelope/mocks/AntelopeConfig'; -export * from 'src/antelope/mocks/ChainStore'; -export * from 'src/antelope/mocks/ContractStore'; -export * from 'src/antelope/mocks/EVMStore'; -export * from 'src/antelope/mocks/PlatformStore'; - -export const CURRENT_CONTEXT = 'current'; diff --git a/src/antelope/stores/utils/media-utils.ts b/src/antelope/stores/utils/media-utils.ts deleted file mode 100644 index ebd089e24..000000000 --- a/src/antelope/stores/utils/media-utils.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NFTSourceTypes, NftSourceType } from 'src/antelope/types'; - -/** - * given a webm source, determine if it's audio or video - * @param source - * @returns NFTSourceTypes.AUDIO or NFTSourceTypes.VIDEO - */ -export async function determineWebmType(source: string): Promise { - return new Promise((resolve, reject) => { - // Create a video element - const video = document.createElement('video'); - - // Listen for the 'loadedmetadata' event - video.addEventListener('loadedmetadata', () => { - // If videoHeight or videoWidth is 0, then it's audio-only. - if (video.videoHeight === 0 || video.videoWidth === 0) { - resolve(NFTSourceTypes.AUDIO); - } else { - resolve(NFTSourceTypes.VIDEO); - } - }); - - // Handle error - video.addEventListener('error', () => { - reject(new Error('Failed to load video metadata.')); - }); - - // Set the URL as the video source - video.src = source; - }); -} - -/** - * given a url, determine if it's an image - * @param url - * @returns true if it's an image - */ -export function urlIsPicture(url: string): boolean { - return Boolean(url.match(/\.(gif|avif|apng|jpe?g|jfif|p?jpe?g|png|svg|webp)$/)); -} - -/** - * given a url, determine if it's audio - * @param url - * @returns true if it's audio - */ -export async function urlIsAudio(url: string) { - const isNotWebm = !url.match(/\.(webm)$/); - - if (isNotWebm) { - return Boolean(url.match(/\.(mp3|wav|aac)$/)); - } - - const type = await determineWebmType(url); - - return type === NFTSourceTypes.AUDIO; -} - -/** - * given a url, determine if it's a video - * @param url - * @returns true if it's video - */ -export async function urlIsVideo(url: string) { - const isNotWebm = !url.match(/\.(webm)$/); - - if (isNotWebm) { - return Boolean(url.match(/\.(mp4|ogg)$/)); - } - - const type = await determineWebmType(url); - - return type === NFTSourceTypes.VIDEO; -} diff --git a/src/antelope/stores/utils/nft-utils.ts b/src/antelope/stores/utils/nft-utils.ts deleted file mode 100644 index 685ea5889..000000000 --- a/src/antelope/stores/utils/nft-utils.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { IndexerNftMetadata, NFTSourceTypes, NftSourceType } from 'src/antelope/types'; -import { urlIsAudio, urlIsPicture, urlIsVideo } from 'src/antelope/stores/utils/media-utils'; - -export const IPFS_GATEWAY = 'https://ipfs.telos.net/ipfs/'; - -/** - * Given an imageCache URL, tokenUri, and metadata, extract the image URL, mediaType, and mediaSource - * - * @param imageCache - the imageCache URL - * @param tokenUri - the tokenUri - * @param metadata - the NFT metadata object - * - * @returns {Promise} - */ -export async function extractNftMetadata( - imageCache: string, - tokenUri: string, - metadata: IndexerNftMetadata, -): Promise<{ image: string | undefined; mediaType: NftSourceType; mediaSource: string | undefined }> { - let mediaType: NftSourceType = NFTSourceTypes.IMAGE; - let image = ''; - let mediaSource: string | undefined = undefined; - - let _metadata = metadata; - - if (typeof _metadata === 'string') { - try { - _metadata = JSON.parse(_metadata); - } catch (error) { - _metadata = { tokenUri }; - } - } - - // We are going to test the imageCache URL to see if it is a valid URL - if (imageCache) { - // first we create a regExp for the valid URL. e.g: "https://nfts.telos.net/40/0x552fd5743432eC2dAe222531e8b88bf7d2410FBc/344" - const regExp = new RegExp('^(https?:\\/\\/)?' + // protocol - '(nfts.telos.net\\/)' + // domain name - '(\\d+\\/)' + // chain id - '(0x[0-9a-fA-F]+\\/)' + // contract address - '(\\d+)$'); // token id - - // then we test the imageCache URL against the regExp - const match = regExp.test(imageCache); - if (match) { - // we return the 1440.webp version of it - image = imageCache.concat('/1440.webp'); - } - } - // if there's an image in the metadata, we return that - if (!image && _metadata?.image && urlIsPicture(_metadata.image)) { - image = _metadata.image as string; - } - - if (!image && _metadata) { - // this NFT is not a simple image and could be anything (including an image). - // We need to look at the metadata - // we iterate over the metadata properties - for (const property in _metadata) { - const _value = _metadata[property]; - - if (typeof _value !== 'string') { - continue; - } - - const value = _value.replace('ipfs://', IPFS_GATEWAY) as string; - - // if the value is a string and contains a valid url of a known media format, use it. - // image formats: .gif, .avif, .apng, .jpeg, .jpg, .jfif, .pjpeg, .pjp, .png, .svg, .webp - if ( - !image && // if we already have a preview, we don't need to keep looking - urlIsPicture(value) - ) { - image = value; - } - // audio formats: .mp3, .wav, .aac, .webm - if ( - !mediaSource && // if we already have a source, we don't need to keep looking - await urlIsAudio(value) - ) { - mediaType = NFTSourceTypes.AUDIO; - mediaSource = value; - } - // video formats: .mp4, .webm, .ogg - if ( - !mediaSource && // if we already have a source, we don't need to keep looking - await urlIsVideo(value) - ) { - mediaType = NFTSourceTypes.VIDEO; - mediaSource = value; - } - - const regex = /^data:(image|audio|video)\/\w+;base64,[\w+/=]+$/; - - const match = value.match(regex); - - if (match) { - const contentType = match[1]; - - if (contentType === 'image' && !image) { - image = value; - } else if (contentType === 'audio' && !mediaSource) { - mediaType = NFTSourceTypes.AUDIO; - mediaSource = value; - } else if (contentType === 'video' && !mediaSource) { - mediaType = NFTSourceTypes.VIDEO; - mediaSource = value; - } - } - - } - } - - if (!image && tokenUri && (!_metadata || Object.keys(_metadata).length === 0)) { - // if there is no metadata, attempt to use the tokenUri - if (await urlIsVideo(tokenUri)) { - mediaType = NFTSourceTypes.VIDEO; - } else if (await urlIsAudio(tokenUri)) { - mediaType = NFTSourceTypes.AUDIO; - } - } - - const metadataImageIsMediaUrl = await urlIsVideo(_metadata?.image ?? '') || await urlIsAudio(_metadata?.image ?? '') || urlIsPicture(_metadata?.image ?? ''); - - if (_metadata?.image?.includes(IPFS_GATEWAY) && !metadataImageIsMediaUrl) { - mediaType = await determineIpfsMediaType(_metadata?.image); - - if (mediaType === NFTSourceTypes.IMAGE) { - image = _metadata?.image; - } - mediaSource = _metadata?.image; - } - - return { image, mediaType, mediaSource }; -} - - -/** - * Given an IPFS media URL, determine the media type - * @param url - the IPFS media URL - * @returns {Promise} - the media type - */ -export async function determineIpfsMediaType(url: string): Promise { - try { - const response = await fetch(url); - const contentType = response.headers.get('Content-Type') ?? ''; - - if (contentType.startsWith('image/')) { - return NFTSourceTypes.IMAGE; - } else if (contentType.startsWith('video/')) { - return NFTSourceTypes.VIDEO; - } else if (contentType.startsWith('audio/')) { - return NFTSourceTypes.AUDIO; - } else { - return NFTSourceTypes.UNKNOWN; - } - } catch (error) { - throw new Error('Error determining IPFS media type'); - } -} diff --git a/src/antelope/types/ABIv1.ts b/src/antelope/types/ABIv1.ts deleted file mode 100644 index 36e3c46c5..000000000 --- a/src/antelope/types/ABIv1.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface ABIv1 { - abi: { - actions: ActionV1[]; - structs: StructV1[]; - tables: TableV1[]; - } | null; - account_name: string; -} - -interface ActionV1 { - name: string; - ricardian_contract: string; - type: string; -} - -interface TableV1 { - index_type: string; - key_names: unknown[]; - key_types: unknown[]; - name: string; - type: string; -} - -interface StructV1 { - base: string; - fields: FieldV1[]; - name: string; -} - -interface FieldV1 { - name: string; - type: string; -} diff --git a/src/antelope/types/Actions.ts b/src/antelope/types/Actions.ts deleted file mode 100644 index 23aa3c962..000000000 --- a/src/antelope/types/Actions.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { API } from '@greymass/eosio'; -import { TokenSourceInfo } from 'src/antelope/types'; - -export interface ActionData { - actions: Action[]; - cached: boolean; - executed: boolean; - lib: number; - query_time_ms: number; - total: { - value: number; - relation: string; - }; -} - -export interface GetActionsResponse { - data: { - actions: Action[], - total: { - value: number - } - }, -} - -export interface Action { - '@timestamp': string; - account_ram_deltas: AccountRamDelta[]; - act: Account; - action_ordinal: number; - block_num: number; - cpu_usage_us: number; - creator_action_ordinal: number; - global_sequence: number; - net_usage_words: number; - notified: string[]; - producer: string; - signatures: string[]; - timestamp: string; - trx_id: string; - receipts: Receipt[]; - account: string; - authorization: { - actor: string; - permission: string; - }[]; - data: { - executer: string; - proposal_name: string; - proposer: string; - }; - name: string; -} - -interface AccountRamDelta { - account: string; - delta: number; -} - -export interface Account { - account: string; - authorization: Authorization[]; - data: unknown; - name: string; -} - -export interface Authorization { - actor: string; - permission: string; -} - -interface Resource { - used: number; - available: number; - max: number; -} - -export type AccountDetails = { - account: { - account_name: string; - core_liquid_balance: string; - cpu_limit: Resource; - cpu_weight: number; - created: string; - net_limit: Resource; - net_weight: number; - permissions: Permission[]; - privileged: boolean; - ram_quota: number; - ram_usage: number; - refund_request: Refund; - rex_info: null | { - matured_rex: string; - vote_stake: string; - rex_balance: string; - rex_maturities: Maturities[]; - }; - subjective_cpu_bill_limit: Resource; - total_resources: { - owner: string; - net_weight: string; - cpu_weight: string; - ram_bytes: number; - }; - voter_info: { - owner: string; - proxy: string; - producers: string[]; - staked: number; - last_stake: number; - last_vote_weight: string; - proxied_vote_weight: number; - is_proxy: number; - }; - self_delegated_bandwidth: { net_weight: string; cpu_weight: string }; - }; - actions: Action[]; - links: string[]; - query_time_ms: number; - tokens: TokenSourceInfo[]; - total_actions: number; -}; - -interface Key { - key: string; - weight: number; -} - -interface ActorPermission { - permission: { actor: string; permission: 'eosio.code' }; - weight: number; -} -interface RequiredAuth { - accounts: ActorPermission[]; - keys: Key[]; - threshold: number; - waits: []; -} - -export interface Permission extends API.v1.AccountPermission { - children: Permission[]; - permission_links: PermissionLinks[]; -} - -export interface NewAccountData { - active: RequiredAuth; - creator: string; - newact: string; - owner: RequiredAuth; - name: string; -} - -export interface PermissionLinksData { - cached: boolean; - links: PermissionLinks[]; - query_time_ms: number; - total: { - value: number; - relation: string; - }; -} - -export interface PermissionLinks { - account: string; - action: string; - block_num: number; - code: string; - permission: string; - timestamp: string; -} - -export interface TransferData { - from: string; - to: string; - amount: number; - symbol: string; - memo: string; - quantity: number; -} - -export interface Refund { - cpu_amount: string; - net_amount: string; - owner: string; - request_time: string; -} -export interface TableByScope { - code: string; - scope: string; - table: string; - payer: string; - count: number; -} - -export interface Block { - timestamp: string; - producer: string; - confirmed: number; - previous: string; - transaction_mroot: string; - action_mroot: string; - schedule_version: number; - new_producers: null | string; - producer_signature: string; - transactions: { - cpu_usage_us: number; - net_usage_words: number; - status: string; - trx: { - id: string; - transaction: { - actions: Action[]; - }; - }; - }[]; - id: string; - block_num: number; - ref_block_prefix: number; -} - -export interface Receipt { - auth_sequence: Auth_sequence[]; - global_sequence: string; - receiver: string; - recv_sequence: string; -} - -export interface Auth_sequence { - account: string; - sequence: string; -} - -interface Maturities { - first: string; - second: number; -} -export interface Get_actions { - actions: Action[]; - cached: boolean; - } -export interface Error { - cause?: { - json?: { - error?: { - what?: string; - }; - }; - }; -} diff --git a/src/antelope/types/AntelopeError.ts b/src/antelope/types/AntelopeError.ts deleted file mode 100644 index 7220438ee..000000000 --- a/src/antelope/types/AntelopeError.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface AntelopeErrorPayload { - [key:string]: unknown -} - -export class AntelopeError extends Error { - public payload?: AntelopeErrorPayload; - constructor( - message: string | undefined, - public _payload?: unknown, - ) { - super(message); - if (_payload) { - this.payload = _payload as { [key:string]: unknown}; - } - } -} - diff --git a/src/antelope/types/Api.ts b/src/antelope/types/Api.ts deleted file mode 100644 index 1e319acd2..000000000 --- a/src/antelope/types/Api.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -import { - Checksum160, - Checksum256, - Float64, - Name, - NameType, - UInt128, - UInt64, - UInt32Type, - API, - PublicKey, - Float128, -} from '@greymass/eosio'; - -import { - AccountDetails, - Action, - PermissionLinks, - TableByScope, - TransactionV1, - Block, - ActionData, - Get_actions, - HyperionActionsFilter, - TokenSourceInfo, -} from 'src/antelope/types'; - -export const NativeCurrencyAddress = '___NATIVE_CURRENCY___'; - -export type AccountCreatorInfo = { - creator: string; - timestamp: string; - trx_id: string; -} - -export type TableIndexType = - | Name - | UInt64 - | UInt128 - | Float64 - | Checksum256 - | Checksum160; - -export interface TableIndexTypes { - float128: Float128; - float64: Float64; - i128: UInt128; - i64: UInt64; - name: Name; - ripemd160: Checksum160; - sha256: Checksum256; -} -export interface GetTableRowsParamsKeyed extends GetTableRowsParams { - // Index key type, determined automatically when passing a typed `upper_bound` or `lower_bound`. - key_type: Key; -} - -export interface GetTableRowsParamsTyped extends GetTableRowsParams { - // Result type for each row. - type: Row; -} - -export interface GetTableRowsParams { - // The name of the smart contract that controls the provided table. - code: NameType; - // Name of the table to query. - table: NameType; - // The account to which this data belongs, if omitted will be set to be same as `code`. - scope?: string | TableIndexType; - // Lower lookup bound. - lower_bound?: Index; - // Upper lookup bound. - upper_bound?: Index; - // How many rows to fetch, defaults to 10 if unset. - limit?: UInt32Type; - // Whether to iterate records in reverse order. - reverse?: boolean; - // Position of the index used, defaults to primary. - index_position?: 'primary' | 'secondary' | 'tertiary' | 'fourth' | 'fifth' | 'sixth' | 'seventh' | 'eighth' | 'ninth' | 'tenth'; - // Whether node should try to decode row data using code abi. - // Determined automatically based the `type` param if omitted. - json?: boolean; - // Set to true to populate the ram_payers array in the response. - show_payer?: boolean; -} - - - -export interface GetTableRowsResponse { - rows: Row[]; - more: boolean; - ram_payers?: Name[]; - next_key?: Index; -} - -export type ApiClient = { - getAccount: (address: string) => Promise; - getKeyAccounts: (key: PublicKey) => Promise<{ account_names: Name[] }>; - getHyperionAccountData: (address: string) => Promise; - getCreator: (address: string) => Promise; - getTokens: (address: string) => Promise; - getTransactions: (filter: HyperionActionsFilter) => Promise; - getTransaction: (address: string) => Promise; - getTransactionV1: (id: string) => Promise; - getChildren: (address: string) => Promise; - getPermissionLinks: (address: string) => Promise; - getTableByScope: (data: unknown) => Promise; - getBlock: (block: string) => Promise; - getActions: (address: string, filter: string) => Promise; - getApy: () => Promise; -}; diff --git a/src/antelope/types/Basic.ts b/src/antelope/types/Basic.ts deleted file mode 100644 index 726b78431..000000000 --- a/src/antelope/types/Basic.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Label = string; -export type Network = 'telos' | 'telos-evm' | string; -export type Address = string; diff --git a/src/antelope/types/ChainInfo.ts b/src/antelope/types/ChainInfo.ts deleted file mode 100644 index 0f894167e..000000000 --- a/src/antelope/types/ChainInfo.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface ChainInfo { - block_cpu_limit: number; - block_net_limit: number; - chain_id: string; - fork_db_head_block_id: string; - fork_db_head_block_num: number; - head_block_id: string; - head_block_num: number; - head_block_producer: string; - head_block_time: string; - last_irreversible_block_id: string; - last_irreversible_block_num: number; - last_irreversible_block_time: string; - server_full_version_string: string; - server_version: string; - server_version_string: string; - virtual_block_cpu_limit: number; - virtual_block_net_limit: number; -} diff --git a/src/antelope/types/ChainSettings.ts b/src/antelope/types/ChainSettings.ts deleted file mode 100644 index febf42050..000000000 --- a/src/antelope/types/ChainSettings.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { RpcEndpoint } from 'universal-authenticator-library'; -import { IndexerTransactionsFilter, NFTClass, PriceChartData, TokenClass } from 'src/antelope/types'; - -export interface ChainSettings { - init(): Promise; - isNative(): boolean; - isTestnet(): boolean; - getNetwork(): string; - getSystemToken(): TokenClass; - getTokenList(): Promise; - getDisplay(): string; - getSmallLogoPath(): string; - getLargeLogoPath(): string; - getChainId(): string; - getHyperionEndpoint(): string; - getRPCEndpoint(): RpcEndpoint; - getApiEndpoint(): string; - getPriceData(): Promise; - getUsdPrice(): Promise; - getSystemTokens(): TokenClass[]; - getNFTsInventory(address: string, filter: IndexerTransactionsFilter): Promise; - getNFTsCollection(contract: string, filter: IndexerTransactionsFilter): Promise; - trackAnalyticsEvent(params: Record): void; - getApy(): Promise; -} diff --git a/src/antelope/types/EvmBlockData.ts b/src/antelope/types/EvmBlockData.ts deleted file mode 100644 index 0854895ad..000000000 --- a/src/antelope/types/EvmBlockData.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface EvmBlockData { - difficulty: string; - extraData: string; - gasLimit: string; - gasUsed: string; - hash: string; - logsBloom: string; - miner: string; - mixHash: string; - nonce: string; - number: string; - parentHash: string; - receiptsRoot: string; - sha3Uncles: string; - size: string; - stateRoot: string; - timestamp: string; - totalDifficulty: string; - transactions: unknown[]; - transactionsRoot: string; - uncles: unknown[]; -} - - diff --git a/src/antelope/types/EvmContractData.ts b/src/antelope/types/EvmContractData.ts deleted file mode 100644 index 5961e1c00..000000000 --- a/src/antelope/types/EvmContractData.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { ethers } from 'ethers'; -import type { TokenSourceInfo } from 'src/antelope/types'; -import type { EvmABI } from 'src/antelope/types'; - -export interface EvmContractConstructorData { - address: string; - name: string; - manager?: EvmContractManagerI; - creationInfo?: EvmContractCreationInfo | null; - abi?: EvmABI | string; - token?: TokenSourceInfo; - verified?: boolean; - supportedInterfaces: string[]; - properties?: EvmContractCalldata; -} - -export interface EvmContractManagerI { - getSigner: () => Promise; - getWeb3Provider: () => Promise; - getFunctionIface: (hash:string) => Promise; - getEventIface: (hash:string) => Promise; -} - -export interface EvmContractCreationInfo { - block?: number | null; - block_num?: number; // same as block, kept for legacy usage - creator?: string | null; - transaction: string; - creation_trx: string; // same as transaction, kept for legacy usage - timestamp?: string; // string number like "1679649071" - abi?: string | EvmABI; -} - -export interface EvmContractMetadata { - compiler?: { - version: string; - }; - language?: string; - output?: { - abi: EvmABI; - devdoc: { - kind: string; - methods: Record; - version: number; - }; - userdoc: { - kind: string; - methods: Record; - version: number; - } - }; - settings?: { - compilationTarget: Record; - evmVersion: string; - libraries: Record; - metadata: { - bytecodeHash: string; - useLiteralContent: boolean; - }; - optimizer: { - enabled: boolean; - runs: number; - }; - remappings: Record[] - } - sources?: Record; - version?: number; -} - -export interface EvmContractCalldata { - decimals?: number; - holders?: string; // string representation of number - marketdata_updated?: string; // epoch - name?: string; - price?: string; // string representation of number, USD price - supply?: string; // string representation of number - symbol?: string; -} - -export interface EvmContractData { - symbol?: string; - creator?: string; - address: string; - fromTrace?: boolean; - abi?: string | EvmABI - trace_address?: string; // same attribute (raw) - traceAddress?: string; // same attribute (processed) - logoURI?: string; - supply?: string; // string representation of number - calldata?: string; // string holding JSON - decimals?: number | null; - name?: string | null; - block?: number; - supportedInterfaces?: string[]; - transaction?: string; -} - -export interface EvmContractFactoryData extends EvmContractData { - metadata?: string; - timestamp?: string; - manager?: EvmContractManagerI; -} - -export type EvmFunctionParam = string | number | boolean | ethers.BigNumber; diff --git a/src/antelope/types/EvmLog.ts b/src/antelope/types/EvmLog.ts deleted file mode 100644 index 6b687ecb2..000000000 --- a/src/antelope/types/EvmLog.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ethers } from 'ethers'; -import { TokenSourceInfo } from 'src/antelope/types'; - -export interface EvmLog { - address: string; - blockHash: string; - blockNumber: number; - data: string; - logIndex: string; - removed: boolean; - topics: string[]; - transactionHash: string; - transactionIndex: string; -} - -export type EvmLogs = EvmLog[]; - -export interface EvmFormatedLog extends ethers.utils.LogDescription { - inputs: ethers.utils.ParamType[]; - function_signature: string; - isTransfer: boolean; - logIndex: string, - address: string, - token: TokenSourceInfo | null, - name: string, -} diff --git a/src/antelope/types/EvmRexDeposit.ts b/src/antelope/types/EvmRexDeposit.ts deleted file mode 100644 index a0f79a8a6..000000000 --- a/src/antelope/types/EvmRexDeposit.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ethers } from 'ethers'; - -export interface EvmRexDeposit { - // amount of REX tokens deposited (expressed in system tokens - e.g. TLOS) - amount: ethers.BigNumber; - - // a big number representing the time (seconds since epoch) at which the deposit will be available for withdrawal - // data.until.toNumber() should be salfe to use as a timestamp - until: ethers.BigNumber; -} diff --git a/src/antelope/types/EvmTransaction.ts b/src/antelope/types/EvmTransaction.ts deleted file mode 100644 index 5b8051b0d..000000000 --- a/src/antelope/types/EvmTransaction.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { ethers } from 'ethers'; -import { NftTokenInterface } from 'src/antelope/types'; - -export type EvmTransactionTopic = string; - -export interface EvmTransactionLog { - address: string; - blockHash: string; - blockNumber: number; - data: string; - logIndex: number; - removed: boolean; - topics: EvmTransactionTopic[]; - transactionHash: string; -} - -export interface EvmTransaction { - blockNumber: number; - contractAddress?: string; - cumulativeGasUsed: string; // string representation of hex number - from: string; - gasLimit: string; // string representation of hex number - gasPrice: string; // string representation of hex number - gasUsed: string; // string representation of hex number - hash: string; - index: number; - input: string; - nonce: number; - output: string; - logs?: string; - r: string; - s: string; - status: string; // string representation of hex number - timestamp: number; // epoch in milliseconds - to: string | null; // null if contract creation - v: string; - value: string; // string representation of hex number -} - -export interface EvmTransactionParsed extends EvmTransaction { - gasLimitBn: ethers.BigNumber; - gasPriceBn: ethers.BigNumber; - gasUsedBn: ethers.BigNumber; - valueBn: ethers.BigNumber; - logsArray: EvmTransactionLog[]; -} - -export interface EvmContractFunctionParameter { - name: string; - type: string; - arrayChildren: string | false; - value: (string | number | boolean | null | ethers.BigNumber)[]; -} - -export interface TransactionValueData { - amount: number; - symbol: string; - fiatValue?: number; -} - -export interface NftTransactionData { - quantity: number; - tokenId: string; - tokenName: string; - collectionAddress: string; - collectionName?: string; - imgSrc?: string; - videoSrc?: string; - audioSrc?: string; - type: 'image' | 'video' | 'audio' | 'unknown'; - nftInterface: NftTokenInterface; -} - -export interface ShapedTransactionRow { - id: string; // transaction ID - epoch: number; // epoch in milliseconds - // action should be 'send', 'receive', 'swap', 'contractCreation', or some other action like 'approve' - actionName: string; - from: string; // address - fromPrettyName?: string; - to: string; // address - toPrettyName?: string; - gasUsed?: number; // gas used in TLOS - gasFiatValue?: number; // gas used in Fiat - failed?: boolean; - - // ERC20 data - valuesIn: TransactionValueData[]; - valuesOut: TransactionValueData[]; - - // ERC721 & ERC1155 data - nftsIn: NftTransactionData[]; - nftsOut: NftTransactionData[]; -} - -export interface IndexerContractData { - symbol: string; - creator: string; - address: string; - fromTrace: boolean; - trace_address: string; - logoURI: string; - supply: string; // string representation of an integer - calldata: string; - decimals: number | null; - name: string; - block: number; - supportedInterfaces: ('erc20'|'erc721'|'erc1155'|'none')[], - transaction: string; // creation tx for contract -} - -export interface ParsedIndexerAccountTransactionsContract extends IndexerContractData { - price?: string; // string representation of number - holders?: number; - marketdata_updated?: string; // epoch -} - -export interface EVMTransactionsPaginationData { - total: number; - more: boolean; -} - -export interface IndexerAccountTransactionsResponse { - contracts: { - [contractHash: string]: IndexerContractData - }; - results: EvmTransaction[] - total_count: number; - more: boolean; -} - -export type EvmTransactionResponse = ethers.providers.TransactionResponse; -export interface TransactionResponse { - hash: string; - wait: () => Promise; -} -export interface NativeTransactionResponse extends TransactionResponse { - __?: string; -} - -export interface IndexerAccountTransfersResponse { - contracts: { - [contractHash: string]: IndexerContractData - }; - results: EvmTransfer[] - total_count?: number; // included if includePagination is true in the request - more?: boolean; // included if includePagination is true in the request -} - -export interface EvmTransfer { - amount: string, // a string representing an integer - contract: string, // contract address of the token being transferred - blockNumber: number, // an integer representing the block number of the transfer - from: string, // address of the sender - to: string; // address of the receiver - type: 'erc20' | 'erc721' | 'erc1155', // type of token being transferred - transaction: string; // transaction hash - timestamp: number; // integer representing ms from epoch - id?: string; // id of the NFT transferred (ERC721 or ERC1155 only) -} - diff --git a/src/antelope/types/ExceptionError.ts b/src/antelope/types/ExceptionError.ts deleted file mode 100644 index 5954e2fdd..000000000 --- a/src/antelope/types/ExceptionError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ExceptionError { - code: number; - message: string; - stack: string; -} diff --git a/src/antelope/types/Filters.ts b/src/antelope/types/Filters.ts deleted file mode 100644 index 265ef4036..000000000 --- a/src/antelope/types/Filters.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface HyperionAbiSignatureFilter { - type?: string; - hex?: string; -} - -export interface HyperionActionsFilter { - page?: number; - skip?: number;// skip overrides `page` - limit?: number; - account?: string; - notified?: string; - sort?: 'desc' | 'asc'; - after?: string; - before?: string; - extras?: { [key: string]: string }; - address?: string; - block?: string; - hash?: string; -} - -export interface IndexerTransactionsFilter { - address: string; - limit?: number; // integer value to limit number of results - offset?: number; // integer value to offset the results of the query - includeAbi?: boolean; // indicate whether to include abi - sort?: 'DESC' | 'ASC'; // sort transactions by id (DESC or ASC) - includePagination?: boolean; // include the total count and more flag in response - logTopic?: string; // match to the transaction logs' first topic - full?: string; // Add internal transactions to the response - forceMetadata?: number; // 1 to force metadata to be returned -} - -export interface IndexerTransfersFilter { - account: string; - type?: 'erc20' | 'erc721' | 'erc1155'; // filter by token type - limit?: number; // integer value to limit number of results - offset?: number; // integer value to offset the results of the query - includePagination?: boolean; // include the total count and more flag in response - endBlock?: number; // last block to include in the query - startBlock?: number; // first block to include in the query - contract?: string; // filter by contract address - includeAbi?: boolean; // indicate whether to include abi -} diff --git a/src/antelope/types/IndexerTypes.ts b/src/antelope/types/IndexerTypes.ts deleted file mode 100644 index 671238cff..000000000 --- a/src/antelope/types/IndexerTypes.ts +++ /dev/null @@ -1,259 +0,0 @@ -// Indexer Nft Response -------- - -export const INVALID_METADATA = '___INVALID_METADATA___'; // string given by indexer for NFTs with invalid metadata - -interface IndexerNftResponse { - success: boolean; - contracts: { - [address: string]: IndexerContract; - }; -} - -export interface IndexerCollectionNftsResponse extends IndexerNftResponse { - results: IndexerCollectionNftResult[]; -} - -export interface IndexerAccountNftsResponse extends IndexerNftResponse { - results: IndexerAccountNftResponse[]; -} - -export interface IndexerNftItemAttribute { - value: string; - trait_type: string; - display_type?: string; -} - -export type IndexerNftMetadata = { - dna?: string; - date?: number; - name?: string; - image?: string; - edition?: number; - compiler?: string; - imageHash?: string; - attributes?: IndexerNftItemAttribute[]; - description?: string; - [key: string]: unknown; -} | null; - -interface IndexerNftResult { - metadata: string; - tokenId: string; - contract: string; - updated: number; - imageCache?: string; - tokenUri?: string; -} - -// results from the /contract/{address}/nfts endpoint -export interface IndexerCollectionNftResult extends IndexerNftResult { - supply?: number; // present only for ERC1155 - owner?: string; // present only for ERC721 -} - -// results from the /account/{address}/nfts endpoint -export interface IndexerAccountNftResponse extends IndexerNftResult { - amount?: number; // present only for ERC1155 - minter: string; - blockMinted: number; - tokenIdSupply?: number; // present only for ERC1155 - owner: string; -} - -// used as an intermediate type for constructing NFTs from IndexerAccountNftResponse/IndexerCollectionNftResult -export interface GenericIndexerNft { - metadata: Record | string; // object or JSON object string - tokenId: string; - contract: string; - updated: number; - imageCache?: string; - tokenUri?: string; - supply?: number; // present only for ERC1155 - minter?: string; - blockMinted?: number; - owner?: string; // present only for ERC721 -} - -export interface IndexerContract { - symbol: string; - creator: string; - address: string; - fromTrace: boolean; - trace_address: string; - supply: string; - calldata?: { - name?: string; - supply?: string; - symbol?: string; - }, - decimals: number | null; - name: string; - block: number; - supportedInterfaces?: string[]; - transaction: string; -} - - - - -// ------- - -export interface IndexerTokenInfo { - symbol: string; - creator: string; - address: string; - fromTrace: boolean; - trace_address: string; - logoURI: string; - supply: string; - calldata: IndexerTokenMarketData; - decimals: number; - name: string; - block: number; - supportedInterfaces: string[]; - transaction: string; -} - -export interface IndexerTokenMarketData { - name?: string; - price?: number; - supply?: string; - symbol?: string; - volume?: string; - holders?: string; - decimals?: number; - marketcap?: string; - max_supply_ibc?: string; - total_supply_ibc?: string; - marketdata_updated?: string; -} - -export interface IndexerTokenBalance { - address: string; - balance: string; - contract: string; - updated: number; -} - - -export interface IndexerAccountBalances { - success: boolean; - contracts: { - [address: string]: IndexerTokenInfo; - }; - results: IndexerTokenBalance[]; -} - -export interface IndexerHealthResponse { - success: boolean; - blockNumber: number; - blockTimestamp: string; - secondsBehind: number; -} - -export interface IndexerTokenHoldersResponse { - contracts: { - [address: string]: IndexerContract; - }; - results: { - address: string; // holder address - balance: number; - tokenid: number; - updated: number; // ms since epoch - }[]; -} - -// Allowances -interface IndexerAllowanceResult { - owner: string; // address of the token owner; - contract: string; // address of the token contract - updated: number; // timestamp of the last time the allowance was updated - ms since epoch -} - -export interface IndexerErc20AllowanceResult extends IndexerAllowanceResult { - amount: string; // string representation of a number; the amount of tokens the owner has approved for the spender in the token's smallest unit - spender: string; // address of the spender contract -} - -export interface IndexerErc721AllowanceResult extends IndexerAllowanceResult { - single: false; // whether the allowance is for a single token or for the entire collection - approved: boolean; // whether the user has approved the spender - operator: string; // address of the spender contract - - tokenId?: string | number; // only present if single === true -} - -export interface IndexerErc1155AllowanceResult extends IndexerAllowanceResult { - approved: boolean; // whether the user has approved the spender - operator: string; // address of the spender contract -} - -export interface IndexerAllowanceResponseErc20 { - contracts: { - [address: string]: IndexerContract; - } - results: IndexerErc20AllowanceResult[], -} - -export interface IndexerAllowanceResponseErc721 { - contracts: { - [address: string]: IndexerContract; - } - results: IndexerErc721AllowanceResult[], -} - -export interface IndexerAllowanceResponseErc1155 { - contracts: { - [address: string]: IndexerContract; - } - results: IndexerErc1155AllowanceResult[], -} - -export type IndexerAllowanceResponse = IndexerAllowanceResponseErc20 | IndexerAllowanceResponseErc721 | IndexerAllowanceResponseErc1155; - - -// old version ---------- - -export interface IndexerNftItemResult { - metadata: { - dna?: string; - date?: number; - name?: string; - image?: string; - edition?: number; - compiler?: string; - imageHash?: string; - attributes?: IndexerNftItemAttribute[]; - description?: string; - } | { - [key: string]: unknown; - } | null; - owner: string; // address - minter: string; // address - tokenId: string; - tokenUri: string; - contract: string; // address - imageCache?: string; // url - blockMinted: number; - updated: number; // epoch - transaction: string; // tx hash -} - -export interface IndexerNftContract { - symbol: string; - creator: string; - address: string; - fromTrace: boolean; - trace_address: string; - supply: string; - calldata?: { - name?: string; - supply?: string; - symbol?: string; - }, - decimals: number | null; - name: string; - block: number; - supportedInterfaces: string[]; - transaction: string; -} diff --git a/src/antelope/types/KeyAccounts.ts b/src/antelope/types/KeyAccounts.ts deleted file mode 100644 index efe01a8d9..000000000 --- a/src/antelope/types/KeyAccounts.ts +++ /dev/null @@ -1 +0,0 @@ -export interface KeyAccounts { account_names: string[] } diff --git a/src/antelope/types/NFTClass.ts b/src/antelope/types/NFTClass.ts deleted file mode 100644 index aedc2d32b..000000000 --- a/src/antelope/types/NFTClass.ts +++ /dev/null @@ -1,394 +0,0 @@ -/* eslint-disable max-len */ -// NFT interfaces --------------- - -import { IndexerNftContract, IndexerNftItemAttribute, IndexerNftItemResult } from 'src/antelope/types'; - -export interface NftAttribute { - label: string; - text: string; -} - -// NFT which has been processed for display in the UI -export interface ShapedNFT { - name: string; - id: string; - description?: string; - ownerAddress: string; - contractAddress: string; - contractPrettyName?: string; - attributes: NftAttribute[]; - imageSrcFull?: string; // if this is empty, the UI will display a generic image icon - imageSrcIcon?: string; // as a result of shaping, this will always have a value if imageSrcFull is defined - - // only one of audioSrc or videoSrc should be present, not both - audioSrc?: string; - - // during the shaping process, if there is a video but no image given in the metadata, - // the first frame of the video should be extracted and set as the imageSrcFull & imageSrcIcon - videoSrc?: string; -} - -export type NftSourceType = 'image' | 'video' | 'audio' | 'unknown'; -export const NFTSourceTypes: Record = { - IMAGE: 'image', - VIDEO: 'video', - AUDIO: 'audio', - UNKNOWN: 'unknown', -}; - -export type NftTokenInterface = 'ERC721' | 'ERC1155'; - -// NFT classes ------------------ - -export class NFTContractClass { - indexer: IndexerNftContract; - constructor( - source: IndexerNftContract, - ) { - this.indexer = source; - } - - get address(): string { - return this.indexer.address; - } - - get name(): string | undefined { - return this.indexer.calldata?.name; - } -} - -export class NFTItemClass { - indexer: IndexerNftItemResult; - ready = true; - preview: string; - type: NftSourceType; - source: string | undefined; - contract: NFTContractClass; - - constructor( - item: IndexerNftItemResult, - public _contract: NFTContractClass, - ) { - this.contract = _contract; - this.indexer = item; - const { preview, type, source } = this.extractMetadata(); - this.preview = preview; - this.type = type as NftSourceType; - this.source = source; - } - - extractMetadata(): { preview:string, type:string, source:string | undefined } { - let type = NFTSourceTypes.IMAGE; - let preview = ''; - let source: string | undefined = undefined; - - // We are going to test the imageCache URL to see if it is a valid URL - if (this.indexer.imageCache) { - - // first we create a regExp for the valid URL. e.g: "https://nfts.telos.net/40/0x552fd5743432eC2dAe222531e8b88bf7d2410FBc/344" - const regExp = new RegExp('^(https?:\\/\\/)?' + // protocol - '(nfts.telos.net\\/)' + // domain name - '(\\d+\\/)' + // chain id - '(0x[0-9a-fA-F]+\\/)' + // contract address - '(\\d+)$'); // token id - - // then we test the imageCache URL against the regExp - const match = regExp.test(this.indexer.imageCache); - if (match) { - // we return the 1440.webp version of it - preview = this.indexer.imageCache.concat('/1440.webp'); - } - } - // if there's an image in the metadata, we return that - if (!preview && this.indexer.metadata?.image) { - preview = this.indexer.metadata.image as string; - } - - if (!preview && this.indexer.metadata) { - // this NFT is not a simple image and could be anything (including an image). - // We need to look at the metadata - const metadata = this.indexer.metadata as { [key: string]: string }; - // we iterate over the metadata properties - for (const property in metadata) { - const value = metadata[property]; - if (!value) { - continue; - } - // if the value is a string and contains a valid url of a known media format, use it. - // image formats: .gif, .avif, .apng, .jpeg, .jpg, .jfif, .pjpeg, .pjp, .png, .svg, .webp - if ( - !preview && // if we already have a preview, we don't need to keep looking - typeof value === 'string' && - value.match(/\.(gif|avif|apng|jpe?g|jfif|p?jpe?g|png|svg|webp)$/) - ) { - preview = value; - } - // audio formats: .mp3, .wav, .aac, .webm - if ( - !source && // if we already have a source, we don't need to keep looking - typeof value === 'string' && - value.match(/\.(mp3|wav|aac|webm)$/) - ) { - type = NFTSourceTypes.AUDIO; - source = value; - } - // video formats: .mp4, .webm, .ogg - if ( - !source && // if we already have a source, we don't need to keep looking - typeof value === 'string' && - value.match(/\.(mp4|webm|ogg)$/) - ) { - type = NFTSourceTypes.VIDEO; - source = value; - } - - const regex = /^data:(image|audio|video)\/\w+;base64,[\w+/=]+$/; - - const match = value.match(regex); - - if (match) { - const contentType = match[1]; - - if (contentType === 'image' && !preview) { - preview = value; - } else if (contentType === 'audio' && !source) { - type = NFTSourceTypes.AUDIO; - source = value; - } else if (contentType === 'video' && !source) { - type = NFTSourceTypes.VIDEO; - source = value; - } - } - - } - - // particular case of media format webm. We need to determine if it is a video or audio - if (source && source.match(/\.webm$/)) { - this.ready = false; - - this.determineWebmType(source).then((_type) => { - if (_type === NFTSourceTypes.VIDEO) { - this.type = NFTSourceTypes.VIDEO; - this.extractFirstFrameFromVideo(source as string).then((_preview) => { - this.preview = _preview; - this.ready = true; - this.notifyWatchers(); - }); - } else { - this.notifyWatchers(); - } - }); - } else { - if (type === NFTSourceTypes.VIDEO) { - this.ready = false; - this.type = NFTSourceTypes.VIDEO; - this.extractFirstFrameFromVideo(source as string).then((_preview) => { - this.preview = _preview; - this.ready = true; - this.notifyWatchers(); - }); - } - } - } - - return { preview, type, source }; - } - - async determineWebmType(source: string): Promise { - return new Promise((resolve, reject) => { - const video = document.createElement('video'); - - video.onloadedmetadata = function() { - if (video.videoWidth > 0 && video.videoHeight > 0) { - resolve(NFTSourceTypes.VIDEO); - } else { - resolve(NFTSourceTypes.AUDIO); - } - }; - - video.onerror = function(e) { - reject({ error: e, source }); - }; - - video.src = source; - }); - } - - async extractFirstFrameFromVideo(source: string): Promise { - return this.extractFrameFromVideo(source, 0); - } - - async extractFrameFromVideo(source: string, time: number): Promise { - // this function seams not to wer in most of the cases. It returns a transparent image - return new Promise((resolve, reject) => { - const video = document.createElement('video'); - - video.onloadedmetadata = function() { - video.currentTime = time; - - const canvas = document.createElement('canvas'); - - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - - const ctx = canvas.getContext('2d'); - if (ctx) { - // let's draw the video in the canvas - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - - // now we test the color of the pixel in the middle of the canvas - const pixelData = ctx.getImageData((canvas.width / 2), (canvas.height / 2), 1, 1).data; - if (pixelData[3] === 0) { - // if it is transparent, it means that we don't have a preview for this video - resolve(''); - } else { - // if the pixel is not transparent, we return the canvas as a dataURL - resolve(canvas.toDataURL()); - } - } else { - reject({ error: 'no context', source }); - } - }; - - video.onerror = function(e) { - reject({ error: e, source }); - }; - - video.src = source; - video.setAttribute('crossOrigin', 'anonymous'); - video.preload = 'metadata'; - video.load(); - }); - } - - - get name(): string { - return (this.indexer.metadata?.name || '') as string; - } - - get tokenId(): string { - return this.indexer.tokenId; - } - - get description(): string | undefined { - return (this.indexer.metadata?.description) as string | undefined; - } - - get owner(): string { - return this.indexer.owner || this.indexer.minter; - } - - get attributes(): NftAttribute[] { - return ((this.indexer.metadata?.attributes || []) as IndexerNftItemAttribute[]).map(attr => ({ - label: attr.trait_type, - text: attr.value, - })); - } - - get image(): string { - return this.preview; - } - - get icon(): string | undefined { - return this.preview; - } - - watchers: (() => void)[] = []; - watch(cb: () => void): void { - this.watchers.push(cb); - } - - notifyWatchers(): void { - this.watchers.forEach(w => w()); - } -} - -export class NFTClass implements ShapedNFT { - - item: NFTItemClass; - - constructor( - item: NFTItemClass, - ) { - this.item = item; - } - - // API -- - - // ShapedNFT support -- - get name(): string { - return this.item.name; - } - - get id(): string { - return this.item.tokenId; - } - - get description(): string | undefined { - return this.item.description; - } - - get ownerAddress(): string { - return this.item.owner; - } - - get contractAddress(): string { - return this.item.contract.address; - } - - get contractPrettyName(): string | undefined { - return this.item.contract.name; - } - - get attributes(): NftAttribute[] { - return this.item.attributes; - } - - get imageSrcFull(): string | undefined { - return this.item.image; - } - - get imageSrcIcon(): string | undefined { - return this.item.icon; - } - - get audioSrc(): string | undefined { - return this.item.type === NFTSourceTypes.AUDIO ? this.item.source : undefined; - } - - get videoSrc(): string | undefined { - return this.item.type === NFTSourceTypes.VIDEO ? this.item.source : undefined; - } - - getShapedNFT(): ShapedNFT { - return { - name: this.name, - id: this.id, - description: this.description, - ownerAddress: this.ownerAddress, - contractAddress: this.contractAddress, - contractPrettyName: this.contractPrettyName, - attributes: this.attributes, - imageSrcFull: this.imageSrcFull, - imageSrcIcon: this.imageSrcIcon, - audioSrc: this.audioSrc, - videoSrc: this.videoSrc, - }; - } - - // this jey property is very usefull to provide a unique key to the v-for directive - // because it is based on the content of the shapedNFT object - get key(): string { - const json = JSON.stringify(this.getShapedNFT()); - let counter = 0; - for (let i = 0; i < json.length; i++) { - counter += json.charCodeAt(i); - } - return counter.toString(); - } - - watch(cb: () => void): void { - this.item.watch(cb); - } -} - diff --git a/src/antelope/types/OpenSeaTypes.ts b/src/antelope/types/OpenSeaTypes.ts deleted file mode 100644 index 49f044356..000000000 --- a/src/antelope/types/OpenSeaTypes.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable max-len */ -export interface OpenSeaNFTMetadata { - image?: string; // This is the URL to the image of the item. Can be just about any type of image (including SVGs, which will be cached into PNGs by OpenSea), and can be IPFS URLs or paths. We recommend using a 350 x 350 image. - image_data?: string; // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you\'re not including the image parameter. - external_url?: string; // This is the URL that will appear below the asset\'s image on OpenSea and will allow users to leave OpenSea and view the item on your site. - description?: string; // A human readable description of the item. Markdown is supported. - name?: string; // Name of the item. - attributes?: string; // These are the attributes for the item, which will show up on the OpenSea page for the item. (see below) - background_color?: string; // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #. - animation_url?: string; // A URL to a multi-media attachment for the item. The file extensions GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG are supported, along with the audio-only extensions MP3, WAV, and OGA. - // animation_url also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. Scripts and relative paths within the HTML page are now supported. However, access to browser extensions is not supported. - youtube_url?: string; // A URL to a YouTube video. -} - diff --git a/src/antelope/types/PriceData.ts b/src/antelope/types/PriceData.ts deleted file mode 100644 index 6b929abc2..000000000 --- a/src/antelope/types/PriceData.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface PriceChartData { - lastUpdated: number; - tokenPrice: number; - dayChange: number; - dayVolume: number; - marketCap: number; - prices: DateTuple[]; -} - -export interface PriceHistory { - data: { - prices: DateTuple[]; - }; -} - -export type DateTuple = [number | string, number]; - -export interface PriceStats { - status: number; - data: { - [tokenId: string]: { - last_updated_at: number; - usd: number; - usd_24h_change: number; - usd_24h_vol: number; - usd_market_cap: number; - }; - }; -} - - diff --git a/src/antelope/types/Producers.ts b/src/antelope/types/Producers.ts deleted file mode 100644 index 55ada9933..000000000 --- a/src/antelope/types/Producers.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface GetProducers { - rows: Producer[]; -} - -export interface Producer { - owner: string; - is_active: number; - total_votes: number; - location: string; - name: string; -} - -export interface ProducerSchedule { - active: { - version: string; - producers: { - producer_name: string; - authority: unknown; - }[]; - }; - pending: null; - proposed: null; -} - -export interface ProducerScheduleData { - active: { producers: { producer_name: string }[] }; -} diff --git a/src/antelope/types/Proposals.ts b/src/antelope/types/Proposals.ts deleted file mode 100644 index fc0399f6d..000000000 --- a/src/antelope/types/Proposals.ts +++ /dev/null @@ -1,82 +0,0 @@ -export interface GetProposalsProps { - proposer?: string; - proposal?: string; - requested?: string; - provided?: string; - executed?: boolean; - limit?: number; - skip?: number; -} - -export interface Proposal { - block_num: number; - executed: false; - primary_key: string; - proposal_name: string; - proposer: string; - provided_approvals: { - actor: string; - permission: string; - time: string; - }[]; - requested_approvals: { - actor: string; - permission: string; - time: string; - }[]; -} - -export interface GetProposals { - proposals: Proposal[]; - total: { - value: number; - }; -} - -export interface ProposalTableRow { - primaryKey: string; - proposalName: string; - approvalStatus: string; - proposer: string; - isSigned?: boolean; -} - -export interface ProposalForm { - [x: string]: unknown; - proposer: string; - proposal_name: string; - - requested: { - actor: string; - permission: string; - }[]; - - trx: { - expiration: string; - ref_block_num: number; - ref_block_prefix: number; - max_net_usage_words: number; - max_cpu_usage_ms: number; - delay_sec: number; - context_free_actions: string[]; - transaction_extensions: string[]; - actions: { - account: string; - name: string; - authorization: { - actor: string; - permission: string; - }[]; - data: { - [key: string]: string | number; - }; - }[]; - }; -} - -export interface RequestedApprovals { - actor: string; - permission: string; - status: boolean; - isBp: boolean; -} diff --git a/src/antelope/types/Providers.ts b/src/antelope/types/Providers.ts deleted file mode 100644 index 4fe98ac4f..000000000 --- a/src/antelope/types/Providers.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -export interface EthereumProvider { - // ethereum provider standard API ----- - isMetaMask?: boolean; - isStatus?: boolean; - host?: string; - path?: string; - sendAsync?: (request: { method: string, params?: Array }, callback: (error: unknown, response: unknown) => void) => void; - send?: (request: { method: string, params?: Array }, callback: (error: unknown, response: unknown) => void) => void; - request: (request: { method: string, params?: Array }) => Promise; - - // event and listeners ----- - once?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; - on(eventName: string | symbol, listener: (...args: unknown[]) => void): this; - off?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; - addListener?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; - removeListener?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; - removeAllListeners?(event?: string | symbol): this; - - // internal injected API ----- - __initialized: boolean; -} - diff --git a/src/antelope/types/Theme.ts b/src/antelope/types/Theme.ts deleted file mode 100644 index dfe49cefd..000000000 --- a/src/antelope/types/Theme.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Theme { - primary?: string; - secondary?: string; - accent?: string; - dark?: string; - positive?: string; - negative?: string; - info?: string; - warning?: string; - 'color-map'?: string; - 'color-primary-gradient'?: string; - 'color-secondary-gradient'?: string; - 'color-tertiary-gradient'?: string; - 'color-progress-gradient'?: string; - 'color-producer-card-background'?: string; - 'color-select-box-background'?: string; -} diff --git a/src/antelope/types/TokenClass.ts b/src/antelope/types/TokenClass.ts deleted file mode 100644 index 9365a8dea..000000000 --- a/src/antelope/types/TokenClass.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { ethers } from 'ethers'; -import { toStringNumber } from 'src/antelope/wallets/utils/currency-utils'; -import { WEI_PRECISION, formatWei } from 'src/antelope/wallets/utils'; - -export const TOKEN_PRICE_DECIMALS = 18; - -// A type to represent the possible EVM token types -export const ERC20_TYPE = 'ERC20'; -export const ERC721_TYPE = 'ERC721'; -export const ERC1155_TYPE = 'ERC1155'; -export const ERC777_TYPE = 'ERC777'; -export const ERC827_TYPE = 'ERC827'; -export const ERC1400_TYPE = 'ERC1400'; -export const ERC223_TYPE = 'ERC223'; -export type EvmTokenType = - typeof ERC20_TYPE | - typeof ERC721_TYPE | - typeof ERC1155_TYPE | - typeof ERC777_TYPE | - typeof ERC827_TYPE | - typeof ERC1400_TYPE; - -// MarketSourceInfo is a type to represent all the information that can be retrieved from the market API -// It is used to creat a TokenMarketData class object -export interface MarketSourceInfo { - volume?: string; // ej: '20637702616.664093', - maxGlobalSupply?: never; // ej: null ¿? - networkSupply?: string; // ej: '554177.374691', - symbol?: string; // ej: 'USDT', - marketcap?: string; // ej: '82852725529.35149', - address?: string; // ej: '0xeFAeeE334F0Fd1712f9a8cc375f427D9Cdd40d73', - holders?: string; // ej: '509', - price: string; // ej: '1.000089', - decimals?: number; // ej: 6, - globalSupply?: string; // ej: '86090638895.068830', - updated?: string; // ej: '1684330029866' -} - -// A type to represent the source information for a token -// It is used to create a Token class object -export interface TokenSourceInfo { - symbol: string; // Token symbol - contract?: string; // Token contract account name (for native) - address?: string; // Token contract address (for EVM) - chainId: string; // Chain ID (40 & 41 for Telos EVM) or hash (for native) - network: string; // short name of the network (used for the token id) - name: string; // Token name (as a title) - decimals?: number; // Token amount of digits after the decimal point (as used in EVM) - precision?: number; // Token amount of digits after the decimal point (as used in native) - type?: EvmTokenType; // Token type (ERC20, ERC721, etc.) - logo?: string; // Token logo uri (as used in native) - logoURI?: string; // Token logo uri (as used in EVM) - metadata?: string; // Token contract metadata (as used in EVM) - isSystem: boolean; // True if the token is the main system token - isNative: boolean; // True if the token is a Antelope native blockchain token (false for EVM) - amount?: number | string; // posible balance amount - balance?: string; // posible balance amount - fullBalance?: string; // posible balance amount -} - -// A class to represent the price market information for a token -export class TokenMarketData { - readonly info: MarketSourceInfo; // Market information - private _price: ethers.BigNumber; // pre calculated Token price - - constructor(sourceInfo: MarketSourceInfo) { - this.info = sourceInfo; - try { - this._price = ethers.utils.parseUnits(sourceInfo.price, TOKEN_PRICE_DECIMALS); - } catch (e) { - this._price = ethers.constants.Zero; - } - } - - // Returns the token price - get price(): ethers.BigNumber { - return this._price; - } -} - -export class TokenPrice { - readonly market: TokenMarketData | null; - constructor(market: TokenMarketData | null) { - if (market?.price.gt(ethers.constants.Zero)) { - this.market = market; - } else { - this.market = null; - } - } - - get decimals(): number { - return this.market?.info.decimals || TOKEN_PRICE_DECIMALS; - } - - // Returns the token price as BigNumber - get value(): ethers.BigNumber { - return this.market?.price || ethers.constants.Zero; - } - - // Returns the token price as string containing a float number - get str(): string { - return ethers.utils.formatUnits(this.value, TOKEN_PRICE_DECIMALS); - } - - // Returns the inverse of the token price as BigNumber - get inverse(): ethers.BigNumber { - return ethers.utils.parseUnits('1', TOKEN_PRICE_DECIMALS * 2).div(this.value); - } - - // Returns the inverse of the token price as string containing a float number - get inverseStr(): string { - return ethers.utils.formatUnits(this.inverse, TOKEN_PRICE_DECIMALS); - } - - get isAvailable(): boolean { - return this.market !== null && this.market.price.gt(ethers.constants.Zero); - } - - // this supports the token.price.toString() expression - toString(): string { - return this.value.toString(); - } - - - // this function transforms a token amount into fiat amount and returns it as BigNumber - getAmountInFiat(tokensAmount: string | number | ethers.BigNumber): ethers.BigNumber { - // get the BigNumber value - let tokensAmountBn: ethers.BigNumber = ethers.constants.Zero; - if (typeof tokensAmount === 'string' || typeof tokensAmount === 'number') { - tokensAmountBn = ethers.utils.parseUnits(toStringNumber(tokensAmount), this.decimals); - } else { - tokensAmountBn = tokensAmount; - } - const fiatAmount = tokensAmountBn.mul(this.value).div(ethers.utils.parseUnits('1', this.decimals)); - return fiatAmount; - } - - // this function transforms a token amount into fiat amount and returns it as string containing a float number - getAmountInFiatStr(tokensAmount: string | number | ethers.BigNumber, decimals = 2): string { - return `${formatWei(this.getAmountInFiat(tokensAmount), TOKEN_PRICE_DECIMALS, decimals)}`; - } - - // this function transforms a fiat amount into token amount and returns it as BigNumber - getAmountInTokens(fiatAmount: string | number | ethers.BigNumber): ethers.BigNumber { - // get the BigNumber value - let fiatAmountBn: ethers.BigNumber = ethers.constants.Zero; - if (typeof fiatAmount === 'string' || typeof fiatAmount === 'number') { - fiatAmountBn = ethers.utils.parseUnits(toStringNumber(fiatAmount), this.decimals); - } else { - fiatAmountBn = fiatAmount; - } - const tokensAmount = fiatAmountBn.mul(ethers.utils.parseUnits('1', this.decimals)).div(this.value); - return tokensAmount; - } - - // this function transforms a fiat amount into token amount and returns it as string containing a float number - getAmountInTokensStr(fiatAmount: string | number | ethers.BigNumber, decimals = 2): string { - return `${formatWei(this.getAmountInTokens(fiatAmount), this.decimals, decimals)}`; - } - - // this function transforms a token amount into another given token amount and returns it as BigNumber - getAmountInThisToken(tokensAmount: string | number | ethers.BigNumber, targetToken: TokenClass): ethers.BigNumber { - // get the BigNumber value - let tokensAmountBn: ethers.BigNumber = ethers.constants.Zero; - if (typeof tokensAmount === 'string' || typeof tokensAmount === 'number') { - tokensAmountBn = ethers.utils.parseUnits(toStringNumber(tokensAmount), this.decimals); - } else { - tokensAmountBn = tokensAmount; - } - const targetAmount = tokensAmountBn.mul(this.value).div(targetToken.price.value); - return targetAmount; - } -} - -// A class to represent a blockchain token -export class TokenClass implements TokenSourceInfo { - readonly id: string; // Unique ID for the token -- - readonly symbol: string; // Token symbol - readonly name: string; // Token name (as a title) - readonly logo?: string; // Token logo uri - readonly contract: string; // Token contract address (for EVM) or account name (for native) - readonly chainId: string; // Chain ID (40 & 41 for Telos EVM) or hash (for native) - readonly network: string; // short name of the network (used for the token id) - readonly decimals: number; // Token amount of digits after the decimal point (same as precision for native) - readonly isSystem: boolean; // True if the token is the system token - readonly isNative: boolean; // True if the token is a native blockchain token - readonly type: EvmTokenType; // Token type (ERC20, ERC721, etc.) - private _price: TokenPrice; // Token price object - - constructor(sourceInfo: TokenSourceInfo) { - this.symbol = sourceInfo.symbol; - this.contract = sourceInfo.contract ?? sourceInfo.address ?? ''; - this.chainId = sourceInfo.chainId; - this.network = sourceInfo.network; - this.name = sourceInfo.name; - this.decimals = sourceInfo.decimals ?? sourceInfo.precision ?? WEI_PRECISION; - this.isSystem = sourceInfo.isSystem; - this.isNative = sourceInfo.isNative; - this.logo = sourceInfo.logo ?? sourceInfo.logoURI; - this.type = (sourceInfo.type?.toUpperCase() ?? ERC20_TYPE) as EvmTokenType; - this.id = `${this.symbol}-${this.contract}-${this.network}`; - this._price = new TokenPrice(null); - } - - // Sets the market data for the token to update token price - set market(market: TokenMarketData | null) { - this._price = new TokenPrice(market); - } - - get market(): TokenMarketData | null { - return this._price.market; - } - - // Returns the URI for the token logo - get logoURI(): string | undefined { - return this.logo; - } - - get address(): string { - return this.contract; - } - - get precision(): number { - return this.decimals; - } - - // Returns the token price - get price(): TokenPrice { - return this._price; - } - - // Returns the token source info - get sourceInfo(): TokenSourceInfo { - return { - symbol: this.symbol, - name: this.name, - logo: this.logo, - logoURI: this.logoURI, - contract: this.contract, - address: this.address, - chainId: this.chainId, - network: this.network, - decimals: this.decimals, - precision: this.precision, - isSystem: this.isSystem, - isNative: this.isNative, - amount: 0, - balance: '0', - fullBalance: '0', - }; - } - - toString(): string { - return this.symbol; - } -} - -// A class to represent the balance of a token -export class TokenBalance { - readonly token: TokenClass; - private _balanceStr: string; - private _balanceBn: ethers.BigNumber; - - constructor(token: TokenClass, balanceBn: ethers.BigNumber) { - this.token = token; - this._balanceBn = balanceBn; - this._balanceStr = `${ethers.utils.formatUnits(balanceBn, this.token.decimals)} ${this.token.symbol}`; - } - - set balance(balanceBn: ethers.BigNumber) { - this._balanceBn = balanceBn; - this._balanceStr = `${ethers.utils.formatUnits(balanceBn, this.token.decimals)} ${this.token.symbol}`; - } - - get balance(): ethers.BigNumber { - return this._balanceBn; - } - - // amount is an alias for balance - get amount(): ethers.BigNumber { - return this.balance; - } - - // value is an alias for balance - get value(): ethers.BigNumber { - return this.balance; - } - - get str(): string { - return this._balanceStr.split(' ')[0]; - } - - // Returns the fiat balance based on the current token price and balance - get fiatBalance(): ethers.BigNumber { - const price = this.token.price.value; - const fiatDouble = this.balance.mul(price); - const fiat = fiatDouble.div(ethers.utils.parseUnits('1', this.decimals)); - return fiat; - } - - get fiatStr(): string { - const fiat = this.fiatBalance; - return `${formatWei(fiat, TOKEN_PRICE_DECIMALS, 2)}`; - } - - get id(): string { - return this.token.id; - } - get symbol(): string { - return this.token.symbol; - } - get name(): string { - return this.token.name; - } - get logo(): string | undefined { - return this.token.logo; - } - get contract(): string { - return this.token.contract; - } - get chainId(): string { - return this.token.chainId; - } - get decimals(): number { - return this.token.decimals; - } - get isSystem(): boolean { - return this.token.isSystem; - } - get isNative(): boolean { - return this.token.isNative; - } - - toString(): string { - return this._balanceStr; - } -} diff --git a/src/antelope/types/TransactionV1.ts b/src/antelope/types/TransactionV1.ts deleted file mode 100644 index 60cffbbd9..000000000 --- a/src/antelope/types/TransactionV1.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface TransactionV1 { - id: string; - trx: { - receipt: { - status: string; - cpu_usage_us: number; - net_usage_words: number; - }; - trx: { - expiration: string; - ref_block_num: number; - ref_block_prefix: number; - max_net_usage_words: number; - max_cpu_usage_ms: number; - delay_sec: number; - }; - }; - block_time: string; - block_num: number; - last_irreversible_block: number; -} diff --git a/src/antelope/types/index.ts b/src/antelope/types/index.ts deleted file mode 100644 index 9c121127f..000000000 --- a/src/antelope/types/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -// interfaces for antelope -export * from 'src/antelope/types/ABIv1'; -export * from 'src/antelope/types/Actions'; -export * from 'src/antelope/types/AntelopeError'; -export * from 'src/antelope/types/Api'; -export * from 'src/antelope/types/ChainInfo'; -export * from 'src/antelope/types/ChainSettings'; -export * from 'src/antelope/types/EvmBlockData'; -export * from 'src/antelope/types/EvmContractData'; -export * from 'src/antelope/types/EvmLog'; -export * from 'src/antelope/types/EvmRexDeposit'; -export * from 'src/antelope/types/EvmTransaction'; -export * from 'src/antelope/types/ExceptionError'; -export * from 'src/antelope/types/Filters'; -export * from 'src/antelope/types/IndexerTypes'; -export * from 'src/antelope/types/KeyAccounts'; -export * from 'src/antelope/types/Basic'; -export * from 'src/antelope/types/NFTClass'; -export * from 'src/antelope/types/OpenSeaTypes'; -export * from 'src/antelope/types/PriceData'; -export * from 'src/antelope/types/Proposals'; -export * from 'src/antelope/types/Producers'; -export * from 'src/antelope/types/Providers'; -export * from 'src/antelope/types/Theme'; -export * from 'src/antelope/types/TokenClass'; -export * from 'src/antelope/types/TransactionV1'; - - -// classes for antelope -export * from 'src/antelope/types/AntelopeError'; - -// interfaces for antelope evm-abi -export * from 'src/antelope/wallets/utils/abi'; 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/authenticators/BraveAuth.ts b/src/antelope/wallets/authenticators/BraveAuth.ts deleted file mode 100644 index 1f7d25748..000000000 --- a/src/antelope/wallets/authenticators/BraveAuth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { EthereumProvider } from 'src/antelope/types'; -import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; - -const name = 'Brave'; -export const BraveAuthName = name; -export class BraveAuth extends InjectedProviderAuth { - - // this is just a dummy label to identify the authenticator base class - constructor(label = name) { - super(label); - } - - // InjectedProviderAuth API ------------------------------------------------------ - - getProvider(): EthereumProvider | null { - return window.ethereum as unknown as EthereumProvider ?? null; - } - - // 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 BraveAuth(label); - } -} diff --git a/src/antelope/wallets/authenticators/EVMAuthenticator.ts b/src/antelope/wallets/authenticators/EVMAuthenticator.ts deleted file mode 100644 index ae72463ef..000000000 --- a/src/antelope/wallets/authenticators/EVMAuthenticator.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -// EVMAuthenticator class - -import { SendTransactionResult, WriteContractResult } from '@wagmi/core'; -import { BigNumber, ethers } from 'ethers'; -import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; -import { CURRENT_CONTEXT, getAntelope, useAccountStore } from 'src/antelope/mocks'; -import { EVMChainSettings } from 'src/antelope/mocks'; -import { useChainStore } from 'src/antelope/mocks'; -import { useEVMStore } from 'src/antelope/mocks'; -import { isTracingAll, useFeedbackStore } from 'src/antelope/mocks/FeedbackStore'; -import { usePlatformStore } from 'src/antelope/mocks'; -import { AntelopeError, EvmABI, EvmFunctionParam, EvmTransactionResponse, ExceptionError, TokenClass, addressString } from 'src/antelope/types'; - -export abstract class EVMAuthenticator { - - readonly label: string; - readonly trace: (message: string, ...args: unknown[]) => void; - - constructor(label: string) { - this.label = label; - const name = `${this.getName()}(${label})`; - this.trace = createTraceFunction(name); - useFeedbackStore().setDebug(name, isTracingAll()); - } - abstract getName(): string; - abstract logout(): Promise; - abstract getSystemTokenBalance(address: addressString | string): Promise; - abstract getERC20TokenBalance(address: addressString | string, tokenAddress: addressString | string): Promise; - abstract signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise; - abstract transferTokens(token: TokenClass, amount: BigNumber, to: addressString | string): Promise; - abstract prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string): Promise; - abstract wrapSystemToken(amount: BigNumber): Promise; - abstract unwrapSystemToken(amount: BigNumber): Promise; - abstract stakeSystemTokens(amount: BigNumber): Promise; - abstract unstakeSystemTokens(amount: BigNumber): Promise; - abstract withdrawUnstakedTokens(): Promise; - abstract isConnectedTo(chainId: string): Promise; - abstract externalProvider(): Promise; - abstract web3Provider(): Promise; - abstract getSigner(): Promise; - - // to easily clone the authenticator - abstract newInstance(label: string): EVMAuthenticator; - - // indicates the authenticator is ready to transfer tokens - readyForTransfer(): boolean { - return true; - } - - // returns the associated account address acording to the label - getAccountAddress(): addressString { - return useAccountStore().getAccount(this.label).account as addressString; - } - - // returns the associated chain settings acording to the label - getChainSettings(): EVMChainSettings { - return (useChainStore().getChain(this.label).settings as EVMChainSettings); - } - - async login(network: string, trackAnalyticsEvents?: boolean): Promise { - this.trace('login', network); - this.trace('Login analytics enabled =', trackAnalyticsEvents); - - const chain = useChainStore(); - try { - chain.setChain(CURRENT_CONTEXT, network); - - const checkProvider = await this.ensureCorrectChain() as ethers.providers.Web3Provider; - - const accounts = await checkProvider.listAccounts(); - if (accounts.length > 0) { - return accounts[0] as addressString; - } else { - if (!checkProvider.provider.request) { - throw new AntelopeError('antelope.evm.error_support_provider_request'); - } - const accessGranted = await checkProvider.provider.request({ method: 'eth_requestAccounts' }); - if (accessGranted.length < 1) { - return null; - } - return accessGranted[0] as addressString; - } - } catch (error) { - if ((error as unknown as ExceptionError).code === 4001) { - throw new AntelopeError('antelope.evm.error_connect_rejected'); - } else { - console.error('Error:', error); - throw new AntelopeError('antelope.evm.error_login'); - } - } - } - - async autoLogin(network: string, account: string, trackAnalyticsEvents?: boolean): Promise { - this.trace('autoLogin', network, account); - this.trace('AutoLogin analytics enabled =', trackAnalyticsEvents); - - const chain = useChainStore(); - try { - chain.setChain(CURRENT_CONTEXT, network); - return account as addressString; - } catch (error) { - if ((error as unknown as ExceptionError).code === 4001) { - throw new AntelopeError('antelope.evm.error_connect_rejected'); - } else { - console.error('Error:', error); - throw new AntelopeError('antelope.evm.error_login'); - } - } - } - - async ensureCorrectChain(): Promise { - this.trace('ensureCorrectChain'); - if (usePlatformStore().isMobile) { - // we don't have tools to check the chain on mobile - return useEVMStore().ensureCorrectChain(this); - } else { - const showSwitchNotification = !(await this.isConnectedToCorrectChain()); - return useEVMStore().ensureCorrectChain(this).then((result) => { - if (showSwitchNotification) { - const ant = getAntelope(); - const networkName = useChainStore().getChain(this.label).settings.getDisplay(); - ant.config.notifyNeutralMessageHandler( - ant.config.localizationHandler('antelope.wallets.network_switch_success', { networkName }), - ); - } - return result; - }); - } - } - - isConnectedToCorrectChain(): Promise { - const correctChainId = useChainStore().getChain(this.label).settings.getChainId(); - return this.isConnectedTo(correctChainId); - } -} diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts deleted file mode 100644 index b69c7a741..000000000 --- a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts +++ /dev/null @@ -1,331 +0,0 @@ -/* eslint-disable max-len */ - - -import { BigNumber, ethers } from 'ethers'; -import { BehaviorSubject } from 'rxjs'; -import { map, filter } from 'rxjs/operators'; -import { useEVMStore, useFeedbackStore } from 'src/antelope'; -import { - AntelopeError, - EthereumProvider, - EvmABI, - EvmFunctionParam, - EvmTransactionResponse, - TokenClass, - addressString, - erc20Abi, - escrowAbiWithdraw, - stlosAbiDeposit, - stlosAbiWithdraw, - wtlosAbiDeposit, - wtlosAbiWithdraw, -} from 'src/antelope/types'; -import { BraveAuthName, EVMAuthenticator, MetamaskAuthName, SafePalAuthName } from 'src/antelope/wallets'; -import { TELOS_ANALYTICS_EVENT_NAMES, TELOS_NETWORK_NAMES } from 'src/antelope/mocks/chain-constants'; - -export abstract class InjectedProviderAuth extends EVMAuthenticator { - onReady = new BehaviorSubject(false); - - // this is just a dummy label to identify the authenticator base class - constructor(label: string) { - super(label); - useEVMStore().initInjectedProvider(this); - } - abstract getProvider(): EthereumProvider | null; - - async getSigner(): Promise { - const web3Provider = await this.web3Provider(); - return web3Provider.getSigner(); - } - - async ensureInitializedProvider(): Promise { - return new Promise((resolve, reject) => { - this.onReady.asObservable().pipe( - filter(ready => ready), - map(() => this.getProvider()), - ).subscribe((provider) => { - if (provider) { - resolve(provider); - } else { - reject(new AntelopeError('antelope.evm.error_no_provider')); - } - }); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private handleCatchError(error: any): AntelopeError { - if ('ACTION_REJECTED' === ((error as {code:string}).code)) { - return new AntelopeError('antelope.evm.error_transaction_canceled'); - } else { - // unknown error we print on console - console.error(error); - return new AntelopeError('antelope.evm.error_send_transaction', { error }); - } - } - - // this action is used by MetamaskAuth.transferTokens() - async sendSystemToken(to: string, value: ethers.BigNumber): Promise { - this.trace('sendSystemToken', to, value); - - // Send the transaction - return (await this.getSigner()).sendTransaction({ - to, - value, - }).then( - (transaction: ethers.providers.TransactionResponse) => transaction, - ).catch((error) => { - throw this.handleCatchError(error); - }); - } - - // EVMAuthenticator API ---------------------------------------------------------- - - async signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise { - this.trace('signCustomTransaction', contract, [abi], parameters, value?.toString()); - - const method = abi[0].name; - if (abi.length > 1) { - console.warn( - `signCustomTransaction: abi contains more than one function, - we assume the first one (${method}) is the one to be called`, - ); - } - - const signer = await this.getSigner(); - const contractInstance = new ethers.Contract(contract, abi, signer); - const transaction = await contractInstance[method](...parameters, { value }); - return transaction; - } - - async wrapSystemToken(amount: BigNumber): Promise { - this.trace('wrapSystemToken', amount.toString()); - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiDeposit, - [], - amount, - ).catch((error) => { - throw this.handleCatchError(error); - }); - } - - async unwrapSystemToken(amount: BigNumber): Promise { - this.trace('unwrapSystemToken', amount.toString()); - - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - const value = amount.toHexString(); - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiWithdraw, - [value], - ).catch((error) => { - throw this.handleCatchError(error); - }); - } - - async login(network: string, trackAnalyticsEvents?: boolean): Promise { - const chainSettings = this.getChainSettings(); - const authName = this.getName(); - const isTelos = TELOS_NETWORK_NAMES.includes(network); - - this.trace('login', network); - useFeedbackStore().setLoading(`${this.getName()}.login`); - - if (isTelos && trackAnalyticsEvents) { - this.trace('login', 'trackAnalyticsEvent -> login started'); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); - } - - const response = await super.login(network, trackAnalyticsEvents).then((res) => { - if (isTelos && trackAnalyticsEvents && TELOS_NETWORK_NAMES.includes(network)) { - let successfulLoginEventName = ''; - - if (authName === MetamaskAuthName) { - successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulMetamask; - } else if (authName === SafePalAuthName) { - successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulSafepal; - } else if (authName === BraveAuthName) { - successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulBrave; - } - - if (successfulLoginEventName) { - this.trace('login', 'trackAnalyticsEvent -> login succeeded', authName, successfulLoginEventName); - chainSettings.trackAnalyticsEvent(successfulLoginEventName); - } - - this.trace('login', 'trackAnalyticsEvent -> generic login succeeded', TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); - } - - return res; - }).catch((error) => { - // if the user rejects the connection, we don't want to track it as an error - if ( - trackAnalyticsEvents && - isTelos && - error.message !== 'antelope.evm.error_connect_rejected' - ) { - let failedLoginEventName = ''; - - if (authName === MetamaskAuthName) { - failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedMetamask; - } else if (authName === SafePalAuthName) { - failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedSafepal; - } else if (authName === BraveAuthName) { - failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedBrave; - } - - if (failedLoginEventName) { - this.trace('login', 'trackAnalyticsEvent -> login failed', authName, failedLoginEventName); - chainSettings.trackAnalyticsEvent(failedLoginEventName); - } - } - }).finally(() => { - useFeedbackStore().unsetLoading(`${this.getName()}.login`); - }); - - return response ?? null; - } - - 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]); - try { - 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'); - } - } catch (e) { - console.error('getERC20TokenBalance', e, address, token); - throw e; - } - } - - async transferTokens(token: TokenClass, amount: ethers.BigNumber, to: addressString): Promise { - this.trace('transferTokens', token, amount, to); - if (token.isSystem) { - return this.sendSystemToken(to, amount); - } else { - const value = amount.toHexString(); - const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); - return this.signCustomTransaction( - token.address, - transferAbi, - [to, value], - ); - } - } - - prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { - this.trace('prepareTokenForTransfer', [token], amount, to); - return new Promise((resolve) => { - resolve(); - }); - } - - /** - * This method creates a Transaction to stake system tokens - * @param amount amount of system tokens to stake - * @returns transaction response with the hash and a wait() method to wait confirmation - */ - async stakeSystemTokens(amount: BigNumber): Promise { - this.trace('stakeSystemTokens', amount.toString()); - - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiDeposit, - [], - amount, - ).catch((error) => { - throw this.handleCatchError(error); - }); - } - - /** - * This method creates a Transaction to unstake system tokens - * @param amount amount of system tokens to unstake - * @returns transaction response with the hash and a wait() method to wait confirmation - */ - async unstakeSystemTokens(amount: BigNumber): Promise { - this.trace('unstakeSystemTokens', amount.toString()); - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - const value = amount.toHexString(); - const from = this.getAccountAddress(); - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiWithdraw, - [value, from, from], - ); - } - - /** - * This method creates a Transaction to withdraw all unblocked staked tokens - */ - async withdrawUnstakedTokens() : Promise { - this.trace('withdrawUnstakedTokens'); - - // prepare variables - const chainSettings = this.getChainSettings(); - const escrowContractAddress = chainSettings.getEscrowContractAddress(); - - return this.signCustomTransaction( - escrowContractAddress, - escrowAbiWithdraw, - [], - ); - } - - async isConnectedTo(chainId: string): Promise { - this.trace('isConnectedTo', chainId); - return useEVMStore().isProviderOnTheCorrectChain(await this.web3Provider(), chainId); - } - - async externalProvider(): Promise { - return this.ensureInitializedProvider(); - } - - async web3Provider(): Promise { - this.trace('web3Provider'); - const web3Provider = new ethers.providers.Web3Provider(await this.externalProvider()); - await web3Provider.ready; - return web3Provider; - } - - async ensureCorrectChain(): Promise { - this.trace('ensureCorrectChain'); - return super.ensureCorrectChain(); - } - -} diff --git a/src/antelope/wallets/authenticators/MetamaskAuth.ts b/src/antelope/wallets/authenticators/MetamaskAuth.ts deleted file mode 100644 index 05cc06a22..000000000 --- a/src/antelope/wallets/authenticators/MetamaskAuth.ts +++ /dev/null @@ -1,32 +0,0 @@ - -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 - constructor(label = name) { - super(label); - } - - // InjectedProviderAuth API ------------------------------------------------------ - - getProvider(): EthereumProvider | null { - return window.ethereum as unknown as EthereumProvider ?? null; - } - - // 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 MetamaskAuth(label); - } - -} diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts deleted file mode 100644 index dca668bc9..000000000 --- a/src/antelope/wallets/authenticators/OreIdAuth.ts +++ /dev/null @@ -1,426 +0,0 @@ -/* eslint-disable max-len */ -import { AuthProvider, ChainNetwork, OreId, OreIdOptions, JSONObject, UserChainAccount } from 'oreid-js'; -import { BigNumber, ethers } from 'ethers'; -import { WebPopup } from 'oreid-webpopup'; -import { - EvmABI, - EvmFunctionParam, - erc20Abi, - escrowAbiWithdraw, - stlosAbiDeposit, - stlosAbiWithdraw, - wtlosAbiDeposit, - wtlosAbiWithdraw, -} from 'src/antelope/types'; -import { EVMAuthenticator } from 'src/antelope/wallets'; -import { - AntelopeError, - TokenClass, - addressString, - EvmTransactionResponse, -} from 'src/antelope/types'; -import { useFeedbackStore } from 'src/antelope'; -import { useChainStore } from 'src/antelope'; -import { RpcEndpoint } from 'universal-authenticator-library'; -import { TELOS_ANALYTICS_EVENT_NAMES } from 'src/antelope/mocks/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); - } - - // returns the associated account address acording to the label - getAccountAddress(): addressString { - return this.userChainAccount?.chainAccount as addressString; - } - - 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(); - } - localStorage.removeItem('autoLogin'); - localStorage.removeItem('rawAddress'); - return Promise.resolve(); - } - - async getSystemTokenBalance(address: addressString | string): Promise { - this.trace('getSystemTokenBalance', address); - try { - const provider = await this.web3Provider(); - if (provider) { - return provider.getBalance(address); - } else { - throw new AntelopeError('antelope.evm.error_no_provider'); - } - } catch (e) { - console.error('getSystemTokenBalance', e, address); - throw e; - } - } - - async getERC20TokenBalance(address: addressString, token: addressString): Promise { - this.trace('getERC20TokenBalance', [address, token]); - try { - 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'); - } - } catch (e) { - console.error('getERC20TokenBalance', e, address, token); - throw e; - } - } - - async prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { - this.trace('prepareTokenForTransfer', [token], amount, to); - } - - /** - * 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 { - - 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 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 (abi.length > 1) { - console.warn( - `signCustomTransaction: abi contains more than one function, - we assume 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); - } - - async wrapSystemToken(amount: BigNumber): Promise { - this.trace('wrapSystemToken', amount); - this.checkIntegrity(); - - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiDeposit, - [], - amount, - ); - } - - async unwrapSystemToken(amount: BigNumber): Promise { - this.trace('unwrapSystemToken', amount.toString()); - this.checkIntegrity(); - - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - const value = amount.toHexString(); - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiWithdraw, - [value], - ); - } - - async stakeSystemTokens(amount: BigNumber): Promise { - this.trace('stakeSystemTokens', amount.toString()); - this.checkIntegrity(); - - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiDeposit, - [], - amount, - ); - } - - async unstakeSystemTokens(amount: BigNumber): Promise { - this.trace('unstakeSystemTokens', amount.toString()); - this.checkIntegrity(); - - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - const value = amount.toHexString(); - const from = this.getAccountAddress(); - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiWithdraw, - [value, from, from], - ); - } - - async withdrawUnstakedTokens() : Promise { - this.trace('withdrawUnstakedTokens'); - this.checkIntegrity(); - - // prepare variables - const chainSettings = this.getChainSettings(); - const escrowContractAddress = chainSettings.getEscrowContractAddress(); - - return this.signCustomTransaction( - escrowContractAddress, - escrowAbiWithdraw, - [], - ); - } - - async transferTokens(token: TokenClass, amount: ethers.BigNumber, to: addressString): Promise { - this.trace('transferTokens', token, amount, to); - this.checkIntegrity(); - - // prepare variables - const from = this.getAccountAddress(); - const value = amount.toHexString(); - const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); - - if (token.isSystem) { - return this.performOreIdTransaction(from, { - from, - to, - value, - }); - } else { - return this.signCustomTransaction( - token.address, - transferAbi, - [to, value], - ); - } - } - - async isConnectedTo(chainId: string): Promise { - this.trace('isConnectedTo', chainId); - return true; - } - - 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; - } - } - - async externalProvider(): Promise { - this.trace('externalProvider'); - return new Promise((resolve) => { - resolve(null as unknown as ethers.providers.ExternalProvider); - }); - } - - async getSigner(): Promise { - this.trace('getSigner'); - const provider = await this.web3Provider(); - return provider.getSigner(); - } - -} diff --git a/src/antelope/wallets/authenticators/SafePalAuth.ts b/src/antelope/wallets/authenticators/SafePalAuth.ts deleted file mode 100644 index 4574c4367..000000000 --- a/src/antelope/wallets/authenticators/SafePalAuth.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 - constructor(label = name) { - super(label); - } - - // InjectedProviderAuth API ------------------------------------------------------ - - getProvider(): EthereumProvider | null { - return (window as unknown as {safepalProvider:unknown}).safepalProvider as EthereumProvider ?? null; - } - - // 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 SafePalAuth(label); - } -} diff --git a/src/antelope/wallets/authenticators/WalletConnectAuth.ts b/src/antelope/wallets/authenticators/WalletConnectAuth.ts deleted file mode 100644 index f0d9a4737..000000000 --- a/src/antelope/wallets/authenticators/WalletConnectAuth.ts +++ /dev/null @@ -1,512 +0,0 @@ -/* eslint-disable no-async-promise-executor */ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable no-unused-vars */ -/* eslint-disable no-undef */ -/* eslint-disable max-len */ -import { - PrepareSendTransactionResult, - PrepareWriteContractResult, - SendTransactionResult, - sendTransaction, - disconnect, - InjectedConnector, - fetchBalance, - getAccount, - prepareSendTransaction, - prepareWriteContract, - writeContract, - WriteContractResult, -} from '@wagmi/core'; -import { - EthereumClient, -} from '@web3modal/ethereum'; -import { Web3Modal, Web3ModalConfig } from '@web3modal/html'; -import { BigNumber, ethers } from 'ethers'; -import { useChainStore } from 'src/antelope'; -import { useContractStore } from 'src/antelope'; -import { useFeedbackStore } from 'src/antelope'; -import { usePlatformStore } from 'src/antelope'; -import { - AntelopeError, - EvmABI, - EvmFunctionParam, - TokenClass, - addressString, - erc20Abi, - escrowAbiWithdraw, - stlosAbiDeposit, - stlosAbiWithdraw, - wtlosAbiDeposit, - wtlosAbiWithdraw, -} from 'src/antelope/types'; -import { EVMAuthenticator } from 'src/antelope/wallets'; -import { RpcEndpoint } from 'universal-authenticator-library'; -import { toRaw } from 'vue'; -import { TELOS_ANALYTICS_EVENT_NAMES, TELOS_NETWORK_NAMES } from 'src/antelope/mocks/chain-constants'; - -const name = 'WalletConnect'; - -export class WalletConnectAuth extends EVMAuthenticator { - // debounce methods do not allow for async functions to be awaited; they return a promise which resolves immediately - // thus, we need to implement out own debounce so that we can await the async function (in this case, _prepareTokenForTransfer) - private _debounceTimer = setTimeout(() => {}, 0); - private _debouncedPrepareTokenConfigResolver: ((value: unknown) => void) | null; - private web3Modal: Web3Modal; - private unsubscribeWeb3Modal: null | (() => void) = null; - private usingQR = false; - - options: Web3ModalConfig; - wagmiClient: EthereumClient; - // this is just a dummy label to identify the authenticator base class - constructor(options: Web3ModalConfig, wagmiClient: EthereumClient, label = name) { - super(label); - this.options = options; - this.wagmiClient = wagmiClient; - this._debouncedPrepareTokenConfigResolver = null; - this.web3Modal = new Web3Modal(this.options, this.wagmiClient); - } - - // 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 WalletConnectAuth(this.options, this.wagmiClient, label); - } - - async walletConnectLogin(network: string, trackAnalyticsEvents: boolean): Promise { - this.trace('walletConnectLogin'); - const chainSettings = this.getChainSettings(); - const isOnTelos = TELOS_NETWORK_NAMES.includes(chainSettings.getNetwork()); - - try { - this.clearAuthenticator(); - const address = getAccount().address as addressString; - - // We are successfully logged in. Let's find out if we are using QR - this.usingQR = false; - const injected = new InjectedConnector(); - const provider = toRaw(await injected.getProvider()); - if (typeof provider === 'undefined') { - this.usingQR = true; - } else { - const providerAddress = (provider._state?.accounts) ? provider._state?.accounts[0] : ''; - const sameAddress = providerAddress.toLocaleLowerCase() === address.toLocaleLowerCase(); - this.usingQR = !sameAddress; - this.trace('walletConnectLogin', 'providerAddress:', providerAddress, 'address:', address, 'sameAddress:', sameAddress); - } - this.trace('walletConnectLogin', 'using QR:', this.usingQR); - - // We are already logged in. Now let's try to force the wallet to connect to the correct network - try { - if (!usePlatformStore().isMobile) { - await super.login(network); - } - } catch (e) { - // we are already logged in. So we just ignore the error - console.error(e); - } - - if (isOnTelos && trackAnalyticsEvents) { - this.trace( - 'login', - 'trackAnalyticsEvent -> login successful', - 'WalletConnect', - TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulWalletConnect, - ); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulWalletConnect); - this.trace( - 'login', - 'trackAnalyticsEvent -> generic login successful', - TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful, - ); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); - } - - return address; - } catch (e) { - // This is a non-expected error - console.error(e); - if (isOnTelos && trackAnalyticsEvents) { - this.trace( - 'walletConnectLogin', - 'trackAnalyticsEvent -> login failed', - 'WalletConnect', - TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect, - ); - const chainSettings = this.getChainSettings(); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect); - } - throw new AntelopeError('antelope.evm.error_login'); - } finally { - useFeedbackStore().unsetLoading(`${this.getName()}.login`); - } - } - - async login(network: string, trackAnalyticsEvents: boolean): Promise { - this.trace('login', network); - const wagmiConnected = () => localStorage.getItem('wagmi.connected'); - const chainSettings = this.getChainSettings(); - const isOnTelos = TELOS_NETWORK_NAMES.includes(chainSettings.getNetwork()); - - useFeedbackStore().setLoading(`${this.getName()}.login`); - if (wagmiConnected()) { - // We are in auto-login process. So log loginStarted before calling the walletConnectLogin method - if (isOnTelos && trackAnalyticsEvents) { - this.trace( - 'login', - 'trackAnalyticsEvent -> login started', - 'WalletConnect', - TELOS_ANALYTICS_EVENT_NAMES.loginStarted, - ); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); - } - return this.walletConnectLogin(network, trackAnalyticsEvents); - } else { - return new Promise((resolve) => { - this.trace('login', 'web3Modal.openModal()'); - - this.unsubscribeWeb3Modal = this.web3Modal.subscribeModal(async (newState: {open:boolean}) => { - this.trace('login', 'web3Modal.subscribeModal ', toRaw(newState), wagmiConnected); - - if (isOnTelos && newState.open === true && trackAnalyticsEvents) { - this.trace( - 'login', - 'trackAnalyticsEvent -> login started', - 'WalletConnect', - TELOS_ANALYTICS_EVENT_NAMES.loginStarted, - ); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); - } - - if (newState.open === false) { - useFeedbackStore().unsetLoading(`${this.getName()}.login`); - - if (isOnTelos && !wagmiConnected() && trackAnalyticsEvents) { - this.trace( - 'login', - 'trackAnalyticsEvent -> login failed', - 'WalletConnect', - TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect, - ); - chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect); - } - - // this prevents multiple subscribers from being attached to the web3Modal - // without this, every time the user logs out and back in again, this subscribeModal handler - // runs one more time than the last time - if (this.unsubscribeWeb3Modal) { - this.unsubscribeWeb3Modal(); - } - } - - if (wagmiConnected()) { - resolve(this.walletConnectLogin(network, true)); - } - }); - this.web3Modal.openModal(); - }); - } - } - - // having this two properties attached to the authenticator instance may bring some problems - // so after we use them we need to clear them to avoid that problems - clearAuthenticator(): void { - this.trace('clearAuthenticator'); - this.usingQR = false; - this.options = null as unknown as Web3ModalConfig; - this.wagmiClient = null as unknown as EthereumClient; - } - - async logout(): Promise { - this.trace('logout'); - if (localStorage.getItem('wagmi.connected')){ - await disconnect(); - } - } - - async getSystemTokenBalance(address: addressString): Promise { - this.trace('getSystemTokenBalance', address); - const chainId = +useChainStore().getChain(this.label).settings.getChainId(); - const balanceBn = await fetchBalance({ address, chainId }); - return BigNumber.from(balanceBn.value); - } - - async getERC20TokenBalance(address: addressString, token: addressString): Promise { - this.trace('getERC20TokenBalance', [address, token]); - const chainId = +useChainStore().getChain(this.label).settings.getChainId(); - const balance = await fetchBalance({ address, chainId, token }).then(balanceBn => balanceBn.value); - return BigNumber.from(balance); - } - - async signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise { - this.trace('signCustomTransaction', contract, [abi], parameters, value?.toString()); - - const method = abi[0].name; - if (abi.length > 1) { - console.warn( - `signCustomTransaction: abi contains more than one function, - we assume the first one (${method}) is the one to be called`, - ); - } - - const chainSettings = this.getChainSettings(); - - const config = { - chainId: +chainSettings.getChainId(), - address: contract, - abi: abi, - functionName: method, - args: parameters, - } as { - chainId: number; - address: addressString; - abi: EvmABI; - functionName: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any[]; - value?: bigint; - }; - - if (value) { - config.value = BigInt(value.toString()); - } - - this.trace('signCustomTransaction', 'prepareWriteContract ->', config); - const sendConfig = await prepareWriteContract(config); - - this.trace('signCustomTransaction', 'writeContract ->', sendConfig); - return await writeContract(sendConfig); - } - - - async transferTokens(token: TokenClass, amount: BigNumber, to: addressString): Promise { - this.trace('transferTokens', token, amount, to); - if (!this.sendConfig) { - throw new AntelopeError(token.isSystem ? - 'antelope.wallets.error_system_token_transfer_config' : - 'antelope.wallets.error_token_transfer_config', - ); - } else { - if (token.isSystem) { - return await sendTransaction(this.sendConfig as PrepareSendTransactionResult); - } else { - // prepare variables - const value = amount.toHexString(); - const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); - - return this.signCustomTransaction( - token.address, - transferAbi, - [to, value], - ); - } - } - } - - readyForTransfer(): boolean { - return !!this.sendConfig; - } - - sendConfig: PrepareSendTransactionResult | PrepareWriteContractResult | null = null; - private _debouncedPrepareTokenConfig(token: TokenClass | null, amount: BigNumber, to: string) { - // If there is already a pending call, clear it - if (this._debouncedPrepareTokenConfigResolver) { - clearTimeout(this._debounceTimer); - this._debouncedPrepareTokenConfigResolver(null); // Resolve with null when debounced - } - - // Create a new promise for this call - const promise = new Promise((resolve) => { - this._debouncedPrepareTokenConfigResolver = resolve; - }); - - // Set a timer to call the function after the delay - this._debounceTimer = setTimeout(async () => { - clearTimeout(this._debounceTimer); - const result = await this._prepareTokenForTransfer(token, amount, to); // Call the function - - if (this._debouncedPrepareTokenConfigResolver) { - this._debouncedPrepareTokenConfigResolver(result); // Resolve the promise with the result - } - }, 500); - - // Return the promise - return promise; - } - async _prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string) { - this.trace('prepareTokenForTransfer', [token], amount, to); - if (token) { - if (token.isSystem) { - this.sendConfig = await prepareSendTransaction({ - to, - value: BigInt(amount.toString()), - chainId: +useChainStore().getChain(this.label).settings.getChainId(), - }); - } else { - const abi = useContractStore().getTokenABI(token.type); - const functionName = 'transfer'; - this.sendConfig = await prepareWriteContract({ - chainId: +useChainStore().getChain(this.label).settings.getChainId(), - address: token.address as addressString, - abi, - functionName, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: [to, amount] as any[], - }); - } - } else { - this.sendConfig = null; - } - } - - async prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string): Promise { - this.sendConfig = null; - await this._debouncedPrepareTokenConfig(token, amount, to); - } - - async wrapSystemToken(amount: BigNumber): Promise { - this.trace('wrapSystemToken', amount); - - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiDeposit, - [], - amount, - ); - } - - async unwrapSystemToken(amount: BigNumber): Promise { - this.trace('unwrapSystemToken', amount); - - // prepare variables - const chainSettings = this.getChainSettings(); - const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; - - return this.signCustomTransaction( - wrappedSystemTokenContractAddress, - wtlosAbiWithdraw, - [amount.toString()], - ); - } - - async stakeSystemTokens(amount: BigNumber): Promise { - this.trace('stakeSystemTokens', amount); - - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiDeposit, - [], - amount, - ); - } - - async unstakeSystemTokens(amount: BigNumber): Promise { - this.trace('unstakeSystemTokens', amount); - - // prepare variables - const chainSettings = this.getChainSettings(); - const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; - const address = this.getAccountAddress(); - - return this.signCustomTransaction( - stakedSystemTokenContractAddress, - stlosAbiWithdraw, - [amount.toString(), address, address], - ); - } - - async withdrawUnstakedTokens(): Promise { - this.trace('withdrawUnstakedTokens'); - - // prepare variables - const chainSettings = this.getChainSettings(); - const escrowContractAddress = chainSettings.getEscrowContractAddress(); - - return this.signCustomTransaction( - escrowContractAddress, - escrowAbiWithdraw, - [], - ); - } - - 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; - this.trace('isConnectedTo', chainId, correct ? 'OK!' : 'not connected'); - resolve(correct); - }); - } - - async web3Provider(): Promise { - let web3Provider = null; - if (usePlatformStore().isMobile || this.usingQR) { - const p:RpcEndpoint = this.getChainSettings().getRPCEndpoint(); - const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; - web3Provider = new ethers.providers.JsonRpcProvider(url); - this.trace('web3Provider', 'JsonRpcProvider ->', web3Provider); - - // This is a hack to make the QR code work. - // this code is going to be used in EVMAuthenticator.ts login method - const listAccounts: () => Promise<`0x${string}`[]> = async () => [getAccount().address as addressString]; - web3Provider.listAccounts = listAccounts; - - } else { - web3Provider = new ethers.providers.Web3Provider(await this.externalProvider()); - this.trace('web3Provider', 'Web3Provider ->', web3Provider); - } - await web3Provider.ready; - return web3Provider as ethers.providers.Web3Provider; - } - - async getSigner(): Promise { - this.trace('getSigner'); - const web3Provider = await this.web3Provider(); - const signer = web3Provider.getSigner(); - this.trace('getSigner', 'signer ->', signer); - return signer; - } - - async externalProvider(): Promise { - this.trace('externalProvider'); - return new Promise(async (resolve) => { - const injected = new InjectedConnector(); - const provider = toRaw(await injected.getProvider()); - if (!provider) { - throw new AntelopeError('antelope.evm.error_no_provider'); - } - resolve(provider as unknown as ethers.providers.ExternalProvider); - }); - } - - async ensureCorrectChain(): Promise { - this.trace('ensureCorrectChain', 'QR:', this.usingQR); - if (this.usingQR) { - // we don't have tools to check the chain when using QR - return this.web3Provider(); - } else { - return super.ensureCorrectChain(); - } - } - -} diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts deleted file mode 100644 index 07b7d65ca..000000000 --- a/src/antelope/wallets/index.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; -export * from 'src/antelope/wallets/authenticators/OreIdAuth'; -export * from 'src/antelope/wallets/authenticators/InjectedProviderAuth'; -export * from 'src/antelope/wallets/authenticators/MetamaskAuth'; -export * from 'src/antelope/wallets/authenticators/SafePalAuth'; -export * from 'src/antelope/wallets/authenticators/WalletConnectAuth'; -export * from 'src/antelope/wallets/authenticators/BraveAuth'; -export * from 'src/antelope/mocks'; diff --git a/src/antelope/wallets/init.ts b/src/antelope/wallets/init.ts deleted file mode 100644 index 632611e42..000000000 --- a/src/antelope/wallets/init.ts +++ /dev/null @@ -1,90 +0,0 @@ -// register wallets ---------------------------------------------------------------- - -import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'; -import { Web3ModalConfig } from '@web3modal/html'; -import { OreIdOptions } from 'oreid-js'; -import { MetamaskAuth, OreIdAuth, SafePalAuth, WalletConnectAuth, BraveAuth } from 'src/antelope/wallets'; -import { configureChains, createConfig } from '@wagmi/core'; -import { telos, telosTestnet } from '@wagmi/core/chains'; -import { getAntelope } from 'src/antelope/mocks/AntelopeConfig'; -import { App } from 'vue'; -import { AntelopeError } from 'src/antelope/types'; - -/** - * This function is used to register the EVMAuthenticators that will be used by the app. - */ -export function initAntelope(app: App) { - const oreIdOptions: OreIdOptions = { - appName: process.env.APP_NAME, - appId: process.env.OREID_APP_ID as string, - }; - - const projectId = process.env.PROJECT_ID || '14ec76c44bae7d461fa0f5fd5f8a9da1'; - const chains = [telos, telosTestnet]; - - const { publicClient } = configureChains(chains, [w3mProvider({ projectId })]); - - // Wagmi Client -- - const wagmiConfig = createConfig({ - autoConnect: true, - connectors: w3mConnectors({ projectId, chains }), - publicClient, - }); - - const wagmiClient = new EthereumClient(wagmiConfig, chains); - - // Wagmi Options -- - const explorerRecommendedWalletIds = [ - // MetaMask - 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', - // SafePal - // '0b415a746fb9ee99cce155c2ceca0c6f6061b1dbca2d722b3ba16381d0562150', - ]; - const explorerExcludedWalletIds = 'ALL' as const; // Web3Modal option excludes all but recomended - const wagmiOptions: Web3ModalConfig = { projectId, explorerRecommendedWalletIds, explorerExcludedWalletIds }; - - const ant = getAntelope(); - ant.config.init(app); - - // settting notification handlers -- - ant.config.setNotifySuccessfulTrxHandler(app.config.globalProperties.$notifySuccessTransaction); - ant.config.setNotifySuccessMessageHandler(app.config.globalProperties.$notifySuccessMessage); - ant.config.setNotifySuccessCopyHandler(app.config.globalProperties.$notifySuccessCopy); - ant.config.setNotifyFailureMessage(app.config.globalProperties.$notifyFailure); - ant.config.setNotifyFailureWithAction(app.config.globalProperties.$notifyFailureWithAction); - ant.config.setNotifyDisconnectedHandler(app.config.globalProperties.$notifyDisconnected); - ant.config.setNotifyNeutralMessageHandler(app.config.globalProperties.$notifyNeutralMessage); - ant.config.setNotifyRememberInfoHandler(app.config.globalProperties.$notifyRememberInfo); - - - // setting authenticators getter -- - ant.config.setAuthenticatorsGetter( - () => app.config.globalProperties.$ual.getAuthenticators().availableAuthenticators); - - // setting translation handler -- - ant.config.setLocalizationHandler( - (key:string, payload?: Record) => app.config.globalProperties.$t(key, payload ? payload : {})); - - // setting transaction error handler -- - ant.config.setTransactionErrorHandler((err: object) => { - if (err instanceof AntelopeError) { - const evmErr = err as AntelopeError; - if (evmErr.message === 'antelope.evm.error_transaction_canceled') { - ant.config.notifyNeutralMessageHandler(ant.config.localizationHandler(evmErr.message)); - } else { - ant.config.notifyFailureMessage(ant.config.localizationHandler(evmErr.message), evmErr.payload); - } - } else { - ant.config.notifyFailureMessage(ant.config.localizationHandler('evm_wallet.general_error')); - } - }); - - // set evm authenticators -- - ant.wallets.addEVMAuthenticator(new WalletConnectAuth(wagmiOptions, wagmiClient)); - ant.wallets.addEVMAuthenticator(new OreIdAuth(oreIdOptions)); - ant.wallets.addEVMAuthenticator(new MetamaskAuth()); - ant.wallets.addEVMAuthenticator(new SafePalAuth()); - ant.wallets.addEVMAuthenticator(new BraveAuth()); - -} - diff --git a/src/antelope/wallets/utils/abi/erc1155.ts b/src/antelope/wallets/utils/abi/erc1155.ts deleted file mode 100644 index 5686fbc79..000000000 --- a/src/antelope/wallets/utils/abi/erc1155.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const erc1155Abi = [{ - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'account', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'operator', - 'type': 'address', - }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], - 'name': 'ApprovalForAll', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'from', - 'type': 'address', - }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { - 'indexed': false, - 'internalType': 'uint256[]', - 'name': 'ids', - 'type': 'uint256[]', - }, { 'indexed': false, 'internalType': 'uint256[]', 'name': 'values', 'type': 'uint256[]' }], - 'name': 'TransferBatch', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'from', - 'type': 'address', - }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { - 'indexed': false, - 'internalType': 'uint256', - 'name': 'id', - 'type': 'uint256', - }, { 'indexed': false, 'internalType': 'uint256', 'name': 'value', 'type': 'uint256' }], - 'name': 'TransferSingle', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': false, 'internalType': 'string', 'name': 'value', 'type': 'string' }, { - 'indexed': true, - 'internalType': 'uint256', - 'name': 'id', - 'type': 'uint256', - }], - 'name': 'URI', - 'type': 'event', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { - 'internalType': 'uint256', - 'name': 'id', - 'type': 'uint256', - }], - 'name': 'balanceOf', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address[]', 'name': 'accounts', 'type': 'address[]' }, { - 'internalType': 'uint256[]', - 'name': 'ids', - 'type': 'uint256[]', - }], - 'name': 'balanceOfBatch', - 'outputs': [{ 'internalType': 'uint256[]', 'name': '', 'type': 'uint256[]' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'operator', - 'type': 'address', - }], - 'name': 'isApprovedForAll', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'internalType': 'uint256[]', 'name': 'ids', 'type': 'uint256[]' }, { - 'internalType': 'uint256[]', - 'name': 'amounts', - 'type': 'uint256[]', - }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], - 'name': 'safeBatchTransferFrom', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }, { - 'internalType': 'uint256', - 'name': 'amount', - 'type': 'uint256', - }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], - 'name': 'safeTransferFrom', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { - 'internalType': 'bool', - 'name': 'approved', - 'type': 'bool', - }], - 'name': 'setApprovalForAll', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], - 'name': 'supportsInterface', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }], - 'name': 'uri', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc20.ts b/src/antelope/wallets/utils/abi/erc20.ts deleted file mode 100644 index 3da6be29c..000000000 --- a/src/antelope/wallets/utils/abi/erc20.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const erc20Abi = [ - { - 'inputs': [], - 'name': 'name', - 'outputs': [ - { - 'internalType': 'string', - 'name': '', - 'type': 'string', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'inputs': [], - 'name': 'symbol', - 'outputs': [ - { - 'internalType': 'string', - 'name': '', - 'type': 'string', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'inputs': [], - 'name': 'decimals', - 'outputs': [ - { - 'internalType': 'uint8', - 'name': '', - 'type': 'uint8', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'inputs': [], - 'name': 'totalSupply', - 'outputs': [ - { - 'internalType': 'uint256', - 'name': '', - 'type': 'uint256', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'inputs': [ - { - 'internalType': 'address', - 'name': 'account', - 'type': 'address', - }, - ], - 'name': 'balanceOf', - 'outputs': [ - { - 'internalType': 'uint256', - 'name': '', - 'type': 'uint256', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'inputs': [ - { - 'internalType': 'address', - 'name': 'recipient', - 'type': 'address', - }, - { - 'internalType': 'uint256', - 'name': 'amount', - 'type': 'uint256', - }, - ], - 'name': 'transfer', - 'outputs': [ - { - 'internalType': 'bool', - 'name': '', - 'type': 'bool', - }, - ], - 'stateMutability': 'nonpayable', - 'type': 'function', - }, - { - 'inputs': [ - { - 'internalType': 'address', - 'name': 'sender', - 'type': 'address', - }, - { - 'internalType': 'address', - 'name': 'recipient', - 'type': 'address', - }, - { - 'internalType': 'uint256', - 'name': 'amount', - 'type': 'uint256', - }, - ], - 'name': 'transferFrom', - 'outputs': [ - { - 'internalType': 'bool', - 'name': '', - 'type': 'bool', - }, - ], - 'stateMutability': 'nonpayable', - 'type': 'function', - }, - { - 'inputs': [ - { - 'internalType': 'address', - 'name': 'spender', - 'type': 'address', - }, - { - 'internalType': 'uint256', - 'name': 'amount', - 'type': 'uint256', - }, - ], - 'name': 'approve', - 'outputs': [ - { - 'internalType': 'bool', - 'name': '', - 'type': 'bool', - }, - ], - 'stateMutability': 'nonpayable', - 'type': 'function', - }, - { - 'inputs': [ - { - 'internalType': 'address', - 'name': 'owner', - 'type': 'address', - }, - { - 'internalType': 'address', - 'name': 'spender', - 'type': 'address', - }, - ], - 'name': 'allowance', - 'outputs': [ - { - 'internalType': 'uint256', - 'name': '', - 'type': 'uint256', - }, - ], - 'stateMutability': 'view', - 'type': 'function', - }, - { - 'anonymous': false, - 'inputs': [ - { - 'indexed': true, - 'internalType': 'address', - 'name': 'from', - 'type': 'address', - }, - { - 'indexed': true, - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, - { - 'indexed': false, - 'internalType': 'uint256', - 'name': 'value', - 'type': 'uint256', - }, - ], - 'name': 'Transfer', - 'type': 'event', - }, - { - 'anonymous': false, - 'inputs': [ - { - 'indexed': true, - 'internalType': 'address', - 'name': 'owner', - 'type': 'address', - }, - { - 'indexed': true, - 'internalType': 'address', - 'name': 'spender', - 'type': 'address', - }, - { - 'indexed': false, - 'internalType': 'uint256', - 'name': 'value', - 'type': 'uint256', - }, - ], - 'name': 'Approval', - 'type': 'event', - }, -] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc721.ts b/src/antelope/wallets/utils/abi/erc721.ts deleted file mode 100644 index 40e98c55b..000000000 --- a/src/antelope/wallets/utils/abi/erc721.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const erc721Abi = [{ - 'inputs': [{ - 'internalType': 'string', - 'name': '_name', - 'type': 'string', - }, { 'internalType': 'string', 'name': '_symbol', 'type': 'string' }, { - 'internalType': 'uint256', - 'name': '_maxTokens', - 'type': 'uint256', - }, { 'internalType': 'address', 'name': '_linkToken', 'type': 'address' }, { - 'internalType': 'address', - 'name': '_chainlinkCoordinator', - 'type': 'address', - }, { 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { - 'internalType': 'bytes32', - 'name': '_chainlinkHash', - 'type': 'bytes32', - }], - 'stateMutability': 'nonpayable', - 'type': 'constructor', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'approved', - 'type': 'address', - }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'Approval', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'operator', - 'type': 'address', - }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], - 'name': 'ApprovalForAll', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ - 'indexed': true, - 'internalType': 'address', - 'name': 'previousOwner', - 'type': 'address', - }, { 'indexed': true, 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], - 'name': 'OwnershipTransferred', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'baseURI', 'type': 'string' }], - 'name': 'SetBaseURI', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ - 'indexed': false, - 'internalType': 'uint256', - 'name': 'chainlinkFee', - 'type': 'uint256', - }, { 'indexed': false, 'internalType': 'bytes32', 'name': 'chainlinkHash', 'type': 'bytes32' }], - 'name': 'SetChainlinkConfig', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'defaultURI', 'type': 'string' }], - 'name': 'SetDefaultURI', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'minter', 'type': 'address' }], - 'name': 'SetMinter', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': false, 'internalType': 'uint256', 'name': 'seed', 'type': 'uint256' }, { - 'indexed': false, - 'internalType': 'bytes32', - 'name': 'requestId', - 'type': 'bytes32', - }], - 'name': 'SetRandomSeed', - 'type': 'event', -}, { - 'anonymous': false, - 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'indexed': true, - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'Transfer', - 'type': 'event', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'to', 'type': 'address' }, { - 'internalType': 'uint256', - 'name': 'tokenId', - 'type': 'uint256', - }], - 'name': 'approve', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }], - 'name': 'balanceOf', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'baseURI', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'chainlinkFee', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'chainlinkHash', - 'outputs': [{ 'internalType': 'bytes32', 'name': '', 'type': 'bytes32' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'defaultURI', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'finalBaseURI', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'getApproved', - 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'operator', - 'type': 'address', - }], - 'name': 'isApprovedForAll', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'maxTokens', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256' }], - 'name': 'metadataOf', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': '_to', 'type': 'address' }, { - 'internalType': 'uint256', - 'name': '_count', - 'type': 'uint256', - }], - 'name': 'mintMultiple', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'minter', - 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'name', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'owner', - 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'ownerOf', - 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'bytes32', 'name': 'requestId', 'type': 'bytes32' }, { - 'internalType': 'uint256', - 'name': 'randomness', - 'type': 'uint256', - }], - 'name': 'rawFulfillRandomness', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'renounceOwnership', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'safeTransferFrom', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }, { - 'internalType': 'bytes', - 'name': '_data', - 'type': 'bytes', - }], - 'name': 'safeTransferFrom', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'seed', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'seedReveal', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { - 'internalType': 'bool', - 'name': 'approved', - 'type': 'bool', - }], - 'name': 'setApprovalForAll', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'string', 'name': 'baseURI_', 'type': 'string' }, { - 'internalType': 'bool', - 'name': 'finalBaseUri_', - 'type': 'bool', - }], - 'name': 'setBaseURI', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { - 'internalType': 'bytes32', - 'name': '_chainlinkHash', - 'type': 'bytes32', - }], - 'name': 'setChainlinkConfig', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'string', 'name': '_defaultURI', 'type': 'string' }], - 'name': 'setDefaultURI', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': '_minter', 'type': 'address' }], - 'name': 'setMinter', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], - 'name': 'supportsInterface', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'symbol', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': 'index', 'type': 'uint256' }], - 'name': 'tokenByIndex', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { - 'internalType': 'uint256', - 'name': 'index', - 'type': 'uint256', - }], - 'name': 'tokenOfOwnerByIndex', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'tokenURI', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [], - 'name': 'totalSupply', - 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], - 'stateMutability': 'view', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { - 'internalType': 'address', - 'name': 'to', - 'type': 'address', - }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'transferFrom', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}, { - 'inputs': [{ 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], - 'name': 'transferOwnership', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', -}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc721Metadata.ts b/src/antelope/wallets/utils/abi/erc721Metadata.ts deleted file mode 100644 index 277a28bf4..000000000 --- a/src/antelope/wallets/utils/abi/erc721Metadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const erc721MetadataAbi = [{ - 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], - 'name': 'tokenURI', - 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], - 'stateMutability': 'view', - 'type': 'function', -}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/escrowAbi.ts b/src/antelope/wallets/utils/abi/escrowAbi.ts deleted file mode 100644 index 56b9ecda6..000000000 --- a/src/antelope/wallets/utils/abi/escrowAbi.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const escrowAbiWithdraw: EvmABI = [ - { - inputs: [], - name: 'withdraw', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -]; diff --git a/src/antelope/wallets/utils/abi/index.ts b/src/antelope/wallets/utils/abi/index.ts deleted file mode 100644 index 4a83a7dcf..000000000 --- a/src/antelope/wallets/utils/abi/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -export * from 'src/antelope/wallets/utils/abi/erc721'; -export * from 'src/antelope/wallets/utils/abi/erc721Metadata'; -export * from 'src/antelope/wallets/utils/abi/erc1155'; -export * from 'src/antelope/wallets/utils/abi/erc20'; -export * from 'src/antelope/wallets/utils/abi/supportsInterface'; -export * from 'src/antelope/wallets/utils/abi/wrapAbi'; -export * from 'src/antelope/wallets/utils/abi/stlosAbi'; -export * from 'src/antelope/wallets/utils/abi/escrowAbi'; -export * from 'src/antelope/wallets/utils/abi/signature/transfer_signatures'; - -export type StateMutabilityType = 'pure' | 'view' | 'nonpayable' | 'payable'; -export type addressString = `0x${string}`; // required wagmi type - -export type EvmABI = EvmABIEntry[]; - -export interface EvmABIEntry { - constant?: boolean; - payable?: boolean; - anonymous?: boolean; - inputs?: EvmABIEntryInput[]; - outputs?: EvmABIEntryOutput[]; - stateMutability?: StateMutabilityType; - name: string; - type: string; -} - -export interface EvmABIEntryInput { - indexed: boolean; - internalType: string; - name: string; - type: string; -} - -export interface EvmABIEntryOutput { - internalType: string; - name: string; - type: string; -} - -export interface AbiSignature { - text_signature: string; -} - diff --git a/src/antelope/wallets/utils/abi/signature/events_signatures.ts b/src/antelope/wallets/utils/abi/signature/events_signatures.ts deleted file mode 100644 index 9370fd475..000000000 --- a/src/antelope/wallets/utils/abi/signature/events_signatures.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable max-len */ -export const events_signatures = { - '0xddf252ad': 'event Transfer(address indexed from, address indexed to, uint256 value)', - '0x8c5be1e5': 'event Approval(address indexed owner, address indexed spender, uint256 value)', - '0x71bab65c': 'event Harvest(address indexed sender, uint256 performanceFee, uint256 callFee)', - '0x884edad9': 'event Withdraw(address indexed user, uint256 amount)', - '0xf279e6a1': 'event Withdraw(address indexed user, uint256 indexed pid, uint256 amount)', - '0x90890809': 'event Deposit(address indexed user, uint256 indexed pid, uint256 amount)', - '0xe1fffcc4': 'event Deposit(address indexed sender, uint256 value)', - '0x4c209b5f': 'event Mint(address indexed sender, uint256 amount0, uint256 amount1)', - '0xd78ad95f': 'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)', - '0xa2c38e2d': 'event Claim(address indexed account, uint256 amount, bool indexed automatic)', - '0xee503bee': 'event DividendWithdrawn(address indexed to, uint256 weiAmount)', - '0x38567aa9': 'event NewTransmission(uint32 indexed aggregatorRoundId, int192 answer, address transmitter, uint32 observationsTimestamp)', - '0x0109fc6f': 'event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt)', - '0x0559884f': 'event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt)', - '0x1c411e9a': 'event Sync(uint112 reserve0, uint112 reserve1)', - '0x5beea7b3': 'event EvInventoryUpdate(uint256 indexed id, tuple(address,address,address,uint256,uint256,uint256,uint8,uint8) inventory)', - '0x8be0079c': 'event OwnershipTransferred(address previousOwner, address newOwner)', - '0x34fcbac0': 'event Claim(address indexed user, uint256 indexed pid, uint256 amount)', - '0xda919360': 'event BorrowAllowanceDelegated(address indexed fromUser, address indexed toUser, address asset, uint256 amount)', - '0xdccd412f': 'event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to)', - '0x4a39dc06': 'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids,uint256[] amounts)', - '0xc3d58168': 'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 amount)', -} as { [prefix: string]: string }; - diff --git a/src/antelope/wallets/utils/abi/signature/functions_signatures.ts b/src/antelope/wallets/utils/abi/signature/functions_signatures.ts deleted file mode 100644 index cefc921ec..000000000 --- a/src/antelope/wallets/utils/abi/signature/functions_signatures.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable max-len */ -export const functions_overrides = { - '0xa9059cbb': 'function transfer(address to, uint amount)', - '0xaac48653': 'function mint(address account, uint256 id, uint256 amount, uint256 maximum, string tokenUri, bytes data)', - '0xf5298aca': 'function burn(address account,uint256 id,uint256 value)', - '0x18cbafe5': 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)', - '0x095ea7b3': 'function approve(address spender, uint256 amount)', - '0x7ff36ab5': 'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)', - '0xfb3bdb41': 'function swapETHForExactTokens(uint256 amountOut, address[] path, address to, uint256 deadline)', - '0x38ed1739': 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', - '0xded9382a': 'function removeLiquidityETHWithPermit(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', - '0xf305d719': 'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline)', - '0xa22cb465': 'function setApprovalForAll(address to, bool approved)', - '0x23b872dd': 'function transferFrom(address sender, address recipient, uint256 amount)', - '0xf242432a': 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', - '0xe8e33700': 'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)', - '0x4a25d94a': 'function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline)', - '0x5c11d795': 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', - '0x0cd5840b': 'function create(address[] offerProperties_, uint256[] offerIds_, uint256[] offerValues_, address[] demandProperties_, uint256[] demandIds_, uint256[] demandValues_)', - '0xb583cc2c': 'function setSaleStartEnd(string eventCode, uint256 start, uint256 end)', - '0xbea9849e': 'function setUniswapRouter(address _new)', - '0x860665b3': 'function openTrove(uint256 _maxFeePercentage, uint256 _LUSDAmount, address _upperHint, address _lowerHint)', - '0x2195995c': 'function removeLiquidityWithPermit(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', - '0x70a08231': 'function balanceOf(address)', - '0xf23a6e61': 'function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data)', - '0x3542aee2': 'function mintByOwner(address to, uint256 tokenType)', -} as { [prefix: string]: string }; - diff --git a/src/antelope/wallets/utils/abi/signature/index.ts b/src/antelope/wallets/utils/abi/signature/index.ts deleted file mode 100644 index 88a3fc857..000000000 --- a/src/antelope/wallets/utils/abi/signature/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from 'src/antelope/wallets/utils/abi/signature/events_signatures'; -export * from 'src/antelope/wallets/utils/abi/signature/functions_signatures'; -export * from 'src/antelope/wallets/utils/abi/signature/transfer_signatures'; diff --git a/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts b/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts deleted file mode 100644 index ba5deb690..000000000 --- a/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const TRANSFER_SIGNATURES = ['0xddf252ad', '0xa9059cbb', '0xf242432a', '0xc3d58168']; -export const ERC1155_TRANSFER_SIGNATURE = '0xc3d58168'; diff --git a/src/antelope/wallets/utils/abi/stlosAbi.ts b/src/antelope/wallets/utils/abi/stlosAbi.ts deleted file mode 100644 index 66cdf41d6..000000000 --- a/src/antelope/wallets/utils/abi/stlosAbi.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const stlosAbiDeposit: EvmABI = [ - { - constant: false, - inputs: [], - name: 'depositTLOS', - outputs: [], - payable: true, - stateMutability: 'payable', - type: 'function', - }, -]; - -export const stlosAbiWithdraw: EvmABI = [ - { - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'assets', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'receiver', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'owner', - type: 'address', - }, - ], - name: 'withdraw', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, -]; diff --git a/src/antelope/wallets/utils/abi/supportsInterface.ts b/src/antelope/wallets/utils/abi/supportsInterface.ts deleted file mode 100644 index 04a96ace3..000000000 --- a/src/antelope/wallets/utils/abi/supportsInterface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const supportsInterfaceAbi = [{ - 'constant': true, - 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], - 'name': 'supportsInterface', - 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], - 'payable': false, - 'stateMutability': 'view', - 'type': 'function', -}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/wrapAbi.ts b/src/antelope/wallets/utils/abi/wrapAbi.ts deleted file mode 100644 index 4bad7a463..000000000 --- a/src/antelope/wallets/utils/abi/wrapAbi.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { EvmABI } from 'src/antelope/wallets/utils/abi'; - -export const wtlosAbiDeposit: EvmABI = [ - { - constant: false, - inputs: [], - name: 'deposit', - outputs: [], - payable: true, - stateMutability: 'payable', - type: 'function', - }, -]; - -export const wtlosAbiWithdraw: EvmABI = [ - { - constant: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'wad', - type: 'uint256', - }, - ], - name: 'withdraw', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, -]; diff --git a/src/antelope/wallets/utils/contracts/EvmContract.ts b/src/antelope/wallets/utils/contracts/EvmContract.ts deleted file mode 100644 index 385cdad81..000000000 --- a/src/antelope/wallets/utils/contracts/EvmContract.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* eslint-disable max-len */ -import { ContractInterface, ethers } from 'ethers'; -import { markRaw } from 'vue'; -import { - AntelopeError, EvmContractCalldata, - EvmABI, - EvmContractCreationInfo, - EvmContractConstructorData, - EvmContractManagerI, - EvmFormatedLog, - EvmLog, - EvmLogs, - TokenSourceInfo, - TRANSFER_SIGNATURES, -} from 'src/antelope/types'; -import { Interface } from 'ethers/lib/utils'; - - -export default class EvmContract { - private readonly _name: string; - private readonly _abi?: EvmABI | null; - private readonly _address: string; - private readonly _creationInfo?: EvmContractCreationInfo | null; - private readonly _interface?: ContractInterface | null; - private readonly _supportedInterfaces: string[]; - private readonly _properties?: EvmContractCalldata; - private readonly _manager?: EvmContractManagerI; - private readonly _token?: TokenSourceInfo | null; - - private _verified?: boolean; - - constructor({ - name, - abi, - address, - creationInfo, - verified, - supportedInterfaces = ['none'], - properties, - manager, - token, - }: EvmContractConstructorData) { - this._name = name; - this._address = address; - this._creationInfo = creationInfo; - this._verified = verified ?? false; - this._properties = properties; - this._manager = manager; - - if (abi) { - this._abi = typeof abi === 'string' ? JSON.parse(abi) : abi; - this._interface = markRaw(new ethers.utils.Interface(abi)); - } - - if (token) { - this._token = token; - } - - const indexOfNone = supportedInterfaces.indexOf('none'); - this._supportedInterfaces = []; - for (let i = 0; i < supportedInterfaces.length; i++){ - if (i !== indexOfNone) { - this._supportedInterfaces.push(supportedInterfaces[i]); - } - } - } - - - get name() { - return this._name; - } - - get abi() { - return this._abi; - } - - get address() { - return this._address; - } - - get creationInfo() { - return this._creationInfo; - } - - get iface() { - return this._interface; - } - - get verified() { - return this._verified ?? false; - } - - set verified(verified: boolean) { - this._verified = verified; - } - - get supportedInterfaces() { - return this._supportedInterfaces; - } - - get creationBlock() { - return this._creationInfo?.block; - } - - get creationTrx() { - return this._creationInfo?.transaction; - } - - get creator() { - return this._creationInfo?.creator; - } - - get properties() { - return this._properties; - } - - get token() { - return this._token; - } - - isNonFungible() { - return (this._supportedInterfaces.includes('erc721')); - } - - isToken() { - if (this._supportedInterfaces.length === 0) { - return false; - } - - return ( - this._supportedInterfaces.includes('erc721') || - this._supportedInterfaces.includes('erc1155') || - this._supportedInterfaces.includes('erc20') - ); - } - - async getContractInstance() { - if (!this.abi){ - throw new AntelopeError('antelope.utils.error_contract_instance'); - } - const signer = await this._manager?.getSigner(); - - return new ethers.Contract(this.address, this.abi, signer); - } - - async parseTransaction(data:string) { - if (this.iface && this.iface instanceof Interface) { - try { - return await this.iface.parseTransaction({ data }); - } catch (e) { - console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); - } - } else { - try { - // this functionIface is an interface for a single function signature as discovered via 4bytes.directory... only use it for this function - const functionIface = await this._manager?.getFunctionIface(data); - if (functionIface) { - return functionIface.parseTransaction({ data }); - } - } catch (e) { - console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); - } - } - throw new AntelopeError('antelope.utils.error_parsing_transaction'); - } - - async parseLogs(logs: EvmLogs): Promise { - if (this.iface && this.iface instanceof Interface) { - const iface = this.iface; - const parsedArray = await Promise.all(logs.map(async (log) => { - try { - const parsedLog:ethers.utils.LogDescription = iface.parseLog(log); - return this.formatLog(log, parsedLog); - } catch (e) { - return this.parseEvent(log); - } - })); - parsedArray.forEach((parsed) => { - if(parsed.name && parsed.eventFragment?.inputs){ - parsed.inputs = parsed.eventFragment.inputs; - } - }); - return parsedArray; - } - - - return await Promise.all(logs.map(async (log) => { - const parsedLog = await this.parseEvent(log); - if(parsedLog.name && parsedLog.eventFragment?.inputs){ - parsedLog.inputs = parsedLog.eventFragment.inputs; - } - return parsedLog; - })); - } - - formatLog(log: EvmLog, parsedLog: ethers.utils.LogDescription): EvmFormatedLog { - if(!parsedLog.signature) { - console.error('No signature found for log! Check if this explodes. Returning EvmLog instead of EvmFormatedLog. '); - return log as unknown as EvmFormatedLog; - } - const function_signature = log.topics[0].substring(0, 10); - return { - ... parsedLog, - function_signature, - isTransfer: TRANSFER_SIGNATURES.includes(function_signature), - logIndex: log.logIndex, - address: log.address, - token: this._token, - name: parsedLog.signature, - } as EvmFormatedLog; - } - - async parseEvent(log: EvmLog): Promise { - const eventIface = await this._manager?.getEventIface(log.topics[0]); - if (eventIface) { - try { - const parsedLog:ethers.utils.LogDescription = eventIface.parseLog(log); - return this.formatLog(log, parsedLog); - } catch(e) { - throw new AntelopeError('antelope.utils.error_parsing_log_event', log); - } - } else { - throw new AntelopeError('antelope.utils.error_parsing_log_event', log); - } - } -} - -export interface Erc20Transfer { - index: number; - address: string; - value: string; // string representation of hex number - decimals?: number; - to: string; - from: string; - symbol?: string; -} diff --git a/src/antelope/wallets/utils/contracts/EvmContractFactory.ts b/src/antelope/wallets/utils/contracts/EvmContractFactory.ts deleted file mode 100644 index 8d49bbad0..000000000 --- a/src/antelope/wallets/utils/contracts/EvmContractFactory.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable max-len */ -import { erc1155Abi, erc20Abi, erc721Abi } from 'src/antelope/wallets/utils/abi'; -import EvmContract from 'src/antelope/wallets/utils/contracts/EvmContract'; -import { AntelopeError, EvmContractCalldata, EvmContractMetadata, EvmContractFactoryData } from 'src/antelope/types'; - -export default class EvmContractFactory { - buildContract(data: EvmContractFactoryData): EvmContract { - if (!data || !data.address) { - throw new AntelopeError('antelope.contracts.contract_data_required'); - } - - let verified = false; - if (typeof data.abi !== 'undefined' && data.abi.length > 0) { - data.abi = (typeof data.abi === 'string') ? JSON.parse(data.abi) : data.abi; - } else if (typeof data.metadata !== 'undefined' && data.metadata?.length > 0) { - const metadata: EvmContractMetadata = JSON.parse(data.metadata); - data.abi = metadata?.output?.abi; - } - if (data.abi) { - verified = true; - } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc20')) { - data.abi = erc20Abi; - } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc721')) { - data.abi = erc721Abi; - } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc1155')) { - data.abi = erc1155Abi; - } - - const properties: EvmContractCalldata = (data.calldata) ? JSON.parse(data.calldata) : {}; - - if (!data.name) { - if (properties?.name){ - data.name = properties.name; - } else if (data.metadata) { - const metadata: EvmContractMetadata = JSON.parse(data.metadata); - - if(metadata?.settings?.compilationTarget){ - data.name = Object.values(metadata?.settings?.compilationTarget)[0]; - } - } - } - const abi = typeof data.abi === 'string' ? JSON.parse(data.abi) : data.abi; - - return new EvmContract({ - address: data.address, - name: data.name ?? '', - verified: verified, - creationInfo: { - creator: data.creator, - transaction: data.transaction ?? '', - creation_trx: data.transaction ?? '', - block: data.block, - block_num: data.block, - timestamp: data.timestamp ?? '', - abi: data.abi, - }, - supportedInterfaces: data.supportedInterfaces ?? [], - abi, - properties, - manager: data.manager, - }); - } -} diff --git a/src/antelope/wallets/utils/currency-utils.ts b/src/antelope/wallets/utils/currency-utils.ts deleted file mode 100644 index be3f2c673..000000000 --- a/src/antelope/wallets/utils/currency-utils.ts +++ /dev/null @@ -1,387 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -import { BigNumber } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; -import { formatUnits } from '@ethersproject/units'; -import Decimal from 'decimal.js'; -import { WEI_PRECISION } from 'src/antelope/wallets/utils'; - -/** - * Given a number or string, returns a string representation of the number with up to 18 decimal places - * @param value - number or string to convert to string - * @returns {string} string representation of the number - */ - -export function toStringNumber(value: number | string): string { - if (typeof value === 'string') { - return value; - } else if (typeof value === 'number') { - const num = new Decimal(value); - return num.toFixed(WEI_PRECISION); - } else { - throw new Error('Invalid value type: ' + typeof value); - } -} - - -/** - * Given a locale string, returns the character used to separate integer and decimal portions of a number, - * e.g "." as in 123.456 - * @param locale - standard locale code, such as "en-US" - * @returns {string} decimal separator character - */ -export function getDecimalSeparatorForLocale(locale: string) { - const numberWithDecimalSeparator = 1.1; - const formattedNumber = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator); - return formattedNumber.charAt(1); // Get the character between "1" and "1" -} - -/** - * Given a locale string, returns the character used to separate groups of numbers in a large number, - * e.g "," as in 123,456,789.00 - * - * @param { string } locale - standard locale code, such as "en-US" - * - * @returns {string} large number separator character - */ -export function getLargeNumberSeparatorForLocale(locale: string) { - const largeNumber = 1000000; - const formattedNumber = new Intl.NumberFormat(locale).format(largeNumber); - - const nonDigitCharacters = formattedNumber.match(/\D+/g); - - if (!nonDigitCharacters || nonDigitCharacters.length === 0) { - return ''; - } - - return nonDigitCharacters[0]; -} - -/** - * Given a localized number string, returns a BigNumber - * - * @param {string} formatted - localized number string, e.g. "123,456.78" - * @param {number} decimals - number of decimals the number has, e.g. 2 for 123,456.78 - * @param {string} locale - standard locale code, such as "en-US" - * - * @returns {BigNumber} BigNumber representation of the number - */ -export function getBigNumberFromLocalizedNumberString(formatted: string, decimals: number, locale: string): BigNumber { - const decimalSeparator = getDecimalSeparatorForLocale(locale); - const largeNumberSeparator = getLargeNumberSeparatorForLocale(locale); - const notIntegerOrSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}${largeNumberSeparator}]`, 'g'); - - if (formatted.match(notIntegerOrSeparatorRegex)) { - throw new Error('Invalid number format'); - } - - // if decimals is not a positive integer, throw an error - if (decimals % 1 !== 0 || decimals < 0) { - throw new Error('Invalid decimals value'); - } - - // strip any character which is not an integer or decimal separator - const notIntegerOrDecimalSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}]`, 'g'); - let unformatted = formatted.replace(notIntegerOrDecimalSeparatorRegex, ''); - - // if the decimal separator is anything but a dot, replace it with a dot to allow conversion to number - if (decimalSeparator !== '.') { - unformatted = unformatted.replace(/[^0-9.]/g, '.'); - } - - return parseUnits(unformatted, decimals); -} - - -/* -* Formats a currency amount in a localized way -* -* @param {number|BigNumber} amount - the currency amount -* @param {number} precision - the number of decimals that should be displayed. Ignored if abbreviate is true and the value is over 1000 -* @param {string} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale -* @param {boolean} abbreviate - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 -* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency -* @param {boolean?} displayCurrencyAsCode - if currency is defined, controls whether the currency is display as a symbol or code, e.g. $100 or USD 100. Only valid for fiat currencies. -* @param {number?} tokenDecimals - required if amount is BigNumber. The number of decimals a token has, e.g. 18 for TLOS. This option is not used for non-BigNumber amounts -* @param {boolean?} trimZeroes - trim trailing zeroes for decimal values, e.g. '123.000' => '123', '123.45600' => '123.456'. Overrides 'precision' when there are trailing zeroes -* */ -export function prettyPrintCurrency( - amount: number | BigNumber, - precision: number, - locale: string, - abbreviate = false, - currency?: string, - displayCurrencyAsCode?: boolean, - tokenDecimals?: number, - trimZeroes?: boolean, -): string { - if (precision % 1 !== 0 || precision < 0) { - throw new Error('Precision must be a positive integer or zero'); - } - - if (typeof tokenDecimals === 'number' && (tokenDecimals % 1 !== 0 || tokenDecimals < 0)) { - throw new Error('Token decimals must be a positive integer or zero'); - } - - // require token decimals if type is BigNumber - if (typeof amount !== 'number' && typeof tokenDecimals !== 'number') { - throw new Error('Token decimals is required for BigNumber amounts'); - } - - const decimalSeparator = getDecimalSeparatorForLocale(locale); - const trailingZeroesRegex = new RegExp(`(\\d)\\${decimalSeparator}0+(\\D|$)`, 'g'); - - const decimalOptions : Record = { - maximumFractionDigits: precision, - minimumFractionDigits: precision, - minimumIntegerDigits: undefined, - maximumIntegerDigits: undefined, - }; - - const currencyOptions : Record = { - style: currency ? 'currency' : undefined, - currencyDisplay: currency ? (displayCurrencyAsCode ? 'code' : 'symbol') : undefined, - currency, - }; - - if (typeof amount === 'number') { - if (amount < 1 && amount > 0) { - decimalOptions.maximumIntegerDigits = 1; - decimalOptions.minimumIntegerDigits = 1; - } else if (abbreviate) { - const forceFractionDisplay = amount < 1000 && amount > -1000 ; - - decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; - decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; - decimalOptions.maximumIntegerDigits = 3; - } - - let finalFormattedValue = Intl.NumberFormat( - locale, - { - notation: abbreviate ? 'compact' : undefined, - ...currencyOptions, - ...decimalOptions, - }).format(amount); - - if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { - finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, '$1'); - } - - return finalFormattedValue; - } else { - if (amount.lt(1) && amount.gt(0)) { - decimalOptions.maximumIntegerDigits = 1; - decimalOptions.minimumIntegerDigits = 1; - } else if (abbreviate) { - const forceFractionDisplay = amount.lt(1000) && amount.gt(-1000); - - decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; - decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; - decimalOptions.maximumIntegerDigits = 3; - } - - // Intl format method only takes number / bigint, and a BigNumber value cannot have a fractional amount, - // and also decimals may be more places than maximum JS precision. - // As such, decimals must be handled specially for BigNumber amounts. - - const amountAsString = formatUnits(amount, tokenDecimals); // amount string, like "1.0" - - const [integerString, decimalString] = amountAsString.split('.'); - - const formattedInteger = Intl.NumberFormat( - locale, - { notation: abbreviate ? 'compact' : undefined }, - ).format(BigInt(integerString)); - - const formattedDecimal = decimalString.slice(0, precision || 1).padEnd(precision, '0'); - - let finalFormattedValue; - - if (abbreviate) { - finalFormattedValue = formattedInteger; // drop decimals for abbreviated amounts - } else { - finalFormattedValue = `${formattedInteger}${decimalSeparator}${formattedDecimal}`; - } - - if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { - finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, '$1'); - } - - if (precision === 2 && tokenDecimals === 2 && currency) { - // value is a fiat currency with 2 decimals, so add the currency symbol - if (displayCurrencyAsCode) { - finalFormattedValue = `${finalFormattedValue}\u00A0${currency}`; - } else { - const symbol = getCurrencySymbol(locale, currency); - finalFormattedValue = `${symbol}${finalFormattedValue}`; - } - - } else if (currency) { - finalFormattedValue += ` ${currency}`; - } - - return finalFormattedValue; - } -} - - -/** - * Converts a currency amount from one token to another - * - * @param {BigNumber} tokenOneAmount - the amount of token one - * @param {number} tokenOneDecimals - the number of decimals token one has - * @param {number} tokenTwoDecimals - the number of decimals token two has - * @param {string|number} conversionFactor - the conversion rate from token one to token two - * - * @returns {BigNumber} the amount of token two equivalent to the amount of token one - */ -export function convertCurrency(tokenOneAmount: BigNumber, tokenOneDecimals: number, tokenTwoDecimals: number, conversionFactor: string | number): BigNumber { - const conversionRate = toStringNumber(conversionFactor); - const leadingZeroesRegex = /^0+/g; - const trailingZeroesRegex = /0+$/g; - const floatRegex = /^\d+(\.\d+)?$/g; - - if (!Number.isInteger(tokenOneDecimals) || tokenOneDecimals <= 0) { - throw new Error('Token one decimals must be a positive integer or zero'); - } - - if (!Number.isInteger(tokenTwoDecimals) || tokenTwoDecimals <= 0) { - throw new Error('Token two decimals must be a positive integer or zero'); - } - - if (!floatRegex.test(conversionRate) || Number(conversionRate) <= 0) { - throw new Error('Conversion rate must be a positive floating point number or integer'); - } - - if (tokenOneAmount.lt(0)) { - throw new Error('Token one amount must be positive'); - } - - const tenBn = BigNumber.from(10); - - // represents the maximum significant figures of conversion calculations - const precisionCutoffBn = BigNumber.from(256); - - const [rawConversionRateIntegers, rawConversionRateDecimals = ''] = conversionRate.split('.'); - const conversionRateIntegers = rawConversionRateIntegers.replace(leadingZeroesRegex, ''); - const conversionRateDecimals = rawConversionRateDecimals.replace(trailingZeroesRegex, ''); - - const numberOfConversionRateDecimals = conversionRateDecimals.length; - - const conversionRateScalingFactor = BigNumber.from(numberOfConversionRateDecimals).add(precisionCutoffBn); - const conversionRateAsIntegerString = conversionRateIntegers.concat((conversionRateDecimals ?? '')); - - const conversionRateBn = BigNumber.from(conversionRateAsIntegerString); - const scaledConversionRate = conversionRateBn.mul(tenBn.pow(conversionRateScalingFactor)); - - // normalize amount to 256 precision - const normalizedAmount = tokenOneAmount.mul(tenBn.pow((precisionCutoffBn.sub(tokenOneDecimals)))); - - // multiply amount by conversion rate integer - const normalizedScaledAmountTwo = normalizedAmount.mul(scaledConversionRate); - - // denormalize from 256 precision to tokenTwoDecimals - const denormalizedScaledAmountTwo = normalizedScaledAmountTwo.div(tenBn.pow((precisionCutoffBn.sub(tokenTwoDecimals)))); - - // remove conversion rate scaling - return denormalizedScaledAmountTwo.div(tenBn.pow(conversionRateScalingFactor.add(numberOfConversionRateDecimals))); -} - - -/** - * Inverts a floating point number, useful for taking a conversion rate from token A to token B and getting the - * conversion rate from token B to token A - * - * @param {number|string} float - the floating point number to invert - * - * @returns {string} the inverted floating point number rounded to 18 decimal places - */ -export function getFloatReciprocal(float: number | string) { - const floatRegex = /^\d+(\.\d+)?$/g; - const trailingZeroesRegex = /0+$/g; - const trailingDotRegex = /\.$/g; - - if (!floatRegex.test(float.toString())) { - throw new Error('Conversion rate must be a positive floating point number or integer'); - } - - if (parseFloat(float.toString()) === 0) { - throw new Error('Error inverting: cannot divide by zero'); - } - - return new Decimal(1) - .dividedBy(float) - .toFixed(WEI_PRECISION) - .replace(trailingZeroesRegex, '') - .replace(trailingDotRegex, ''); -} - -/** - * Given a locale and currency code, returns the symbol for the currency, e.g. '$' for USD - * @param {string} locale - locale code, e.g. 'en-US' - * @param {string} currencyCode - standard currency code, e.g. 'USD' - */ -export function getCurrencySymbol(locale: string, currencyCode: string) { - const formatter = new Intl.NumberFormat(locale, { - style: 'currency', - currency: currencyCode, - currencyDisplay: 'symbol', - }); - - const parts = formatter.formatToParts(123); - - let symbol; - - for (let i = 0; i < parts.length; i++) { - if (parts[i].type === 'currency') { - symbol = parts[i].value; - break; - } - } - - return symbol; -} - -/** - * Launches a prompt in MetaMask to add a given token as a tracked token, allowing the user to view their balance of - * that token at a glance from MetaMask - * - * @param {string} address - the address of the token contract - * @param {string} symbol - the token's ticker symbol, e.g. 'STLOS' - * @param {string} image - permalink url of the token's icon - * @param {string} type - Ethereum standard of the token; default is 'ERC20' - * @param {number} decimals - the number of decimals constituting the token's precision, default is 18 - * - * @returns {Promise} - */ -export async function promptAddToMetamask( - address: string, - symbol: string, - image: string, - type: string, - decimals: number, -): Promise { - if (!window.ethereum) { - return Promise.reject(); - } - - type MetamaskEthereum = { - request: (args: { method: string, params: Record }) => Promise - }; - - const ethereum = window.ethereum as unknown as MetamaskEthereum; - - return ethereum.request({ - method: 'wallet_watchAsset', - params: { - type, - options: { - address, - symbol, - decimals, - image, - }, - }, - }); -} diff --git a/src/antelope/wallets/utils/date-utils.ts b/src/antelope/wallets/utils/date-utils.ts deleted file mode 100644 index 598108120..000000000 --- a/src/antelope/wallets/utils/date-utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ -/** - * Useful date-related constants - */ -export const HOUR_SECONDS = 60 * 60; -export const DAY_SECONDS = 24 * HOUR_SECONDS; - - -/** - * Returns true if the given epochMs is less than the given number of minutes ago - * @param epochMs seconds since epoch representing the date to check - * @param minutes number of minutes to check against - * @returns {boolean} true if the given epochMs is less than the given number of minutes ago - */ -export function dateIsWithinXMinutes(epochMs: number, minutes: number) { - if (epochMs <= 0) { - throw new Error('epochMs must be greater than 0'); - } - - if (epochMs % 1 !== 0) { - throw new Error('epochMs must be an integer'); - } - - // make a date object which represents the time X minutes ago - const xMinsAgo = new Date(); - xMinsAgo.setMinutes(xMinsAgo.getMinutes() - minutes); - - // return true if the date is within the defined timeframe - return new Date(epochMs) > xMinsAgo; -} - - -/** - * Translates a number of seconds to a natural language time period using the given translation function. - * - * @param {number|null} seconds number of seconds since epoch representing the date to check - * @param {function} $t translation function. Should accept a string (just the keyname without a path) and return a translated string - * @returns {string} plain english time period - */ -export function formatUnstakePeriod(seconds: number|null, $t: (key:string) => string) { - if (seconds === null) { - return '--'; - } - - let quantity; - let unit; - - if (seconds < HOUR_SECONDS) { - quantity = seconds / 60; - unit = $t('minutes'); - } else if (seconds < DAY_SECONDS) { - quantity = seconds / HOUR_SECONDS; - unit = $t('hours'); - } else { - quantity = seconds / DAY_SECONDS; - unit = $t('days'); - } - - if (!Number.isInteger(quantity)) { - quantity = quantity.toFixed(1); - } - - return `${quantity} ${unit}`; -} - diff --git a/src/antelope/wallets/utils/index.ts b/src/antelope/wallets/utils/index.ts deleted file mode 100644 index 06556d983..000000000 --- a/src/antelope/wallets/utils/index.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* eslint-disable max-len */ -export * from 'src/antelope/wallets/utils/abi/signature'; -import { BigNumber, ethers } from 'ethers'; -import { formatUnits } from '@ethersproject/units'; -import { EvmABIEntry } from 'src/antelope/types'; -import { fromUnixTime, format } from 'date-fns'; -import { toStringNumber } from 'src/antelope/wallets/utils/currency-utils'; -import { prettyPrintCurrency } from 'src/antelope/wallets/utils/currency-utils'; -import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; - -const REVERT_FUNCTION_SELECTOR = '0x08c379a0'; -const REVERT_PANIC_SELECTOR = '0x4e487b71'; - -export const WEI_PRECISION = 18; - -/** - * divideFloat performs a division of two float numbers represented as strings or native numbers. - * @param a is the numerator expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') - * @param b is the denominator expressed as a number or a string representing a number - * @returns a string representing the result of the division also as a float number - */ -export function divideFloat(a: string | number, b: string | number): string { - const a_str = toStringNumber(a); - const b_str = toStringNumber(b); - const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; - const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; - const decimals = 2 * Math.max(a_decimals, b_decimals); - const A = ethers.utils.parseUnits(a_str, decimals); - const B = ethers.utils.parseUnits(b_str, b_decimals); - const result = A.div(B); - return formatUnits(result.toString(), decimals-b_decimals); -} - -/** - * multiplyFloat performs a multiplication of two float numbers represented as strings or native numbers. - * @param a is the first factor expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') - * @param b is the second factor expressed as a number or a string representing a number - * @returns a string representing the result of the multiplication also as a float number - */ -export function multiplyFloat(a: string | number, b: string | number): string { - const a_str = toStringNumber(a); - const b_str = toStringNumber(b); - const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; - const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; - const decimals = a_decimals + b_decimals; - const A = ethers.utils.parseUnits(a_str, decimals); - const B = ethers.utils.parseUnits(b_str, decimals); - const result = A.mul(B); - return formatUnits(result.toString(), decimals+decimals); -} - -export function formatWei(bn: string | number | ethers.BigNumber, tokenDecimals: number, displayDecimals = 4): string { - const amount = ethers.BigNumber.from(bn); - const formatted = formatUnits(amount.toString(), tokenDecimals || WEI_PRECISION); - const str = formatted.toString(); - // Use string, do not convert to number so we never lose precision - if (displayDecimals > 0 && str.includes('.')) { - const parts = str.split('.'); - return parts[0] + '.' + parts[1].slice(0, displayDecimals); - } - return str; -} - -export function isValidAddressFormat(ethAddressString: string): boolean { - const pattern = /^0x[a-fA-F0-9]{40}$/; - return pattern.test(ethAddressString); -} - -export function getTopicHash(topic: string): string { - return `0x${topic.substring(topic.length - 40)}`; -} - -export function toChecksumAddress(address: string): string { - if (!address) { - return address; - } - - let addy = address.toLowerCase().replace('0x', ''); - if (addy.length !== 40) { - addy = addy.padStart(40, '0'); - } - - const hash = keccak256(toUtf8Bytes(addy)).replace('0x', ''); - let ret = '0x'; - - for (let i = 0; i < addy.length; i++) { - if (parseInt(hash[i], 16) >= 8) { - ret += addy[i].toUpperCase(); - } else { - ret += addy[i]; - } - } - - return ret; -} - -export function parseErrorMessage(output: string): string { - if (!output) { - return ''; - } - - let message = ''; - if (output.startsWith(REVERT_FUNCTION_SELECTOR)) { - message = parseRevertReason(output); - } - - if (output.startsWith(REVERT_PANIC_SELECTOR)) { - message = parsePanicReason(output); - } - - return message.replace(/[^a-zA-Z0-9 /./'/"/,/@/+/-/_/(/)/[]/g, ''); -} - -export function parseRevertReason(revertOutput: string): string { - if (!revertOutput || revertOutput.length < 138) { - return ''; - } - - let reason = ''; - const trimmedOutput = revertOutput.substr(138); - for (let i = 0; i < trimmedOutput.length; i += 2) { - reason += String.fromCharCode(parseInt(trimmedOutput.substr(i, 2), 16)); - } - return reason; -} - -export function parsePanicReason(revertOutput: string): string { - const trimmedOutput = revertOutput.slice(-2); - let reason; - - switch (trimmedOutput) { - case '01': - reason = 'If you call assert with an argument that evaluates to false.'; - break; - case '11': - reason = 'If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.'; - break; - case '12': - reason = 'If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).'; - break; - case '21': - reason = 'If you convert a value that is too big or negative into an enum type.'; - break; - case '31': - reason = 'If you call .pop() on an empty array.'; - break; - case '32': - reason = 'If you access an array, bytesN or an array slice at an out-of-bounds or negative index ' + - '(i.e. x[i] where i >= x.length or i < 0).'; - break; - case '41': - reason = 'If you allocate too much memory or create an array that is too large.'; - break; - case '51': - reason = 'If you call a zero-initialized variable of internal function type.'; - break; - default: - reason = 'Default panic message'; - } - return reason; -} - -export function sortAbiFunctionsByName(fns: EvmABIEntry[]): EvmABIEntry[] { - return fns.sort( - (entryA, entryB) => { - const upperA = entryA.name.toUpperCase(); - const upperB = entryB.name.toUpperCase(); - return (upperA < upperB) ? -1 : (upperA > upperB) ? 1 : 0; - }, - ); -} - -/** - * Determine whether the user's device is an Apple touch device - * - * @return {boolean} - */ -export function getClientIsApple() { - return [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod', - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); -} - -/** - * Given a Date object, return the pretty-printed timezone offset, e.g. "+05:00" - * - * @param {Date} date - * @return {string} - */ -export function getFormattedUtcOffset(date: Date): string { - const pad = (value: number) => value < 10 ? '0' + value : value; - const sign = (date.getTimezoneOffset() > 0) ? '-' : '+'; - const offset = Math.abs(date.getTimezoneOffset()); - const hours = pad(Math.floor(offset / 60)); - const minutes = pad(offset % 60); - return sign + hours + ':' + minutes; -} - -/** - * Given a unix timestamp, returns a date in the form of Jan 1, 2023 07:45:22 AM - * - * @param epoch - * - * @return string - */ -export function getLongDate(epoch: number): string { - const offset = getFormattedUtcOffset(new Date(epoch)); - return `${format(fromUnixTime(epoch), 'MMM d, yyyy hh:mm:ss a')} (UTC ${offset})`; -} - - -/** - * Given a unix timestamp, returns string with the date in a given format showing UTC offset optionally. - * @param epoch seconds since epoch - * @param timeFormat a string containing the format of the date to be returned (based on date-fns format) - * @param showUtc whether to show the UTC offset - * @returns {string} the formatted date - */ -export function getFormatedDate(epoch: number, timeFormat = 'MMM d, yyyy hh:mm:ss a', showUtc = false): string { - const offset = getFormattedUtcOffset(new Date(epoch)); - const utc = showUtc ? ` (UTC ${offset})` : ''; - return `${format(fromUnixTime(epoch), timeFormat)}${utc}`; -} - -/* -* Determines whether the amount is too large (more than six characters long) to be displayed in full on mobile devices -* -* @param {number} amount - the currency amount -* return {boolean} - true if the amount is too large to be displayed in full on mobile devices -* */ -export function isAmountTooLarge(amount: number | string): boolean { - const primaryAmountIsTooLarge = - (typeof amount === 'number' && amount.toString().length > 6) || - (typeof amount === 'string' && amount.length > 6); - - return primaryAmountIsTooLarge; -} - - - -/* -* Formats a token balance amount in a localized way, using 4 decimals, -* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true -* -* @param {number} amount - the currency amount -* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale -* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 -* @param {string?} symbol - symbol for the currency to be used, e.g. 'TLOS'. If defined, the symbol will be displayed, e.g. 123.00 TLOS. -* return {string} - the formatted amount -* */ -export function prettyPrintBalance(amount: number | string, locale: string, tiny: boolean, symbol = '') { - return ['', ' ' + symbol].join(prettyPrintCurrency(+amount, 4, locale, tiny ? isAmountTooLarge(amount) : false)); -} - -/* -* Formats a fiat balance amount in a localized way, using 2 decimals, -* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true -* -* @param {number} amount - the currency amount -* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale -* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 -* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency -* return {string} - the formatted amount -* */ -export function prettyPrintFiatBalance(fiatAmount: number | string, locale: string, tiny: boolean, currency = 'USD') { - return prettyPrintCurrency(+fiatAmount, 2, locale, tiny ? isAmountTooLarge(fiatAmount) : false, currency); -} - -/** - * Converts gas price, which is in its own unit, to TLOS - * - * @param {string} gasUsed - amount of gas used as string representation of a number/hex - * @param {string} gasPrice - gas price in TLOS as string representation of a number/hex - * - * @return {string} gas in TLOS as a number string - */ -export function getGasInTlos(gasUsed: string, gasPrice: string) { - return formatWei( - BigNumber.from(gasPrice) - .mul(gasUsed).toLocaleString(), - WEI_PRECISION, - 5, - ); -} - -/** - * Takes an ethereum hash ('0x...') and returns a shortened version, like '0x0000...0000' - * @param {string} hash - a string beginning with 0x and containing only 0-9, a-f, or A-F - * - * @return {string} shortened hash - */ -export function getShortenedHash(hash: string) { - const textIsAddress = /^0x[0-9a-fA-F]+$/.test(hash); - - if (textIsAddress) { - return hash.slice(0, 6) + '...' + hash.slice(-4); - } else { - throw new Error('Invalid hash ' + hash); - } -} diff --git a/src/antelope/wallets/utils/text-utils.ts b/src/antelope/wallets/utils/text-utils.ts deleted file mode 100644 index 1d936809f..000000000 --- a/src/antelope/wallets/utils/text-utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable max-len */ - -/** - * Given some text, ellipsizes the text if it exceeds a specific length - * - * @param text - * @param maxLength - * @returns {string} - */ -export function truncateText(text: string, maxLength = 10): string { - if (text.length <= maxLength) { - return text; - } - - return `${text.slice(0, maxLength)}...`; -} - -/** - * Given an address, returns a shortened version like `0x0000...0000` - * - * @param address - * @param maxLength - * @returns {string} - */ -export function truncateAddress(address: string): string { - return `${address.slice(0, 6)}...${address.slice(-4)}`; -} - -/** - * Given a name and an id, returns the name without the ID. Generally in the UI, NFT name and ID are displayed next to each other; - * this function prevents the ID from duplicated - * @param name - * @param id - * @returns {string} - * @example - * getShapedNftName('SomeNft #1234', '1234') // 'SomeNft' - */ -export function getShapedNftName(name: string, id: string): string { - let shapedName = name; - if (name.includes(id)) { - shapedName = name.replace(id, ''); - - if (shapedName[shapedName.length - 1] === '#') { - shapedName = shapedName.slice(0, -1); - } - } - return shapedName.trim(); -} diff --git a/src/antelope/wallets/utils/trx-utils.ts b/src/antelope/wallets/utils/trx-utils.ts deleted file mode 100644 index cc6b35270..000000000 --- a/src/antelope/wallets/utils/trx-utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { usePlatformStore } from 'src/antelope'; -import { ethers } from 'ethers'; -import { AccountModel } from 'src/antelope/mocks'; -import { AntelopeError, TransactionResponse } from 'src/antelope/types'; -import { EVMAuthenticator } from 'src/antelope/wallets'; - - -export async function subscribeForTransactionReceipt(account: AccountModel, response: TransactionResponse): Promise<{ - newResponse: TransactionResponse; - receipt: ethers.providers.TransactionReceipt; -}> { - if (account.isNative) { - throw new AntelopeError('Not implemented yet for native'); - } else { - const authenticator = account.authenticator as EVMAuthenticator; - const provider = await authenticator.web3Provider(); - const result = { - newResponse: { ...response } as TransactionResponse, - receipt: {} as ethers.providers.TransactionReceipt, - }; - if (provider) { - const whenConfirmed = provider.waitForTransaction(response.hash); - // we add the wait method to the response, - // so that the caller can subscribe to the confirmation event - result.newResponse.wait = async () => whenConfirmed; - return result; - } else { - if (usePlatformStore().isMobile) { - response.wait = async () => Promise.resolve({} as ethers.providers.TransactionReceipt); - return result; - } else { - throw new AntelopeError('antelope.evm.error_no_provider'); - } - } - } -} From a2ce76dea82aeb2a6cf5726263bdb299e7315b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viterbo=20Rodr=C3=ADguez?= Date: Thu, 30 May 2024 10:42:49 -0300 Subject: [PATCH 02/26] first part --- .eslintrc.js | 1 - src/antelope/chains/EVMChainSettings.ts | 545 ++++++++++++++++++ .../chains/evm/telos-evm-testnet/index.ts | 180 ++++++ src/antelope/chains/evm/telos-evm/index.ts | 175 ++++++ src/antelope/index.ts | 8 + src/antelope/mocks/AccountStore.ts | 186 ++++++ src/antelope/mocks/AntelopeConfig.ts | 337 +++++++++++ src/antelope/mocks/ChainStore.ts | 155 +++++ src/antelope/mocks/ContractStore.ts | 17 + src/antelope/mocks/EVMStore.ts | 171 ++++++ src/antelope/mocks/FeedbackStore.ts | 40 ++ src/antelope/mocks/PlatformStore.ts | 23 + src/antelope/mocks/UserStore.ts | 8 + src/antelope/mocks/chain-constants.ts | 16 + src/antelope/mocks/index.ts | 9 + src/antelope/stores/utils/abi/erc1155.ts | 133 +++++ src/antelope/stores/utils/abi/erc20.ts | 251 ++++++++ src/antelope/stores/utils/abi/erc721.ts | 369 ++++++++++++ .../stores/utils/abi/erc721Metadata.ts | 9 + src/antelope/stores/utils/abi/escrowAbi.ts | 11 + src/antelope/stores/utils/abi/index.ts | 43 ++ .../stores/utils/abi/setApprovalForAllAbi.ts | 13 + .../utils/abi/signature/events_signatures.ts | 25 + .../abi/signature/functions_signatures.ts | 27 + .../stores/utils/abi/signature/index.ts | 3 + .../abi/signature/transfer_signatures.ts | 2 + src/antelope/stores/utils/abi/stlosAbi.ts | 98 ++++ .../stores/utils/abi/supportsInterface.ts | 11 + src/antelope/stores/utils/abi/wrapAbi.ts | 32 + .../stores/utils/contracts/EvmContract.ts | 258 +++++++++ .../utils/contracts/EvmContractFactory.ts | 62 ++ src/antelope/stores/utils/currency-utils.ts | 385 +++++++++++++ src/antelope/stores/utils/date-utils.ts | 108 ++++ src/antelope/stores/utils/index.ts | 266 +++++++++ src/antelope/stores/utils/media-utils.ts | 74 +++ src/antelope/stores/utils/nft-utils.ts | 150 +++++ src/antelope/stores/utils/text-utils.ts | 67 +++ src/antelope/stores/utils/trx-utils.ts | 35 ++ src/antelope/types/ABIv1.ts | 33 ++ src/antelope/types/Actions.ts | 248 ++++++++ src/antelope/types/AntelopeError.ts | 17 + src/antelope/types/Api.ts | 113 ++++ src/antelope/types/Basic.ts | 3 + src/antelope/types/ChainInfo.ts | 19 + src/antelope/types/ChainSettings.ts | 23 + src/antelope/types/EvmBlockData.ts | 24 + src/antelope/types/EvmContractData.ts | 109 ++++ src/antelope/types/EvmLog.ts | 26 + src/antelope/types/EvmRexDeposit.ts | 10 + src/antelope/types/EvmTransaction.ts | 161 ++++++ src/antelope/types/ExceptionError.ts | 5 + src/antelope/types/Filters.ts | 43 ++ src/antelope/types/IndexerTypes.ts | 259 +++++++++ src/antelope/types/KeyAccounts.ts | 1 + src/antelope/types/NFTClass.ts | 394 +++++++++++++ src/antelope/types/OpenSeaTypes.ts | 14 + src/antelope/types/PriceData.ts | 31 + src/antelope/types/Producers.ts | 27 + src/antelope/types/Proposals.ts | 82 +++ src/antelope/types/Providers.ts | 24 + src/antelope/types/Theme.ts | 17 + src/antelope/types/TokenClass.ts | 337 +++++++++++ src/antelope/types/TransactionV1.ts | 21 + src/antelope/types/index.ts | 33 ++ src/antelope/types/ual-oreid.d.ts | 1 + .../wallets/authenticators/BraveAuth.ts | 30 + .../authenticators/EVMAuthenticator.ts | 137 +++++ .../authenticators/InjectedProviderAuth.ts | 331 +++++++++++ .../wallets/authenticators/MetamaskAuth.ts | 32 + .../wallets/authenticators/OreIdAuth.ts | 426 ++++++++++++++ .../wallets/authenticators/SafePalAuth.ts | 30 + .../authenticators/WalletConnectAuth.ts | 512 ++++++++++++++++ src/antelope/wallets/index.ts | 9 + src/antelope/wallets/init.ts | 90 +++ src/antelope/wallets/utils/abi/erc1155.ts | 133 +++++ src/antelope/wallets/utils/abi/erc20.ts | 226 ++++++++ src/antelope/wallets/utils/abi/erc721.ts | 356 ++++++++++++ .../wallets/utils/abi/erc721Metadata.ts | 9 + src/antelope/wallets/utils/abi/escrowAbi.ts | 11 + src/antelope/wallets/utils/abi/index.ts | 43 ++ .../utils/abi/signature/events_signatures.ts | 26 + .../abi/signature/functions_signatures.ts | 28 + .../wallets/utils/abi/signature/index.ts | 3 + .../abi/signature/transfer_signatures.ts | 2 + src/antelope/wallets/utils/abi/stlosAbi.ts | 49 ++ .../wallets/utils/abi/supportsInterface.ts | 11 + src/antelope/wallets/utils/abi/wrapAbi.ts | 32 + .../wallets/utils/contracts/EvmContract.ts | 236 ++++++++ .../utils/contracts/EvmContractFactory.ts | 63 ++ src/antelope/wallets/utils/currency-utils.ts | 387 +++++++++++++ src/antelope/wallets/utils/date-utils.ts | 66 +++ src/antelope/wallets/utils/index.ts | 307 ++++++++++ src/antelope/wallets/utils/text-utils.ts | 48 ++ src/antelope/wallets/utils/trx-utils.ts | 36 ++ src/assets/tokens/telos.png | Bin 0 -> 71552 bytes src/components/LoginModal.vue | 10 +- src/components/TransactionTable.vue | 2 - src/lib/contract/ContractManager.js | 2 +- src/lib/price.ts | 135 +++++ 99 files changed, 10183 insertions(+), 13 deletions(-) create mode 100644 src/antelope/chains/EVMChainSettings.ts create mode 100644 src/antelope/chains/evm/telos-evm-testnet/index.ts create mode 100644 src/antelope/chains/evm/telos-evm/index.ts create mode 100644 src/antelope/index.ts create mode 100644 src/antelope/mocks/AccountStore.ts create mode 100644 src/antelope/mocks/AntelopeConfig.ts create mode 100644 src/antelope/mocks/ChainStore.ts create mode 100644 src/antelope/mocks/ContractStore.ts create mode 100644 src/antelope/mocks/EVMStore.ts create mode 100644 src/antelope/mocks/FeedbackStore.ts create mode 100644 src/antelope/mocks/PlatformStore.ts create mode 100644 src/antelope/mocks/UserStore.ts create mode 100644 src/antelope/mocks/chain-constants.ts create mode 100644 src/antelope/mocks/index.ts create mode 100644 src/antelope/stores/utils/abi/erc1155.ts create mode 100644 src/antelope/stores/utils/abi/erc20.ts create mode 100644 src/antelope/stores/utils/abi/erc721.ts create mode 100644 src/antelope/stores/utils/abi/erc721Metadata.ts create mode 100644 src/antelope/stores/utils/abi/escrowAbi.ts create mode 100644 src/antelope/stores/utils/abi/index.ts create mode 100644 src/antelope/stores/utils/abi/setApprovalForAllAbi.ts create mode 100644 src/antelope/stores/utils/abi/signature/events_signatures.ts create mode 100644 src/antelope/stores/utils/abi/signature/functions_signatures.ts create mode 100644 src/antelope/stores/utils/abi/signature/index.ts create mode 100644 src/antelope/stores/utils/abi/signature/transfer_signatures.ts create mode 100644 src/antelope/stores/utils/abi/stlosAbi.ts create mode 100644 src/antelope/stores/utils/abi/supportsInterface.ts create mode 100644 src/antelope/stores/utils/abi/wrapAbi.ts create mode 100644 src/antelope/stores/utils/contracts/EvmContract.ts create mode 100644 src/antelope/stores/utils/contracts/EvmContractFactory.ts create mode 100644 src/antelope/stores/utils/currency-utils.ts create mode 100644 src/antelope/stores/utils/date-utils.ts create mode 100644 src/antelope/stores/utils/index.ts create mode 100644 src/antelope/stores/utils/media-utils.ts create mode 100644 src/antelope/stores/utils/nft-utils.ts create mode 100644 src/antelope/stores/utils/text-utils.ts create mode 100644 src/antelope/stores/utils/trx-utils.ts create mode 100644 src/antelope/types/ABIv1.ts create mode 100644 src/antelope/types/Actions.ts create mode 100644 src/antelope/types/AntelopeError.ts create mode 100644 src/antelope/types/Api.ts create mode 100644 src/antelope/types/Basic.ts create mode 100644 src/antelope/types/ChainInfo.ts create mode 100644 src/antelope/types/ChainSettings.ts create mode 100644 src/antelope/types/EvmBlockData.ts create mode 100644 src/antelope/types/EvmContractData.ts create mode 100644 src/antelope/types/EvmLog.ts create mode 100644 src/antelope/types/EvmRexDeposit.ts create mode 100644 src/antelope/types/EvmTransaction.ts create mode 100644 src/antelope/types/ExceptionError.ts create mode 100644 src/antelope/types/Filters.ts create mode 100644 src/antelope/types/IndexerTypes.ts create mode 100644 src/antelope/types/KeyAccounts.ts create mode 100644 src/antelope/types/NFTClass.ts create mode 100644 src/antelope/types/OpenSeaTypes.ts create mode 100644 src/antelope/types/PriceData.ts create mode 100644 src/antelope/types/Producers.ts create mode 100644 src/antelope/types/Proposals.ts create mode 100644 src/antelope/types/Providers.ts create mode 100644 src/antelope/types/Theme.ts create mode 100644 src/antelope/types/TokenClass.ts create mode 100644 src/antelope/types/TransactionV1.ts create mode 100644 src/antelope/types/index.ts create mode 100644 src/antelope/types/ual-oreid.d.ts create mode 100644 src/antelope/wallets/authenticators/BraveAuth.ts create mode 100644 src/antelope/wallets/authenticators/EVMAuthenticator.ts create mode 100644 src/antelope/wallets/authenticators/InjectedProviderAuth.ts create mode 100644 src/antelope/wallets/authenticators/MetamaskAuth.ts create mode 100644 src/antelope/wallets/authenticators/OreIdAuth.ts create mode 100644 src/antelope/wallets/authenticators/SafePalAuth.ts create mode 100644 src/antelope/wallets/authenticators/WalletConnectAuth.ts create mode 100644 src/antelope/wallets/index.ts create mode 100644 src/antelope/wallets/init.ts create mode 100644 src/antelope/wallets/utils/abi/erc1155.ts create mode 100644 src/antelope/wallets/utils/abi/erc20.ts create mode 100644 src/antelope/wallets/utils/abi/erc721.ts create mode 100644 src/antelope/wallets/utils/abi/erc721Metadata.ts create mode 100644 src/antelope/wallets/utils/abi/escrowAbi.ts create mode 100644 src/antelope/wallets/utils/abi/index.ts create mode 100644 src/antelope/wallets/utils/abi/signature/events_signatures.ts create mode 100644 src/antelope/wallets/utils/abi/signature/functions_signatures.ts create mode 100644 src/antelope/wallets/utils/abi/signature/index.ts create mode 100644 src/antelope/wallets/utils/abi/signature/transfer_signatures.ts create mode 100644 src/antelope/wallets/utils/abi/stlosAbi.ts create mode 100644 src/antelope/wallets/utils/abi/supportsInterface.ts create mode 100644 src/antelope/wallets/utils/abi/wrapAbi.ts create mode 100644 src/antelope/wallets/utils/contracts/EvmContract.ts create mode 100644 src/antelope/wallets/utils/contracts/EvmContractFactory.ts create mode 100644 src/antelope/wallets/utils/currency-utils.ts create mode 100644 src/antelope/wallets/utils/date-utils.ts create mode 100644 src/antelope/wallets/utils/index.ts create mode 100644 src/antelope/wallets/utils/text-utils.ts create mode 100644 src/antelope/wallets/utils/trx-utils.ts create mode 100644 src/assets/tokens/telos.png create mode 100644 src/lib/price.ts diff --git a/.eslintrc.js b/.eslintrc.js index d2f28d9c4..e87118a50 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,7 +51,6 @@ module.exports = { 'curly': 'error', 'brace-style': ['error', '1tbs', { 'allowSingleLine': false }], 'no-restricted-imports': ['error', { - 'patterns': ['.*'], // disallow relative imports }], 'no-return-assign': ['error', 'always'], 'no-param-reassign': 'error', diff --git a/src/antelope/chains/EVMChainSettings.ts b/src/antelope/chains/EVMChainSettings.ts new file mode 100644 index 000000000..0e454488a --- /dev/null +++ b/src/antelope/chains/EVMChainSettings.ts @@ -0,0 +1,545 @@ +import { RpcEndpoint } from 'universal-authenticator-library'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios'; +import { + AbiSignature, + ChainSettings, + EvmBlockData, + EvmContractCreationInfo, + HyperionAbiSignatureFilter, + IndexerAccountBalances, + IndexerTokenMarketData, + PriceChartData, + IndexerTransactionsFilter, + IndexerAccountTransactionsResponse, + TokenClass, + TokenSourceInfo, + TokenBalance, + MarketSourceInfo, + TokenMarketData, + IndexerHealthResponse, + addressString, + IndexerTransfersFilter, + IndexerAccountTransfersResponse, +} from 'src/antelope/types'; +import EvmContract from 'src/antelope/stores/utils/contracts/EvmContract'; +import { ethers } from 'ethers'; +import { toStringNumber } from 'src/antelope/stores/utils/currency-utils'; +import { dateIsWithinXMinutes } from 'src/antelope/stores/utils/date-utils'; +import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; +import { getAntelope } from 'src/antelope'; +import { WEI_PRECISION, PRICE_UPDATE_INTERVAL_IN_MIN } from 'src/antelope/stores/utils'; +import { BehaviorSubject, filter } from 'rxjs'; + + +export default abstract class EVMChainSettings implements ChainSettings { + // to avoid init() being called twice + protected ready = false; + + protected initPromise: Promise; + + // Short Name of the network + protected network: string; + + // External query API support + protected hyperion: AxiosInstance = axios.create({ baseURL: this.getHyperionEndpoint() }); + + // External query API support + protected api: AxiosInstance = axios.create({ baseURL: this.getApiEndpoint() }); + + // External trusted metadata bucket for EVM contracts + protected contractsBucket: AxiosInstance = axios.create({ baseURL: this.getTrustedContractsBucket() }); + + // External indexer API support + protected indexer: AxiosInstance = axios.create({ baseURL: this.getIndexerApiEndpoint() }); + + // indexer health check promise + protected _indexerHealthState: { + promise: Promise | null; + state: IndexerHealthResponse + } = { + promise: null, + state: this.deathHealthResponse, + }; + + // Token list promise + tokenListPromise: Promise | null = null; + + // EvmContracts cache mapped by address + protected contracts: Record; + resolve?: (value: EvmContract | false) => void; + }> = {}; + + // this variable helps to show the indexer health warning only once per session + indexerHealthWarningShown = false; + + // This variable is used to simulate a bad indexer health state + indexerBadHealthSimulated = false; + + // This observable is used to check if the indexer health state was already checked + indexerChecked$ = new BehaviorSubject(false); + + // This function is used to trace the execution of the code + trace = createTraceFunction('EVMChainSettings'); + + simulateIndexerDown(isBad: boolean) { + this.indexerBadHealthSimulated = isBad; + } + + constructor(network: string) { + this.network = network; + + const MAX_REQUESTS_COUNT = 5; + const INTERVAL_MS = 10; + let pendingRequests = 0; + + // Interceptor handlers -- these handlers are used to limit the number of concurrent requests + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestHandler = (value: InternalAxiosRequestConfig) => new Promise>((resolve) => { + const interval = setInterval(() => { + if (pendingRequests < MAX_REQUESTS_COUNT) { + pendingRequests++; + clearInterval(interval); + resolve(value); + } + }, INTERVAL_MS); + }); + + const responseHandler = (response: AxiosResponse) => { + pendingRequests = Math.max(0, pendingRequests - 1); + return Promise.resolve(response); + }; + + const erorrHandler = (error: unknown) => { + pendingRequests = Math.max(0, pendingRequests - 1); + return Promise.reject(error); + }; + + // Axios Request Interceptor + this.hyperion.interceptors.request.use(requestHandler); + this.indexer.interceptors.request.use(requestHandler); + + // Axios Response Interceptor + this.hyperion.interceptors.response.use(responseHandler, erorrHandler); + this.indexer.interceptors.response.use(responseHandler, erorrHandler); + + // Check indexer health state periodically + this.initPromise = new Promise((resolve) => { + this.updateIndexerHealthState().finally(() => { + // we resolve the promise (in any case) that will be returned by init() + resolve(); + }); + }); + } + + async initialized() { + return this.initPromise; + } + + async init(): Promise { + this.trace('init'); + // this is called only when this chain is needed to avoid initialization of all chains + if (this.ready) { + return this.initPromise; + } + this.ready = true; + + // this setTimeout is a work arround because we can't call getAntelope() function before it initializes + setTimeout(() => { + const timer = setInterval(async () => { + try { + await this.updateIndexerHealthState(); + } catch (e) { + clearInterval(timer); + console.error('Indexer API not working for this chain:', this.getNetwork(), e); + } + }, getAntelope().config.indexerHealthCheckInterval); + }, 1000); + + // Update system token price + this.getUsdPrice().then((value:number) => { + const sys_token = this.getSystemToken(); + const price = value.toString(); + const marketInfo = { price } as MarketSourceInfo; + const marketData = new TokenMarketData(marketInfo); + sys_token.market = marketData; + + const wsys_token = this.getWrappedSystemToken(); + wsys_token.market = marketData; + }); + + return this.initPromise; + } + + get deathHealthResponse() { + return { + success: false, + blockNumber: 0, + blockTimestamp: '', + secondsBehind: Number.POSITIVE_INFINITY, + } as IndexerHealthResponse; + } + + async updateIndexerHealthState() { + // resolve if this chain has indexer api support and is working fine + const promise = + Promise.resolve(this.hasIndexerSupport()) + .then(hasIndexerSupport => + hasIndexerSupport ? + this.indexer.get('/v1/health') : + Promise.resolve({ data: this.deathHealthResponse } as AxiosResponse), + ) + .then(response => response.data as unknown as IndexerHealthResponse); + + // initial state + this._indexerHealthState = { + promise, + state: this.deathHealthResponse, + }; + + // update indexer health state + promise.then((state) => { + this._indexerHealthState.state = state; + this.indexerChecked$.next(true); + }); + + return promise; + } + + /** + * This function checks if the indexer is healthy and warns the user if it is not. + * This warning should appear only once per session. + */ + checkAndWarnIndexerHealth() { + this.indexerChecked$.pipe( + // This filter only allows to continue if the indexer health was already checked + filter(indexerChecked => indexerChecked === true), + ).subscribe(() => { + if (!this.indexerHealthWarningShown && !this.isIndexerHealthy()) { + this.indexerHealthWarningShown = true; + const ant = getAntelope(); + ant.config.notifyNeutralMessageHandler( + ant.config.localizationHandler('antelope.chain.indexer_bad_health_warning'), + ); + } + }); + } + + isIndexerHealthy(): boolean { + if (this.indexerBadHealthSimulated) { + return false; + } else { + return ( + this._indexerHealthState.state.success && + this._indexerHealthState.state.secondsBehind < getAntelope().config.indexerHealthThresholdSeconds + ); + } + } + + get indexerHealthState(): IndexerHealthResponse { + return this._indexerHealthState.state; + } + + isNative() { + return false; + } + + // only testnet chains should override this + isTestnet() { + return false; + } + + getNetwork(): string { + return this.network; + } + + getLargeLogoPath(): string { + return `~/assets/${this.network}/logo_lg.svg`; + } + + getSmallLogoPath(): string { + return `~/assets/${this.network}/logo_sm.svg`; + } + + abstract getSystemToken(): TokenClass; + abstract getStakedSystemToken(): TokenClass; + abstract getWrappedSystemToken(): TokenClass; + abstract getEscrowContractAddress(): addressString; + abstract getChainId(): string; + abstract getDisplay(): string; + abstract getHyperionEndpoint(): string; + abstract getRPCEndpoint(): RpcEndpoint; + abstract getApiEndpoint(): string; + abstract getPriceData(): Promise; + abstract getUsdPrice(): Promise; + abstract getBuyMoreOfTokenLink(): string; + abstract getWeiPrecision(): number; + abstract getExplorerUrl(): string; + abstract getEcosystemUrl(): string; + abstract getBridgeUrl(): string; + abstract getTrustedContractsBucket(): string; + abstract getSystemTokens(): TokenClass[]; + abstract getIndexerApiEndpoint(): string; + abstract hasIndexerSupport(): boolean; + + async getApy(): Promise { + const response = await this.api.get('apy/evm'); + return response.data as string; + } + + async getBalances(account: string): Promise { + this.trace('getBalances', account); + if (!this.hasIndexerSupport()) { + console.error('Indexer API not supported for this chain:', this.getNetwork()); + return []; + } + return Promise.all([ + this.indexer.get(`v1/account/${account}/balances`, { + params: { + limit: 50, + offset: 0, + includePagination: false, + }, + }), + this.getUsdPrice(), + ]).then(async ([response, systemTokenPrice]) => { + // parse to IndexerAccountBalances + const balances = response.data as IndexerAccountBalances; + + const tokenList = await this.getTokenList(); + const tokens: TokenBalance[] = []; + + for (const result of balances.results) { + const token = tokenList.find(t => t.address.toLowerCase() === result.contract.toLowerCase()); + const contractData = balances.contracts[result.contract] ?? {}; + // fixing calldata + const callDataStr = contractData.calldata as string | object ?? '{}'; + + try { + if (typeof callDataStr === 'string') { + contractData.calldata = JSON.parse(callDataStr); + } else if (token?.isSystem) { + // system token systemTokenPrice + contractData.calldata = { + price: systemTokenPrice, + } as IndexerTokenMarketData; + } + } catch (e) { + console.error('Error parsing calldata', `"${callDataStr}"`, e); + } + + if (token) { + const balance = ethers.BigNumber.from(result.balance); + const tokenBalance = new TokenBalance(token, balance); + tokens.push(tokenBalance); + const priceIsCurrent = + !!contractData.calldata?.marketdata_updated && + dateIsWithinXMinutes(+contractData.calldata?.marketdata_updated, PRICE_UPDATE_INTERVAL_IN_MIN); + + // If we have market data we use it, as long as the price was updated within the last 10 minutes + if (typeof contractData.calldata === 'object' && priceIsCurrent) { + const price = (+(contractData.calldata.price ?? 0)).toFixed(12); + const marketInfo = { ...contractData.calldata, price } as MarketSourceInfo; + const marketData = new TokenMarketData(marketInfo); + token.market = marketData; + } + } + } + return tokens; + }).catch((error) => { + console.error(error); + return []; + }); + } + + constructTokenId(token: TokenSourceInfo): string { + return `${token.symbol}-${token.address}-${this.getNetwork()}`; + } + + async getEVMTransactions(filter: IndexerTransactionsFilter): Promise { + const address = filter.address; + const limit = filter.limit; + const offset = filter.offset; + const includeAbi = filter.includeAbi; + const sort = filter.sort; + const includePagination = true; + const logTopic = filter.logTopic; + const full = filter.full ?? true; + + let aux = {}; + + if (limit !== undefined) { + aux = { limit, ...aux }; + } + if (offset !== undefined) { + aux = { offset, ...aux }; + } + if (includeAbi !== undefined) { + aux = { includeAbi, ...aux }; + } + if (sort !== undefined) { + aux = { sort, ...aux }; + } + if (includePagination !== undefined) { + aux = { includePagination, ...aux }; + } + if (logTopic !== undefined) { + aux = { logTopic, ...aux }; + } + if (full !== undefined) { + aux = { full, ...aux }; + } + + const params: AxiosRequestConfig = aux as AxiosRequestConfig; + const url = `v1/address/${address}/transactions`; + + // The following performs a GET request to the indexer endpoint. + // Then it pipes the response to the IndexerAccountTransactionsResponse type. + // Notice that the promise is not awaited, but returned instead immediately. + return this.indexer.get(url, { params }) + .then(response => response.data as IndexerAccountTransactionsResponse); + } + + async getEvmNftTransfers({ + account, + type, + limit, + offset, + includePagination, + endBlock, + startBlock, + contract, + includeAbi, + }: IndexerTransfersFilter): Promise { + let aux = {}; + + if (limit !== undefined) { + aux = { limit, ...aux }; + } + if (offset !== undefined) { + aux = { offset, ...aux }; + } + if (includeAbi !== undefined) { + aux = { includeAbi, ...aux }; + } + if (type !== undefined) { + aux = { type, ...aux }; + } + if (includePagination !== undefined) { + aux = { includePagination, ...aux }; + } + if (endBlock !== undefined) { + aux = { endBlock, ...aux }; + } + if (startBlock !== undefined) { + aux = { startBlock, ...aux }; + } + if (contract !== undefined) { + aux = { contract, ...aux }; + } + + const params = aux as AxiosRequestConfig; + const url = `v1/account/${account}/transfers`; + + return this.indexer.get(url, { params }) + .then(response => response.data as IndexerAccountTransfersResponse) + .then((data) => { + // set supportedInterfaces property if undefined in the response + Object.values(data.contracts).forEach((contract) => { + if (contract.supportedInterfaces === null && type !== undefined) { + contract.supportedInterfaces = [type]; + } + }); + return data; + }); + } + + async getTokenList(): Promise { + if (this.tokenListPromise) { + return this.tokenListPromise; + } + + const url = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/telosevm.tokenlist.json'; + this.tokenListPromise = axios.get(url) + .then(results => results.data.tokens as unknown as {chainId:number, logoURI: string}[]) + .then(tokens => tokens.filter(({ chainId }) => chainId === +this.getChainId())) + .then(tokens => tokens.map(t => ({ + ...t, + network: this.getNetwork(), + logoURI: t.logoURI?.replace('ipfs://', 'https://w3s.link/ipfs/') ?? require('src/assets/tokens/telos.png'), + }) as unknown as TokenSourceInfo)) + .then(tokens => tokens.map(t => new TokenClass(t))) + .then(tokens => [this.getSystemToken(), this.getWrappedSystemToken(), this.getStakedSystemToken(), ...tokens]); + + return this.tokenListPromise; + } + + async getAbiSignature(filter: HyperionAbiSignatureFilter): Promise { + const params: AxiosRequestConfig = filter as AxiosRequestConfig; + return this.hyperion.get('/v2/evm/get_abi_signature', { params }) + .then(response => response.data as AbiSignature); + } + + async fetchContractCreationInfo(address: string): Promise { + return this.hyperion.get(`/v2/evm/get_contract?contract=${address}`) + .then(response => response.data as EvmContractCreationInfo); + } + + async getContractMetadata(checksumAddress: string): Promise { + return this.contractsBucket.get(`${checksumAddress}/metadata.json`) + .then(response => response.data.content as string); + } + + rpcCounter = 0; + nextId(): number { + return ++this.rpcCounter; + } + + async doRPC({ method, params }: AxiosRequestConfig): Promise { + const rpcPayload = { + jsonrpc: '2.0', + id: this.nextId(), + method, + params, + }; + return this.hyperion.post('/evm', rpcPayload) + .then(response => response.data as T); + } + + getIndexer() { + return this.indexer; + } + + async getGasPrice(): Promise { + return this.doRPC<{result:string}>({ + method: 'eth_gasPrice' as Method, + params: [], + }).then(response => ethers.BigNumber.from(response.result)); + } + + async getEstimatedGas(limit: number): Promise<{ system:ethers.BigNumber, fiat:ethers.BigNumber }> { + const gasPrice: ethers.BigNumber = await this.getGasPrice(); + const tokenPrice: number = await this.getUsdPrice(); + const price = ethers.utils.parseUnits(toStringNumber(tokenPrice), WEI_PRECISION); + const system = gasPrice.mul(limit); + const fiatDouble = system.mul(price); + const fiat = fiatDouble.div(ethers.utils.parseUnits('1', WEI_PRECISION)); + return { system, fiat }; + } + async getLatestBlock(): Promise { + return this.doRPC<{result:string}>({ + method: 'eth_blockNumber' as Method, + params: [], + }).then(response => ethers.BigNumber.from(response.result)); + } + + async getBlockByNumber(blockNumber: string): Promise { + return this.doRPC<{result:EvmBlockData}>({ + method: 'eth_getBlockByNumber' as Method, + params: [parseInt(blockNumber).toString(16), false], + }).then((response) => { + console.error('type of response.result', typeof response.result, [response.result]); + return response.result as EvmBlockData; + }); + } +} diff --git a/src/antelope/chains/evm/telos-evm-testnet/index.ts b/src/antelope/chains/evm/telos-evm-testnet/index.ts new file mode 100644 index 000000000..933854472 --- /dev/null +++ b/src/antelope/chains/evm/telos-evm-testnet/index.ts @@ -0,0 +1,180 @@ +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { NativeCurrencyAddress, PriceChartData, addressString } from 'src/antelope/types'; +import { TokenClass, TokenSourceInfo } from 'src/antelope/types'; +import { useUserStore } from 'src/antelope'; +import { getFiatPriceFromIndexer, getCoingeckoPriceChartData, getCoingeckoUsdPrice } from 'src/lib/price'; + +const LOGO = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/telos.png'; +const CHAIN_ID = '41'; +export const NETWORK = 'telos-evm-testnet'; +const DISPLAY = 'Telos EVM (Testnet)'; +const TOKEN = new TokenClass({ + name: 'Telos', + symbol: 'TLOS', + network: NETWORK, + decimals: 18, + address: NativeCurrencyAddress, + logo: LOGO, + logoURI: LOGO, + isNative: false, + isSystem: true, +} as TokenSourceInfo); + +const S_TOKEN = new TokenClass({ + name: 'Staked Telos', + symbol: 'STLOS', + network: NETWORK, + decimals: 18, + address: '0xa9991E4daA44922D00a78B6D986cDf628d46C4DD', + logo: 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/stlos.png', + isNative: false, + isSystem: false, +} as TokenSourceInfo); + +const W_TOKEN = new TokenClass({ + name: 'Wrapped Telos', + symbol: 'WTLOS', + network: NETWORK, + decimals: 18, + address: '0xaE85Bf723A9e74d6c663dd226996AC1b8d075AA9', + logo: 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/wtlos.png', + isNative: false, + isSystem: false, +} as TokenSourceInfo); + +const RPC_ENDPOINT = { + protocol: 'https', + host: 'testnet.telos.net', + port: 443, + path: '/evm', +}; +const ESCROW_CONTRACT_ADDRESS = '0x7E9cF9fBc881652B05BB8F26298fFAB538163b6f'; +const API_ENDPOINT = 'https://api-dev.telos.net/v1'; +const WEI_PRECISION = 18; +const EXPLORER_URL = 'https://testnet.teloscan.io'; +const ECOSYSTEM_URL = 'https://www.telos.net/ecosystem'; +const BRIDGE_URL = 'https://telos-bridge-testnet.netlify.app/bridge'; + +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: { trackEvent: (eventName: string) => void }; + +export default class TelosEVMTestnet extends EVMChainSettings { + isTestnet() { + return true; + } + + getNetwork(): string { + return NETWORK; + } + + getChainId(): string { + return CHAIN_ID; + } + + getDisplay(): string { + return DISPLAY; + } + + getHyperionEndpoint(): string { + return NETWORK_EVM_ENDPOINT; + } + + getRPCEndpoint(): RpcEndpoint { + return RPC_ENDPOINT; + } + + getApiEndpoint(): string { + return API_ENDPOINT; + } + + getPriceData(): Promise { + return getCoingeckoPriceChartData('telos'); + } + + getSystemToken(): TokenClass { + return TOKEN; + } + + getStakedSystemToken(): TokenClass { + return S_TOKEN; + } + + getWrappedSystemToken(): TokenClass { + return W_TOKEN; + } + + getEscrowContractAddress(): addressString { + return ESCROW_CONTRACT_ADDRESS; + } + + async getUsdPrice(): Promise { + if (this.hasIndexerSupport() && this.isIndexerHealthy()) { + const nativeTokenSymbol = this.getSystemToken().symbol; + const fiatCode = useUserStore().fiatCurrency; + const fiatPrice = await getFiatPriceFromIndexer(nativeTokenSymbol, NativeCurrencyAddress, fiatCode, this.indexer, this); + + if (fiatPrice !== 0) { + return fiatPrice; + } + } + + return await getCoingeckoUsdPrice('telos'); + } + + getLargeLogoPath(): string { + return LOGO; + } + + getSmallLogoPath(): string { + return LOGO; + } + + getWeiPrecision(): number { + return WEI_PRECISION; + } + + getExplorerUrl(): string { + return EXPLORER_URL; + } + + getEcosystemUrl(): string { + return ECOSYSTEM_URL; + } + + getBridgeUrl(): string { + return BRIDGE_URL; + } + + getTrustedContractsBucket(): string { + return CONTRACTS_BUCKET; + } + + getBuyMoreOfTokenLink(): string { + return 'https://app.telos.net/testnet/evm-faucet'; + } + + getSystemTokens(): TokenClass[] { + return [TOKEN, S_TOKEN, W_TOKEN]; + } + + getIndexerApiEndpoint(): string { + return INDEXER_ENDPOINT; + } + + hasIndexerSupport(): boolean { + return true; + } + + trackAnalyticsEvent(eventName: string): void { + if (typeof fathom === 'undefined') { + console.warn(`Failed to track event with name '${eventName}': Fathom Analytics not loaded`); + return; + } + + fathom.trackEvent(eventName); + } +} diff --git a/src/antelope/chains/evm/telos-evm/index.ts b/src/antelope/chains/evm/telos-evm/index.ts new file mode 100644 index 000000000..4f3e49ff7 --- /dev/null +++ b/src/antelope/chains/evm/telos-evm/index.ts @@ -0,0 +1,175 @@ +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { NativeCurrencyAddress, PriceChartData, addressString } from 'src/antelope/types'; +import { TokenClass, TokenSourceInfo } from 'src/antelope/types'; +import { useUserStore } from 'src/antelope'; +import { getFiatPriceFromIndexer, getCoingeckoPriceChartData, getCoingeckoUsdPrice } from 'src/lib/price'; + +const LOGO = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/telos.png'; +const CHAIN_ID = '40'; +export const NETWORK = 'telos-evm'; +const DISPLAY = 'Telos EVM'; +const TOKEN = new TokenClass({ + name: 'Telos', + symbol: 'TLOS', + network: NETWORK, + decimals: 18, + address: NativeCurrencyAddress, + logo: LOGO, + logoURI: LOGO, + isNative: false, + isSystem: true, +} as TokenSourceInfo); + +const S_TOKEN = new TokenClass({ + name: 'Staked Telos', + symbol: 'STLOS', + network: NETWORK, + decimals: 18, + address: '0xB4B01216a5Bc8F1C8A33CD990A1239030E60C905', + logo: 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/stlos.png', + isNative: false, + isSystem: false, +} as TokenSourceInfo); + +const W_TOKEN = new TokenClass({ + name: 'Wrapped Telos', + symbol: 'WTLOS', + network: NETWORK, + decimals: 18, + address: '0xD102cE6A4dB07D247fcc28F366A623Df0938CA9E', + logo: 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/wtlos.png', + isNative: false, + isSystem: false, +} as TokenSourceInfo); + +const RPC_ENDPOINT = { + protocol: 'https', + host: 'mainnet.telos.net', + port: 443, + path: '/evm', +}; +const ESCROW_CONTRACT_ADDRESS = '0x95F5713A1422Aa3FBD3DCB8D553945C128ee3855'; +const API_ENDPOINT = 'https://api.telos.net/v1'; +const WEI_PRECISION = 18; +const EXPLORER_URL = 'https://teloscan.io'; +const ECOSYSTEM_URL = 'https://www.telos.net/ecosystem'; +const BRIDGE_URL = 'https://bridge.telos.net/bridge'; +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: { trackEvent: (eventName: string) => void }; + +export default class TelosEVM extends EVMChainSettings { + getNetwork(): string { + return NETWORK; + } + + getChainId(): string { + return CHAIN_ID; + } + + getDisplay(): string { + return DISPLAY; + } + + getHyperionEndpoint(): string { + return NETWORK_EVM_ENDPOINT; + } + + getRPCEndpoint(): RpcEndpoint { + return RPC_ENDPOINT; + } + + getApiEndpoint(): string { + return API_ENDPOINT; + } + + getPriceData(): Promise { + return getCoingeckoPriceChartData('telos'); + } + + getSystemToken(): TokenClass { + return TOKEN; + } + + getStakedSystemToken(): TokenClass { + return S_TOKEN; + } + + getWrappedSystemToken(): TokenClass { + return W_TOKEN; + } + + getEscrowContractAddress(): addressString { + return ESCROW_CONTRACT_ADDRESS; + } + + async getUsdPrice(): Promise { + if (this.hasIndexerSupport() && this.isIndexerHealthy()) { + const nativeTokenSymbol = this.getSystemToken().symbol; + const fiatCode = useUserStore().fiatCurrency; + const fiatPrice = await getFiatPriceFromIndexer(nativeTokenSymbol, NativeCurrencyAddress, fiatCode, this.indexer, this); + + if (fiatPrice !== 0) { + return fiatPrice; + } + } + + return await getCoingeckoUsdPrice('telos'); + } + + getLargeLogoPath(): string { + return LOGO; + } + + getSmallLogoPath(): string { + return LOGO; + } + + getWeiPrecision(): number { + return WEI_PRECISION; + } + + getExplorerUrl(): string { + return EXPLORER_URL; + } + + getEcosystemUrl(): string { + return ECOSYSTEM_URL; + } + + getBridgeUrl(): string { + return BRIDGE_URL; + } + + getTrustedContractsBucket(): string { + return CONTRACTS_BUCKET; + } + + getBuyMoreOfTokenLink(): string { + return 'https://www.telos.net/buy'; + } + + getSystemTokens(): TokenClass[] { + return [TOKEN, S_TOKEN, W_TOKEN]; + } + + getIndexerApiEndpoint(): string { + return INDEXER_ENDPOINT; + } + + hasIndexerSupport(): boolean { + return true; + } + + trackAnalyticsEvent(eventName: string): void { + if (typeof fathom === 'undefined') { + console.warn(`Failed to track event with name '${eventName}': Fathom Analytics not loaded`); + return; + } + + fathom.trackEvent(eventName); + } +} diff --git a/src/antelope/index.ts b/src/antelope/index.ts new file mode 100644 index 000000000..1389dd777 --- /dev/null +++ b/src/antelope/index.ts @@ -0,0 +1,8 @@ +export * from 'src/antelope/mocks/AccountStore'; +export * from 'src/antelope/mocks/AntelopeConfig'; +export * from 'src/antelope/mocks/ChainStore'; +export * from 'src/antelope/mocks/UserStore'; +export * from 'src/antelope/mocks/ContractStore'; +export * from 'src/antelope/mocks/EVMStore'; +export * from 'src/antelope/mocks/FeedbackStore'; +export * from 'src/antelope/mocks/PlatformStore'; diff --git a/src/antelope/mocks/AccountStore.ts b/src/antelope/mocks/AccountStore.ts new file mode 100644 index 000000000..75ed07f47 --- /dev/null +++ b/src/antelope/mocks/AccountStore.ts @@ -0,0 +1,186 @@ +/* eslint-disable max-len */ +/* eslint-disable no-unused-vars */ +// Mocking AccountStore ----------------------------------- +// useAccountStore().getAccount(this.label).account as addressString; +import { EVMAuthenticator } from 'src/antelope/wallets'; +import { AntelopeError, addressString } from 'src/antelope/types'; +import { CURRENT_CONTEXT, TeloscanEVMChainSettings, createTraceFunction, useChainStore } from 'src/antelope/mocks'; +import { BigNumber } from 'ethers'; 'src/antelope/mocks/FeedbackStore'; +import { getAntelope } from 'src/antelope/mocks/AntelopeConfig'; +import { useFeedbackStore } from 'src/antelope/mocks'; +import { EvmABI, EvmFunctionParam, Label, TransactionResponse } from 'src/antelope/types'; +import { subscribeForTransactionReceipt } from 'src/antelope/wallets/utils/trx-utils'; + +export interface AccountModel { + label: typeof CURRENT_CONTEXT; + isNative: boolean; + authenticator: EVMAuthenticator; + account: addressString; +} + +export interface EvmAccountModel extends AccountModel { + address: addressString; + displayAddress: string; + isNative: false; + associatedNative: string; + authenticator: EVMAuthenticator; +} + +let currentAuthenticator = {} as EVMAuthenticator; +let currentAccount = null as addressString | null; + +interface LoginEVMActionData { + authenticator: EVMAuthenticator + network: string, + autoLogAccount?: string, +} + +class AccountStore { + + trace: (action: string, ...args: unknown[]) => void; + + constructor() { + this.trace = createTraceFunction('EVMStore'); + } + + getAccount(label: string) { + return { + label, + isNative: false, + authenticator: currentAuthenticator, + account: currentAccount, + } as AccountModel; + } + + async loginEVM({ authenticator, network, autoLogAccount }: LoginEVMActionData, trackAnalyticsEvents: boolean): Promise { + currentAuthenticator = authenticator; + currentAccount = autoLogAccount + ? await authenticator.autoLogin(network, autoLogAccount, trackAnalyticsEvents) + : await authenticator.login(network, trackAnalyticsEvents); + + if (currentAccount) { + const account = useAccountStore().getAccount(authenticator.label); + getAntelope().events.onLoggedIn.next(account); + } + return !!currentAccount; + } + + logout() { + currentAuthenticator.logout(); + currentAuthenticator = {} as EVMAuthenticator; + currentAccount = null; + getAntelope().events.onLoggedOut.next(); + } + + get loggedAccount() { + return this.getAccount(CURRENT_CONTEXT); + } + + get currentAccount() { + return this.getAccount(CURRENT_CONTEXT); + } + + async subscribeForTransactionReceipt(account: EvmAccountModel, response: TransactionResponse): Promise { + this.trace('subscribeForTransactionReceipt', account.account, response.hash); + return subscribeForTransactionReceipt(account, response).then(({ newResponse, receipt }) => { + newResponse.wait().then(() => { + this.trace('subscribeForTransactionReceipt', newResponse.hash, 'receipt:', receipt.status, receipt); + }); + return newResponse; + }); + } + + async signCustomTransaction( + label: Label, + actionMessage: string, + actionError: string, + contract: string, + abi: EvmABI, + parameters: EvmFunctionParam[], + value?: BigNumber, + ): Promise{ + const ant = getAntelope(); + const funcname = 'signCustomTransaction'; + this.trace(funcname, label, contract, abi, parameters, value?.toString()); + + if (! await this.assertNetworkConnection(label)) { + throw new AntelopeError('antelope.evm.error_switch_chain_rejected'); + } + + try { + useFeedbackStore().setLoading(funcname); + const account = this.loggedAccount as EvmAccountModel; + const authenticator = this.loggedAccount.authenticator as EVMAuthenticator; + const chainSettings = useChainStore().loggedChain.settings as unknown as TeloscanEVMChainSettings; + + const tx = await authenticator.signCustomTransaction(contract, abi, parameters, value) + .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); + + // we create tne neutral notification + const dismiss = ant.config.notifyNeutralMessageHandler(actionMessage); + + tx.wait().then(() => { + ant.config.notifySuccessfulTrxHandler( + `${chainSettings.getExplorerUrl()}/tx/${tx.hash}`, + ); + }).catch((err) => { + console.error(err); + }).finally(() => { + dismiss(); + }); + + return tx; + } catch (error) { + const trxError = ant.config.transactionError(actionError, error); + ant.config.transactionErrorHandler(trxError, funcname); + throw trxError; + } finally { + useFeedbackStore().unsetLoading(funcname); + } + } + + async isConnectedToCorrectNetwork(label: string): Promise { + this.trace('isConnectedToCorrectNetwork', label); + try { + useFeedbackStore().setLoading('account.isConnectedToCorrectNetwork'); + const authenticator = useAccountStore().getAccount(label)?.authenticator as EVMAuthenticator; + return authenticator.isConnectedToCorrectChain(); + } catch (error) { + console.error('Error: ', error); + return Promise.resolve(false); + } finally { + useFeedbackStore().unsetLoading('account.isConnectedToCorrectNetwork'); + } + } + + async assertNetworkConnection(label: string): Promise { + if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + const ant = getAntelope(); + const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; + try { + await authenticator.ensureCorrectChain(); + if (!await useAccountStore().isConnectedToCorrectNetwork(label)) { + resolve(false); + } else { + resolve(true); + } + } catch (error) { + const message = (error as Error).message; + if (message === 'antelope.evm.error_switch_chain_rejected') { + ant.config.notifyNeutralMessageHandler(message); + } + resolve(false); + } + }); + } else { + return true; + } + } + +} + +const accountStore = new AccountStore(); + +export const useAccountStore = () => accountStore; diff --git a/src/antelope/mocks/AntelopeConfig.ts b/src/antelope/mocks/AntelopeConfig.ts new file mode 100644 index 000000000..da5ea3190 --- /dev/null +++ b/src/antelope/mocks/AntelopeConfig.ts @@ -0,0 +1,337 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// Mocking Antelope and Config ----------------------------------- +import { EVMAuthenticator } from 'src/antelope/wallets/authenticators/EVMAuthenticator'; +import { AntelopeError, AntelopeErrorPayload } from 'src/antelope/types'; +import { App } from 'vue'; +import { Authenticator } from 'universal-authenticator-library'; +import { Subject } from 'rxjs'; +import { AccountModel } from 'src/antelope/mocks/AccountStore'; + +export interface ComplexMessage { + tag: string, + class: string, + text: string, +} + +export class AntelopeWallets { + private authenticators: Map = new Map(); + init() { + // dummie function + } + addEVMAuthenticator(authenticator: EVMAuthenticator) { + this.authenticators.set(authenticator.getName(), authenticator); + } + getAuthenticator(name: string) { + return this.authenticators.get(name); + } +} + +export class AntelopeConfig { + transactionError(description: string, error: unknown): AntelopeError { + if (error instanceof AntelopeError) { + return error as AntelopeError; + } + const msgOrObject = this.errorMessageExtractor(error); + // if it matches antelope.*.error_* + if (typeof msgOrObject === 'string') { + return new AntelopeError(msgOrObject, { error }); + } else { + return new AntelopeError(description, { error: msgOrObject }); + } + } + + // indexer health threshold -- + private __indexer_health_threshold = 10; // 10 seconds + + // indexer health check interval -- + private __indexer_health_check_interval = 1000 * 60 * 5; // 5 minutes expressed in milliseconds + + // notifucation handlers -- + private __notify_error_handler: (message: string) => void = m => alert(`Error: ${m}`); + private __notify_success_handler: (message: string) => void = alert; + private __notify_warning_handler: (message: string) => void = alert; + + // notification handlers -- + private __notify_successful_trx_handler: (link: string) => void = alert; + private __notify_success_message_handler: (message: string, payload?: never) => void = alert; + private __notify_success_copy_handler: () => void = alert; + private __notify_failure_message_handler: (message: string, payload?: AntelopeErrorPayload) => void = alert; + private __notify_failure_action_handler: (message: string, payload?: AntelopeErrorPayload) => void = alert; + private __notify_disconnected_handler: () => void = alert; + private __notify_neutral_message_handler: (message: string) => (() => void) = () => (() => void 0); + private __notify_remember_info_handler: (title: string, message: string | ComplexMessage[], + payload: string, key: string) => (() => void) = () => (() => void 0); + + // ual authenticators list getter -- + private __authenticators_getter: () => Authenticator[] = () => []; + + // localization handler -- + private __localization_handler: (key: string, payload?: Record) => string = (key: string) => key; + + // transaction error handler -- + private __transaction_error_handler: (err: AntelopeError, trxFailed: string) => void = () => void 0; + + // error to string handler -- + private __error_message_extractor: (error: unknown) => object | string | null = (error: unknown) => { + try { + type EVMError = {code:string}; + const evmErr = error as EVMError; + + // high priority generic errors + switch (evmErr.code) { + case 'CALL_EXCEPTION': return 'antelope.evm.error_call_exception'; + case 'INSUFFICIENT_FUNDS': return 'antelope.evm.error_insufficient_funds'; + case 'MISSING_NEW': return 'antelope.evm.error_missing_new'; + case 'NONCE_EXPIRED': return 'antelope.evm.error_nonce_expired'; + case 'NUMERIC_FAULT': return 'antelope.evm.error_numeric_fault'; + case 'REPLACEMENT_UNDERPRICED': return 'antelope.evm.error_replacement_underpriced'; + case 'TRANSACTION_REPLACED': return 'antelope.evm.error_transaction_replaced'; + case 'USER_REJECTED': return 'antelope.evm.error_user_rejected'; + case 'ACTION_REJECTED': return 'antelope.evm.error_transaction_canceled'; + } + + if (typeof error === 'object') { + const candidates = ['message', 'reason']; + + const extractDeepestErrorMessage = (error: unknown): string => { + if (typeof error !== 'object' || error === null) { + return 'unknown'; // We return 'unknown' if it is not an object or is null + } + const queue: {node: unknown, depth: number}[] = [{ node: error, depth: 0 }]; + let deepestMessage = 'unknown'; + let maxDepth = -1; + + while (queue.length > 0) { + const { node, depth } = queue.shift()!; // Sacamos el primer elemento de la cola + + const nodeKeys = Object.keys(node as Record); + for (const key of nodeKeys) { + const value = (node as Record)[key]; + if (candidates.includes(key) && typeof value === 'string') { + // If we find a message in a deeper level, we update it + if (depth > maxDepth) { + deepestMessage = value; + maxDepth = depth; + } + } else if (typeof value === 'object' && value !== null) { + // If the value is an object, we add it to the queue to explore its children + queue.push({ node: value, depth: depth + 1 }); + } + } + } + + return deepestMessage; + }; + const messageFound = extractDeepestErrorMessage(error as Record); + if (messageFound !== 'unknown') { + return messageFound; + } + } + + // low priority generic errors + switch (evmErr.code) { + case 'UNPREDICTABLE_GAS_LIMIT': return 'antelope.evm.error_unpredictable_gas_limit'; + } + + if (typeof error === 'string') { + return { text: error }; + } + if (typeof error === 'number') { + return { number: error.toString() }; + } + if (typeof error === 'boolean') { + return { boolean: error.toString() }; + } + if (typeof error === 'undefined') { + return { value: 'undefined' }; + } + if (typeof error === 'object') { + return error; + } + return { }; + } catch (er) { + return { }; + } + }; + + // Vue.App holder -- + private __app: App | null = null; + + constructor() { + // + } + + init(app: App) { + this.__app = app; + } + + get app() { + return this.__app; + } + + get indexerHealthThresholdSeconds() { + return this.__indexer_health_threshold; + } + + get indexerHealthCheckInterval() { + return this.__indexer_health_check_interval; + } + + get notifyErrorHandler() { + return this.__notify_error_handler; + } + + get notifySuccessHandler() { + return this.__notify_success_handler; + } + + get notifyWarningHandler() { + return this.__notify_warning_handler; + } + + get notifySuccessfulTrxHandler() { + return this.__notify_successful_trx_handler; + } + + get notifySuccessMessageHandler() { + return this.__notify_success_message_handler; + } + + get notifySuccessCopyHandler() { + return this.__notify_success_copy_handler; + } + + get notifyFailureMessage() { + return this.__notify_failure_message_handler; + } + + get notifyFailureWithAction() { + return this.__notify_failure_action_handler; + } + + get notifyDisconnectedHandler() { + return this.__notify_disconnected_handler; + } + + get notifyNeutralMessageHandler() { + return this.__notify_neutral_message_handler; + } + + get notifyRememberInfoHandler() { + return this.__notify_remember_info_handler; + } + + get authenticatorsGetter() { + return this.__authenticators_getter; + } + + get localizationHandler() { + return this.__localization_handler; + } + + get transactionErrorHandler() { + return this.__transaction_error_handler; + } + + get errorMessageExtractor() { + return this.__error_message_extractor; + } + + // setting indexer constants -- + public setIndexerHealthThresholdSeconds(threshold: number) { + this.__indexer_health_threshold = threshold; + } + + public setIndexerHealthCheckInterval(interval: number) { + this.__indexer_health_check_interval = interval; + } + + // setting notification handlers -- + public setNotifyErrorHandler(handler: (message: string) => void) { + this.__notify_error_handler = handler; + } + + public setNotifySuccessHandler(handler: (message: string) => void) { + this.__notify_success_handler = handler; + } + + public setNotifyWarningHandler(handler: (message: string) => void) { + this.__notify_warning_handler = handler; + } + + public setNotifySuccessfulTrxHandler(handler: (link: string) => void) { + this.__notify_successful_trx_handler = handler; + } + + public setNotifySuccessMessageHandler(handler: (message: string, payload?: never) => void) { + this.__notify_success_message_handler = handler; + } + + public setNotifySuccessCopyHandler(handler: () => void) { + this.__notify_success_copy_handler = handler; + } + + public setNotifyFailureMessage(handler: (message: string, payload?: AntelopeErrorPayload) => void) { + this.__notify_failure_message_handler = handler; + } + + public setNotifyFailureWithAction(handler: (message: string, payload?: AntelopeErrorPayload) => void) { + this.__notify_failure_action_handler = handler; + } + + public setNotifyDisconnectedHandler(handler: () => void) { + this.__notify_disconnected_handler = handler; + } + + public setNotifyNeutralMessageHandler(handler: (message: string) => (() => void)) { + this.__notify_neutral_message_handler = handler; + } + + public setNotifyRememberInfoHandler(handler: ( + title: string, + message: string | ComplexMessage[], + payload: string, + key: string, + ) => (() => void)) { + this.__notify_remember_info_handler = handler; + } + + // setting authenticators getter -- + public setAuthenticatorsGetter(getter: () => Authenticator[]) { + this.__authenticators_getter = getter; + } + + // setting translation handler -- + public setLocalizationHandler(handler: (key: string, payload?: Record) => string) { + this.__localization_handler = handler; + } + + // setting transaction error handler -- + public setTransactionErrorHandler(handler: (err: AntelopeError, trxFailed: string) => void) { + this.__transaction_error_handler = handler; + } + + // setting error to string handler -- + public setErrorMessageExtractor(handler: (catched: unknown) => object | string | null) { + this.__error_message_extractor = handler; + } + +} + +const config = new AntelopeConfig(); +const wallets = new AntelopeWallets(); +const events = { + onLoggedIn: new Subject(), + onLoggedOut: new Subject(), +}; +const Antelope = { + config, + wallets, + events, +}; + +export const getAntelope = () => Antelope; +// ---------------------------------------------------------------- + diff --git a/src/antelope/mocks/ChainStore.ts b/src/antelope/mocks/ChainStore.ts new file mode 100644 index 000000000..e2c84e80e --- /dev/null +++ b/src/antelope/mocks/ChainStore.ts @@ -0,0 +1,155 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// Mocking ChainStore ----------------------------------- +declare const fathom: { trackEvent: (eventName: string) => void }; + +import { RpcEndpoint } from 'universal-authenticator-library'; +import { ChainSettings, NativeCurrencyAddress, TokenClass } from 'src/antelope/types'; +import TelosEVM from 'src/antelope/chains/evm/telos-evm'; +import TelosEVMTestnet from 'src/antelope/chains/evm/telos-evm-testnet'; +import { ethers } from 'ethers'; + +export interface TeloscanEVMChainSettings { + getStakedSystemToken(): TokenClass; + getWrappedSystemToken: () => TokenClass; + getChainId: () => string; + getDisplay: () => string; + trackAnalyticsEvent: (name: string) => void; + getRPCEndpoint: () => RpcEndpoint; + getEscrowContractAddress: () => string; + getNetwork: () => string; + getSystemToken: () => TokenClass; + getExplorerUrl: () => string; + getSmallLogoPath: () => string; + getLargeLogoPath: () => string; +} + +export const evmSettings: { [network: string]: TeloscanEVMChainSettings } = { + 'telos-evm': new TelosEVM('telos-evm'), + 'telos-evm-testnet': new TelosEVMTestnet('telos-evm-testnet'), +}; + +export const chains: { [network: string]: ChainModel } = {}; + +export interface ChainModel { + settings: TeloscanEVMChainSettings; +} + +export interface EvmChainModel { + settings: TeloscanEVMChainSettings; + gasPrice: ethers.BigNumber; +} + +const newChainModel = (network: string, isNative: boolean): ChainModel => { + const model = { + lastUpdate: 0, + apy: '', + stakeRatio: ethers.constants.Zero, + unstakeRatio: ethers.constants.Zero, + settings: evmSettings[network], + tokens: [], + } as unknown as ChainModel; + if (!isNative) { + (model as unknown as EvmChainModel).gasPrice = ethers.constants.Zero; + } + return model; +}; + +/* +const settings = { + getChainId: () => process.env.NETWORK_EVM_CHAIN_ID, + getDisplay: () => process.env.NETWORK_EVM_DISPLAY, + trackAnalyticsEvent(eventName: string): void { + if (typeof fathom === 'undefined') { + console.warn(`Failed to track event with name ${eventName}: Fathom Analytics not loaded`); + return; + } + + fathom.trackEvent(eventName); + }, + getRPCEndpoint: () => { + // extract the url parts + const regex = /^(https?):\/\/([^:/]+)(?::(\d+))?(\/.*)?$/; + const match = (process.env.NETWORK_EVM_RPC as string).match(regex); + if (!match) { + throw new Error('Invalid RPC endpoint'); + } + // We destructure the result of the match to get each component + const [, protocol, host, port, path] = match; + return { + protocol, + host, + port: port ? parseInt(port, 10) : 443, + path: path || '/', + }; + }, + getEscrowContractAddress: () => process.env.TELOS_ESCROW_CONTRACT_ADDRESS, + getStakedSystemToken: () => ({ + address: process.env.STAKED_TLOS_CONTRACT_ADDRESS, + decimals: 18, + symbol: 'STLOS', + } as TokenClass), + getWrappedSystemToken: () => ({ + address: process.env.WRAPPED_TLOS_CONTRACT_ADDRESS, + decimals: 18, + symbol: 'WTLOS', + } as TokenClass), + getSystemToken: () => ({ + name: 'Telos', + address: NativeCurrencyAddress, + decimals: 18, + symbol: 'TLOS', + } as TokenClass), + getNetwork: () => process.env.NETWORK_EVM_NAME, + getExplorerUrl: () => window.location.origin, + getSmallLogoPath: () => 'small-icon-url', + getLargeLogoPath: () => 'large-icon-url', +} as TeloscanEVMChainSettings; +*/ + +let current = { + settings: evmSettings['telos-evm'], +} as unknown as ChainModel; + +const ChainStore = { + currentChain: current as unknown as ChainModel, + loggedChain: current as unknown as ChainModel, + loggedEvmChain: current as unknown as ChainModel, + getNetworkSettings: (network: string) => current.settings, + getChain: (label: string) => ChainStore.currentChain, + setChain: (label: string, network: string) => { + console.error('ChainStore.setChain', label, network); + if (network in evmSettings) { + + // create the chain model if it doesn't exist + if (!chains[network]) { + chains[network] = newChainModel(network, false); + } + + // make the change only if they are different + if (network !== current.settings.getNetwork()) { + current = chains[network]; + ChainStore.currentChain = current; + ChainStore.loggedChain = current; + ChainStore.loggedEvmChain = current; + } + } else { + throw new Error(`Network '${network}' not supported`); + } + }, +}; + +export const useChainStore = () => ChainStore; + +/* + +// TODO: put this code somewhere else +setTimeout(() => { + if (process.env.NETWORK === 'mainnet') { + ChainStore.setChain('mainnet', 'telos-evm'); + } else { + ChainStore.setChain('testnet', 'telos-evm-testnet'); + } +}, 1000); + +*/ diff --git a/src/antelope/mocks/ContractStore.ts b/src/antelope/mocks/ContractStore.ts new file mode 100644 index 000000000..863b0f8a5 --- /dev/null +++ b/src/antelope/mocks/ContractStore.ts @@ -0,0 +1,17 @@ +/* eslint-disable max-len */ + +import { EvmABI, erc1155Abi, erc20Abi, erc721Abi } from 'src/antelope/types'; + +// Mocking ContractStore ----------------------------------- +const ContractStore = { + getTokenABI(type:string): EvmABI { + if(type === 'erc721'){ + return erc721Abi; + } else if(type === 'erc1155'){ + return erc1155Abi; + } + return erc20Abi; + }, +}; + +export const useContractStore = () => ContractStore; diff --git a/src/antelope/mocks/EVMStore.ts b/src/antelope/mocks/EVMStore.ts new file mode 100644 index 000000000..51388e3b2 --- /dev/null +++ b/src/antelope/mocks/EVMStore.ts @@ -0,0 +1,171 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +// Mocking EVMStore ----------------------------------- +import { ethers } from 'ethers'; +import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; +import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; +import { TeloscanEVMChainSettings, useChainStore, useFeedbackStore, useAccountStore } from 'src/antelope/mocks'; +import { AntelopeError, EthereumProvider, ExceptionError } from 'src/antelope/types'; +import { RpcEndpoint } from 'universal-authenticator-library'; + +class EVMStore { + trace: (action: string, ...args: unknown[]) => void; + + constructor() { + this.trace = createTraceFunction('EVMStore'); + } + + // actions --- + async initInjectedProvider(authenticator: InjectedProviderAuth): Promise { + this.trace('initInjectedProvider', authenticator.getName(), [authenticator.getProvider()]); + const provider: EthereumProvider | null = authenticator.getProvider(); + const evm = useEVMStore(); + + if (provider && !provider.__initialized) { + this.trace('initInjectedProvider', authenticator.getName(), 'initializing provider'); + // ensure this provider actually has the correct methods + // Check consistency of the provider + const methods = ['request', 'on']; + const candidate = provider as unknown as Record; + for (const method of methods) { + if (typeof candidate[method] !== 'function') { + console.warn(`MetamaskAuth.getProvider: method ${method} not found`); + throw new AntelopeError('antelope.evm.error_invalid_provider'); + } + } + + provider.on('accountsChanged', async (value) => { + const accounts = value as string[]; + const network = useChainStore().currentChain.settings.getNetwork(); + evm.trace('provider.accountsChanged', ...accounts); + + if (accounts.length > 0) { + // If we are here one of two possible things had happened: + // 1. The user has just logged in to the wallet + // 2. The user has switched the account in the wallet + + // if we are in case 1, then we are in the middle of the login process and we don't need to do anything + // We can tell because the account store has no logged account + + // But if we are in case 2 and have a logged account, we need to re-login the account using the same authenticator + // overwriting the previous logged account, which in turn will trigger all account data to be reloaded + if (useAccountStore().loggedAccount) { + // if the user is already authenticated we try to re login the account using the same authenticator + const authenticator = useAccountStore().loggedAccount.authenticator as EVMAuthenticator; + if (!authenticator) { + console.error('Inconsistency: logged account authenticator is null', authenticator); + } else { + useAccountStore().loginEVM({ authenticator, network }, false); + } + } + } else { + // the user has disconnected the all the accounts from the wallet so we logout + useAccountStore().logout(); + } + }); + + // This initialized property is not part of the standard provider, it's just a flag to know if we already initialized the provider + provider.__initialized = true; + evm.addInjectedProvider(authenticator); + } + authenticator.onReady.next(true); + } + + addInjectedProvider(authenticator: InjectedProviderAuth) { + this.trace('addInjectedProvider', authenticator.getName()); + } + + async switchChainInjected(InjectedProvider: ethers.providers.ExternalProvider): Promise { + this.trace('switchChainInjected', [InjectedProvider]); + useFeedbackStore().setLoading('evm.switchChainInjected'); + const provider = InjectedProvider; + if (provider) { + const chainSettings = useChainStore().loggedChain.settings as unknown as TeloscanEVMChainSettings; + const chainId = parseInt(chainSettings.getChainId(), 10); + const chainIdParam = `0x${chainId.toString(16)}`; + if (!provider.request) { + useFeedbackStore().unsetLoading('evm.switchChainInjected'); + throw new AntelopeError('antelope.evm.error_support_provider_request'); + } + try { + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainIdParam }], + }); + return true; + } catch (error) { + const chainNotAddedCodes = [ + 4902, + -32603, // https://github.com/MetaMask/metamask-mobile/issues/2944 + ]; + + if (chainNotAddedCodes.includes((error as unknown as ExceptionError).code)) { // 'Chain hasn't been added' + const p:RpcEndpoint = chainSettings.getRPCEndpoint(); + const rpcUrl = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + try { + if (!provider.request) { + throw new AntelopeError('antelope.evm.error_support_provider_request'); + } + const payload = { + method: 'wallet_addEthereumChain', + params: [{ + chainId: chainIdParam, + chainName: chainSettings.getDisplay(), + nativeCurrency: { + name: chainSettings.getSystemToken().name, + symbol: chainSettings.getSystemToken().symbol, + decimals: chainSettings.getSystemToken().decimals, + }, + rpcUrls: [rpcUrl], + blockExplorerUrls: [chainSettings.getExplorerUrl()], + iconUrls: [chainSettings.getSmallLogoPath(), chainSettings.getLargeLogoPath()], + }], + }; + await provider.request(payload); + return true; + } catch (e) { + if ((e as unknown as ExceptionError).code === 4001) { + throw new AntelopeError('antelope.evm.error_add_chain_rejected'); + } else { + console.error('Error:', e); + throw new AntelopeError('antelope.evm.error_add_chain'); + } + } + } else if ((error as unknown as ExceptionError).code === 4001) { + throw new AntelopeError('antelope.evm.error_switch_chain_rejected'); + } else { + console.error('Error:', error); + throw new AntelopeError('antelope.evm.error_switch_chain'); + } + } finally { + useFeedbackStore().unsetLoading('evm.switchChainInjected'); + } + } else { + useFeedbackStore().unsetLoading('evm.switchChainInjected'); + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } + + async isProviderOnTheCorrectChain(provider: ethers.providers.Web3Provider, correctChainId: string): Promise { + const { chainId } = await provider.getNetwork(); + const response = +chainId === +correctChainId; + this.trace('isProviderOnTheCorrectChain', provider, ' -> ', response); + return response; + } + + async ensureCorrectChain(authenticator: EVMAuthenticator): Promise { + this.trace('ensureCorrectChain', authenticator); + const checkProvider = await authenticator.web3Provider(); + let response = checkProvider; + const correctChainId = useChainStore().currentChain.settings.getChainId(); + if (!await this.isProviderOnTheCorrectChain(checkProvider, correctChainId)) { + const provider = await authenticator.externalProvider(); + await this.switchChainInjected(provider); + response = new ethers.providers.Web3Provider(provider); + } + return response; + } + +} + +export const useEVMStore = () => new EVMStore(); diff --git a/src/antelope/mocks/FeedbackStore.ts b/src/antelope/mocks/FeedbackStore.ts new file mode 100644 index 000000000..08116bd57 --- /dev/null +++ b/src/antelope/mocks/FeedbackStore.ts @@ -0,0 +1,40 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Mocking FeedbackStore ----------------------------------- +// auxiliary tracing functions +export const createTraceFunction = (store_name: string) => function(action: string, ...args: unknown[]) { + if (trace) { + const titlecase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + const eventName = `${titlecase(store_name)}.${action}()`; + console.debug(eventName, [...args]); + } +}; + + +// only if we are NOT in production mode search in the url for the trace flag +// to turn on the Antelope trace mode +let trace = false; +if (process.env.NODE_ENV !== 'production') { + const urlParams = new URLSearchParams(window.location.search); + trace = urlParams.get('trace') === 'true'; +} +export const isTracingAll = () => trace; +export const createInitFunction = () => function() { + // dummie function +}; + +const FeedBackStoreMock = { + loading: [] as string[], + unsetLoading(key: string) { + this.loading = this.loading.filter((k: string) => k !== key); + }, + setLoading(key: string) { + this.loading.push(key); + }, + setDebug(name: string, value: boolean) { + // dummie function + }, +}; + +export const useFeedbackStore = () => FeedBackStoreMock; diff --git a/src/antelope/mocks/PlatformStore.ts b/src/antelope/mocks/PlatformStore.ts new file mode 100644 index 000000000..f983bfe1f --- /dev/null +++ b/src/antelope/mocks/PlatformStore.ts @@ -0,0 +1,23 @@ +/* eslint-disable max-len */ +// Mocking PlatformStore ----------------------------------- +const PlatformStore = { + isBrowser: false, + isBraveBrowser: false, + isIOSMobile: false, + isMobile: false, +}; + +// detect brave browser +const type_navegator = navigator as unknown as { brave?:{isBrave:()=>Promise} }; +if (type_navegator.brave) { + type_navegator.brave.isBrave().then((isBrave) => { + PlatformStore.isBraveBrowser = isBrave; + }); +} + +// detect mobile +const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i; +PlatformStore.isMobile = (mobileRegex.test(navigator.userAgent)); +PlatformStore.isIOSMobile = ((/iPhone|iPad|iPod/i).test(navigator.userAgent)); + +export const usePlatformStore = () => PlatformStore; diff --git a/src/antelope/mocks/UserStore.ts b/src/antelope/mocks/UserStore.ts new file mode 100644 index 000000000..c9805546c --- /dev/null +++ b/src/antelope/mocks/UserStore.ts @@ -0,0 +1,8 @@ +// Mocking UserStore ----------------------------------- + +const UserStore = { + fiatLocale: 'en-US', + fiatCurrency: 'USD', +}; + +export const useUserStore = () => UserStore; diff --git a/src/antelope/mocks/chain-constants.ts b/src/antelope/mocks/chain-constants.ts new file mode 100644 index 000000000..0fee6a1da --- /dev/null +++ b/src/antelope/mocks/chain-constants.ts @@ -0,0 +1,16 @@ +export const TELOS_CHAIN_IDS = ['40', '41']; +export const TELOS_NETWORK_NAMES = ['telos-evm', 'telos-evm-testnet']; +export const TELOS_ANALYTICS_EVENT_NAMES = { + loginStarted: 'Login Started', + loginSuccessful: 'Login Successful', + loginSuccessfulMetamask: 'Login Successful - Metamask', + 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', + loginFailedBrave: 'Login Failed - Brave', +}; diff --git a/src/antelope/mocks/index.ts b/src/antelope/mocks/index.ts new file mode 100644 index 000000000..a9ab5b7e7 --- /dev/null +++ b/src/antelope/mocks/index.ts @@ -0,0 +1,9 @@ +export * from 'src/antelope/mocks/FeedbackStore'; +export * from 'src/antelope/mocks/AccountStore'; +export * from 'src/antelope/mocks/AntelopeConfig'; +export * from 'src/antelope/mocks/ChainStore'; +export * from 'src/antelope/mocks/ContractStore'; +export * from 'src/antelope/mocks/EVMStore'; +export * from 'src/antelope/mocks/PlatformStore'; + +export const CURRENT_CONTEXT = 'current'; diff --git a/src/antelope/stores/utils/abi/erc1155.ts b/src/antelope/stores/utils/abi/erc1155.ts new file mode 100644 index 000000000..6621da79c --- /dev/null +++ b/src/antelope/stores/utils/abi/erc1155.ts @@ -0,0 +1,133 @@ +import { EvmABI } from '.'; + +export const erc1155Abi = [{ + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], + 'name': 'ApprovalForAll', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'indexed': false, + 'internalType': 'uint256[]', + 'name': 'ids', + 'type': 'uint256[]', + }, { 'indexed': false, 'internalType': 'uint256[]', 'name': 'values', 'type': 'uint256[]' }], + 'name': 'TransferBatch', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }, { 'indexed': false, 'internalType': 'uint256', 'name': 'value', 'type': 'uint256' }], + 'name': 'TransferSingle', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': false, 'internalType': 'string', 'name': 'value', 'type': 'string' }, { + 'indexed': true, + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }], + 'name': 'URI', + 'type': 'event', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }], + 'name': 'balanceOf', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address[]', 'name': 'accounts', 'type': 'address[]' }, { + 'internalType': 'uint256[]', + 'name': 'ids', + 'type': 'uint256[]', + }], + 'name': 'balanceOfBatch', + 'outputs': [{ 'internalType': 'uint256[]', 'name': '', 'type': 'uint256[]' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }], + 'name': 'isApprovedForAll', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256[]', 'name': 'ids', 'type': 'uint256[]' }, { + 'internalType': 'uint256[]', + 'name': 'amounts', + 'type': 'uint256[]', + }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], + 'name': 'safeBatchTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }, { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'internalType': 'bool', + 'name': 'approved', + 'type': 'bool', + }], + 'name': 'setApprovalForAll', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }], + 'name': 'uri', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/stores/utils/abi/erc20.ts b/src/antelope/stores/utils/abi/erc20.ts new file mode 100644 index 000000000..f97efb36b --- /dev/null +++ b/src/antelope/stores/utils/abi/erc20.ts @@ -0,0 +1,251 @@ +import { EvmABI } from '.'; + +export const erc20Abi = [ + { + 'inputs': [], + 'name': 'name', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'symbol', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'decimals', + 'outputs': [ + { + 'internalType': 'uint8', + 'name': '', + 'type': 'uint8', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'account', + 'type': 'address', + }, + ], + 'name': 'balanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'recipient', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'transfer', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'sender', + 'type': 'address', + }, + { + 'internalType': 'address', + 'name': 'recipient', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'transferFrom', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'approve', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'owner', + 'type': 'address', + }, + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + ], + 'name': 'allowance', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256', + }, + ], + 'name': 'Transfer', + 'type': 'event', + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address', + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256', + }, + ], + 'name': 'Approval', + 'type': 'event', + }, +] as EvmABI; + +export const erc20AbiApprove = [{ + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'approve', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/stores/utils/abi/erc721.ts b/src/antelope/stores/utils/abi/erc721.ts new file mode 100644 index 000000000..10a3dfc2c --- /dev/null +++ b/src/antelope/stores/utils/abi/erc721.ts @@ -0,0 +1,369 @@ +import { EvmABI } from '.'; + +export const erc721Abi = [{ + 'inputs': [{ + 'internalType': 'string', + 'name': '_name', + 'type': 'string', + }, { 'internalType': 'string', 'name': '_symbol', 'type': 'string' }, { + 'internalType': 'uint256', + 'name': '_maxTokens', + 'type': 'uint256', + }, { 'internalType': 'address', 'name': '_linkToken', 'type': 'address' }, { + 'internalType': 'address', + 'name': '_chainlinkCoordinator', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { + 'internalType': 'bytes32', + 'name': '_chainlinkHash', + 'type': 'bytes32', + }], + 'stateMutability': 'nonpayable', + 'type': 'constructor', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'approved', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'Approval', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], + 'name': 'ApprovalForAll', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ + 'indexed': true, + 'internalType': 'address', + 'name': 'previousOwner', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], + 'name': 'OwnershipTransferred', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'baseURI', 'type': 'string' }], + 'name': 'SetBaseURI', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ + 'indexed': false, + 'internalType': 'uint256', + 'name': 'chainlinkFee', + 'type': 'uint256', + }, { 'indexed': false, 'internalType': 'bytes32', 'name': 'chainlinkHash', 'type': 'bytes32' }], + 'name': 'SetChainlinkConfig', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'defaultURI', 'type': 'string' }], + 'name': 'SetDefaultURI', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'minter', 'type': 'address' }], + 'name': 'SetMinter', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': false, 'internalType': 'uint256', 'name': 'seed', 'type': 'uint256' }, { + 'indexed': false, + 'internalType': 'bytes32', + 'name': 'requestId', + 'type': 'bytes32', + }], + 'name': 'SetRandomSeed', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'Transfer', + 'type': 'event', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'tokenId', + 'type': 'uint256', + }], + 'name': 'approve', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }], + 'name': 'balanceOf', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'baseURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'chainlinkFee', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'chainlinkHash', + 'outputs': [{ 'internalType': 'bytes32', 'name': '', 'type': 'bytes32' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'defaultURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'finalBaseURI', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'getApproved', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }], + 'name': 'isApprovedForAll', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'maxTokens', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256' }], + 'name': 'metadataOf', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': '_to', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': '_count', + 'type': 'uint256', + }], + 'name': 'mintMultiple', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'minter', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'name', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'owner', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'ownerOf', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes32', 'name': 'requestId', 'type': 'bytes32' }, { + 'internalType': 'uint256', + 'name': 'randomness', + 'type': 'uint256', + }], + 'name': 'rawFulfillRandomness', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'renounceOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }, { + 'internalType': 'bytes', + 'name': '_data', + 'type': 'bytes', + }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'seed', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'seedReveal', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'internalType': 'bool', + 'name': 'approved', + 'type': 'bool', + }], + 'name': 'setApprovalForAll', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'string', 'name': 'baseURI_', 'type': 'string' }, { + 'internalType': 'bool', + 'name': 'finalBaseUri_', + 'type': 'bool', + }], + 'name': 'setBaseURI', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { + 'internalType': 'bytes32', + 'name': '_chainlinkHash', + 'type': 'bytes32', + }], + 'name': 'setChainlinkConfig', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'string', 'name': '_defaultURI', 'type': 'string' }], + 'name': 'setDefaultURI', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': '_minter', 'type': 'address' }], + 'name': 'setMinter', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'symbol', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'index', 'type': 'uint256' }], + 'name': 'tokenByIndex', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'index', + 'type': 'uint256', + }], + 'name': 'tokenOfOwnerByIndex', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'tokenURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'transferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], + 'name': 'transferOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}] as EvmABI; + +export const erc721ApproveAbi = [{ + 'inputs': [{ 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'tokenId', + 'type': 'uint256', + }], + 'name': 'approve', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}] as unknown as EvmABI; + diff --git a/src/antelope/stores/utils/abi/erc721Metadata.ts b/src/antelope/stores/utils/abi/erc721Metadata.ts new file mode 100644 index 000000000..57cab7444 --- /dev/null +++ b/src/antelope/stores/utils/abi/erc721Metadata.ts @@ -0,0 +1,9 @@ +import { EvmABI } from '.'; + +export const erc721MetadataAbi = [{ + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'tokenURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/stores/utils/abi/escrowAbi.ts b/src/antelope/stores/utils/abi/escrowAbi.ts new file mode 100644 index 000000000..2f9309de9 --- /dev/null +++ b/src/antelope/stores/utils/abi/escrowAbi.ts @@ -0,0 +1,11 @@ +import { EvmABI } from '.'; + +export const escrowAbiWithdraw: EvmABI = [ + { + inputs: [], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/antelope/stores/utils/abi/index.ts b/src/antelope/stores/utils/abi/index.ts new file mode 100644 index 000000000..d2e651ef0 --- /dev/null +++ b/src/antelope/stores/utils/abi/index.ts @@ -0,0 +1,43 @@ +export * from 'src/antelope/stores/utils/abi/erc721'; +export * from 'src/antelope/stores/utils/abi/erc721Metadata'; +export * from 'src/antelope/stores/utils/abi/erc1155'; +export * from 'src/antelope/stores/utils/abi/erc20'; +export * from 'src/antelope/stores/utils/abi/supportsInterface'; +export * from 'src/antelope/stores/utils/abi/wrapAbi'; +export * from 'src/antelope/stores/utils/abi/stlosAbi'; +export * from 'src/antelope/stores/utils/abi/escrowAbi'; +export * from 'src/antelope/stores/utils/abi/signature/transfer_signatures'; + +export type StateMutabilityType = 'pure' | 'view' | 'nonpayable' | 'payable'; +export type addressString = `0x${string}`; // required wagmi type + +export type EvmABI = EvmABIEntry[]; + +export interface EvmABIEntry { + constant?: boolean; + payable?: boolean; + anonymous?: boolean; + inputs?: EvmABIEntryInput[]; + outputs?: EvmABIEntryOutput[]; + stateMutability?: StateMutabilityType; + name: string; + type: string; +} + +export interface EvmABIEntryInput { + indexed: boolean; + internalType: string; + name: string; + type: string; +} + +export interface EvmABIEntryOutput { + internalType: string; + name: string; + type: string; +} + +export interface AbiSignature { + text_signature: string; +} + diff --git a/src/antelope/stores/utils/abi/setApprovalForAllAbi.ts b/src/antelope/stores/utils/abi/setApprovalForAllAbi.ts new file mode 100644 index 000000000..c719fb132 --- /dev/null +++ b/src/antelope/stores/utils/abi/setApprovalForAllAbi.ts @@ -0,0 +1,13 @@ +import { EvmABI } from '.'; + +export const setApprovalForAllAbi = [{ + 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'internalType': 'bool', + 'name': 'approved', + 'type': 'bool', + }], + 'name': 'setApprovalForAll', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}] as unknown as EvmABI; diff --git a/src/antelope/stores/utils/abi/signature/events_signatures.ts b/src/antelope/stores/utils/abi/signature/events_signatures.ts new file mode 100644 index 000000000..3b73b357d --- /dev/null +++ b/src/antelope/stores/utils/abi/signature/events_signatures.ts @@ -0,0 +1,25 @@ +export const events_signatures = { + '0xddf252ad': 'event Transfer(address indexed from, address indexed to, uint256 value)', + '0x8c5be1e5': 'event Approval(address indexed owner, address indexed spender, uint256 value)', + '0x71bab65c': 'event Harvest(address indexed sender, uint256 performanceFee, uint256 callFee)', + '0x884edad9': 'event Withdraw(address indexed user, uint256 amount)', + '0xf279e6a1': 'event Withdraw(address indexed user, uint256 indexed pid, uint256 amount)', + '0x90890809': 'event Deposit(address indexed user, uint256 indexed pid, uint256 amount)', + '0xe1fffcc4': 'event Deposit(address indexed sender, uint256 value)', + '0x4c209b5f': 'event Mint(address indexed sender, uint256 amount0, uint256 amount1)', + '0xd78ad95f': 'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)', + '0xa2c38e2d': 'event Claim(address indexed account, uint256 amount, bool indexed automatic)', + '0xee503bee': 'event DividendWithdrawn(address indexed to, uint256 weiAmount)', + '0x38567aa9': 'event NewTransmission(uint32 indexed aggregatorRoundId, int192 answer, address transmitter, uint32 observationsTimestamp)', + '0x0109fc6f': 'event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt)', + '0x0559884f': 'event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt)', + '0x1c411e9a': 'event Sync(uint112 reserve0, uint112 reserve1)', + '0x5beea7b3': 'event EvInventoryUpdate(uint256 indexed id, tuple(address,address,address,uint256,uint256,uint256,uint8,uint8) inventory)', + '0x8be0079c': 'event OwnershipTransferred(address previousOwner, address newOwner)', + '0x34fcbac0': 'event Claim(address indexed user, uint256 indexed pid, uint256 amount)', + '0xda919360': 'event BorrowAllowanceDelegated(address indexed fromUser, address indexed toUser, address asset, uint256 amount)', + '0xdccd412f': 'event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to)', + '0x4a39dc06': 'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids,uint256[] amounts)', + '0xc3d58168': 'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 amount)', +} as { [prefix: string]: string }; + diff --git a/src/antelope/stores/utils/abi/signature/functions_signatures.ts b/src/antelope/stores/utils/abi/signature/functions_signatures.ts new file mode 100644 index 000000000..86a82d404 --- /dev/null +++ b/src/antelope/stores/utils/abi/signature/functions_signatures.ts @@ -0,0 +1,27 @@ +export const functions_overrides = { + '0xa9059cbb': 'function transfer(address to, uint amount)', + '0xaac48653': 'function mint(address account, uint256 id, uint256 amount, uint256 maximum, string tokenUri, bytes data)', + '0xf5298aca': 'function burn(address account,uint256 id,uint256 value)', + '0x18cbafe5': 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)', + '0x095ea7b3': 'function approve(address spender, uint256 amount)', + '0x7ff36ab5': 'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)', + '0xfb3bdb41': 'function swapETHForExactTokens(uint256 amountOut, address[] path, address to, uint256 deadline)', + '0x38ed1739': 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', + '0xded9382a': 'function removeLiquidityETHWithPermit(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', + '0xf305d719': 'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline)', + '0xa22cb465': 'function setApprovalForAll(address to, bool approved)', + '0x23b872dd': 'function transferFrom(address sender, address recipient, uint256 amount)', + '0xf242432a': 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', + '0xe8e33700': 'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)', + '0x4a25d94a': 'function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline)', + '0x5c11d795': 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', + '0x0cd5840b': 'function create(address[] offerProperties_, uint256[] offerIds_, uint256[] offerValues_, address[] demandProperties_, uint256[] demandIds_, uint256[] demandValues_)', + '0xb583cc2c': 'function setSaleStartEnd(string eventCode, uint256 start, uint256 end)', + '0xbea9849e': 'function setUniswapRouter(address _new)', + '0x860665b3': 'function openTrove(uint256 _maxFeePercentage, uint256 _LUSDAmount, address _upperHint, address _lowerHint)', + '0x2195995c': 'function removeLiquidityWithPermit(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', + '0x70a08231': 'function balanceOf(address)', + '0xf23a6e61': 'function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data)', + '0x3542aee2': 'function mintByOwner(address to, uint256 tokenType)', +} as { [prefix: string]: string }; + diff --git a/src/antelope/stores/utils/abi/signature/index.ts b/src/antelope/stores/utils/abi/signature/index.ts new file mode 100644 index 000000000..843a94b3b --- /dev/null +++ b/src/antelope/stores/utils/abi/signature/index.ts @@ -0,0 +1,3 @@ +export * from 'src/antelope/stores/utils/abi/signature/events_signatures'; +export * from 'src/antelope/stores/utils/abi/signature/functions_signatures'; +export * from 'src/antelope/stores/utils/abi/signature/transfer_signatures'; diff --git a/src/antelope/stores/utils/abi/signature/transfer_signatures.ts b/src/antelope/stores/utils/abi/signature/transfer_signatures.ts new file mode 100644 index 000000000..ba5deb690 --- /dev/null +++ b/src/antelope/stores/utils/abi/signature/transfer_signatures.ts @@ -0,0 +1,2 @@ +export const TRANSFER_SIGNATURES = ['0xddf252ad', '0xa9059cbb', '0xf242432a', '0xc3d58168']; +export const ERC1155_TRANSFER_SIGNATURE = '0xc3d58168'; diff --git a/src/antelope/stores/utils/abi/stlosAbi.ts b/src/antelope/stores/utils/abi/stlosAbi.ts new file mode 100644 index 000000000..9a6bb33b0 --- /dev/null +++ b/src/antelope/stores/utils/abi/stlosAbi.ts @@ -0,0 +1,98 @@ +import { EvmABI } from '.'; + +export const stlosAbiDeposit: EvmABI = [ + { + constant: false, + inputs: [], + name: 'depositTLOS', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +]; + +export const stlosAbiWithdraw: EvmABI = [ + { + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'assets', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'withdraw', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +]; + + + +export const stlosAbiPreviewRedeem: EvmABI = [ + { + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'shares', + type: 'uint256', + }, + ], + name: 'previewRedeem', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +]; + +export const stlosAbiPreviewDeposit: EvmABI = [ + { + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'assets', + type: 'uint256', + }, + ], + name: 'previewDeposit', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +]; + diff --git a/src/antelope/stores/utils/abi/supportsInterface.ts b/src/antelope/stores/utils/abi/supportsInterface.ts new file mode 100644 index 000000000..0a4298bb4 --- /dev/null +++ b/src/antelope/stores/utils/abi/supportsInterface.ts @@ -0,0 +1,11 @@ +import { EvmABI } from '.'; + +export const supportsInterfaceAbi = [{ + 'constant': true, + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'payable': false, + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/stores/utils/abi/wrapAbi.ts b/src/antelope/stores/utils/abi/wrapAbi.ts new file mode 100644 index 000000000..01346414b --- /dev/null +++ b/src/antelope/stores/utils/abi/wrapAbi.ts @@ -0,0 +1,32 @@ +import { EvmABI } from '.'; + +export const wtlosAbiDeposit: EvmABI = [ + { + constant: false, + inputs: [], + name: 'deposit', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +]; + +export const wtlosAbiWithdraw: EvmABI = [ + { + constant: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'wad', + type: 'uint256', + }, + ], + name: 'withdraw', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/antelope/stores/utils/contracts/EvmContract.ts b/src/antelope/stores/utils/contracts/EvmContract.ts new file mode 100644 index 000000000..d3dc8a76c --- /dev/null +++ b/src/antelope/stores/utils/contracts/EvmContract.ts @@ -0,0 +1,258 @@ +import { BigNumber, ContractInterface, ethers } from 'ethers'; +import { markRaw } from 'vue'; +import { + AntelopeError, EvmContractCalldata, + EvmABI, + EvmContractCreationInfo, + EvmContractConstructorData, + EvmContractManagerI, + EvmFormatedLog, + EvmLog, + EvmLogs, + TokenSourceInfo, + TRANSFER_SIGNATURES, +} from 'src/antelope/types'; +import { Interface } from 'ethers/lib/utils'; +import { parseUnits } from 'ethers/lib/utils'; + + +export default class EvmContract { + private readonly _name: string; + private readonly _abi?: EvmABI | null; + private readonly _address: string; + private readonly _creationInfo?: EvmContractCreationInfo | null; + private readonly _interface?: ContractInterface | null; + private readonly _supportedInterfaces: string[]; + private readonly _properties?: EvmContractCalldata; + private readonly _manager?: EvmContractManagerI; + private readonly _token?: TokenSourceInfo | null; + + private _contractInstance?: ethers.Contract; + private _verified?: boolean; + + constructor({ + name, + abi, + address, + creationInfo, + verified, + supportedInterfaces = ['none'], + properties, + manager, + token, + }: EvmContractConstructorData) { + this._name = name; + this._address = address; + this._creationInfo = creationInfo; + this._verified = verified ?? false; + this._properties = properties; + this._manager = manager; + + if (abi) { + this._abi = typeof abi === 'string' ? JSON.parse(abi) : abi; + this._interface = markRaw(new ethers.utils.Interface(abi)); + } + + if (token) { + this._token = token; + } + + const indexOfNone = supportedInterfaces.indexOf('none'); + this._supportedInterfaces = []; + for (let i = 0; i < supportedInterfaces.length; i++){ + if (i !== indexOfNone) { + this._supportedInterfaces.push(supportedInterfaces[i]); + } + } + } + + + get name() { + return this._name; + } + + get abi() { + return this._abi; + } + + get address() { + return this._address; + } + + get creationInfo() { + return this._creationInfo; + } + + get iface() { + return this._interface; + } + + get verified() { + return this._verified ?? false; + } + + set verified(verified: boolean) { + this._verified = verified; + } + + get supportedInterfaces() { + return this._supportedInterfaces; + } + + get creationBlock() { + return this._creationInfo?.block; + } + + get creationTrx() { + return this._creationInfo?.transaction; + } + + get creator() { + return this._creationInfo?.creator; + } + + get properties() { + return this._properties; + } + + get token() { + return this._token; + } + + get maxSupply() { + if (!this.isToken() || !this._properties?.supply || !this._properties?.decimals) { + return BigNumber.from(0); + } + + return parseUnits(this._properties.supply, this._properties.decimals); + } + + isNonFungible() { + return (this._supportedInterfaces.includes('erc721')); + } + + isToken() { + if (this._supportedInterfaces.length === 0) { + return false; + } + + return ( + this._supportedInterfaces.includes('erc721') || + this._supportedInterfaces.includes('erc1155') || + this._supportedInterfaces.includes('erc20') + ); + } + + async getContractInstance() { + if (!this.abi){ + throw new AntelopeError('antelope.utils.error_contract_instance'); + } + + if (this._contractInstance) { + return this._contractInstance; + } + + const signer = await this._manager?.getSigner(); + let provider; + + if (!signer) { + provider = await this._manager?.getWeb3Provider(); + } + + const contract = new ethers.Contract(this.address, this.abi, signer ?? provider ?? undefined); + this._contractInstance = contract; + + return contract; + } + + async parseTransaction(data:string) { + if (this.iface && this.iface instanceof Interface) { + try { + return await this.iface.parseTransaction({ data }); + } catch (e) { + console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); + } + } else { + try { + // this functionIface is an interface for a single function signature as discovered via 4bytes.directory... only use it for this function + const functionIface = await this._manager?.getFunctionIface(data); + if (functionIface) { + return functionIface.parseTransaction({ data }); + } + } catch (e) { + console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); + } + } + throw new AntelopeError('antelope.utils.error_parsing_transaction'); + } + + async parseLogs(logs: EvmLogs): Promise { + if (this.iface && this.iface instanceof Interface) { + const iface = this.iface; + const parsedArray = await Promise.all(logs.map(async (log) => { + try { + const parsedLog:ethers.utils.LogDescription = iface.parseLog(log); + return this.formatLog(log, parsedLog); + } catch (e) { + return this.parseEvent(log); + } + })); + parsedArray.forEach((parsed) => { + if(parsed.name && parsed.eventFragment?.inputs){ + parsed.inputs = parsed.eventFragment.inputs; + } + }); + return parsedArray; + } + + + return await Promise.all(logs.map(async (log) => { + const parsedLog = await this.parseEvent(log); + if(parsedLog.name && parsedLog.eventFragment?.inputs){ + parsedLog.inputs = parsedLog.eventFragment.inputs; + } + return parsedLog; + })); + } + + formatLog(log: EvmLog, parsedLog: ethers.utils.LogDescription): EvmFormatedLog { + if(!parsedLog.signature) { + console.error('No signature found for log! Check if this explodes. Returning EvmLog instead of EvmFormatedLog. '); + return log as unknown as EvmFormatedLog; + } + const function_signature = log.topics[0].substring(0, 10); + return { + ... parsedLog, + function_signature, + isTransfer: TRANSFER_SIGNATURES.includes(function_signature), + logIndex: log.logIndex, + address: log.address, + token: this._token, + name: parsedLog.signature, + } as EvmFormatedLog; + } + + async parseEvent(log: EvmLog): Promise { + const eventIface = await this._manager?.getEventIface(log.topics[0]); + if (eventIface) { + try { + const parsedLog:ethers.utils.LogDescription = eventIface.parseLog(log); + return this.formatLog(log, parsedLog); + } catch(e) { + throw new AntelopeError('antelope.utils.error_parsing_log_event', log); + } + } else { + throw new AntelopeError('antelope.utils.error_parsing_log_event', log); + } + } +} + +export interface Erc20Transfer { + index: number; + address: string; + value: string; // string representation of hex number + decimals?: number; + to: string; + from: string; + symbol?: string; +} diff --git a/src/antelope/stores/utils/contracts/EvmContractFactory.ts b/src/antelope/stores/utils/contracts/EvmContractFactory.ts new file mode 100644 index 000000000..22898af0d --- /dev/null +++ b/src/antelope/stores/utils/contracts/EvmContractFactory.ts @@ -0,0 +1,62 @@ +import { erc1155Abi, erc20Abi, erc721Abi } from 'src/antelope/stores/utils/abi'; +import EvmContract from 'src/antelope/stores/utils/contracts/EvmContract'; +import { AntelopeError, EvmContractCalldata, EvmContractMetadata, EvmContractFactoryData } from 'src/antelope/types'; + +export default class EvmContractFactory { + buildContract(data: EvmContractFactoryData): EvmContract { + if (!data || !data.address) { + throw new AntelopeError('antelope.contracts.contract_data_required'); + } + + let verified = false; + if (typeof data.abi !== 'undefined' && data.abi.length > 0) { + data.abi = (typeof data.abi === 'string') ? JSON.parse(data.abi) : data.abi; + } else if (typeof data.metadata !== 'undefined' && data.metadata?.length > 0) { + const metadata: EvmContractMetadata = JSON.parse(data.metadata); + data.abi = metadata?.output?.abi; + } + if (data.abi) { + verified = true; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc20')) { + data.abi = erc20Abi; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc721')) { + data.abi = erc721Abi; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc1155')) { + data.abi = erc1155Abi; + } + + const properties: EvmContractCalldata = (data.calldata) ? JSON.parse(data.calldata) : {}; + + if (!data.name) { + if (properties?.name){ + data.name = properties.name; + } else if (data.metadata) { + const metadata: EvmContractMetadata = JSON.parse(data.metadata); + + if(metadata?.settings?.compilationTarget){ + data.name = Object.values(metadata?.settings?.compilationTarget)[0]; + } + } + } + const abi = typeof data.abi === 'string' ? JSON.parse(data.abi) : data.abi; + + return new EvmContract({ + address: data.address, + name: data.name ?? '', + verified: verified, + creationInfo: { + creator: data.creator, + transaction: data.transaction ?? '', + creation_trx: data.transaction ?? '', + block: data.block, + block_num: data.block, + timestamp: data.timestamp ?? '', + abi: data.abi, + }, + supportedInterfaces: data.supportedInterfaces ?? [], + abi, + properties, + manager: data.manager, + }); + } +} diff --git a/src/antelope/stores/utils/currency-utils.ts b/src/antelope/stores/utils/currency-utils.ts new file mode 100644 index 000000000..1b78e1688 --- /dev/null +++ b/src/antelope/stores/utils/currency-utils.ts @@ -0,0 +1,385 @@ +import { BigNumber } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { formatUnits } from '@ethersproject/units'; +import Decimal from 'decimal.js'; +import { WEI_PRECISION } from 'src/antelope/stores/utils'; + +/** + * Given a number or string, returns a string representation of the number with up to 18 decimal places + * @param value - number or string to convert to string + * @returns {string} string representation of the number + */ + +export function toStringNumber(value: number | string): string { + if (typeof value === 'string') { + return value; + } else if (typeof value === 'number') { + const num = new Decimal(value); + return num.toFixed(WEI_PRECISION); + } else { + throw new Error('Invalid value type: ' + typeof value); + } +} + + +/** + * Given a locale string, returns the character used to separate integer and decimal portions of a number, + * e.g "." as in 123.456 + * @param locale - standard locale code, such as "en-US" + * @returns {string} decimal separator character + */ +export function getDecimalSeparatorForLocale(locale: string) { + const numberWithDecimalSeparator = 1.1; + const formattedNumber = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator); + return formattedNumber.charAt(1); // Get the character between "1" and "1" +} + +/** + * Given a locale string, returns the character used to separate groups of numbers in a large number, + * e.g "," as in 123,456,789.00 + * + * @param { string } locale - standard locale code, such as "en-US" + * + * @returns {string} large number separator character + */ +export function getLargeNumberSeparatorForLocale(locale: string) { + const largeNumber = 1000000; + const formattedNumber = new Intl.NumberFormat(locale).format(largeNumber); + + const nonDigitCharacters = formattedNumber.match(/\D+/g); + + if (!nonDigitCharacters || nonDigitCharacters.length === 0) { + return ''; + } + + return nonDigitCharacters[0]; +} + +/** + * Given a localized number string, returns a BigNumber + * + * @param {string} formatted - localized number string, e.g. "123,456.78" + * @param {number} decimals - number of decimals the number has, e.g. 2 for 123,456.78 + * @param {string} locale - standard locale code, such as "en-US" + * + * @returns {BigNumber} BigNumber representation of the number + */ +export function getBigNumberFromLocalizedNumberString(formatted: string, decimals: number, locale: string): BigNumber { + const decimalSeparator = getDecimalSeparatorForLocale(locale); + const largeNumberSeparator = getLargeNumberSeparatorForLocale(locale); + const notIntegerOrSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}${largeNumberSeparator}]`, 'g'); + + if (formatted.match(notIntegerOrSeparatorRegex)) { + throw new Error('Invalid number format'); + } + + // if decimals is not a positive integer, throw an error + if (decimals % 1 !== 0 || decimals < 0) { + throw new Error('Invalid decimals value'); + } + + // strip any character which is not an integer or decimal separator + const notIntegerOrDecimalSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}]`, 'g'); + let unformatted = formatted.replace(notIntegerOrDecimalSeparatorRegex, ''); + + // if the decimal separator is anything but a dot, replace it with a dot to allow conversion to number + if (decimalSeparator !== '.') { + unformatted = unformatted.replace(/[^0-9.]/g, '.'); + } + + return parseUnits(unformatted, decimals); +} + + +/* +* Formats a currency amount in a localized way +* +* @param {number|BigNumber} amount - the currency amount +* @param {number} precision - the number of decimals that should be displayed. Ignored if abbreviate is true and the value is over 1000 +* @param {string} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} abbreviate - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency +* @param {boolean?} displayCurrencyAsCode - if currency is defined, controls whether the currency is display as a symbol or code, e.g. $100 or USD 100. Only valid for fiat currencies. +* @param {number?} tokenDecimals - required if amount is BigNumber. The number of decimals a token has, e.g. 18 for TLOS. This option is not used for non-BigNumber amounts +* @param {boolean?} trimZeroes - trim trailing zeroes for decimal values, e.g. '123.000' => '123', '123.45600' => '123.456'. Overrides 'precision' when there are trailing zeroes +* */ +export function prettyPrintCurrency( + amount: number | BigNumber, + precision: number, + locale: string, + abbreviate = false, + currency?: string, + displayCurrencyAsCode?: boolean, + tokenDecimals?: number, + trimZeroes?: boolean, +) { + if (precision % 1 !== 0 || precision < 0) { + throw new Error('Precision must be a positive integer or zero'); + } + + if (typeof tokenDecimals === 'number' && (tokenDecimals % 1 !== 0 || tokenDecimals < 0)) { + throw new Error('Token decimals must be a positive integer or zero'); + } + + // require token decimals if type is BigNumber + if (typeof amount !== 'number' && typeof tokenDecimals !== 'number') { + throw new Error('Token decimals is required for BigNumber amounts'); + } + + const decimalSeparator = getDecimalSeparatorForLocale(locale); + const trailingZeroesRegex = new RegExp(`\\${decimalSeparator}?0+(\\D|$)`, 'g'); + + const decimalOptions : Record = { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + minimumIntegerDigits: undefined, + maximumIntegerDigits: undefined, + }; + + const currencyOptions : Record = { + style: currency ? 'currency' : undefined, + currencyDisplay: currency ? (displayCurrencyAsCode ? 'code' : 'symbol') : undefined, + currency, + }; + + if (typeof amount === 'number') { + if (amount < 1 && amount > 0) { + decimalOptions.maximumIntegerDigits = 1; + decimalOptions.minimumIntegerDigits = 1; + } else if (abbreviate) { + const forceFractionDisplay = amount < 1000 && amount > -1000 ; + + decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.maximumIntegerDigits = 3; + } + + let finalFormattedValue = Intl.NumberFormat( + locale, + { + notation: abbreviate ? 'compact' : undefined, + ...currencyOptions, + ...decimalOptions, + }).format(amount); + + if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { + finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, ''); + } + + return finalFormattedValue; + } else { + if (amount.lt(1) && amount.gt(0)) { + decimalOptions.maximumIntegerDigits = 1; + decimalOptions.minimumIntegerDigits = 1; + } else if (abbreviate) { + const forceFractionDisplay = amount.lt(1000) && amount.gt(-1000); + + decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.maximumIntegerDigits = 3; + } + + // Intl format method only takes number / bigint, and a BigNumber value cannot have a fractional amount, + // and also decimals may be more places than maximum JS precision. + // As such, decimals must be handled specially for BigNumber amounts. + + const amountAsString = tokenDecimals === 0 ? amount.toNumber().toString() : formatUnits(amount, tokenDecimals); // amount string, like "1.0" + + const [integerString, decimalString = '0'] = amountAsString.split('.'); + + const formattedInteger = Intl.NumberFormat( + locale, + { notation: abbreviate ? 'compact' : undefined }, + ).format(BigInt(integerString)); + + const formattedDecimal = decimalString.slice(0, precision || 1).padEnd(precision, '0'); + + let finalFormattedValue; + + if (abbreviate) { + finalFormattedValue = formattedInteger; // drop decimals for abbreviated amounts + } else { + finalFormattedValue = `${formattedInteger}${decimalSeparator}${formattedDecimal}`; + } + + if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { + finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, ''); + } + + if (precision === 2 && tokenDecimals === 2 && currency) { + // value is a fiat currency with 2 decimals, so add the currency symbol + if (displayCurrencyAsCode) { + finalFormattedValue = `${finalFormattedValue}\u00A0${currency}`; + } else { + const symbol = getCurrencySymbol(locale, currency); + finalFormattedValue = `${symbol}${finalFormattedValue}`; + } + + } else if (currency) { + finalFormattedValue += ` ${currency}`; + } + + return finalFormattedValue; + } +} + + +/** + * Converts a currency amount from one token to another + * + * @param {BigNumber} tokenOneAmount - the amount of token one + * @param {number} tokenOneDecimals - the number of decimals token one has + * @param {number} tokenTwoDecimals - the number of decimals token two has + * @param {string|number} conversionFactor - the conversion rate from token one to token two + * + * @returns {BigNumber} the amount of token two equivalent to the amount of token one + */ +export function convertCurrency(tokenOneAmount: BigNumber, tokenOneDecimals: number, tokenTwoDecimals: number, conversionFactor: string | number): BigNumber { + const conversionRate = toStringNumber(conversionFactor); + const leadingZeroesRegex = /^0+/g; + const trailingZeroesRegex = /0+$/g; + const floatRegex = /^\d+(\.\d+)?$/g; + + if (!Number.isInteger(tokenOneDecimals) || tokenOneDecimals <= 0) { + throw new Error('Token one decimals must be a positive integer or zero'); + } + + if (!Number.isInteger(tokenTwoDecimals) || tokenTwoDecimals <= 0) { + throw new Error('Token two decimals must be a positive integer or zero'); + } + + if (!floatRegex.test(conversionRate) || Number(conversionRate) <= 0) { + throw new Error('Conversion rate must be a positive floating point number or integer'); + } + + if (tokenOneAmount.lt(0)) { + throw new Error('Token one amount must be positive'); + } + + const tenBn = BigNumber.from(10); + + // represents the maximum significant figures of conversion calculations + const precisionCutoffBn = BigNumber.from(256); + + const [rawConversionRateIntegers, rawConversionRateDecimals = ''] = conversionRate.split('.'); + const conversionRateIntegers = rawConversionRateIntegers.replace(leadingZeroesRegex, ''); + const conversionRateDecimals = rawConversionRateDecimals.replace(trailingZeroesRegex, ''); + + const numberOfConversionRateDecimals = conversionRateDecimals.length; + + const conversionRateScalingFactor = BigNumber.from(numberOfConversionRateDecimals).add(precisionCutoffBn); + const conversionRateAsIntegerString = conversionRateIntegers.concat((conversionRateDecimals ?? '')); + + const conversionRateBn = BigNumber.from(conversionRateAsIntegerString); + const scaledConversionRate = conversionRateBn.mul(tenBn.pow(conversionRateScalingFactor)); + + // normalize amount to 256 precision + const normalizedAmount = tokenOneAmount.mul(tenBn.pow((precisionCutoffBn.sub(tokenOneDecimals)))); + + // multiply amount by conversion rate integer + const normalizedScaledAmountTwo = normalizedAmount.mul(scaledConversionRate); + + // denormalize from 256 precision to tokenTwoDecimals + const denormalizedScaledAmountTwo = normalizedScaledAmountTwo.div(tenBn.pow((precisionCutoffBn.sub(tokenTwoDecimals)))); + + // remove conversion rate scaling + return denormalizedScaledAmountTwo.div(tenBn.pow(conversionRateScalingFactor.add(numberOfConversionRateDecimals))); +} + + +/** + * Inverts a floating point number, useful for taking a conversion rate from token A to token B and getting the + * conversion rate from token B to token A + * + * @param {number|string} float - the floating point number to invert + * + * @returns {string} the inverted floating point number rounded to 18 decimal places + */ +export function getFloatReciprocal(float: number | string) { + const floatRegex = /^\d+(\.\d+)?$/g; + const trailingZeroesRegex = /0+$/g; + const trailingDotRegex = /\.$/g; + + if (!floatRegex.test(float.toString())) { + throw new Error('Conversion rate must be a positive floating point number or integer'); + } + + if (parseFloat(float.toString()) === 0) { + throw new Error('Error inverting: cannot divide by zero'); + } + + return new Decimal(1) + .dividedBy(float) + .toFixed(WEI_PRECISION) + .replace(trailingZeroesRegex, '') + .replace(trailingDotRegex, ''); +} + +/** + * Given a locale and currency code, returns the symbol for the currency, e.g. '$' for USD + * @param {string} locale - locale code, e.g. 'en-US' + * @param {string} currencyCode - standard currency code, e.g. 'USD' + */ +export function getCurrencySymbol(locale: string, currencyCode: string) { + const formatter = new Intl.NumberFormat(locale, { + style: 'currency', + currency: currencyCode, + currencyDisplay: 'symbol', + }); + + const parts = formatter.formatToParts(123); + + let symbol; + + for (let i = 0; i < parts.length; i++) { + if (parts[i].type === 'currency') { + symbol = parts[i].value; + break; + } + } + + return symbol; +} + +/** + * Launches a prompt in MetaMask to add a given token as a tracked token, allowing the user to view their balance of + * that token at a glance from MetaMask + * + * @param {string} address - the address of the token contract + * @param {string} symbol - the token's ticker symbol, e.g. 'STLOS' + * @param {string} image - permalink url of the token's icon + * @param {string} type - Ethereum standard of the token; default is 'ERC20' + * @param {number} decimals - the number of decimals constituting the token's precision, default is 18 + * + * @returns {Promise} + */ +export async function promptAddToMetamask( + address: string, + symbol: string, + image: string, + type: string, + decimals: number, +): Promise { + if (!window.ethereum) { + return Promise.reject(); + } + + type MetamaskEthereum = { + request: (args: { method: string, params: Record }) => Promise + }; + + const ethereum = window.ethereum as unknown as MetamaskEthereum; + + return ethereum.request({ + method: 'wallet_watchAsset', + params: { + type, + options: { + address, + symbol, + decimals, + image, + }, + }, + }); +} diff --git a/src/antelope/stores/utils/date-utils.ts b/src/antelope/stores/utils/date-utils.ts new file mode 100644 index 000000000..50d303033 --- /dev/null +++ b/src/antelope/stores/utils/date-utils.ts @@ -0,0 +1,108 @@ +import { fromUnixTime, format } from 'date-fns'; + +/** + * Useful date-related constants + */ +export const MINUTE_SECONDS = 60; +export const HOUR_SECONDS = 60 * MINUTE_SECONDS; +export const DAY_SECONDS = 24 * HOUR_SECONDS; +export const WEEK_SECONDS = 7 * DAY_SECONDS; +export const MONTH_SECONDS = 30 * DAY_SECONDS; +export const YEAR_SECONDS = 365 * DAY_SECONDS; + +export const DEFAULT_DATE_FORMAT = 'MMM d, yyyy hh:mm:ss a'; + +/** + * Returns true if the given epochMs is less than the given number of minutes ago + * @param epochMs seconds since epoch representing the date to check + * @param minutes number of minutes to check against + * @returns {boolean} true if the given epochMs is less than the given number of minutes ago + */ +export function dateIsWithinXMinutes(epochMs: number, minutes: number) { + if (epochMs <= 0) { + throw new Error('epochMs must be greater than 0'); + } + + if (epochMs % 1 !== 0) { + throw new Error('epochMs must be an integer'); + } + + // make a date object which represents the time X minutes ago + const xMinsAgo = new Date(); + xMinsAgo.setMinutes(xMinsAgo.getMinutes() - minutes); + + // return true if the date is within the defined timeframe + return new Date(epochMs) > xMinsAgo; +} + + +/** + * Translates a number of seconds to a natural language time period using the given translation function. + * + * @param {number|null} seconds number of seconds since epoch representing the date to check + * @param {function} $t translation function. Should accept a string (just the keyname without a path) and return a translated string + * @returns {string} plain english time period + */ +export function prettyTimePeriod(seconds: number|null, $t: (key: string) => string, units = '', round = false) { + if (seconds === null) { + return '--'; + } + + let quantity; + let unit; + + if (seconds < HOUR_SECONDS || units === 'minutes') { + quantity = seconds / MINUTE_SECONDS; + unit = $t('minutes'); + } else if (seconds < DAY_SECONDS || units === 'hours') { + quantity = seconds / HOUR_SECONDS; + unit = $t('hours'); + } else if (seconds < WEEK_SECONDS || units === 'days') { + quantity = seconds / DAY_SECONDS; + unit = $t('days'); + } else if (seconds < MONTH_SECONDS || units === 'weeks') { + quantity = seconds / WEEK_SECONDS; + unit = $t('weeks'); + } else if (seconds < YEAR_SECONDS || units === 'months') { + quantity = seconds / MONTH_SECONDS; + unit = $t('months'); + } else { + quantity = seconds / YEAR_SECONDS; + unit = $t('years'); + } + + const fractionDigits = round ? 0 : 1; + + const formatter = new Intl.NumberFormat(navigator.language, { maximumFractionDigits: fractionDigits }); + const formattedQuantity = formatter.format(quantity); + + return `${formattedQuantity} ${unit}`; +} + +/** + * Given a Date object, return the pretty-printed timezone offset, e.g. "+05:00" + * + * @param {Date} date + * @return {string} + */ +export function getFormattedUtcOffset(date: Date): string { + const pad = (value: number) => value < 10 ? '0' + value : value; + const sign = (date.getTimezoneOffset() > 0) ? '-' : '+'; + const offset = Math.abs(date.getTimezoneOffset()); + const hours = pad(Math.floor(offset / 60)); + const minutes = pad(offset % 60); + return sign + hours + ':' + minutes; +} + +/** + * Given a unix timestamp, returns string with the date in a given format showing UTC offset optionally. + * @param epoch seconds since epoch + * @param timeFormat a string containing the format of the date to be returned (based on date-fns format) + * @param showUtc whether to show the UTC offset + * @returns {string} the formatted date + */ +export function getFormattedDate(epoch: number, timeFormat = DEFAULT_DATE_FORMAT, showUtc = false): string { + const offset = getFormattedUtcOffset(new Date(epoch)); + const utc = showUtc ? ` (UTC ${offset})` : ''; + return `${format(fromUnixTime(epoch), timeFormat)}${utc}`; +} diff --git a/src/antelope/stores/utils/index.ts b/src/antelope/stores/utils/index.ts new file mode 100644 index 000000000..1f3181470 --- /dev/null +++ b/src/antelope/stores/utils/index.ts @@ -0,0 +1,266 @@ +export * from 'src/antelope/stores/utils/abi/signature'; +import { BigNumber, ethers } from 'ethers'; +import { formatUnits } from '@ethersproject/units'; +import { EvmABIEntry } from 'src/antelope/types'; +import { toStringNumber } from 'src/antelope/stores/utils/currency-utils'; +import { prettyPrintCurrency } from 'src/antelope/stores/utils/currency-utils'; +import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; + +const REVERT_FUNCTION_SELECTOR = '0x08c379a0'; +const REVERT_PANIC_SELECTOR = '0x4e487b71'; + +export const PRICE_UPDATE_INTERVAL_IN_MIN = 30; +export const WEI_PRECISION = 18; + +/** + * divideFloat performs a division of two float numbers represented as strings or native numbers. + * @param a is the numerator expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') + * @param b is the denominator expressed as a number or a string representing a number + * @returns a string representing the result of the division also as a float number + */ +export function divideFloat(a: string | number, b: string | number): string { + const a_str = toStringNumber(a); + const b_str = toStringNumber(b); + const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; + const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; + const decimals = 2 * Math.max(a_decimals, b_decimals); + const A = ethers.utils.parseUnits(a_str, decimals); + const B = ethers.utils.parseUnits(b_str, b_decimals); + const result = A.div(B); + return formatUnits(result.toString(), decimals-b_decimals); +} + +/** + * multiplyFloat performs a multiplication of two float numbers represented as strings or native numbers. + * @param a is the first factor expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') + * @param b is the second factor expressed as a number or a string representing a number + * @returns a string representing the result of the multiplication also as a float number + */ +export function multiplyFloat(a: string | number, b: string | number): string { + const a_str = toStringNumber(a); + const b_str = toStringNumber(b); + const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; + const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; + const decimals = a_decimals + b_decimals; + const A = ethers.utils.parseUnits(a_str, decimals); + const B = ethers.utils.parseUnits(b_str, decimals); + const result = A.mul(B); + return formatUnits(result.toString(), decimals+decimals); +} + +export function formatWei(bn: string | number | ethers.BigNumber, tokenDecimals: number, displayDecimals = 4): string { + const amount = ethers.BigNumber.from(bn); + const formatted = formatUnits(amount.toString(), tokenDecimals || WEI_PRECISION); + const str = formatted.toString(); + // Use string, do not convert to number so we never lose precision + if (displayDecimals > 0 && str.includes('.')) { + const parts = str.split('.'); + return parts[0] + '.' + parts[1].slice(0, displayDecimals); + } + return str; +} + +export function isValidAddressFormat(ethAddressString: string): boolean { + const pattern = /^0x[a-fA-F0-9]{40}$/; + return pattern.test(ethAddressString); +} + +export function getTopicHash(topic: string): string { + return `0x${topic.substring(topic.length - 40)}`; +} + +export function toChecksumAddress(address: string): string { + if (!address) { + return address; + } + + let addy = address.toLowerCase().replace('0x', ''); + if (addy.length !== 40) { + addy = addy.padStart(40, '0'); + } + + const hash = keccak256(toUtf8Bytes(addy)).replace('0x', ''); + let ret = '0x'; + + for (let i = 0; i < addy.length; i++) { + if (parseInt(hash[i], 16) >= 8) { + ret += addy[i].toUpperCase(); + } else { + ret += addy[i]; + } + } + + return ret; +} + +export function parseErrorMessage(output: string): string { + if (!output) { + return ''; + } + + let message = ''; + if (output.startsWith(REVERT_FUNCTION_SELECTOR)) { + message = parseRevertReason(output); + } + + if (output.startsWith(REVERT_PANIC_SELECTOR)) { + message = parsePanicReason(output); + } + + return message.replace(/[^a-zA-Z0-9 /./'/"/,/@/+/-/_/(/)/[]/g, ''); +} + +export function parseRevertReason(revertOutput: string): string { + if (!revertOutput || revertOutput.length < 138) { + return ''; + } + + let reason = ''; + const trimmedOutput = revertOutput.substr(138); + for (let i = 0; i < trimmedOutput.length; i += 2) { + reason += String.fromCharCode(parseInt(trimmedOutput.substr(i, 2), 16)); + } + return reason; +} + +export function parsePanicReason(revertOutput: string): string { + const trimmedOutput = revertOutput.slice(-2); + let reason; + + switch (trimmedOutput) { + case '01': + reason = 'If you call assert with an argument that evaluates to false.'; + break; + case '11': + reason = 'If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.'; + break; + case '12': + reason = 'If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).'; + break; + case '21': + reason = 'If you convert a value that is too big or negative into an enum type.'; + break; + case '31': + reason = 'If you call .pop() on an empty array.'; + break; + case '32': + reason = 'If you access an array, bytesN or an array slice at an out-of-bounds or negative index ' + + '(i.e. x[i] where i >= x.length or i < 0).'; + break; + case '41': + reason = 'If you allocate too much memory or create an array that is too large.'; + break; + case '51': + reason = 'If you call a zero-initialized variable of internal function type.'; + break; + default: + reason = 'Default panic message'; + } + return reason; +} + +export function sortAbiFunctionsByName(fns: EvmABIEntry[]): EvmABIEntry[] { + return fns.sort( + (entryA, entryB) => { + const upperA = entryA.name.toUpperCase(); + const upperB = entryB.name.toUpperCase(); + return (upperA < upperB) ? -1 : (upperA > upperB) ? 1 : 0; + }, + ); +} + +/** + * Determine whether the user's device is an Apple touch device + * + * @return {boolean} + */ +export function getClientIsApple() { + return [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod', + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); +} + + +/* +* Determines whether the amount is too large (more than six characters long) to be displayed in full on mobile devices +* +* @param {number} amount - the currency amount +* return {boolean} - true if the amount is too large to be displayed in full on mobile devices +* */ +export function isAmountTooLarge(amount: number | string): boolean { + const primaryAmountIsTooLarge = + (typeof amount === 'number' && amount.toString().length > 6) || + (typeof amount === 'string' && amount.length > 6); + + return primaryAmountIsTooLarge; +} + + + +/* +* Formats a token balance amount in a localized way, using 4 decimals, +* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true +* +* @param {number} amount - the currency amount +* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} symbol - symbol for the currency to be used, e.g. 'TLOS'. If defined, the symbol will be displayed, e.g. 123.00 TLOS. +* return {string} - the formatted amount +* */ +export function prettyPrintBalance(amount: number | string, locale: string, tiny: boolean, symbol = '') { + return ['', ' ' + symbol].join(prettyPrintCurrency(+amount, 4, locale, tiny ? isAmountTooLarge(amount) : false)); +} + +/* +* Formats a fiat balance amount in a localized way, using 2 decimals, +* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true +* +* @param {number} amount - the currency amount +* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency +* return {string} - the formatted amount +* */ +export function prettyPrintFiatBalance(fiatAmount: number | string, locale: string, tiny: boolean, currency = 'USD') { + return prettyPrintCurrency(+fiatAmount, 2, locale, tiny ? isAmountTooLarge(fiatAmount) : false, currency); +} + +/** + * Converts gas price, which is in its own unit, to TLOS + * + * @param {string} gasUsed - amount of gas used as string representation of a number/hex + * @param {string} gasPrice - gas price in TLOS as string representation of a number/hex + * + * @return {string} gas in TLOS as a number string + */ +export function getGasInTlos(gasUsed: string, gasPrice: string) { + return formatWei( + BigNumber.from(gasPrice) + .mul(gasUsed).toLocaleString(), + WEI_PRECISION, + 5, + ); +} + +/** + * Takes an ethereum hash ('0x...') and returns a shortened version, like '0x0000...0000' + * @param {string} hash - a string beginning with 0x and containing only 0-9, a-f, or A-F + * + * @return {string} shortened hash + */ +export function getShortenedHash(hash: string) { + const textIsAddress = /^0x[0-9a-fA-F]+$/.test(hash); + + if (textIsAddress) { + return hash.slice(0, 6) + '...' + hash.slice(-4); + } else { + throw new Error('Invalid hash ' + hash); + } +} diff --git a/src/antelope/stores/utils/media-utils.ts b/src/antelope/stores/utils/media-utils.ts new file mode 100644 index 000000000..ebd089e24 --- /dev/null +++ b/src/antelope/stores/utils/media-utils.ts @@ -0,0 +1,74 @@ +import { NFTSourceTypes, NftSourceType } from 'src/antelope/types'; + +/** + * given a webm source, determine if it's audio or video + * @param source + * @returns NFTSourceTypes.AUDIO or NFTSourceTypes.VIDEO + */ +export async function determineWebmType(source: string): Promise { + return new Promise((resolve, reject) => { + // Create a video element + const video = document.createElement('video'); + + // Listen for the 'loadedmetadata' event + video.addEventListener('loadedmetadata', () => { + // If videoHeight or videoWidth is 0, then it's audio-only. + if (video.videoHeight === 0 || video.videoWidth === 0) { + resolve(NFTSourceTypes.AUDIO); + } else { + resolve(NFTSourceTypes.VIDEO); + } + }); + + // Handle error + video.addEventListener('error', () => { + reject(new Error('Failed to load video metadata.')); + }); + + // Set the URL as the video source + video.src = source; + }); +} + +/** + * given a url, determine if it's an image + * @param url + * @returns true if it's an image + */ +export function urlIsPicture(url: string): boolean { + return Boolean(url.match(/\.(gif|avif|apng|jpe?g|jfif|p?jpe?g|png|svg|webp)$/)); +} + +/** + * given a url, determine if it's audio + * @param url + * @returns true if it's audio + */ +export async function urlIsAudio(url: string) { + const isNotWebm = !url.match(/\.(webm)$/); + + if (isNotWebm) { + return Boolean(url.match(/\.(mp3|wav|aac)$/)); + } + + const type = await determineWebmType(url); + + return type === NFTSourceTypes.AUDIO; +} + +/** + * given a url, determine if it's a video + * @param url + * @returns true if it's video + */ +export async function urlIsVideo(url: string) { + const isNotWebm = !url.match(/\.(webm)$/); + + if (isNotWebm) { + return Boolean(url.match(/\.(mp4|ogg)$/)); + } + + const type = await determineWebmType(url); + + return type === NFTSourceTypes.VIDEO; +} diff --git a/src/antelope/stores/utils/nft-utils.ts b/src/antelope/stores/utils/nft-utils.ts new file mode 100644 index 000000000..be4591fc8 --- /dev/null +++ b/src/antelope/stores/utils/nft-utils.ts @@ -0,0 +1,150 @@ +import { IndexerNftMetadata, NFTSourceTypes, NftSourceType } from 'src/antelope/types'; +import { urlIsAudio, urlIsPicture, urlIsVideo } from 'src/antelope/stores/utils/media-utils'; + +export const IPFS_GATEWAY = 'https://ipfs.telos.net/ipfs/'; + +/** + * Given an imageCache URL, tokenUri, and metadata, extract the image URL, mediaType, and mediaSource + * + * @param imageCache - the imageCache URL + * @param tokenUri - the tokenUri + * @param metadata - the NFT metadata object + * + * @returns {Promise} + */ +export async function extractNftMetadata( + imageCache: string, + tokenUri: string, + metadata: IndexerNftMetadata, +): Promise<{ image: string | undefined; mediaType: NftSourceType; mediaSource: string | undefined }> { + let mediaType: NftSourceType = NFTSourceTypes.IMAGE; + let image = ''; + let mediaSource: string | undefined = undefined; + + // We are going to test the imageCache URL to see if it is a valid URL + if (imageCache) { + // first we create a regExp for the valid URL. e.g: "https://nfts.telos.net/40/0x552fd5743432eC2dAe222531e8b88bf7d2410FBc/344" + const regExp = new RegExp('^(https?:\\/\\/)?' + // protocol + '(nfts.telos.net\\/)' + // domain name + '(\\d+\\/)' + // chain id + '(0x[0-9a-fA-F]+\\/)' + // contract address + '(\\d+)$'); // token id + + // then we test the imageCache URL against the regExp + const match = regExp.test(imageCache); + if (match) { + // we return the 1440.webp version of it + image = imageCache.concat('/1440.webp'); + } + } + // if there's an image in the metadata, we return that + if (!image && metadata?.image && urlIsPicture(metadata.image)) { + image = metadata.image as string; + } + + if (!image && metadata) { + // this NFT is not a simple image and could be anything (including an image). + // We need to look at the metadata + // we iterate over the metadata properties + for (const property in metadata) { + const _value = metadata[property]; + + if (typeof _value !== 'string') { + continue; + } + + const value = _value.replace('ipfs://', IPFS_GATEWAY) as string; + + // if the value is a string and contains a valid url of a known media format, use it. + // image formats: .gif, .avif, .apng, .jpeg, .jpg, .jfif, .pjpeg, .pjp, .png, .svg, .webp + if ( + !image && // if we already have a preview, we don't need to keep looking + urlIsPicture(value) + ) { + image = value; + } + // audio formats: .mp3, .wav, .aac, .webm + if ( + !mediaSource && // if we already have a source, we don't need to keep looking + await urlIsAudio(value) + ) { + mediaType = NFTSourceTypes.AUDIO; + mediaSource = value; + } + // video formats: .mp4, .webm, .ogg + if ( + !mediaSource && // if we already have a source, we don't need to keep looking + await urlIsVideo(value) + ) { + mediaType = NFTSourceTypes.VIDEO; + mediaSource = value; + } + + const regex = /^data:(image|audio|video)\/\w+;base64,[\w+/=]+$/; + + const match = value.match(regex); + + if (match) { + const contentType = match[1]; + + if (contentType === 'image' && !image) { + image = value; + } else if (contentType === 'audio' && !mediaSource) { + mediaType = NFTSourceTypes.AUDIO; + mediaSource = value; + } else if (contentType === 'video' && !mediaSource) { + mediaType = NFTSourceTypes.VIDEO; + mediaSource = value; + } + } + + } + } + + if (!image && tokenUri && (!metadata || Object.keys(metadata).length === 0)) { + // if there is no metadata, attempt to use the tokenUri + if (await urlIsVideo(tokenUri)) { + mediaType = NFTSourceTypes.VIDEO; + } else if (await urlIsAudio(tokenUri)) { + mediaType = NFTSourceTypes.AUDIO; + } + } + + const metadataImageIsMediaUrl = await urlIsVideo(metadata?.image ?? '') || await urlIsAudio(metadata?.image ?? '') || urlIsPicture(metadata?.image ?? ''); + + if (metadata?.image?.includes(IPFS_GATEWAY) && !metadataImageIsMediaUrl) { + mediaType = await determineIpfsMediaType(metadata?.image); + + if (mediaType === NFTSourceTypes.IMAGE) { + image = metadata?.image; + } + mediaSource = metadata?.image; + } + + return { image, mediaType, mediaSource }; +} + + +/** + * Given an IPFS media URL, determine the media type + * @param url - the IPFS media URL + * @returns {Promise} - the media type + */ +export async function determineIpfsMediaType(url: string): Promise { + try { + const response = await fetch(url); + const contentType = response.headers.get('Content-Type') ?? ''; + + if (contentType.startsWith('image/')) { + return NFTSourceTypes.IMAGE; + } else if (contentType.startsWith('video/')) { + return NFTSourceTypes.VIDEO; + } else if (contentType.startsWith('audio/')) { + return NFTSourceTypes.AUDIO; + } else { + return NFTSourceTypes.UNKNOWN; + } + } catch (error) { + throw new Error('Error determining IPFS media type'); + } +} diff --git a/src/antelope/stores/utils/text-utils.ts b/src/antelope/stores/utils/text-utils.ts new file mode 100644 index 000000000..16d900609 --- /dev/null +++ b/src/antelope/stores/utils/text-utils.ts @@ -0,0 +1,67 @@ + +/** + * Given some text, ellipsizes the text if it exceeds a specific length + * + * @param text + * @param maxLength + * @returns {string} + */ +export function truncateText(text: string, maxLength = 10): string { + if (text.length <= maxLength) { + return text; + } + + return `${text.slice(0, maxLength)}...`; +} + +/** + * Given an address, returns a shortened version like `0x0000...0000` + * + * @param address + * @param maxLength + * @returns {string} + */ +export function truncateAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +/** + * Given a name and an id, returns the name without the ID. Generally in the UI, NFT name and ID are displayed next to each other; + * this function prevents the ID from duplicated + * @param name + * @param id + * @returns {string} + * @example + * getShapedNftName('SomeNft #1234', '1234') // 'SomeNft' + */ +export function getShapedNftName(name: string, id: string): string { + let shapedName = name; + const idAtTheEndRegex = new RegExp(` #?${id}$`); + + if (name.match(idAtTheEndRegex)?.length) { + shapedName = name.replace(idAtTheEndRegex, ''); + + if (shapedName[shapedName.length - 1] === '#') { + shapedName = shapedName.slice(0, -1); + } + } + return shapedName.trim(); +} + +/** + * Given a number, returns a string with the number abbreviated + * @param num - a number + * @returns {string} + * @example + * abbreviateNumber(navigator.language, 1000) // '1K' + * abbreviateNumber(navigator.language, 1200) // '1.2K' + */ +export function abbreviateNumber(language: string, num: number) { + const numberFormatter = new Intl.NumberFormat(language, { + maximumFractionDigits: 0, + maximumSignificantDigits: 4, + notation: 'compact', + }); + + return numberFormatter.format(num); +} diff --git a/src/antelope/stores/utils/trx-utils.ts b/src/antelope/stores/utils/trx-utils.ts new file mode 100644 index 000000000..d1dce824b --- /dev/null +++ b/src/antelope/stores/utils/trx-utils.ts @@ -0,0 +1,35 @@ +import { usePlatformStore } from 'src/antelope'; +import { ethers } from 'ethers'; +import { AntelopeError, TransactionResponse } from 'src/antelope/types'; +import { AccountModel, EVMAuthenticator } from 'src/antelope/wallets'; + + +export async function subscribeForTransactionReceipt(account: AccountModel, response: TransactionResponse): Promise<{ + newResponse: TransactionResponse; + receipt: ethers.providers.TransactionReceipt; +}> { + if (account.isNative) { + throw new AntelopeError('Not implemented yet for native'); + } else { + const authenticator = account.authenticator as EVMAuthenticator; + const provider = await authenticator.web3Provider(); + const result = { + newResponse: { ...response } as TransactionResponse, + receipt: {} as ethers.providers.TransactionReceipt, + }; + if (provider) { + const whenConfirmed = provider.waitForTransaction(response.hash); + // we add the wait method to the response, + // so that the caller can subscribe to the confirmation event + result.newResponse.wait = async () => whenConfirmed; + return result; + } else { + if (usePlatformStore().isMobile) { + response.wait = async () => Promise.resolve({} as ethers.providers.TransactionReceipt); + return result; + } else { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } + } +} diff --git a/src/antelope/types/ABIv1.ts b/src/antelope/types/ABIv1.ts new file mode 100644 index 000000000..36e3c46c5 --- /dev/null +++ b/src/antelope/types/ABIv1.ts @@ -0,0 +1,33 @@ +export interface ABIv1 { + abi: { + actions: ActionV1[]; + structs: StructV1[]; + tables: TableV1[]; + } | null; + account_name: string; +} + +interface ActionV1 { + name: string; + ricardian_contract: string; + type: string; +} + +interface TableV1 { + index_type: string; + key_names: unknown[]; + key_types: unknown[]; + name: string; + type: string; +} + +interface StructV1 { + base: string; + fields: FieldV1[]; + name: string; +} + +interface FieldV1 { + name: string; + type: string; +} diff --git a/src/antelope/types/Actions.ts b/src/antelope/types/Actions.ts new file mode 100644 index 000000000..23aa3c962 --- /dev/null +++ b/src/antelope/types/Actions.ts @@ -0,0 +1,248 @@ +import { API } from '@greymass/eosio'; +import { TokenSourceInfo } from 'src/antelope/types'; + +export interface ActionData { + actions: Action[]; + cached: boolean; + executed: boolean; + lib: number; + query_time_ms: number; + total: { + value: number; + relation: string; + }; +} + +export interface GetActionsResponse { + data: { + actions: Action[], + total: { + value: number + } + }, +} + +export interface Action { + '@timestamp': string; + account_ram_deltas: AccountRamDelta[]; + act: Account; + action_ordinal: number; + block_num: number; + cpu_usage_us: number; + creator_action_ordinal: number; + global_sequence: number; + net_usage_words: number; + notified: string[]; + producer: string; + signatures: string[]; + timestamp: string; + trx_id: string; + receipts: Receipt[]; + account: string; + authorization: { + actor: string; + permission: string; + }[]; + data: { + executer: string; + proposal_name: string; + proposer: string; + }; + name: string; +} + +interface AccountRamDelta { + account: string; + delta: number; +} + +export interface Account { + account: string; + authorization: Authorization[]; + data: unknown; + name: string; +} + +export interface Authorization { + actor: string; + permission: string; +} + +interface Resource { + used: number; + available: number; + max: number; +} + +export type AccountDetails = { + account: { + account_name: string; + core_liquid_balance: string; + cpu_limit: Resource; + cpu_weight: number; + created: string; + net_limit: Resource; + net_weight: number; + permissions: Permission[]; + privileged: boolean; + ram_quota: number; + ram_usage: number; + refund_request: Refund; + rex_info: null | { + matured_rex: string; + vote_stake: string; + rex_balance: string; + rex_maturities: Maturities[]; + }; + subjective_cpu_bill_limit: Resource; + total_resources: { + owner: string; + net_weight: string; + cpu_weight: string; + ram_bytes: number; + }; + voter_info: { + owner: string; + proxy: string; + producers: string[]; + staked: number; + last_stake: number; + last_vote_weight: string; + proxied_vote_weight: number; + is_proxy: number; + }; + self_delegated_bandwidth: { net_weight: string; cpu_weight: string }; + }; + actions: Action[]; + links: string[]; + query_time_ms: number; + tokens: TokenSourceInfo[]; + total_actions: number; +}; + +interface Key { + key: string; + weight: number; +} + +interface ActorPermission { + permission: { actor: string; permission: 'eosio.code' }; + weight: number; +} +interface RequiredAuth { + accounts: ActorPermission[]; + keys: Key[]; + threshold: number; + waits: []; +} + +export interface Permission extends API.v1.AccountPermission { + children: Permission[]; + permission_links: PermissionLinks[]; +} + +export interface NewAccountData { + active: RequiredAuth; + creator: string; + newact: string; + owner: RequiredAuth; + name: string; +} + +export interface PermissionLinksData { + cached: boolean; + links: PermissionLinks[]; + query_time_ms: number; + total: { + value: number; + relation: string; + }; +} + +export interface PermissionLinks { + account: string; + action: string; + block_num: number; + code: string; + permission: string; + timestamp: string; +} + +export interface TransferData { + from: string; + to: string; + amount: number; + symbol: string; + memo: string; + quantity: number; +} + +export interface Refund { + cpu_amount: string; + net_amount: string; + owner: string; + request_time: string; +} +export interface TableByScope { + code: string; + scope: string; + table: string; + payer: string; + count: number; +} + +export interface Block { + timestamp: string; + producer: string; + confirmed: number; + previous: string; + transaction_mroot: string; + action_mroot: string; + schedule_version: number; + new_producers: null | string; + producer_signature: string; + transactions: { + cpu_usage_us: number; + net_usage_words: number; + status: string; + trx: { + id: string; + transaction: { + actions: Action[]; + }; + }; + }[]; + id: string; + block_num: number; + ref_block_prefix: number; +} + +export interface Receipt { + auth_sequence: Auth_sequence[]; + global_sequence: string; + receiver: string; + recv_sequence: string; +} + +export interface Auth_sequence { + account: string; + sequence: string; +} + +interface Maturities { + first: string; + second: number; +} +export interface Get_actions { + actions: Action[]; + cached: boolean; + } +export interface Error { + cause?: { + json?: { + error?: { + what?: string; + }; + }; + }; +} diff --git a/src/antelope/types/AntelopeError.ts b/src/antelope/types/AntelopeError.ts new file mode 100644 index 000000000..7220438ee --- /dev/null +++ b/src/antelope/types/AntelopeError.ts @@ -0,0 +1,17 @@ +export interface AntelopeErrorPayload { + [key:string]: unknown +} + +export class AntelopeError extends Error { + public payload?: AntelopeErrorPayload; + constructor( + message: string | undefined, + public _payload?: unknown, + ) { + super(message); + if (_payload) { + this.payload = _payload as { [key:string]: unknown}; + } + } +} + diff --git a/src/antelope/types/Api.ts b/src/antelope/types/Api.ts new file mode 100644 index 000000000..1e319acd2 --- /dev/null +++ b/src/antelope/types/Api.ts @@ -0,0 +1,113 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +import { + Checksum160, + Checksum256, + Float64, + Name, + NameType, + UInt128, + UInt64, + UInt32Type, + API, + PublicKey, + Float128, +} from '@greymass/eosio'; + +import { + AccountDetails, + Action, + PermissionLinks, + TableByScope, + TransactionV1, + Block, + ActionData, + Get_actions, + HyperionActionsFilter, + TokenSourceInfo, +} from 'src/antelope/types'; + +export const NativeCurrencyAddress = '___NATIVE_CURRENCY___'; + +export type AccountCreatorInfo = { + creator: string; + timestamp: string; + trx_id: string; +} + +export type TableIndexType = + | Name + | UInt64 + | UInt128 + | Float64 + | Checksum256 + | Checksum160; + +export interface TableIndexTypes { + float128: Float128; + float64: Float64; + i128: UInt128; + i64: UInt64; + name: Name; + ripemd160: Checksum160; + sha256: Checksum256; +} +export interface GetTableRowsParamsKeyed extends GetTableRowsParams { + // Index key type, determined automatically when passing a typed `upper_bound` or `lower_bound`. + key_type: Key; +} + +export interface GetTableRowsParamsTyped extends GetTableRowsParams { + // Result type for each row. + type: Row; +} + +export interface GetTableRowsParams { + // The name of the smart contract that controls the provided table. + code: NameType; + // Name of the table to query. + table: NameType; + // The account to which this data belongs, if omitted will be set to be same as `code`. + scope?: string | TableIndexType; + // Lower lookup bound. + lower_bound?: Index; + // Upper lookup bound. + upper_bound?: Index; + // How many rows to fetch, defaults to 10 if unset. + limit?: UInt32Type; + // Whether to iterate records in reverse order. + reverse?: boolean; + // Position of the index used, defaults to primary. + index_position?: 'primary' | 'secondary' | 'tertiary' | 'fourth' | 'fifth' | 'sixth' | 'seventh' | 'eighth' | 'ninth' | 'tenth'; + // Whether node should try to decode row data using code abi. + // Determined automatically based the `type` param if omitted. + json?: boolean; + // Set to true to populate the ram_payers array in the response. + show_payer?: boolean; +} + + + +export interface GetTableRowsResponse { + rows: Row[]; + more: boolean; + ram_payers?: Name[]; + next_key?: Index; +} + +export type ApiClient = { + getAccount: (address: string) => Promise; + getKeyAccounts: (key: PublicKey) => Promise<{ account_names: Name[] }>; + getHyperionAccountData: (address: string) => Promise; + getCreator: (address: string) => Promise; + getTokens: (address: string) => Promise; + getTransactions: (filter: HyperionActionsFilter) => Promise; + getTransaction: (address: string) => Promise; + getTransactionV1: (id: string) => Promise; + getChildren: (address: string) => Promise; + getPermissionLinks: (address: string) => Promise; + getTableByScope: (data: unknown) => Promise; + getBlock: (block: string) => Promise; + getActions: (address: string, filter: string) => Promise; + getApy: () => Promise; +}; diff --git a/src/antelope/types/Basic.ts b/src/antelope/types/Basic.ts new file mode 100644 index 000000000..726b78431 --- /dev/null +++ b/src/antelope/types/Basic.ts @@ -0,0 +1,3 @@ +export type Label = string; +export type Network = 'telos' | 'telos-evm' | string; +export type Address = string; diff --git a/src/antelope/types/ChainInfo.ts b/src/antelope/types/ChainInfo.ts new file mode 100644 index 000000000..0f894167e --- /dev/null +++ b/src/antelope/types/ChainInfo.ts @@ -0,0 +1,19 @@ +export interface ChainInfo { + block_cpu_limit: number; + block_net_limit: number; + chain_id: string; + fork_db_head_block_id: string; + fork_db_head_block_num: number; + head_block_id: string; + head_block_num: number; + head_block_producer: string; + head_block_time: string; + last_irreversible_block_id: string; + last_irreversible_block_num: number; + last_irreversible_block_time: string; + server_full_version_string: string; + server_version: string; + server_version_string: string; + virtual_block_cpu_limit: number; + virtual_block_net_limit: number; +} diff --git a/src/antelope/types/ChainSettings.ts b/src/antelope/types/ChainSettings.ts new file mode 100644 index 000000000..b48c02e56 --- /dev/null +++ b/src/antelope/types/ChainSettings.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-unused-vars */ +import { RpcEndpoint } from 'universal-authenticator-library'; +import { PriceChartData, TokenClass } from 'src/antelope/types'; + +export interface ChainSettings { + init(): Promise; + isNative(): boolean; + isTestnet(): boolean; + getNetwork(): string; + getSystemToken(): TokenClass; + getTokenList(): Promise; + getDisplay(): string; + getSmallLogoPath(): string; + getLargeLogoPath(): string; + getChainId(): string; + getHyperionEndpoint(): string; + getRPCEndpoint(): RpcEndpoint; + getApiEndpoint(): string; + getPriceData(): Promise; + getUsdPrice(): Promise; + getSystemTokens(): TokenClass[]; + getApy(): Promise; +} diff --git a/src/antelope/types/EvmBlockData.ts b/src/antelope/types/EvmBlockData.ts new file mode 100644 index 000000000..0854895ad --- /dev/null +++ b/src/antelope/types/EvmBlockData.ts @@ -0,0 +1,24 @@ +export interface EvmBlockData { + difficulty: string; + extraData: string; + gasLimit: string; + gasUsed: string; + hash: string; + logsBloom: string; + miner: string; + mixHash: string; + nonce: string; + number: string; + parentHash: string; + receiptsRoot: string; + sha3Uncles: string; + size: string; + stateRoot: string; + timestamp: string; + totalDifficulty: string; + transactions: unknown[]; + transactionsRoot: string; + uncles: unknown[]; +} + + diff --git a/src/antelope/types/EvmContractData.ts b/src/antelope/types/EvmContractData.ts new file mode 100644 index 000000000..5961e1c00 --- /dev/null +++ b/src/antelope/types/EvmContractData.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-unused-vars */ +import { ethers } from 'ethers'; +import type { TokenSourceInfo } from 'src/antelope/types'; +import type { EvmABI } from 'src/antelope/types'; + +export interface EvmContractConstructorData { + address: string; + name: string; + manager?: EvmContractManagerI; + creationInfo?: EvmContractCreationInfo | null; + abi?: EvmABI | string; + token?: TokenSourceInfo; + verified?: boolean; + supportedInterfaces: string[]; + properties?: EvmContractCalldata; +} + +export interface EvmContractManagerI { + getSigner: () => Promise; + getWeb3Provider: () => Promise; + getFunctionIface: (hash:string) => Promise; + getEventIface: (hash:string) => Promise; +} + +export interface EvmContractCreationInfo { + block?: number | null; + block_num?: number; // same as block, kept for legacy usage + creator?: string | null; + transaction: string; + creation_trx: string; // same as transaction, kept for legacy usage + timestamp?: string; // string number like "1679649071" + abi?: string | EvmABI; +} + +export interface EvmContractMetadata { + compiler?: { + version: string; + }; + language?: string; + output?: { + abi: EvmABI; + devdoc: { + kind: string; + methods: Record; + version: number; + }; + userdoc: { + kind: string; + methods: Record; + version: number; + } + }; + settings?: { + compilationTarget: Record; + evmVersion: string; + libraries: Record; + metadata: { + bytecodeHash: string; + useLiteralContent: boolean; + }; + optimizer: { + enabled: boolean; + runs: number; + }; + remappings: Record[] + } + sources?: Record; + version?: number; +} + +export interface EvmContractCalldata { + decimals?: number; + holders?: string; // string representation of number + marketdata_updated?: string; // epoch + name?: string; + price?: string; // string representation of number, USD price + supply?: string; // string representation of number + symbol?: string; +} + +export interface EvmContractData { + symbol?: string; + creator?: string; + address: string; + fromTrace?: boolean; + abi?: string | EvmABI + trace_address?: string; // same attribute (raw) + traceAddress?: string; // same attribute (processed) + logoURI?: string; + supply?: string; // string representation of number + calldata?: string; // string holding JSON + decimals?: number | null; + name?: string | null; + block?: number; + supportedInterfaces?: string[]; + transaction?: string; +} + +export interface EvmContractFactoryData extends EvmContractData { + metadata?: string; + timestamp?: string; + manager?: EvmContractManagerI; +} + +export type EvmFunctionParam = string | number | boolean | ethers.BigNumber; diff --git a/src/antelope/types/EvmLog.ts b/src/antelope/types/EvmLog.ts new file mode 100644 index 000000000..6b687ecb2 --- /dev/null +++ b/src/antelope/types/EvmLog.ts @@ -0,0 +1,26 @@ +import { ethers } from 'ethers'; +import { TokenSourceInfo } from 'src/antelope/types'; + +export interface EvmLog { + address: string; + blockHash: string; + blockNumber: number; + data: string; + logIndex: string; + removed: boolean; + topics: string[]; + transactionHash: string; + transactionIndex: string; +} + +export type EvmLogs = EvmLog[]; + +export interface EvmFormatedLog extends ethers.utils.LogDescription { + inputs: ethers.utils.ParamType[]; + function_signature: string; + isTransfer: boolean; + logIndex: string, + address: string, + token: TokenSourceInfo | null, + name: string, +} diff --git a/src/antelope/types/EvmRexDeposit.ts b/src/antelope/types/EvmRexDeposit.ts new file mode 100644 index 000000000..a0f79a8a6 --- /dev/null +++ b/src/antelope/types/EvmRexDeposit.ts @@ -0,0 +1,10 @@ +import { ethers } from 'ethers'; + +export interface EvmRexDeposit { + // amount of REX tokens deposited (expressed in system tokens - e.g. TLOS) + amount: ethers.BigNumber; + + // a big number representing the time (seconds since epoch) at which the deposit will be available for withdrawal + // data.until.toNumber() should be salfe to use as a timestamp + until: ethers.BigNumber; +} diff --git a/src/antelope/types/EvmTransaction.ts b/src/antelope/types/EvmTransaction.ts new file mode 100644 index 000000000..5b8051b0d --- /dev/null +++ b/src/antelope/types/EvmTransaction.ts @@ -0,0 +1,161 @@ +import { ethers } from 'ethers'; +import { NftTokenInterface } from 'src/antelope/types'; + +export type EvmTransactionTopic = string; + +export interface EvmTransactionLog { + address: string; + blockHash: string; + blockNumber: number; + data: string; + logIndex: number; + removed: boolean; + topics: EvmTransactionTopic[]; + transactionHash: string; +} + +export interface EvmTransaction { + blockNumber: number; + contractAddress?: string; + cumulativeGasUsed: string; // string representation of hex number + from: string; + gasLimit: string; // string representation of hex number + gasPrice: string; // string representation of hex number + gasUsed: string; // string representation of hex number + hash: string; + index: number; + input: string; + nonce: number; + output: string; + logs?: string; + r: string; + s: string; + status: string; // string representation of hex number + timestamp: number; // epoch in milliseconds + to: string | null; // null if contract creation + v: string; + value: string; // string representation of hex number +} + +export interface EvmTransactionParsed extends EvmTransaction { + gasLimitBn: ethers.BigNumber; + gasPriceBn: ethers.BigNumber; + gasUsedBn: ethers.BigNumber; + valueBn: ethers.BigNumber; + logsArray: EvmTransactionLog[]; +} + +export interface EvmContractFunctionParameter { + name: string; + type: string; + arrayChildren: string | false; + value: (string | number | boolean | null | ethers.BigNumber)[]; +} + +export interface TransactionValueData { + amount: number; + symbol: string; + fiatValue?: number; +} + +export interface NftTransactionData { + quantity: number; + tokenId: string; + tokenName: string; + collectionAddress: string; + collectionName?: string; + imgSrc?: string; + videoSrc?: string; + audioSrc?: string; + type: 'image' | 'video' | 'audio' | 'unknown'; + nftInterface: NftTokenInterface; +} + +export interface ShapedTransactionRow { + id: string; // transaction ID + epoch: number; // epoch in milliseconds + // action should be 'send', 'receive', 'swap', 'contractCreation', or some other action like 'approve' + actionName: string; + from: string; // address + fromPrettyName?: string; + to: string; // address + toPrettyName?: string; + gasUsed?: number; // gas used in TLOS + gasFiatValue?: number; // gas used in Fiat + failed?: boolean; + + // ERC20 data + valuesIn: TransactionValueData[]; + valuesOut: TransactionValueData[]; + + // ERC721 & ERC1155 data + nftsIn: NftTransactionData[]; + nftsOut: NftTransactionData[]; +} + +export interface IndexerContractData { + symbol: string; + creator: string; + address: string; + fromTrace: boolean; + trace_address: string; + logoURI: string; + supply: string; // string representation of an integer + calldata: string; + decimals: number | null; + name: string; + block: number; + supportedInterfaces: ('erc20'|'erc721'|'erc1155'|'none')[], + transaction: string; // creation tx for contract +} + +export interface ParsedIndexerAccountTransactionsContract extends IndexerContractData { + price?: string; // string representation of number + holders?: number; + marketdata_updated?: string; // epoch +} + +export interface EVMTransactionsPaginationData { + total: number; + more: boolean; +} + +export interface IndexerAccountTransactionsResponse { + contracts: { + [contractHash: string]: IndexerContractData + }; + results: EvmTransaction[] + total_count: number; + more: boolean; +} + +export type EvmTransactionResponse = ethers.providers.TransactionResponse; +export interface TransactionResponse { + hash: string; + wait: () => Promise; +} +export interface NativeTransactionResponse extends TransactionResponse { + __?: string; +} + +export interface IndexerAccountTransfersResponse { + contracts: { + [contractHash: string]: IndexerContractData + }; + results: EvmTransfer[] + total_count?: number; // included if includePagination is true in the request + more?: boolean; // included if includePagination is true in the request +} + +export interface EvmTransfer { + amount: string, // a string representing an integer + contract: string, // contract address of the token being transferred + blockNumber: number, // an integer representing the block number of the transfer + from: string, // address of the sender + to: string; // address of the receiver + type: 'erc20' | 'erc721' | 'erc1155', // type of token being transferred + transaction: string; // transaction hash + timestamp: number; // integer representing ms from epoch + id?: string; // id of the NFT transferred (ERC721 or ERC1155 only) +} + diff --git a/src/antelope/types/ExceptionError.ts b/src/antelope/types/ExceptionError.ts new file mode 100644 index 000000000..5954e2fdd --- /dev/null +++ b/src/antelope/types/ExceptionError.ts @@ -0,0 +1,5 @@ +export interface ExceptionError { + code: number; + message: string; + stack: string; +} diff --git a/src/antelope/types/Filters.ts b/src/antelope/types/Filters.ts new file mode 100644 index 000000000..265ef4036 --- /dev/null +++ b/src/antelope/types/Filters.ts @@ -0,0 +1,43 @@ +export interface HyperionAbiSignatureFilter { + type?: string; + hex?: string; +} + +export interface HyperionActionsFilter { + page?: number; + skip?: number;// skip overrides `page` + limit?: number; + account?: string; + notified?: string; + sort?: 'desc' | 'asc'; + after?: string; + before?: string; + extras?: { [key: string]: string }; + address?: string; + block?: string; + hash?: string; +} + +export interface IndexerTransactionsFilter { + address: string; + limit?: number; // integer value to limit number of results + offset?: number; // integer value to offset the results of the query + includeAbi?: boolean; // indicate whether to include abi + sort?: 'DESC' | 'ASC'; // sort transactions by id (DESC or ASC) + includePagination?: boolean; // include the total count and more flag in response + logTopic?: string; // match to the transaction logs' first topic + full?: string; // Add internal transactions to the response + forceMetadata?: number; // 1 to force metadata to be returned +} + +export interface IndexerTransfersFilter { + account: string; + type?: 'erc20' | 'erc721' | 'erc1155'; // filter by token type + limit?: number; // integer value to limit number of results + offset?: number; // integer value to offset the results of the query + includePagination?: boolean; // include the total count and more flag in response + endBlock?: number; // last block to include in the query + startBlock?: number; // first block to include in the query + contract?: string; // filter by contract address + includeAbi?: boolean; // indicate whether to include abi +} diff --git a/src/antelope/types/IndexerTypes.ts b/src/antelope/types/IndexerTypes.ts new file mode 100644 index 000000000..671238cff --- /dev/null +++ b/src/antelope/types/IndexerTypes.ts @@ -0,0 +1,259 @@ +// Indexer Nft Response -------- + +export const INVALID_METADATA = '___INVALID_METADATA___'; // string given by indexer for NFTs with invalid metadata + +interface IndexerNftResponse { + success: boolean; + contracts: { + [address: string]: IndexerContract; + }; +} + +export interface IndexerCollectionNftsResponse extends IndexerNftResponse { + results: IndexerCollectionNftResult[]; +} + +export interface IndexerAccountNftsResponse extends IndexerNftResponse { + results: IndexerAccountNftResponse[]; +} + +export interface IndexerNftItemAttribute { + value: string; + trait_type: string; + display_type?: string; +} + +export type IndexerNftMetadata = { + dna?: string; + date?: number; + name?: string; + image?: string; + edition?: number; + compiler?: string; + imageHash?: string; + attributes?: IndexerNftItemAttribute[]; + description?: string; + [key: string]: unknown; +} | null; + +interface IndexerNftResult { + metadata: string; + tokenId: string; + contract: string; + updated: number; + imageCache?: string; + tokenUri?: string; +} + +// results from the /contract/{address}/nfts endpoint +export interface IndexerCollectionNftResult extends IndexerNftResult { + supply?: number; // present only for ERC1155 + owner?: string; // present only for ERC721 +} + +// results from the /account/{address}/nfts endpoint +export interface IndexerAccountNftResponse extends IndexerNftResult { + amount?: number; // present only for ERC1155 + minter: string; + blockMinted: number; + tokenIdSupply?: number; // present only for ERC1155 + owner: string; +} + +// used as an intermediate type for constructing NFTs from IndexerAccountNftResponse/IndexerCollectionNftResult +export interface GenericIndexerNft { + metadata: Record | string; // object or JSON object string + tokenId: string; + contract: string; + updated: number; + imageCache?: string; + tokenUri?: string; + supply?: number; // present only for ERC1155 + minter?: string; + blockMinted?: number; + owner?: string; // present only for ERC721 +} + +export interface IndexerContract { + symbol: string; + creator: string; + address: string; + fromTrace: boolean; + trace_address: string; + supply: string; + calldata?: { + name?: string; + supply?: string; + symbol?: string; + }, + decimals: number | null; + name: string; + block: number; + supportedInterfaces?: string[]; + transaction: string; +} + + + + +// ------- + +export interface IndexerTokenInfo { + symbol: string; + creator: string; + address: string; + fromTrace: boolean; + trace_address: string; + logoURI: string; + supply: string; + calldata: IndexerTokenMarketData; + decimals: number; + name: string; + block: number; + supportedInterfaces: string[]; + transaction: string; +} + +export interface IndexerTokenMarketData { + name?: string; + price?: number; + supply?: string; + symbol?: string; + volume?: string; + holders?: string; + decimals?: number; + marketcap?: string; + max_supply_ibc?: string; + total_supply_ibc?: string; + marketdata_updated?: string; +} + +export interface IndexerTokenBalance { + address: string; + balance: string; + contract: string; + updated: number; +} + + +export interface IndexerAccountBalances { + success: boolean; + contracts: { + [address: string]: IndexerTokenInfo; + }; + results: IndexerTokenBalance[]; +} + +export interface IndexerHealthResponse { + success: boolean; + blockNumber: number; + blockTimestamp: string; + secondsBehind: number; +} + +export interface IndexerTokenHoldersResponse { + contracts: { + [address: string]: IndexerContract; + }; + results: { + address: string; // holder address + balance: number; + tokenid: number; + updated: number; // ms since epoch + }[]; +} + +// Allowances +interface IndexerAllowanceResult { + owner: string; // address of the token owner; + contract: string; // address of the token contract + updated: number; // timestamp of the last time the allowance was updated - ms since epoch +} + +export interface IndexerErc20AllowanceResult extends IndexerAllowanceResult { + amount: string; // string representation of a number; the amount of tokens the owner has approved for the spender in the token's smallest unit + spender: string; // address of the spender contract +} + +export interface IndexerErc721AllowanceResult extends IndexerAllowanceResult { + single: false; // whether the allowance is for a single token or for the entire collection + approved: boolean; // whether the user has approved the spender + operator: string; // address of the spender contract + + tokenId?: string | number; // only present if single === true +} + +export interface IndexerErc1155AllowanceResult extends IndexerAllowanceResult { + approved: boolean; // whether the user has approved the spender + operator: string; // address of the spender contract +} + +export interface IndexerAllowanceResponseErc20 { + contracts: { + [address: string]: IndexerContract; + } + results: IndexerErc20AllowanceResult[], +} + +export interface IndexerAllowanceResponseErc721 { + contracts: { + [address: string]: IndexerContract; + } + results: IndexerErc721AllowanceResult[], +} + +export interface IndexerAllowanceResponseErc1155 { + contracts: { + [address: string]: IndexerContract; + } + results: IndexerErc1155AllowanceResult[], +} + +export type IndexerAllowanceResponse = IndexerAllowanceResponseErc20 | IndexerAllowanceResponseErc721 | IndexerAllowanceResponseErc1155; + + +// old version ---------- + +export interface IndexerNftItemResult { + metadata: { + dna?: string; + date?: number; + name?: string; + image?: string; + edition?: number; + compiler?: string; + imageHash?: string; + attributes?: IndexerNftItemAttribute[]; + description?: string; + } | { + [key: string]: unknown; + } | null; + owner: string; // address + minter: string; // address + tokenId: string; + tokenUri: string; + contract: string; // address + imageCache?: string; // url + blockMinted: number; + updated: number; // epoch + transaction: string; // tx hash +} + +export interface IndexerNftContract { + symbol: string; + creator: string; + address: string; + fromTrace: boolean; + trace_address: string; + supply: string; + calldata?: { + name?: string; + supply?: string; + symbol?: string; + }, + decimals: number | null; + name: string; + block: number; + supportedInterfaces: string[]; + transaction: string; +} diff --git a/src/antelope/types/KeyAccounts.ts b/src/antelope/types/KeyAccounts.ts new file mode 100644 index 000000000..efe01a8d9 --- /dev/null +++ b/src/antelope/types/KeyAccounts.ts @@ -0,0 +1 @@ +export interface KeyAccounts { account_names: string[] } diff --git a/src/antelope/types/NFTClass.ts b/src/antelope/types/NFTClass.ts new file mode 100644 index 000000000..aedc2d32b --- /dev/null +++ b/src/antelope/types/NFTClass.ts @@ -0,0 +1,394 @@ +/* eslint-disable max-len */ +// NFT interfaces --------------- + +import { IndexerNftContract, IndexerNftItemAttribute, IndexerNftItemResult } from 'src/antelope/types'; + +export interface NftAttribute { + label: string; + text: string; +} + +// NFT which has been processed for display in the UI +export interface ShapedNFT { + name: string; + id: string; + description?: string; + ownerAddress: string; + contractAddress: string; + contractPrettyName?: string; + attributes: NftAttribute[]; + imageSrcFull?: string; // if this is empty, the UI will display a generic image icon + imageSrcIcon?: string; // as a result of shaping, this will always have a value if imageSrcFull is defined + + // only one of audioSrc or videoSrc should be present, not both + audioSrc?: string; + + // during the shaping process, if there is a video but no image given in the metadata, + // the first frame of the video should be extracted and set as the imageSrcFull & imageSrcIcon + videoSrc?: string; +} + +export type NftSourceType = 'image' | 'video' | 'audio' | 'unknown'; +export const NFTSourceTypes: Record = { + IMAGE: 'image', + VIDEO: 'video', + AUDIO: 'audio', + UNKNOWN: 'unknown', +}; + +export type NftTokenInterface = 'ERC721' | 'ERC1155'; + +// NFT classes ------------------ + +export class NFTContractClass { + indexer: IndexerNftContract; + constructor( + source: IndexerNftContract, + ) { + this.indexer = source; + } + + get address(): string { + return this.indexer.address; + } + + get name(): string | undefined { + return this.indexer.calldata?.name; + } +} + +export class NFTItemClass { + indexer: IndexerNftItemResult; + ready = true; + preview: string; + type: NftSourceType; + source: string | undefined; + contract: NFTContractClass; + + constructor( + item: IndexerNftItemResult, + public _contract: NFTContractClass, + ) { + this.contract = _contract; + this.indexer = item; + const { preview, type, source } = this.extractMetadata(); + this.preview = preview; + this.type = type as NftSourceType; + this.source = source; + } + + extractMetadata(): { preview:string, type:string, source:string | undefined } { + let type = NFTSourceTypes.IMAGE; + let preview = ''; + let source: string | undefined = undefined; + + // We are going to test the imageCache URL to see if it is a valid URL + if (this.indexer.imageCache) { + + // first we create a regExp for the valid URL. e.g: "https://nfts.telos.net/40/0x552fd5743432eC2dAe222531e8b88bf7d2410FBc/344" + const regExp = new RegExp('^(https?:\\/\\/)?' + // protocol + '(nfts.telos.net\\/)' + // domain name + '(\\d+\\/)' + // chain id + '(0x[0-9a-fA-F]+\\/)' + // contract address + '(\\d+)$'); // token id + + // then we test the imageCache URL against the regExp + const match = regExp.test(this.indexer.imageCache); + if (match) { + // we return the 1440.webp version of it + preview = this.indexer.imageCache.concat('/1440.webp'); + } + } + // if there's an image in the metadata, we return that + if (!preview && this.indexer.metadata?.image) { + preview = this.indexer.metadata.image as string; + } + + if (!preview && this.indexer.metadata) { + // this NFT is not a simple image and could be anything (including an image). + // We need to look at the metadata + const metadata = this.indexer.metadata as { [key: string]: string }; + // we iterate over the metadata properties + for (const property in metadata) { + const value = metadata[property]; + if (!value) { + continue; + } + // if the value is a string and contains a valid url of a known media format, use it. + // image formats: .gif, .avif, .apng, .jpeg, .jpg, .jfif, .pjpeg, .pjp, .png, .svg, .webp + if ( + !preview && // if we already have a preview, we don't need to keep looking + typeof value === 'string' && + value.match(/\.(gif|avif|apng|jpe?g|jfif|p?jpe?g|png|svg|webp)$/) + ) { + preview = value; + } + // audio formats: .mp3, .wav, .aac, .webm + if ( + !source && // if we already have a source, we don't need to keep looking + typeof value === 'string' && + value.match(/\.(mp3|wav|aac|webm)$/) + ) { + type = NFTSourceTypes.AUDIO; + source = value; + } + // video formats: .mp4, .webm, .ogg + if ( + !source && // if we already have a source, we don't need to keep looking + typeof value === 'string' && + value.match(/\.(mp4|webm|ogg)$/) + ) { + type = NFTSourceTypes.VIDEO; + source = value; + } + + const regex = /^data:(image|audio|video)\/\w+;base64,[\w+/=]+$/; + + const match = value.match(regex); + + if (match) { + const contentType = match[1]; + + if (contentType === 'image' && !preview) { + preview = value; + } else if (contentType === 'audio' && !source) { + type = NFTSourceTypes.AUDIO; + source = value; + } else if (contentType === 'video' && !source) { + type = NFTSourceTypes.VIDEO; + source = value; + } + } + + } + + // particular case of media format webm. We need to determine if it is a video or audio + if (source && source.match(/\.webm$/)) { + this.ready = false; + + this.determineWebmType(source).then((_type) => { + if (_type === NFTSourceTypes.VIDEO) { + this.type = NFTSourceTypes.VIDEO; + this.extractFirstFrameFromVideo(source as string).then((_preview) => { + this.preview = _preview; + this.ready = true; + this.notifyWatchers(); + }); + } else { + this.notifyWatchers(); + } + }); + } else { + if (type === NFTSourceTypes.VIDEO) { + this.ready = false; + this.type = NFTSourceTypes.VIDEO; + this.extractFirstFrameFromVideo(source as string).then((_preview) => { + this.preview = _preview; + this.ready = true; + this.notifyWatchers(); + }); + } + } + } + + return { preview, type, source }; + } + + async determineWebmType(source: string): Promise { + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + + video.onloadedmetadata = function() { + if (video.videoWidth > 0 && video.videoHeight > 0) { + resolve(NFTSourceTypes.VIDEO); + } else { + resolve(NFTSourceTypes.AUDIO); + } + }; + + video.onerror = function(e) { + reject({ error: e, source }); + }; + + video.src = source; + }); + } + + async extractFirstFrameFromVideo(source: string): Promise { + return this.extractFrameFromVideo(source, 0); + } + + async extractFrameFromVideo(source: string, time: number): Promise { + // this function seams not to wer in most of the cases. It returns a transparent image + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + + video.onloadedmetadata = function() { + video.currentTime = time; + + const canvas = document.createElement('canvas'); + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const ctx = canvas.getContext('2d'); + if (ctx) { + // let's draw the video in the canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + // now we test the color of the pixel in the middle of the canvas + const pixelData = ctx.getImageData((canvas.width / 2), (canvas.height / 2), 1, 1).data; + if (pixelData[3] === 0) { + // if it is transparent, it means that we don't have a preview for this video + resolve(''); + } else { + // if the pixel is not transparent, we return the canvas as a dataURL + resolve(canvas.toDataURL()); + } + } else { + reject({ error: 'no context', source }); + } + }; + + video.onerror = function(e) { + reject({ error: e, source }); + }; + + video.src = source; + video.setAttribute('crossOrigin', 'anonymous'); + video.preload = 'metadata'; + video.load(); + }); + } + + + get name(): string { + return (this.indexer.metadata?.name || '') as string; + } + + get tokenId(): string { + return this.indexer.tokenId; + } + + get description(): string | undefined { + return (this.indexer.metadata?.description) as string | undefined; + } + + get owner(): string { + return this.indexer.owner || this.indexer.minter; + } + + get attributes(): NftAttribute[] { + return ((this.indexer.metadata?.attributes || []) as IndexerNftItemAttribute[]).map(attr => ({ + label: attr.trait_type, + text: attr.value, + })); + } + + get image(): string { + return this.preview; + } + + get icon(): string | undefined { + return this.preview; + } + + watchers: (() => void)[] = []; + watch(cb: () => void): void { + this.watchers.push(cb); + } + + notifyWatchers(): void { + this.watchers.forEach(w => w()); + } +} + +export class NFTClass implements ShapedNFT { + + item: NFTItemClass; + + constructor( + item: NFTItemClass, + ) { + this.item = item; + } + + // API -- + + // ShapedNFT support -- + get name(): string { + return this.item.name; + } + + get id(): string { + return this.item.tokenId; + } + + get description(): string | undefined { + return this.item.description; + } + + get ownerAddress(): string { + return this.item.owner; + } + + get contractAddress(): string { + return this.item.contract.address; + } + + get contractPrettyName(): string | undefined { + return this.item.contract.name; + } + + get attributes(): NftAttribute[] { + return this.item.attributes; + } + + get imageSrcFull(): string | undefined { + return this.item.image; + } + + get imageSrcIcon(): string | undefined { + return this.item.icon; + } + + get audioSrc(): string | undefined { + return this.item.type === NFTSourceTypes.AUDIO ? this.item.source : undefined; + } + + get videoSrc(): string | undefined { + return this.item.type === NFTSourceTypes.VIDEO ? this.item.source : undefined; + } + + getShapedNFT(): ShapedNFT { + return { + name: this.name, + id: this.id, + description: this.description, + ownerAddress: this.ownerAddress, + contractAddress: this.contractAddress, + contractPrettyName: this.contractPrettyName, + attributes: this.attributes, + imageSrcFull: this.imageSrcFull, + imageSrcIcon: this.imageSrcIcon, + audioSrc: this.audioSrc, + videoSrc: this.videoSrc, + }; + } + + // this jey property is very usefull to provide a unique key to the v-for directive + // because it is based on the content of the shapedNFT object + get key(): string { + const json = JSON.stringify(this.getShapedNFT()); + let counter = 0; + for (let i = 0; i < json.length; i++) { + counter += json.charCodeAt(i); + } + return counter.toString(); + } + + watch(cb: () => void): void { + this.item.watch(cb); + } +} + diff --git a/src/antelope/types/OpenSeaTypes.ts b/src/antelope/types/OpenSeaTypes.ts new file mode 100644 index 000000000..49f044356 --- /dev/null +++ b/src/antelope/types/OpenSeaTypes.ts @@ -0,0 +1,14 @@ +/* eslint-disable max-len */ +export interface OpenSeaNFTMetadata { + image?: string; // This is the URL to the image of the item. Can be just about any type of image (including SVGs, which will be cached into PNGs by OpenSea), and can be IPFS URLs or paths. We recommend using a 350 x 350 image. + image_data?: string; // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you\'re not including the image parameter. + external_url?: string; // This is the URL that will appear below the asset\'s image on OpenSea and will allow users to leave OpenSea and view the item on your site. + description?: string; // A human readable description of the item. Markdown is supported. + name?: string; // Name of the item. + attributes?: string; // These are the attributes for the item, which will show up on the OpenSea page for the item. (see below) + background_color?: string; // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #. + animation_url?: string; // A URL to a multi-media attachment for the item. The file extensions GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG are supported, along with the audio-only extensions MP3, WAV, and OGA. + // animation_url also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. Scripts and relative paths within the HTML page are now supported. However, access to browser extensions is not supported. + youtube_url?: string; // A URL to a YouTube video. +} + diff --git a/src/antelope/types/PriceData.ts b/src/antelope/types/PriceData.ts new file mode 100644 index 000000000..6b929abc2 --- /dev/null +++ b/src/antelope/types/PriceData.ts @@ -0,0 +1,31 @@ +export interface PriceChartData { + lastUpdated: number; + tokenPrice: number; + dayChange: number; + dayVolume: number; + marketCap: number; + prices: DateTuple[]; +} + +export interface PriceHistory { + data: { + prices: DateTuple[]; + }; +} + +export type DateTuple = [number | string, number]; + +export interface PriceStats { + status: number; + data: { + [tokenId: string]: { + last_updated_at: number; + usd: number; + usd_24h_change: number; + usd_24h_vol: number; + usd_market_cap: number; + }; + }; +} + + diff --git a/src/antelope/types/Producers.ts b/src/antelope/types/Producers.ts new file mode 100644 index 000000000..55ada9933 --- /dev/null +++ b/src/antelope/types/Producers.ts @@ -0,0 +1,27 @@ +export interface GetProducers { + rows: Producer[]; +} + +export interface Producer { + owner: string; + is_active: number; + total_votes: number; + location: string; + name: string; +} + +export interface ProducerSchedule { + active: { + version: string; + producers: { + producer_name: string; + authority: unknown; + }[]; + }; + pending: null; + proposed: null; +} + +export interface ProducerScheduleData { + active: { producers: { producer_name: string }[] }; +} diff --git a/src/antelope/types/Proposals.ts b/src/antelope/types/Proposals.ts new file mode 100644 index 000000000..fc0399f6d --- /dev/null +++ b/src/antelope/types/Proposals.ts @@ -0,0 +1,82 @@ +export interface GetProposalsProps { + proposer?: string; + proposal?: string; + requested?: string; + provided?: string; + executed?: boolean; + limit?: number; + skip?: number; +} + +export interface Proposal { + block_num: number; + executed: false; + primary_key: string; + proposal_name: string; + proposer: string; + provided_approvals: { + actor: string; + permission: string; + time: string; + }[]; + requested_approvals: { + actor: string; + permission: string; + time: string; + }[]; +} + +export interface GetProposals { + proposals: Proposal[]; + total: { + value: number; + }; +} + +export interface ProposalTableRow { + primaryKey: string; + proposalName: string; + approvalStatus: string; + proposer: string; + isSigned?: boolean; +} + +export interface ProposalForm { + [x: string]: unknown; + proposer: string; + proposal_name: string; + + requested: { + actor: string; + permission: string; + }[]; + + trx: { + expiration: string; + ref_block_num: number; + ref_block_prefix: number; + max_net_usage_words: number; + max_cpu_usage_ms: number; + delay_sec: number; + context_free_actions: string[]; + transaction_extensions: string[]; + actions: { + account: string; + name: string; + authorization: { + actor: string; + permission: string; + }[]; + data: { + [key: string]: string | number; + }; + }[]; + }; +} + +export interface RequestedApprovals { + actor: string; + permission: string; + status: boolean; + isBp: boolean; +} diff --git a/src/antelope/types/Providers.ts b/src/antelope/types/Providers.ts new file mode 100644 index 000000000..4fe98ac4f --- /dev/null +++ b/src/antelope/types/Providers.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +export interface EthereumProvider { + // ethereum provider standard API ----- + isMetaMask?: boolean; + isStatus?: boolean; + host?: string; + path?: string; + sendAsync?: (request: { method: string, params?: Array }, callback: (error: unknown, response: unknown) => void) => void; + send?: (request: { method: string, params?: Array }, callback: (error: unknown, response: unknown) => void) => void; + request: (request: { method: string, params?: Array }) => Promise; + + // event and listeners ----- + once?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; + on(eventName: string | symbol, listener: (...args: unknown[]) => void): this; + off?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; + addListener?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; + removeListener?(eventName: string | symbol, listener: (...args: unknown[]) => void): this; + removeAllListeners?(event?: string | symbol): this; + + // internal injected API ----- + __initialized: boolean; +} + diff --git a/src/antelope/types/Theme.ts b/src/antelope/types/Theme.ts new file mode 100644 index 000000000..dfe49cefd --- /dev/null +++ b/src/antelope/types/Theme.ts @@ -0,0 +1,17 @@ +export interface Theme { + primary?: string; + secondary?: string; + accent?: string; + dark?: string; + positive?: string; + negative?: string; + info?: string; + warning?: string; + 'color-map'?: string; + 'color-primary-gradient'?: string; + 'color-secondary-gradient'?: string; + 'color-tertiary-gradient'?: string; + 'color-progress-gradient'?: string; + 'color-producer-card-background'?: string; + 'color-select-box-background'?: string; +} diff --git a/src/antelope/types/TokenClass.ts b/src/antelope/types/TokenClass.ts new file mode 100644 index 000000000..9365a8dea --- /dev/null +++ b/src/antelope/types/TokenClass.ts @@ -0,0 +1,337 @@ +import { ethers } from 'ethers'; +import { toStringNumber } from 'src/antelope/wallets/utils/currency-utils'; +import { WEI_PRECISION, formatWei } from 'src/antelope/wallets/utils'; + +export const TOKEN_PRICE_DECIMALS = 18; + +// A type to represent the possible EVM token types +export const ERC20_TYPE = 'ERC20'; +export const ERC721_TYPE = 'ERC721'; +export const ERC1155_TYPE = 'ERC1155'; +export const ERC777_TYPE = 'ERC777'; +export const ERC827_TYPE = 'ERC827'; +export const ERC1400_TYPE = 'ERC1400'; +export const ERC223_TYPE = 'ERC223'; +export type EvmTokenType = + typeof ERC20_TYPE | + typeof ERC721_TYPE | + typeof ERC1155_TYPE | + typeof ERC777_TYPE | + typeof ERC827_TYPE | + typeof ERC1400_TYPE; + +// MarketSourceInfo is a type to represent all the information that can be retrieved from the market API +// It is used to creat a TokenMarketData class object +export interface MarketSourceInfo { + volume?: string; // ej: '20637702616.664093', + maxGlobalSupply?: never; // ej: null ¿? + networkSupply?: string; // ej: '554177.374691', + symbol?: string; // ej: 'USDT', + marketcap?: string; // ej: '82852725529.35149', + address?: string; // ej: '0xeFAeeE334F0Fd1712f9a8cc375f427D9Cdd40d73', + holders?: string; // ej: '509', + price: string; // ej: '1.000089', + decimals?: number; // ej: 6, + globalSupply?: string; // ej: '86090638895.068830', + updated?: string; // ej: '1684330029866' +} + +// A type to represent the source information for a token +// It is used to create a Token class object +export interface TokenSourceInfo { + symbol: string; // Token symbol + contract?: string; // Token contract account name (for native) + address?: string; // Token contract address (for EVM) + chainId: string; // Chain ID (40 & 41 for Telos EVM) or hash (for native) + network: string; // short name of the network (used for the token id) + name: string; // Token name (as a title) + decimals?: number; // Token amount of digits after the decimal point (as used in EVM) + precision?: number; // Token amount of digits after the decimal point (as used in native) + type?: EvmTokenType; // Token type (ERC20, ERC721, etc.) + logo?: string; // Token logo uri (as used in native) + logoURI?: string; // Token logo uri (as used in EVM) + metadata?: string; // Token contract metadata (as used in EVM) + isSystem: boolean; // True if the token is the main system token + isNative: boolean; // True if the token is a Antelope native blockchain token (false for EVM) + amount?: number | string; // posible balance amount + balance?: string; // posible balance amount + fullBalance?: string; // posible balance amount +} + +// A class to represent the price market information for a token +export class TokenMarketData { + readonly info: MarketSourceInfo; // Market information + private _price: ethers.BigNumber; // pre calculated Token price + + constructor(sourceInfo: MarketSourceInfo) { + this.info = sourceInfo; + try { + this._price = ethers.utils.parseUnits(sourceInfo.price, TOKEN_PRICE_DECIMALS); + } catch (e) { + this._price = ethers.constants.Zero; + } + } + + // Returns the token price + get price(): ethers.BigNumber { + return this._price; + } +} + +export class TokenPrice { + readonly market: TokenMarketData | null; + constructor(market: TokenMarketData | null) { + if (market?.price.gt(ethers.constants.Zero)) { + this.market = market; + } else { + this.market = null; + } + } + + get decimals(): number { + return this.market?.info.decimals || TOKEN_PRICE_DECIMALS; + } + + // Returns the token price as BigNumber + get value(): ethers.BigNumber { + return this.market?.price || ethers.constants.Zero; + } + + // Returns the token price as string containing a float number + get str(): string { + return ethers.utils.formatUnits(this.value, TOKEN_PRICE_DECIMALS); + } + + // Returns the inverse of the token price as BigNumber + get inverse(): ethers.BigNumber { + return ethers.utils.parseUnits('1', TOKEN_PRICE_DECIMALS * 2).div(this.value); + } + + // Returns the inverse of the token price as string containing a float number + get inverseStr(): string { + return ethers.utils.formatUnits(this.inverse, TOKEN_PRICE_DECIMALS); + } + + get isAvailable(): boolean { + return this.market !== null && this.market.price.gt(ethers.constants.Zero); + } + + // this supports the token.price.toString() expression + toString(): string { + return this.value.toString(); + } + + + // this function transforms a token amount into fiat amount and returns it as BigNumber + getAmountInFiat(tokensAmount: string | number | ethers.BigNumber): ethers.BigNumber { + // get the BigNumber value + let tokensAmountBn: ethers.BigNumber = ethers.constants.Zero; + if (typeof tokensAmount === 'string' || typeof tokensAmount === 'number') { + tokensAmountBn = ethers.utils.parseUnits(toStringNumber(tokensAmount), this.decimals); + } else { + tokensAmountBn = tokensAmount; + } + const fiatAmount = tokensAmountBn.mul(this.value).div(ethers.utils.parseUnits('1', this.decimals)); + return fiatAmount; + } + + // this function transforms a token amount into fiat amount and returns it as string containing a float number + getAmountInFiatStr(tokensAmount: string | number | ethers.BigNumber, decimals = 2): string { + return `${formatWei(this.getAmountInFiat(tokensAmount), TOKEN_PRICE_DECIMALS, decimals)}`; + } + + // this function transforms a fiat amount into token amount and returns it as BigNumber + getAmountInTokens(fiatAmount: string | number | ethers.BigNumber): ethers.BigNumber { + // get the BigNumber value + let fiatAmountBn: ethers.BigNumber = ethers.constants.Zero; + if (typeof fiatAmount === 'string' || typeof fiatAmount === 'number') { + fiatAmountBn = ethers.utils.parseUnits(toStringNumber(fiatAmount), this.decimals); + } else { + fiatAmountBn = fiatAmount; + } + const tokensAmount = fiatAmountBn.mul(ethers.utils.parseUnits('1', this.decimals)).div(this.value); + return tokensAmount; + } + + // this function transforms a fiat amount into token amount and returns it as string containing a float number + getAmountInTokensStr(fiatAmount: string | number | ethers.BigNumber, decimals = 2): string { + return `${formatWei(this.getAmountInTokens(fiatAmount), this.decimals, decimals)}`; + } + + // this function transforms a token amount into another given token amount and returns it as BigNumber + getAmountInThisToken(tokensAmount: string | number | ethers.BigNumber, targetToken: TokenClass): ethers.BigNumber { + // get the BigNumber value + let tokensAmountBn: ethers.BigNumber = ethers.constants.Zero; + if (typeof tokensAmount === 'string' || typeof tokensAmount === 'number') { + tokensAmountBn = ethers.utils.parseUnits(toStringNumber(tokensAmount), this.decimals); + } else { + tokensAmountBn = tokensAmount; + } + const targetAmount = tokensAmountBn.mul(this.value).div(targetToken.price.value); + return targetAmount; + } +} + +// A class to represent a blockchain token +export class TokenClass implements TokenSourceInfo { + readonly id: string; // Unique ID for the token -- + readonly symbol: string; // Token symbol + readonly name: string; // Token name (as a title) + readonly logo?: string; // Token logo uri + readonly contract: string; // Token contract address (for EVM) or account name (for native) + readonly chainId: string; // Chain ID (40 & 41 for Telos EVM) or hash (for native) + readonly network: string; // short name of the network (used for the token id) + readonly decimals: number; // Token amount of digits after the decimal point (same as precision for native) + readonly isSystem: boolean; // True if the token is the system token + readonly isNative: boolean; // True if the token is a native blockchain token + readonly type: EvmTokenType; // Token type (ERC20, ERC721, etc.) + private _price: TokenPrice; // Token price object + + constructor(sourceInfo: TokenSourceInfo) { + this.symbol = sourceInfo.symbol; + this.contract = sourceInfo.contract ?? sourceInfo.address ?? ''; + this.chainId = sourceInfo.chainId; + this.network = sourceInfo.network; + this.name = sourceInfo.name; + this.decimals = sourceInfo.decimals ?? sourceInfo.precision ?? WEI_PRECISION; + this.isSystem = sourceInfo.isSystem; + this.isNative = sourceInfo.isNative; + this.logo = sourceInfo.logo ?? sourceInfo.logoURI; + this.type = (sourceInfo.type?.toUpperCase() ?? ERC20_TYPE) as EvmTokenType; + this.id = `${this.symbol}-${this.contract}-${this.network}`; + this._price = new TokenPrice(null); + } + + // Sets the market data for the token to update token price + set market(market: TokenMarketData | null) { + this._price = new TokenPrice(market); + } + + get market(): TokenMarketData | null { + return this._price.market; + } + + // Returns the URI for the token logo + get logoURI(): string | undefined { + return this.logo; + } + + get address(): string { + return this.contract; + } + + get precision(): number { + return this.decimals; + } + + // Returns the token price + get price(): TokenPrice { + return this._price; + } + + // Returns the token source info + get sourceInfo(): TokenSourceInfo { + return { + symbol: this.symbol, + name: this.name, + logo: this.logo, + logoURI: this.logoURI, + contract: this.contract, + address: this.address, + chainId: this.chainId, + network: this.network, + decimals: this.decimals, + precision: this.precision, + isSystem: this.isSystem, + isNative: this.isNative, + amount: 0, + balance: '0', + fullBalance: '0', + }; + } + + toString(): string { + return this.symbol; + } +} + +// A class to represent the balance of a token +export class TokenBalance { + readonly token: TokenClass; + private _balanceStr: string; + private _balanceBn: ethers.BigNumber; + + constructor(token: TokenClass, balanceBn: ethers.BigNumber) { + this.token = token; + this._balanceBn = balanceBn; + this._balanceStr = `${ethers.utils.formatUnits(balanceBn, this.token.decimals)} ${this.token.symbol}`; + } + + set balance(balanceBn: ethers.BigNumber) { + this._balanceBn = balanceBn; + this._balanceStr = `${ethers.utils.formatUnits(balanceBn, this.token.decimals)} ${this.token.symbol}`; + } + + get balance(): ethers.BigNumber { + return this._balanceBn; + } + + // amount is an alias for balance + get amount(): ethers.BigNumber { + return this.balance; + } + + // value is an alias for balance + get value(): ethers.BigNumber { + return this.balance; + } + + get str(): string { + return this._balanceStr.split(' ')[0]; + } + + // Returns the fiat balance based on the current token price and balance + get fiatBalance(): ethers.BigNumber { + const price = this.token.price.value; + const fiatDouble = this.balance.mul(price); + const fiat = fiatDouble.div(ethers.utils.parseUnits('1', this.decimals)); + return fiat; + } + + get fiatStr(): string { + const fiat = this.fiatBalance; + return `${formatWei(fiat, TOKEN_PRICE_DECIMALS, 2)}`; + } + + get id(): string { + return this.token.id; + } + get symbol(): string { + return this.token.symbol; + } + get name(): string { + return this.token.name; + } + get logo(): string | undefined { + return this.token.logo; + } + get contract(): string { + return this.token.contract; + } + get chainId(): string { + return this.token.chainId; + } + get decimals(): number { + return this.token.decimals; + } + get isSystem(): boolean { + return this.token.isSystem; + } + get isNative(): boolean { + return this.token.isNative; + } + + toString(): string { + return this._balanceStr; + } +} diff --git a/src/antelope/types/TransactionV1.ts b/src/antelope/types/TransactionV1.ts new file mode 100644 index 000000000..60cffbbd9 --- /dev/null +++ b/src/antelope/types/TransactionV1.ts @@ -0,0 +1,21 @@ +export interface TransactionV1 { + id: string; + trx: { + receipt: { + status: string; + cpu_usage_us: number; + net_usage_words: number; + }; + trx: { + expiration: string; + ref_block_num: number; + ref_block_prefix: number; + max_net_usage_words: number; + max_cpu_usage_ms: number; + delay_sec: number; + }; + }; + block_time: string; + block_num: number; + last_irreversible_block: number; +} diff --git a/src/antelope/types/index.ts b/src/antelope/types/index.ts new file mode 100644 index 000000000..9c121127f --- /dev/null +++ b/src/antelope/types/index.ts @@ -0,0 +1,33 @@ +// interfaces for antelope +export * from 'src/antelope/types/ABIv1'; +export * from 'src/antelope/types/Actions'; +export * from 'src/antelope/types/AntelopeError'; +export * from 'src/antelope/types/Api'; +export * from 'src/antelope/types/ChainInfo'; +export * from 'src/antelope/types/ChainSettings'; +export * from 'src/antelope/types/EvmBlockData'; +export * from 'src/antelope/types/EvmContractData'; +export * from 'src/antelope/types/EvmLog'; +export * from 'src/antelope/types/EvmRexDeposit'; +export * from 'src/antelope/types/EvmTransaction'; +export * from 'src/antelope/types/ExceptionError'; +export * from 'src/antelope/types/Filters'; +export * from 'src/antelope/types/IndexerTypes'; +export * from 'src/antelope/types/KeyAccounts'; +export * from 'src/antelope/types/Basic'; +export * from 'src/antelope/types/NFTClass'; +export * from 'src/antelope/types/OpenSeaTypes'; +export * from 'src/antelope/types/PriceData'; +export * from 'src/antelope/types/Proposals'; +export * from 'src/antelope/types/Producers'; +export * from 'src/antelope/types/Providers'; +export * from 'src/antelope/types/Theme'; +export * from 'src/antelope/types/TokenClass'; +export * from 'src/antelope/types/TransactionV1'; + + +// classes for antelope +export * from 'src/antelope/types/AntelopeError'; + +// interfaces for antelope evm-abi +export * from 'src/antelope/wallets/utils/abi'; diff --git a/src/antelope/types/ual-oreid.d.ts b/src/antelope/types/ual-oreid.d.ts new file mode 100644 index 000000000..9b60e1900 --- /dev/null +++ b/src/antelope/types/ual-oreid.d.ts @@ -0,0 +1 @@ +declare module 'ual-oreid'; diff --git a/src/antelope/wallets/authenticators/BraveAuth.ts b/src/antelope/wallets/authenticators/BraveAuth.ts new file mode 100644 index 000000000..1f7d25748 --- /dev/null +++ b/src/antelope/wallets/authenticators/BraveAuth.ts @@ -0,0 +1,30 @@ +import { EthereumProvider } from 'src/antelope/types'; +import { EVMAuthenticator, InjectedProviderAuth } from 'src/antelope/wallets'; + +const name = 'Brave'; +export const BraveAuthName = name; +export class BraveAuth extends InjectedProviderAuth { + + // this is just a dummy label to identify the authenticator base class + constructor(label = name) { + super(label); + } + + // InjectedProviderAuth API ------------------------------------------------------ + + getProvider(): EthereumProvider | null { + return window.ethereum as unknown as EthereumProvider ?? null; + } + + // 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 BraveAuth(label); + } +} diff --git a/src/antelope/wallets/authenticators/EVMAuthenticator.ts b/src/antelope/wallets/authenticators/EVMAuthenticator.ts new file mode 100644 index 000000000..bf8dae816 --- /dev/null +++ b/src/antelope/wallets/authenticators/EVMAuthenticator.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +// EVMAuthenticator class + +import { SendTransactionResult, WriteContractResult } from '@wagmi/core'; +import { BigNumber, ethers } from 'ethers'; +import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; +import { CURRENT_CONTEXT, getAntelope, useAccountStore } from 'src/antelope/mocks'; +import { TeloscanEVMChainSettings } from 'src/antelope/mocks'; +import { useChainStore } from 'src/antelope/mocks'; +import { useEVMStore } from 'src/antelope/mocks'; +import { isTracingAll, useFeedbackStore } from 'src/antelope/mocks/FeedbackStore'; +import { usePlatformStore } from 'src/antelope/mocks'; +import { AntelopeError, EvmABI, EvmFunctionParam, EvmTransactionResponse, ExceptionError, TokenClass, addressString } from 'src/antelope/types'; + +export abstract class EVMAuthenticator { + + readonly label: string; + readonly trace: (message: string, ...args: unknown[]) => void; + + constructor(label: string) { + this.label = label; + const name = `${this.getName()}(${label})`; + this.trace = createTraceFunction(name); + useFeedbackStore().setDebug(name, isTracingAll()); + } + abstract getName(): string; + abstract logout(): Promise; + abstract getSystemTokenBalance(address: addressString | string): Promise; + abstract getERC20TokenBalance(address: addressString | string, tokenAddress: addressString | string): Promise; + abstract signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise; + abstract transferTokens(token: TokenClass, amount: BigNumber, to: addressString | string): Promise; + abstract prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string): Promise; + abstract wrapSystemToken(amount: BigNumber): Promise; + abstract unwrapSystemToken(amount: BigNumber): Promise; + abstract stakeSystemTokens(amount: BigNumber): Promise; + abstract unstakeSystemTokens(amount: BigNumber): Promise; + abstract withdrawUnstakedTokens(): Promise; + abstract isConnectedTo(chainId: string): Promise; + abstract externalProvider(): Promise; + abstract web3Provider(): Promise; + abstract getSigner(): Promise; + + // to easily clone the authenticator + abstract newInstance(label: string): EVMAuthenticator; + + // indicates the authenticator is ready to transfer tokens + readyForTransfer(): boolean { + return true; + } + + // returns the associated account address acording to the label + getAccountAddress(): addressString { + return useAccountStore().getAccount(this.label).account as addressString; + } + + // returns the associated chain settings acording to the label + getChainSettings(): TeloscanEVMChainSettings { + return (useChainStore().getChain(this.label).settings as TeloscanEVMChainSettings); + } + + async login(network: string, trackAnalyticsEvents?: boolean): Promise { + this.trace('login', network); + this.trace('Login analytics enabled =', trackAnalyticsEvents); + + const chain = useChainStore(); + try { + chain.setChain(CURRENT_CONTEXT, network); + + const checkProvider = await this.ensureCorrectChain() as ethers.providers.Web3Provider; + + const accounts = await checkProvider.listAccounts(); + if (accounts.length > 0) { + return accounts[0] as addressString; + } else { + if (!checkProvider.provider.request) { + throw new AntelopeError('antelope.evm.error_support_provider_request'); + } + const accessGranted = await checkProvider.provider.request({ method: 'eth_requestAccounts' }); + if (accessGranted.length < 1) { + return null; + } + return accessGranted[0] as addressString; + } + } catch (error) { + if ((error as unknown as ExceptionError).code === 4001) { + throw new AntelopeError('antelope.evm.error_connect_rejected'); + } else { + console.error('Error:', error); + throw new AntelopeError('antelope.evm.error_login'); + } + } + } + + async autoLogin(network: string, account: string, trackAnalyticsEvents?: boolean): Promise { + this.trace('autoLogin', network, account); + this.trace('AutoLogin analytics enabled =', trackAnalyticsEvents); + + const chain = useChainStore(); + try { + chain.setChain(CURRENT_CONTEXT, network); + return account as addressString; + } catch (error) { + if ((error as unknown as ExceptionError).code === 4001) { + throw new AntelopeError('antelope.evm.error_connect_rejected'); + } else { + console.error('Error:', error); + throw new AntelopeError('antelope.evm.error_login'); + } + } + } + + async ensureCorrectChain(): Promise { + this.trace('ensureCorrectChain'); + if (usePlatformStore().isMobile) { + // we don't have tools to check the chain on mobile + return useEVMStore().ensureCorrectChain(this); + } else { + const showSwitchNotification = !(await this.isConnectedToCorrectChain()); + return useEVMStore().ensureCorrectChain(this).then((result) => { + if (showSwitchNotification) { + const ant = getAntelope(); + const networkName = useChainStore().getChain(this.label).settings.getDisplay(); + ant.config.notifyNeutralMessageHandler( + ant.config.localizationHandler('antelope.wallets.network_switch_success', { networkName }), + ); + } + return result; + }); + } + } + + isConnectedToCorrectChain(): Promise { + const correctChainId = useChainStore().getChain(this.label).settings.getChainId(); + return this.isConnectedTo(correctChainId); + } +} diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts new file mode 100644 index 000000000..b69c7a741 --- /dev/null +++ b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts @@ -0,0 +1,331 @@ +/* eslint-disable max-len */ + + +import { BigNumber, ethers } from 'ethers'; +import { BehaviorSubject } from 'rxjs'; +import { map, filter } from 'rxjs/operators'; +import { useEVMStore, useFeedbackStore } from 'src/antelope'; +import { + AntelopeError, + EthereumProvider, + EvmABI, + EvmFunctionParam, + EvmTransactionResponse, + TokenClass, + addressString, + erc20Abi, + escrowAbiWithdraw, + stlosAbiDeposit, + stlosAbiWithdraw, + wtlosAbiDeposit, + wtlosAbiWithdraw, +} from 'src/antelope/types'; +import { BraveAuthName, EVMAuthenticator, MetamaskAuthName, SafePalAuthName } from 'src/antelope/wallets'; +import { TELOS_ANALYTICS_EVENT_NAMES, TELOS_NETWORK_NAMES } from 'src/antelope/mocks/chain-constants'; + +export abstract class InjectedProviderAuth extends EVMAuthenticator { + onReady = new BehaviorSubject(false); + + // this is just a dummy label to identify the authenticator base class + constructor(label: string) { + super(label); + useEVMStore().initInjectedProvider(this); + } + abstract getProvider(): EthereumProvider | null; + + async getSigner(): Promise { + const web3Provider = await this.web3Provider(); + return web3Provider.getSigner(); + } + + async ensureInitializedProvider(): Promise { + return new Promise((resolve, reject) => { + this.onReady.asObservable().pipe( + filter(ready => ready), + map(() => this.getProvider()), + ).subscribe((provider) => { + if (provider) { + resolve(provider); + } else { + reject(new AntelopeError('antelope.evm.error_no_provider')); + } + }); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private handleCatchError(error: any): AntelopeError { + if ('ACTION_REJECTED' === ((error as {code:string}).code)) { + return new AntelopeError('antelope.evm.error_transaction_canceled'); + } else { + // unknown error we print on console + console.error(error); + return new AntelopeError('antelope.evm.error_send_transaction', { error }); + } + } + + // this action is used by MetamaskAuth.transferTokens() + async sendSystemToken(to: string, value: ethers.BigNumber): Promise { + this.trace('sendSystemToken', to, value); + + // Send the transaction + return (await this.getSigner()).sendTransaction({ + to, + value, + }).then( + (transaction: ethers.providers.TransactionResponse) => transaction, + ).catch((error) => { + throw this.handleCatchError(error); + }); + } + + // EVMAuthenticator API ---------------------------------------------------------- + + async signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise { + this.trace('signCustomTransaction', contract, [abi], parameters, value?.toString()); + + const method = abi[0].name; + if (abi.length > 1) { + console.warn( + `signCustomTransaction: abi contains more than one function, + we assume the first one (${method}) is the one to be called`, + ); + } + + const signer = await this.getSigner(); + const contractInstance = new ethers.Contract(contract, abi, signer); + const transaction = await contractInstance[method](...parameters, { value }); + return transaction; + } + + async wrapSystemToken(amount: BigNumber): Promise { + this.trace('wrapSystemToken', amount.toString()); + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiDeposit, + [], + amount, + ).catch((error) => { + throw this.handleCatchError(error); + }); + } + + async unwrapSystemToken(amount: BigNumber): Promise { + this.trace('unwrapSystemToken', amount.toString()); + + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + const value = amount.toHexString(); + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiWithdraw, + [value], + ).catch((error) => { + throw this.handleCatchError(error); + }); + } + + async login(network: string, trackAnalyticsEvents?: boolean): Promise { + const chainSettings = this.getChainSettings(); + const authName = this.getName(); + const isTelos = TELOS_NETWORK_NAMES.includes(network); + + this.trace('login', network); + useFeedbackStore().setLoading(`${this.getName()}.login`); + + if (isTelos && trackAnalyticsEvents) { + this.trace('login', 'trackAnalyticsEvent -> login started'); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); + } + + const response = await super.login(network, trackAnalyticsEvents).then((res) => { + if (isTelos && trackAnalyticsEvents && TELOS_NETWORK_NAMES.includes(network)) { + let successfulLoginEventName = ''; + + if (authName === MetamaskAuthName) { + successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulMetamask; + } else if (authName === SafePalAuthName) { + successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulSafepal; + } else if (authName === BraveAuthName) { + successfulLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulBrave; + } + + if (successfulLoginEventName) { + this.trace('login', 'trackAnalyticsEvent -> login succeeded', authName, successfulLoginEventName); + chainSettings.trackAnalyticsEvent(successfulLoginEventName); + } + + this.trace('login', 'trackAnalyticsEvent -> generic login succeeded', TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); + } + + return res; + }).catch((error) => { + // if the user rejects the connection, we don't want to track it as an error + if ( + trackAnalyticsEvents && + isTelos && + error.message !== 'antelope.evm.error_connect_rejected' + ) { + let failedLoginEventName = ''; + + if (authName === MetamaskAuthName) { + failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedMetamask; + } else if (authName === SafePalAuthName) { + failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedSafepal; + } else if (authName === BraveAuthName) { + failedLoginEventName = TELOS_ANALYTICS_EVENT_NAMES.loginFailedBrave; + } + + if (failedLoginEventName) { + this.trace('login', 'trackAnalyticsEvent -> login failed', authName, failedLoginEventName); + chainSettings.trackAnalyticsEvent(failedLoginEventName); + } + } + }).finally(() => { + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + }); + + return response ?? null; + } + + 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]); + try { + 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'); + } + } catch (e) { + console.error('getERC20TokenBalance', e, address, token); + throw e; + } + } + + async transferTokens(token: TokenClass, amount: ethers.BigNumber, to: addressString): Promise { + this.trace('transferTokens', token, amount, to); + if (token.isSystem) { + return this.sendSystemToken(to, amount); + } else { + const value = amount.toHexString(); + const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); + return this.signCustomTransaction( + token.address, + transferAbi, + [to, value], + ); + } + } + + prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { + this.trace('prepareTokenForTransfer', [token], amount, to); + return new Promise((resolve) => { + resolve(); + }); + } + + /** + * This method creates a Transaction to stake system tokens + * @param amount amount of system tokens to stake + * @returns transaction response with the hash and a wait() method to wait confirmation + */ + async stakeSystemTokens(amount: BigNumber): Promise { + this.trace('stakeSystemTokens', amount.toString()); + + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiDeposit, + [], + amount, + ).catch((error) => { + throw this.handleCatchError(error); + }); + } + + /** + * This method creates a Transaction to unstake system tokens + * @param amount amount of system tokens to unstake + * @returns transaction response with the hash and a wait() method to wait confirmation + */ + async unstakeSystemTokens(amount: BigNumber): Promise { + this.trace('unstakeSystemTokens', amount.toString()); + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + const value = amount.toHexString(); + const from = this.getAccountAddress(); + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiWithdraw, + [value, from, from], + ); + } + + /** + * This method creates a Transaction to withdraw all unblocked staked tokens + */ + async withdrawUnstakedTokens() : Promise { + this.trace('withdrawUnstakedTokens'); + + // prepare variables + const chainSettings = this.getChainSettings(); + const escrowContractAddress = chainSettings.getEscrowContractAddress(); + + return this.signCustomTransaction( + escrowContractAddress, + escrowAbiWithdraw, + [], + ); + } + + async isConnectedTo(chainId: string): Promise { + this.trace('isConnectedTo', chainId); + return useEVMStore().isProviderOnTheCorrectChain(await this.web3Provider(), chainId); + } + + async externalProvider(): Promise { + return this.ensureInitializedProvider(); + } + + async web3Provider(): Promise { + this.trace('web3Provider'); + const web3Provider = new ethers.providers.Web3Provider(await this.externalProvider()); + await web3Provider.ready; + return web3Provider; + } + + async ensureCorrectChain(): Promise { + this.trace('ensureCorrectChain'); + return super.ensureCorrectChain(); + } + +} diff --git a/src/antelope/wallets/authenticators/MetamaskAuth.ts b/src/antelope/wallets/authenticators/MetamaskAuth.ts new file mode 100644 index 000000000..05cc06a22 --- /dev/null +++ b/src/antelope/wallets/authenticators/MetamaskAuth.ts @@ -0,0 +1,32 @@ + +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 + constructor(label = name) { + super(label); + } + + // InjectedProviderAuth API ------------------------------------------------------ + + getProvider(): EthereumProvider | null { + return window.ethereum as unknown as EthereumProvider ?? null; + } + + // 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 MetamaskAuth(label); + } + +} diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts new file mode 100644 index 000000000..dca668bc9 --- /dev/null +++ b/src/antelope/wallets/authenticators/OreIdAuth.ts @@ -0,0 +1,426 @@ +/* eslint-disable max-len */ +import { AuthProvider, ChainNetwork, OreId, OreIdOptions, JSONObject, UserChainAccount } from 'oreid-js'; +import { BigNumber, ethers } from 'ethers'; +import { WebPopup } from 'oreid-webpopup'; +import { + EvmABI, + EvmFunctionParam, + erc20Abi, + escrowAbiWithdraw, + stlosAbiDeposit, + stlosAbiWithdraw, + wtlosAbiDeposit, + wtlosAbiWithdraw, +} from 'src/antelope/types'; +import { EVMAuthenticator } from 'src/antelope/wallets'; +import { + AntelopeError, + TokenClass, + addressString, + EvmTransactionResponse, +} from 'src/antelope/types'; +import { useFeedbackStore } from 'src/antelope'; +import { useChainStore } from 'src/antelope'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { TELOS_ANALYTICS_EVENT_NAMES } from 'src/antelope/mocks/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); + } + + // returns the associated account address acording to the label + getAccountAddress(): addressString { + return this.userChainAccount?.chainAccount as addressString; + } + + 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(); + } + localStorage.removeItem('autoLogin'); + localStorage.removeItem('rawAddress'); + return Promise.resolve(); + } + + async getSystemTokenBalance(address: addressString | string): Promise { + this.trace('getSystemTokenBalance', address); + try { + const provider = await this.web3Provider(); + if (provider) { + return provider.getBalance(address); + } else { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } catch (e) { + console.error('getSystemTokenBalance', e, address); + throw e; + } + } + + async getERC20TokenBalance(address: addressString, token: addressString): Promise { + this.trace('getERC20TokenBalance', [address, token]); + try { + 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'); + } + } catch (e) { + console.error('getERC20TokenBalance', e, address, token); + throw e; + } + } + + async prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { + this.trace('prepareTokenForTransfer', [token], amount, to); + } + + /** + * 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 { + + 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 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 (abi.length > 1) { + console.warn( + `signCustomTransaction: abi contains more than one function, + we assume 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); + } + + async wrapSystemToken(amount: BigNumber): Promise { + this.trace('wrapSystemToken', amount); + this.checkIntegrity(); + + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiDeposit, + [], + amount, + ); + } + + async unwrapSystemToken(amount: BigNumber): Promise { + this.trace('unwrapSystemToken', amount.toString()); + this.checkIntegrity(); + + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + const value = amount.toHexString(); + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiWithdraw, + [value], + ); + } + + async stakeSystemTokens(amount: BigNumber): Promise { + this.trace('stakeSystemTokens', amount.toString()); + this.checkIntegrity(); + + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiDeposit, + [], + amount, + ); + } + + async unstakeSystemTokens(amount: BigNumber): Promise { + this.trace('unstakeSystemTokens', amount.toString()); + this.checkIntegrity(); + + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + const value = amount.toHexString(); + const from = this.getAccountAddress(); + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiWithdraw, + [value, from, from], + ); + } + + async withdrawUnstakedTokens() : Promise { + this.trace('withdrawUnstakedTokens'); + this.checkIntegrity(); + + // prepare variables + const chainSettings = this.getChainSettings(); + const escrowContractAddress = chainSettings.getEscrowContractAddress(); + + return this.signCustomTransaction( + escrowContractAddress, + escrowAbiWithdraw, + [], + ); + } + + async transferTokens(token: TokenClass, amount: ethers.BigNumber, to: addressString): Promise { + this.trace('transferTokens', token, amount, to); + this.checkIntegrity(); + + // prepare variables + const from = this.getAccountAddress(); + const value = amount.toHexString(); + const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); + + if (token.isSystem) { + return this.performOreIdTransaction(from, { + from, + to, + value, + }); + } else { + return this.signCustomTransaction( + token.address, + transferAbi, + [to, value], + ); + } + } + + async isConnectedTo(chainId: string): Promise { + this.trace('isConnectedTo', chainId); + return true; + } + + 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; + } + } + + async externalProvider(): Promise { + this.trace('externalProvider'); + return new Promise((resolve) => { + resolve(null as unknown as ethers.providers.ExternalProvider); + }); + } + + async getSigner(): Promise { + this.trace('getSigner'); + const provider = await this.web3Provider(); + return provider.getSigner(); + } + +} diff --git a/src/antelope/wallets/authenticators/SafePalAuth.ts b/src/antelope/wallets/authenticators/SafePalAuth.ts new file mode 100644 index 000000000..4574c4367 --- /dev/null +++ b/src/antelope/wallets/authenticators/SafePalAuth.ts @@ -0,0 +1,30 @@ +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 + constructor(label = name) { + super(label); + } + + // InjectedProviderAuth API ------------------------------------------------------ + + getProvider(): EthereumProvider | null { + return (window as unknown as {safepalProvider:unknown}).safepalProvider as EthereumProvider ?? null; + } + + // 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 SafePalAuth(label); + } +} diff --git a/src/antelope/wallets/authenticators/WalletConnectAuth.ts b/src/antelope/wallets/authenticators/WalletConnectAuth.ts new file mode 100644 index 000000000..f0d9a4737 --- /dev/null +++ b/src/antelope/wallets/authenticators/WalletConnectAuth.ts @@ -0,0 +1,512 @@ +/* eslint-disable no-async-promise-executor */ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ +/* eslint-disable max-len */ +import { + PrepareSendTransactionResult, + PrepareWriteContractResult, + SendTransactionResult, + sendTransaction, + disconnect, + InjectedConnector, + fetchBalance, + getAccount, + prepareSendTransaction, + prepareWriteContract, + writeContract, + WriteContractResult, +} from '@wagmi/core'; +import { + EthereumClient, +} from '@web3modal/ethereum'; +import { Web3Modal, Web3ModalConfig } from '@web3modal/html'; +import { BigNumber, ethers } from 'ethers'; +import { useChainStore } from 'src/antelope'; +import { useContractStore } from 'src/antelope'; +import { useFeedbackStore } from 'src/antelope'; +import { usePlatformStore } from 'src/antelope'; +import { + AntelopeError, + EvmABI, + EvmFunctionParam, + TokenClass, + addressString, + erc20Abi, + escrowAbiWithdraw, + stlosAbiDeposit, + stlosAbiWithdraw, + wtlosAbiDeposit, + wtlosAbiWithdraw, +} from 'src/antelope/types'; +import { EVMAuthenticator } from 'src/antelope/wallets'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { toRaw } from 'vue'; +import { TELOS_ANALYTICS_EVENT_NAMES, TELOS_NETWORK_NAMES } from 'src/antelope/mocks/chain-constants'; + +const name = 'WalletConnect'; + +export class WalletConnectAuth extends EVMAuthenticator { + // debounce methods do not allow for async functions to be awaited; they return a promise which resolves immediately + // thus, we need to implement out own debounce so that we can await the async function (in this case, _prepareTokenForTransfer) + private _debounceTimer = setTimeout(() => {}, 0); + private _debouncedPrepareTokenConfigResolver: ((value: unknown) => void) | null; + private web3Modal: Web3Modal; + private unsubscribeWeb3Modal: null | (() => void) = null; + private usingQR = false; + + options: Web3ModalConfig; + wagmiClient: EthereumClient; + // this is just a dummy label to identify the authenticator base class + constructor(options: Web3ModalConfig, wagmiClient: EthereumClient, label = name) { + super(label); + this.options = options; + this.wagmiClient = wagmiClient; + this._debouncedPrepareTokenConfigResolver = null; + this.web3Modal = new Web3Modal(this.options, this.wagmiClient); + } + + // 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 WalletConnectAuth(this.options, this.wagmiClient, label); + } + + async walletConnectLogin(network: string, trackAnalyticsEvents: boolean): Promise { + this.trace('walletConnectLogin'); + const chainSettings = this.getChainSettings(); + const isOnTelos = TELOS_NETWORK_NAMES.includes(chainSettings.getNetwork()); + + try { + this.clearAuthenticator(); + const address = getAccount().address as addressString; + + // We are successfully logged in. Let's find out if we are using QR + this.usingQR = false; + const injected = new InjectedConnector(); + const provider = toRaw(await injected.getProvider()); + if (typeof provider === 'undefined') { + this.usingQR = true; + } else { + const providerAddress = (provider._state?.accounts) ? provider._state?.accounts[0] : ''; + const sameAddress = providerAddress.toLocaleLowerCase() === address.toLocaleLowerCase(); + this.usingQR = !sameAddress; + this.trace('walletConnectLogin', 'providerAddress:', providerAddress, 'address:', address, 'sameAddress:', sameAddress); + } + this.trace('walletConnectLogin', 'using QR:', this.usingQR); + + // We are already logged in. Now let's try to force the wallet to connect to the correct network + try { + if (!usePlatformStore().isMobile) { + await super.login(network); + } + } catch (e) { + // we are already logged in. So we just ignore the error + console.error(e); + } + + if (isOnTelos && trackAnalyticsEvents) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login successful', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulWalletConnect, + ); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessfulWalletConnect); + this.trace( + 'login', + 'trackAnalyticsEvent -> generic login successful', + TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful, + ); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginSuccessful); + } + + return address; + } catch (e) { + // This is a non-expected error + console.error(e); + if (isOnTelos && trackAnalyticsEvents) { + this.trace( + 'walletConnectLogin', + 'trackAnalyticsEvent -> login failed', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect, + ); + const chainSettings = this.getChainSettings(); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect); + } + throw new AntelopeError('antelope.evm.error_login'); + } finally { + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + } + } + + async login(network: string, trackAnalyticsEvents: boolean): Promise { + this.trace('login', network); + const wagmiConnected = () => localStorage.getItem('wagmi.connected'); + const chainSettings = this.getChainSettings(); + const isOnTelos = TELOS_NETWORK_NAMES.includes(chainSettings.getNetwork()); + + useFeedbackStore().setLoading(`${this.getName()}.login`); + if (wagmiConnected()) { + // We are in auto-login process. So log loginStarted before calling the walletConnectLogin method + if (isOnTelos && trackAnalyticsEvents) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login started', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_NAMES.loginStarted, + ); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); + } + return this.walletConnectLogin(network, trackAnalyticsEvents); + } else { + return new Promise((resolve) => { + this.trace('login', 'web3Modal.openModal()'); + + this.unsubscribeWeb3Modal = this.web3Modal.subscribeModal(async (newState: {open:boolean}) => { + this.trace('login', 'web3Modal.subscribeModal ', toRaw(newState), wagmiConnected); + + if (isOnTelos && newState.open === true && trackAnalyticsEvents) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login started', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_NAMES.loginStarted, + ); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginStarted); + } + + if (newState.open === false) { + useFeedbackStore().unsetLoading(`${this.getName()}.login`); + + if (isOnTelos && !wagmiConnected() && trackAnalyticsEvents) { + this.trace( + 'login', + 'trackAnalyticsEvent -> login failed', + 'WalletConnect', + TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect, + ); + chainSettings.trackAnalyticsEvent(TELOS_ANALYTICS_EVENT_NAMES.loginFailedWalletConnect); + } + + // this prevents multiple subscribers from being attached to the web3Modal + // without this, every time the user logs out and back in again, this subscribeModal handler + // runs one more time than the last time + if (this.unsubscribeWeb3Modal) { + this.unsubscribeWeb3Modal(); + } + } + + if (wagmiConnected()) { + resolve(this.walletConnectLogin(network, true)); + } + }); + this.web3Modal.openModal(); + }); + } + } + + // having this two properties attached to the authenticator instance may bring some problems + // so after we use them we need to clear them to avoid that problems + clearAuthenticator(): void { + this.trace('clearAuthenticator'); + this.usingQR = false; + this.options = null as unknown as Web3ModalConfig; + this.wagmiClient = null as unknown as EthereumClient; + } + + async logout(): Promise { + this.trace('logout'); + if (localStorage.getItem('wagmi.connected')){ + await disconnect(); + } + } + + async getSystemTokenBalance(address: addressString): Promise { + this.trace('getSystemTokenBalance', address); + const chainId = +useChainStore().getChain(this.label).settings.getChainId(); + const balanceBn = await fetchBalance({ address, chainId }); + return BigNumber.from(balanceBn.value); + } + + async getERC20TokenBalance(address: addressString, token: addressString): Promise { + this.trace('getERC20TokenBalance', [address, token]); + const chainId = +useChainStore().getChain(this.label).settings.getChainId(); + const balance = await fetchBalance({ address, chainId, token }).then(balanceBn => balanceBn.value); + return BigNumber.from(balance); + } + + async signCustomTransaction(contract: string, abi: EvmABI, parameters: EvmFunctionParam[], value?: BigNumber): Promise { + this.trace('signCustomTransaction', contract, [abi], parameters, value?.toString()); + + const method = abi[0].name; + if (abi.length > 1) { + console.warn( + `signCustomTransaction: abi contains more than one function, + we assume the first one (${method}) is the one to be called`, + ); + } + + const chainSettings = this.getChainSettings(); + + const config = { + chainId: +chainSettings.getChainId(), + address: contract, + abi: abi, + functionName: method, + args: parameters, + } as { + chainId: number; + address: addressString; + abi: EvmABI; + functionName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; + value?: bigint; + }; + + if (value) { + config.value = BigInt(value.toString()); + } + + this.trace('signCustomTransaction', 'prepareWriteContract ->', config); + const sendConfig = await prepareWriteContract(config); + + this.trace('signCustomTransaction', 'writeContract ->', sendConfig); + return await writeContract(sendConfig); + } + + + async transferTokens(token: TokenClass, amount: BigNumber, to: addressString): Promise { + this.trace('transferTokens', token, amount, to); + if (!this.sendConfig) { + throw new AntelopeError(token.isSystem ? + 'antelope.wallets.error_system_token_transfer_config' : + 'antelope.wallets.error_token_transfer_config', + ); + } else { + if (token.isSystem) { + return await sendTransaction(this.sendConfig as PrepareSendTransactionResult); + } else { + // prepare variables + const value = amount.toHexString(); + const transferAbi = erc20Abi.filter(abi => abi.name === 'transfer'); + + return this.signCustomTransaction( + token.address, + transferAbi, + [to, value], + ); + } + } + } + + readyForTransfer(): boolean { + return !!this.sendConfig; + } + + sendConfig: PrepareSendTransactionResult | PrepareWriteContractResult | null = null; + private _debouncedPrepareTokenConfig(token: TokenClass | null, amount: BigNumber, to: string) { + // If there is already a pending call, clear it + if (this._debouncedPrepareTokenConfigResolver) { + clearTimeout(this._debounceTimer); + this._debouncedPrepareTokenConfigResolver(null); // Resolve with null when debounced + } + + // Create a new promise for this call + const promise = new Promise((resolve) => { + this._debouncedPrepareTokenConfigResolver = resolve; + }); + + // Set a timer to call the function after the delay + this._debounceTimer = setTimeout(async () => { + clearTimeout(this._debounceTimer); + const result = await this._prepareTokenForTransfer(token, amount, to); // Call the function + + if (this._debouncedPrepareTokenConfigResolver) { + this._debouncedPrepareTokenConfigResolver(result); // Resolve the promise with the result + } + }, 500); + + // Return the promise + return promise; + } + async _prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string) { + this.trace('prepareTokenForTransfer', [token], amount, to); + if (token) { + if (token.isSystem) { + this.sendConfig = await prepareSendTransaction({ + to, + value: BigInt(amount.toString()), + chainId: +useChainStore().getChain(this.label).settings.getChainId(), + }); + } else { + const abi = useContractStore().getTokenABI(token.type); + const functionName = 'transfer'; + this.sendConfig = await prepareWriteContract({ + chainId: +useChainStore().getChain(this.label).settings.getChainId(), + address: token.address as addressString, + abi, + functionName, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: [to, amount] as any[], + }); + } + } else { + this.sendConfig = null; + } + } + + async prepareTokenForTransfer(token: TokenClass | null, amount: BigNumber, to: string): Promise { + this.sendConfig = null; + await this._debouncedPrepareTokenConfig(token, amount, to); + } + + async wrapSystemToken(amount: BigNumber): Promise { + this.trace('wrapSystemToken', amount); + + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiDeposit, + [], + amount, + ); + } + + async unwrapSystemToken(amount: BigNumber): Promise { + this.trace('unwrapSystemToken', amount); + + // prepare variables + const chainSettings = this.getChainSettings(); + const wrappedSystemTokenContractAddress = chainSettings.getWrappedSystemToken().address as addressString; + + return this.signCustomTransaction( + wrappedSystemTokenContractAddress, + wtlosAbiWithdraw, + [amount.toString()], + ); + } + + async stakeSystemTokens(amount: BigNumber): Promise { + this.trace('stakeSystemTokens', amount); + + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiDeposit, + [], + amount, + ); + } + + async unstakeSystemTokens(amount: BigNumber): Promise { + this.trace('unstakeSystemTokens', amount); + + // prepare variables + const chainSettings = this.getChainSettings(); + const stakedSystemTokenContractAddress = chainSettings.getStakedSystemToken().address as addressString; + const address = this.getAccountAddress(); + + return this.signCustomTransaction( + stakedSystemTokenContractAddress, + stlosAbiWithdraw, + [amount.toString(), address, address], + ); + } + + async withdrawUnstakedTokens(): Promise { + this.trace('withdrawUnstakedTokens'); + + // prepare variables + const chainSettings = this.getChainSettings(); + const escrowContractAddress = chainSettings.getEscrowContractAddress(); + + return this.signCustomTransaction( + escrowContractAddress, + escrowAbiWithdraw, + [], + ); + } + + 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; + this.trace('isConnectedTo', chainId, correct ? 'OK!' : 'not connected'); + resolve(correct); + }); + } + + async web3Provider(): Promise { + let web3Provider = null; + if (usePlatformStore().isMobile || this.usingQR) { + const p:RpcEndpoint = this.getChainSettings().getRPCEndpoint(); + const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + web3Provider = new ethers.providers.JsonRpcProvider(url); + this.trace('web3Provider', 'JsonRpcProvider ->', web3Provider); + + // This is a hack to make the QR code work. + // this code is going to be used in EVMAuthenticator.ts login method + const listAccounts: () => Promise<`0x${string}`[]> = async () => [getAccount().address as addressString]; + web3Provider.listAccounts = listAccounts; + + } else { + web3Provider = new ethers.providers.Web3Provider(await this.externalProvider()); + this.trace('web3Provider', 'Web3Provider ->', web3Provider); + } + await web3Provider.ready; + return web3Provider as ethers.providers.Web3Provider; + } + + async getSigner(): Promise { + this.trace('getSigner'); + const web3Provider = await this.web3Provider(); + const signer = web3Provider.getSigner(); + this.trace('getSigner', 'signer ->', signer); + return signer; + } + + async externalProvider(): Promise { + this.trace('externalProvider'); + return new Promise(async (resolve) => { + const injected = new InjectedConnector(); + const provider = toRaw(await injected.getProvider()); + if (!provider) { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + resolve(provider as unknown as ethers.providers.ExternalProvider); + }); + } + + async ensureCorrectChain(): Promise { + this.trace('ensureCorrectChain', 'QR:', this.usingQR); + if (this.usingQR) { + // we don't have tools to check the chain when using QR + return this.web3Provider(); + } else { + return super.ensureCorrectChain(); + } + } + +} diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts new file mode 100644 index 000000000..07b7d65ca --- /dev/null +++ b/src/antelope/wallets/index.ts @@ -0,0 +1,9 @@ + +export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; +export * from 'src/antelope/wallets/authenticators/OreIdAuth'; +export * from 'src/antelope/wallets/authenticators/InjectedProviderAuth'; +export * from 'src/antelope/wallets/authenticators/MetamaskAuth'; +export * from 'src/antelope/wallets/authenticators/SafePalAuth'; +export * from 'src/antelope/wallets/authenticators/WalletConnectAuth'; +export * from 'src/antelope/wallets/authenticators/BraveAuth'; +export * from 'src/antelope/mocks'; diff --git a/src/antelope/wallets/init.ts b/src/antelope/wallets/init.ts new file mode 100644 index 000000000..632611e42 --- /dev/null +++ b/src/antelope/wallets/init.ts @@ -0,0 +1,90 @@ +// register wallets ---------------------------------------------------------------- + +import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'; +import { Web3ModalConfig } from '@web3modal/html'; +import { OreIdOptions } from 'oreid-js'; +import { MetamaskAuth, OreIdAuth, SafePalAuth, WalletConnectAuth, BraveAuth } from 'src/antelope/wallets'; +import { configureChains, createConfig } from '@wagmi/core'; +import { telos, telosTestnet } from '@wagmi/core/chains'; +import { getAntelope } from 'src/antelope/mocks/AntelopeConfig'; +import { App } from 'vue'; +import { AntelopeError } from 'src/antelope/types'; + +/** + * This function is used to register the EVMAuthenticators that will be used by the app. + */ +export function initAntelope(app: App) { + const oreIdOptions: OreIdOptions = { + appName: process.env.APP_NAME, + appId: process.env.OREID_APP_ID as string, + }; + + const projectId = process.env.PROJECT_ID || '14ec76c44bae7d461fa0f5fd5f8a9da1'; + const chains = [telos, telosTestnet]; + + const { publicClient } = configureChains(chains, [w3mProvider({ projectId })]); + + // Wagmi Client -- + const wagmiConfig = createConfig({ + autoConnect: true, + connectors: w3mConnectors({ projectId, chains }), + publicClient, + }); + + const wagmiClient = new EthereumClient(wagmiConfig, chains); + + // Wagmi Options -- + const explorerRecommendedWalletIds = [ + // MetaMask + 'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', + // SafePal + // '0b415a746fb9ee99cce155c2ceca0c6f6061b1dbca2d722b3ba16381d0562150', + ]; + const explorerExcludedWalletIds = 'ALL' as const; // Web3Modal option excludes all but recomended + const wagmiOptions: Web3ModalConfig = { projectId, explorerRecommendedWalletIds, explorerExcludedWalletIds }; + + const ant = getAntelope(); + ant.config.init(app); + + // settting notification handlers -- + ant.config.setNotifySuccessfulTrxHandler(app.config.globalProperties.$notifySuccessTransaction); + ant.config.setNotifySuccessMessageHandler(app.config.globalProperties.$notifySuccessMessage); + ant.config.setNotifySuccessCopyHandler(app.config.globalProperties.$notifySuccessCopy); + ant.config.setNotifyFailureMessage(app.config.globalProperties.$notifyFailure); + ant.config.setNotifyFailureWithAction(app.config.globalProperties.$notifyFailureWithAction); + ant.config.setNotifyDisconnectedHandler(app.config.globalProperties.$notifyDisconnected); + ant.config.setNotifyNeutralMessageHandler(app.config.globalProperties.$notifyNeutralMessage); + ant.config.setNotifyRememberInfoHandler(app.config.globalProperties.$notifyRememberInfo); + + + // setting authenticators getter -- + ant.config.setAuthenticatorsGetter( + () => app.config.globalProperties.$ual.getAuthenticators().availableAuthenticators); + + // setting translation handler -- + ant.config.setLocalizationHandler( + (key:string, payload?: Record) => app.config.globalProperties.$t(key, payload ? payload : {})); + + // setting transaction error handler -- + ant.config.setTransactionErrorHandler((err: object) => { + if (err instanceof AntelopeError) { + const evmErr = err as AntelopeError; + if (evmErr.message === 'antelope.evm.error_transaction_canceled') { + ant.config.notifyNeutralMessageHandler(ant.config.localizationHandler(evmErr.message)); + } else { + ant.config.notifyFailureMessage(ant.config.localizationHandler(evmErr.message), evmErr.payload); + } + } else { + ant.config.notifyFailureMessage(ant.config.localizationHandler('evm_wallet.general_error')); + } + }); + + // set evm authenticators -- + ant.wallets.addEVMAuthenticator(new WalletConnectAuth(wagmiOptions, wagmiClient)); + ant.wallets.addEVMAuthenticator(new OreIdAuth(oreIdOptions)); + ant.wallets.addEVMAuthenticator(new MetamaskAuth()); + ant.wallets.addEVMAuthenticator(new SafePalAuth()); + ant.wallets.addEVMAuthenticator(new BraveAuth()); + +} + diff --git a/src/antelope/wallets/utils/abi/erc1155.ts b/src/antelope/wallets/utils/abi/erc1155.ts new file mode 100644 index 000000000..5686fbc79 --- /dev/null +++ b/src/antelope/wallets/utils/abi/erc1155.ts @@ -0,0 +1,133 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const erc1155Abi = [{ + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], + 'name': 'ApprovalForAll', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'indexed': false, + 'internalType': 'uint256[]', + 'name': 'ids', + 'type': 'uint256[]', + }, { 'indexed': false, 'internalType': 'uint256[]', 'name': 'values', 'type': 'uint256[]' }], + 'name': 'TransferBatch', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }, { 'indexed': false, 'internalType': 'uint256', 'name': 'value', 'type': 'uint256' }], + 'name': 'TransferSingle', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': false, 'internalType': 'string', 'name': 'value', 'type': 'string' }, { + 'indexed': true, + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }], + 'name': 'URI', + 'type': 'event', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'id', + 'type': 'uint256', + }], + 'name': 'balanceOf', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address[]', 'name': 'accounts', 'type': 'address[]' }, { + 'internalType': 'uint256[]', + 'name': 'ids', + 'type': 'uint256[]', + }], + 'name': 'balanceOfBatch', + 'outputs': [{ 'internalType': 'uint256[]', 'name': '', 'type': 'uint256[]' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'account', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }], + 'name': 'isApprovedForAll', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256[]', 'name': 'ids', 'type': 'uint256[]' }, { + 'internalType': 'uint256[]', + 'name': 'amounts', + 'type': 'uint256[]', + }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], + 'name': 'safeBatchTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }, { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, { 'internalType': 'bytes', 'name': 'data', 'type': 'bytes' }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'internalType': 'bool', + 'name': 'approved', + 'type': 'bool', + }], + 'name': 'setApprovalForAll', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'id', 'type': 'uint256' }], + 'name': 'uri', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc20.ts b/src/antelope/wallets/utils/abi/erc20.ts new file mode 100644 index 000000000..3da6be29c --- /dev/null +++ b/src/antelope/wallets/utils/abi/erc20.ts @@ -0,0 +1,226 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const erc20Abi = [ + { + 'inputs': [], + 'name': 'name', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'symbol', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'decimals', + 'outputs': [ + { + 'internalType': 'uint8', + 'name': '', + 'type': 'uint8', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'account', + 'type': 'address', + }, + ], + 'name': 'balanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'recipient', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'transfer', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'sender', + 'type': 'address', + }, + { + 'internalType': 'address', + 'name': 'recipient', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'transferFrom', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256', + }, + ], + 'name': 'approve', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool', + }, + ], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'owner', + 'type': 'address', + }, + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + ], + 'name': 'allowance', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256', + }, + ], + 'stateMutability': 'view', + 'type': 'function', + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address', + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256', + }, + ], + 'name': 'Transfer', + 'type': 'event', + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address', + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'spender', + 'type': 'address', + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256', + }, + ], + 'name': 'Approval', + 'type': 'event', + }, +] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc721.ts b/src/antelope/wallets/utils/abi/erc721.ts new file mode 100644 index 000000000..40e98c55b --- /dev/null +++ b/src/antelope/wallets/utils/abi/erc721.ts @@ -0,0 +1,356 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const erc721Abi = [{ + 'inputs': [{ + 'internalType': 'string', + 'name': '_name', + 'type': 'string', + }, { 'internalType': 'string', 'name': '_symbol', 'type': 'string' }, { + 'internalType': 'uint256', + 'name': '_maxTokens', + 'type': 'uint256', + }, { 'internalType': 'address', 'name': '_linkToken', 'type': 'address' }, { + 'internalType': 'address', + 'name': '_chainlinkCoordinator', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { + 'internalType': 'bytes32', + 'name': '_chainlinkHash', + 'type': 'bytes32', + }], + 'stateMutability': 'nonpayable', + 'type': 'constructor', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'approved', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'Approval', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }, { 'indexed': false, 'internalType': 'bool', 'name': 'approved', 'type': 'bool' }], + 'name': 'ApprovalForAll', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ + 'indexed': true, + 'internalType': 'address', + 'name': 'previousOwner', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], + 'name': 'OwnershipTransferred', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'baseURI', 'type': 'string' }], + 'name': 'SetBaseURI', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ + 'indexed': false, + 'internalType': 'uint256', + 'name': 'chainlinkFee', + 'type': 'uint256', + }, { 'indexed': false, 'internalType': 'bytes32', 'name': 'chainlinkHash', 'type': 'bytes32' }], + 'name': 'SetChainlinkConfig', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'string', 'name': 'defaultURI', 'type': 'string' }], + 'name': 'SetDefaultURI', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'minter', 'type': 'address' }], + 'name': 'SetMinter', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': false, 'internalType': 'uint256', 'name': 'seed', 'type': 'uint256' }, { + 'indexed': false, + 'internalType': 'bytes32', + 'name': 'requestId', + 'type': 'bytes32', + }], + 'name': 'SetRandomSeed', + 'type': 'event', +}, { + 'anonymous': false, + 'inputs': [{ 'indexed': true, 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'indexed': true, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'Transfer', + 'type': 'event', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'to', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'tokenId', + 'type': 'uint256', + }], + 'name': 'approve', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }], + 'name': 'balanceOf', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'baseURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'chainlinkFee', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'chainlinkHash', + 'outputs': [{ 'internalType': 'bytes32', 'name': '', 'type': 'bytes32' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'defaultURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'finalBaseURI', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'getApproved', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'operator', + 'type': 'address', + }], + 'name': 'isApprovedForAll', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'maxTokens', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256' }], + 'name': 'metadataOf', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': '_to', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': '_count', + 'type': 'uint256', + }], + 'name': 'mintMultiple', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'minter', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'name', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'owner', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'ownerOf', + 'outputs': [{ 'internalType': 'address', 'name': '', 'type': 'address' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes32', 'name': 'requestId', 'type': 'bytes32' }, { + 'internalType': 'uint256', + 'name': 'randomness', + 'type': 'uint256', + }], + 'name': 'rawFulfillRandomness', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'renounceOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }, { + 'internalType': 'bytes', + 'name': '_data', + 'type': 'bytes', + }], + 'name': 'safeTransferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'seed', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'seedReveal', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, { + 'internalType': 'bool', + 'name': 'approved', + 'type': 'bool', + }], + 'name': 'setApprovalForAll', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'string', 'name': 'baseURI_', 'type': 'string' }, { + 'internalType': 'bool', + 'name': 'finalBaseUri_', + 'type': 'bool', + }], + 'name': 'setBaseURI', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': '_chainlinkFee', 'type': 'uint256' }, { + 'internalType': 'bytes32', + 'name': '_chainlinkHash', + 'type': 'bytes32', + }], + 'name': 'setChainlinkConfig', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'string', 'name': '_defaultURI', 'type': 'string' }], + 'name': 'setDefaultURI', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': '_minter', 'type': 'address' }], + 'name': 'setMinter', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'symbol', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'index', 'type': 'uint256' }], + 'name': 'tokenByIndex', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'owner', 'type': 'address' }, { + 'internalType': 'uint256', + 'name': 'index', + 'type': 'uint256', + }], + 'name': 'tokenOfOwnerByIndex', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'tokenURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [{ 'internalType': 'uint256', 'name': '', 'type': 'uint256' }], + 'stateMutability': 'view', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'from', 'type': 'address' }, { + 'internalType': 'address', + 'name': 'to', + 'type': 'address', + }, { 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'transferFrom', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}, { + 'inputs': [{ 'internalType': 'address', 'name': 'newOwner', 'type': 'address' }], + 'name': 'transferOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/erc721Metadata.ts b/src/antelope/wallets/utils/abi/erc721Metadata.ts new file mode 100644 index 000000000..277a28bf4 --- /dev/null +++ b/src/antelope/wallets/utils/abi/erc721Metadata.ts @@ -0,0 +1,9 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const erc721MetadataAbi = [{ + 'inputs': [{ 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256' }], + 'name': 'tokenURI', + 'outputs': [{ 'internalType': 'string', 'name': '', 'type': 'string' }], + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/escrowAbi.ts b/src/antelope/wallets/utils/abi/escrowAbi.ts new file mode 100644 index 000000000..56b9ecda6 --- /dev/null +++ b/src/antelope/wallets/utils/abi/escrowAbi.ts @@ -0,0 +1,11 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const escrowAbiWithdraw: EvmABI = [ + { + inputs: [], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/antelope/wallets/utils/abi/index.ts b/src/antelope/wallets/utils/abi/index.ts new file mode 100644 index 000000000..4a83a7dcf --- /dev/null +++ b/src/antelope/wallets/utils/abi/index.ts @@ -0,0 +1,43 @@ +export * from 'src/antelope/wallets/utils/abi/erc721'; +export * from 'src/antelope/wallets/utils/abi/erc721Metadata'; +export * from 'src/antelope/wallets/utils/abi/erc1155'; +export * from 'src/antelope/wallets/utils/abi/erc20'; +export * from 'src/antelope/wallets/utils/abi/supportsInterface'; +export * from 'src/antelope/wallets/utils/abi/wrapAbi'; +export * from 'src/antelope/wallets/utils/abi/stlosAbi'; +export * from 'src/antelope/wallets/utils/abi/escrowAbi'; +export * from 'src/antelope/wallets/utils/abi/signature/transfer_signatures'; + +export type StateMutabilityType = 'pure' | 'view' | 'nonpayable' | 'payable'; +export type addressString = `0x${string}`; // required wagmi type + +export type EvmABI = EvmABIEntry[]; + +export interface EvmABIEntry { + constant?: boolean; + payable?: boolean; + anonymous?: boolean; + inputs?: EvmABIEntryInput[]; + outputs?: EvmABIEntryOutput[]; + stateMutability?: StateMutabilityType; + name: string; + type: string; +} + +export interface EvmABIEntryInput { + indexed: boolean; + internalType: string; + name: string; + type: string; +} + +export interface EvmABIEntryOutput { + internalType: string; + name: string; + type: string; +} + +export interface AbiSignature { + text_signature: string; +} + diff --git a/src/antelope/wallets/utils/abi/signature/events_signatures.ts b/src/antelope/wallets/utils/abi/signature/events_signatures.ts new file mode 100644 index 000000000..9370fd475 --- /dev/null +++ b/src/antelope/wallets/utils/abi/signature/events_signatures.ts @@ -0,0 +1,26 @@ +/* eslint-disable max-len */ +export const events_signatures = { + '0xddf252ad': 'event Transfer(address indexed from, address indexed to, uint256 value)', + '0x8c5be1e5': 'event Approval(address indexed owner, address indexed spender, uint256 value)', + '0x71bab65c': 'event Harvest(address indexed sender, uint256 performanceFee, uint256 callFee)', + '0x884edad9': 'event Withdraw(address indexed user, uint256 amount)', + '0xf279e6a1': 'event Withdraw(address indexed user, uint256 indexed pid, uint256 amount)', + '0x90890809': 'event Deposit(address indexed user, uint256 indexed pid, uint256 amount)', + '0xe1fffcc4': 'event Deposit(address indexed sender, uint256 value)', + '0x4c209b5f': 'event Mint(address indexed sender, uint256 amount0, uint256 amount1)', + '0xd78ad95f': 'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)', + '0xa2c38e2d': 'event Claim(address indexed account, uint256 amount, bool indexed automatic)', + '0xee503bee': 'event DividendWithdrawn(address indexed to, uint256 weiAmount)', + '0x38567aa9': 'event NewTransmission(uint32 indexed aggregatorRoundId, int192 answer, address transmitter, uint32 observationsTimestamp)', + '0x0109fc6f': 'event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt)', + '0x0559884f': 'event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt)', + '0x1c411e9a': 'event Sync(uint112 reserve0, uint112 reserve1)', + '0x5beea7b3': 'event EvInventoryUpdate(uint256 indexed id, tuple(address,address,address,uint256,uint256,uint256,uint8,uint8) inventory)', + '0x8be0079c': 'event OwnershipTransferred(address previousOwner, address newOwner)', + '0x34fcbac0': 'event Claim(address indexed user, uint256 indexed pid, uint256 amount)', + '0xda919360': 'event BorrowAllowanceDelegated(address indexed fromUser, address indexed toUser, address asset, uint256 amount)', + '0xdccd412f': 'event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to)', + '0x4a39dc06': 'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids,uint256[] amounts)', + '0xc3d58168': 'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 amount)', +} as { [prefix: string]: string }; + diff --git a/src/antelope/wallets/utils/abi/signature/functions_signatures.ts b/src/antelope/wallets/utils/abi/signature/functions_signatures.ts new file mode 100644 index 000000000..cefc921ec --- /dev/null +++ b/src/antelope/wallets/utils/abi/signature/functions_signatures.ts @@ -0,0 +1,28 @@ +/* eslint-disable max-len */ +export const functions_overrides = { + '0xa9059cbb': 'function transfer(address to, uint amount)', + '0xaac48653': 'function mint(address account, uint256 id, uint256 amount, uint256 maximum, string tokenUri, bytes data)', + '0xf5298aca': 'function burn(address account,uint256 id,uint256 value)', + '0x18cbafe5': 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)', + '0x095ea7b3': 'function approve(address spender, uint256 amount)', + '0x7ff36ab5': 'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)', + '0xfb3bdb41': 'function swapETHForExactTokens(uint256 amountOut, address[] path, address to, uint256 deadline)', + '0x38ed1739': 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', + '0xded9382a': 'function removeLiquidityETHWithPermit(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', + '0xf305d719': 'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline)', + '0xa22cb465': 'function setApprovalForAll(address to, bool approved)', + '0x23b872dd': 'function transferFrom(address sender, address recipient, uint256 amount)', + '0xf242432a': 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', + '0xe8e33700': 'function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)', + '0x4a25d94a': 'function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline)', + '0x5c11d795': 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)', + '0x0cd5840b': 'function create(address[] offerProperties_, uint256[] offerIds_, uint256[] offerValues_, address[] demandProperties_, uint256[] demandIds_, uint256[] demandValues_)', + '0xb583cc2c': 'function setSaleStartEnd(string eventCode, uint256 start, uint256 end)', + '0xbea9849e': 'function setUniswapRouter(address _new)', + '0x860665b3': 'function openTrove(uint256 _maxFeePercentage, uint256 _LUSDAmount, address _upperHint, address _lowerHint)', + '0x2195995c': 'function removeLiquidityWithPermit(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s)', + '0x70a08231': 'function balanceOf(address)', + '0xf23a6e61': 'function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data)', + '0x3542aee2': 'function mintByOwner(address to, uint256 tokenType)', +} as { [prefix: string]: string }; + diff --git a/src/antelope/wallets/utils/abi/signature/index.ts b/src/antelope/wallets/utils/abi/signature/index.ts new file mode 100644 index 000000000..88a3fc857 --- /dev/null +++ b/src/antelope/wallets/utils/abi/signature/index.ts @@ -0,0 +1,3 @@ +export * from 'src/antelope/wallets/utils/abi/signature/events_signatures'; +export * from 'src/antelope/wallets/utils/abi/signature/functions_signatures'; +export * from 'src/antelope/wallets/utils/abi/signature/transfer_signatures'; diff --git a/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts b/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts new file mode 100644 index 000000000..ba5deb690 --- /dev/null +++ b/src/antelope/wallets/utils/abi/signature/transfer_signatures.ts @@ -0,0 +1,2 @@ +export const TRANSFER_SIGNATURES = ['0xddf252ad', '0xa9059cbb', '0xf242432a', '0xc3d58168']; +export const ERC1155_TRANSFER_SIGNATURE = '0xc3d58168'; diff --git a/src/antelope/wallets/utils/abi/stlosAbi.ts b/src/antelope/wallets/utils/abi/stlosAbi.ts new file mode 100644 index 000000000..66cdf41d6 --- /dev/null +++ b/src/antelope/wallets/utils/abi/stlosAbi.ts @@ -0,0 +1,49 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const stlosAbiDeposit: EvmABI = [ + { + constant: false, + inputs: [], + name: 'depositTLOS', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +]; + +export const stlosAbiWithdraw: EvmABI = [ + { + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'assets', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'withdraw', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/antelope/wallets/utils/abi/supportsInterface.ts b/src/antelope/wallets/utils/abi/supportsInterface.ts new file mode 100644 index 000000000..04a96ace3 --- /dev/null +++ b/src/antelope/wallets/utils/abi/supportsInterface.ts @@ -0,0 +1,11 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const supportsInterfaceAbi = [{ + 'constant': true, + 'inputs': [{ 'internalType': 'bytes4', 'name': 'interfaceId', 'type': 'bytes4' }], + 'name': 'supportsInterface', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'payable': false, + 'stateMutability': 'view', + 'type': 'function', +}] as EvmABI; diff --git a/src/antelope/wallets/utils/abi/wrapAbi.ts b/src/antelope/wallets/utils/abi/wrapAbi.ts new file mode 100644 index 000000000..4bad7a463 --- /dev/null +++ b/src/antelope/wallets/utils/abi/wrapAbi.ts @@ -0,0 +1,32 @@ +import { EvmABI } from 'src/antelope/wallets/utils/abi'; + +export const wtlosAbiDeposit: EvmABI = [ + { + constant: false, + inputs: [], + name: 'deposit', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +]; + +export const wtlosAbiWithdraw: EvmABI = [ + { + constant: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'wad', + type: 'uint256', + }, + ], + name: 'withdraw', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/antelope/wallets/utils/contracts/EvmContract.ts b/src/antelope/wallets/utils/contracts/EvmContract.ts new file mode 100644 index 000000000..385cdad81 --- /dev/null +++ b/src/antelope/wallets/utils/contracts/EvmContract.ts @@ -0,0 +1,236 @@ +/* eslint-disable max-len */ +import { ContractInterface, ethers } from 'ethers'; +import { markRaw } from 'vue'; +import { + AntelopeError, EvmContractCalldata, + EvmABI, + EvmContractCreationInfo, + EvmContractConstructorData, + EvmContractManagerI, + EvmFormatedLog, + EvmLog, + EvmLogs, + TokenSourceInfo, + TRANSFER_SIGNATURES, +} from 'src/antelope/types'; +import { Interface } from 'ethers/lib/utils'; + + +export default class EvmContract { + private readonly _name: string; + private readonly _abi?: EvmABI | null; + private readonly _address: string; + private readonly _creationInfo?: EvmContractCreationInfo | null; + private readonly _interface?: ContractInterface | null; + private readonly _supportedInterfaces: string[]; + private readonly _properties?: EvmContractCalldata; + private readonly _manager?: EvmContractManagerI; + private readonly _token?: TokenSourceInfo | null; + + private _verified?: boolean; + + constructor({ + name, + abi, + address, + creationInfo, + verified, + supportedInterfaces = ['none'], + properties, + manager, + token, + }: EvmContractConstructorData) { + this._name = name; + this._address = address; + this._creationInfo = creationInfo; + this._verified = verified ?? false; + this._properties = properties; + this._manager = manager; + + if (abi) { + this._abi = typeof abi === 'string' ? JSON.parse(abi) : abi; + this._interface = markRaw(new ethers.utils.Interface(abi)); + } + + if (token) { + this._token = token; + } + + const indexOfNone = supportedInterfaces.indexOf('none'); + this._supportedInterfaces = []; + for (let i = 0; i < supportedInterfaces.length; i++){ + if (i !== indexOfNone) { + this._supportedInterfaces.push(supportedInterfaces[i]); + } + } + } + + + get name() { + return this._name; + } + + get abi() { + return this._abi; + } + + get address() { + return this._address; + } + + get creationInfo() { + return this._creationInfo; + } + + get iface() { + return this._interface; + } + + get verified() { + return this._verified ?? false; + } + + set verified(verified: boolean) { + this._verified = verified; + } + + get supportedInterfaces() { + return this._supportedInterfaces; + } + + get creationBlock() { + return this._creationInfo?.block; + } + + get creationTrx() { + return this._creationInfo?.transaction; + } + + get creator() { + return this._creationInfo?.creator; + } + + get properties() { + return this._properties; + } + + get token() { + return this._token; + } + + isNonFungible() { + return (this._supportedInterfaces.includes('erc721')); + } + + isToken() { + if (this._supportedInterfaces.length === 0) { + return false; + } + + return ( + this._supportedInterfaces.includes('erc721') || + this._supportedInterfaces.includes('erc1155') || + this._supportedInterfaces.includes('erc20') + ); + } + + async getContractInstance() { + if (!this.abi){ + throw new AntelopeError('antelope.utils.error_contract_instance'); + } + const signer = await this._manager?.getSigner(); + + return new ethers.Contract(this.address, this.abi, signer); + } + + async parseTransaction(data:string) { + if (this.iface && this.iface instanceof Interface) { + try { + return await this.iface.parseTransaction({ data }); + } catch (e) { + console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); + } + } else { + try { + // this functionIface is an interface for a single function signature as discovered via 4bytes.directory... only use it for this function + const functionIface = await this._manager?.getFunctionIface(data); + if (functionIface) { + return functionIface.parseTransaction({ data }); + } + } catch (e) { + console.error(`Failed to parse transaction data ${data} using abi for ${this.address}`); + } + } + throw new AntelopeError('antelope.utils.error_parsing_transaction'); + } + + async parseLogs(logs: EvmLogs): Promise { + if (this.iface && this.iface instanceof Interface) { + const iface = this.iface; + const parsedArray = await Promise.all(logs.map(async (log) => { + try { + const parsedLog:ethers.utils.LogDescription = iface.parseLog(log); + return this.formatLog(log, parsedLog); + } catch (e) { + return this.parseEvent(log); + } + })); + parsedArray.forEach((parsed) => { + if(parsed.name && parsed.eventFragment?.inputs){ + parsed.inputs = parsed.eventFragment.inputs; + } + }); + return parsedArray; + } + + + return await Promise.all(logs.map(async (log) => { + const parsedLog = await this.parseEvent(log); + if(parsedLog.name && parsedLog.eventFragment?.inputs){ + parsedLog.inputs = parsedLog.eventFragment.inputs; + } + return parsedLog; + })); + } + + formatLog(log: EvmLog, parsedLog: ethers.utils.LogDescription): EvmFormatedLog { + if(!parsedLog.signature) { + console.error('No signature found for log! Check if this explodes. Returning EvmLog instead of EvmFormatedLog. '); + return log as unknown as EvmFormatedLog; + } + const function_signature = log.topics[0].substring(0, 10); + return { + ... parsedLog, + function_signature, + isTransfer: TRANSFER_SIGNATURES.includes(function_signature), + logIndex: log.logIndex, + address: log.address, + token: this._token, + name: parsedLog.signature, + } as EvmFormatedLog; + } + + async parseEvent(log: EvmLog): Promise { + const eventIface = await this._manager?.getEventIface(log.topics[0]); + if (eventIface) { + try { + const parsedLog:ethers.utils.LogDescription = eventIface.parseLog(log); + return this.formatLog(log, parsedLog); + } catch(e) { + throw new AntelopeError('antelope.utils.error_parsing_log_event', log); + } + } else { + throw new AntelopeError('antelope.utils.error_parsing_log_event', log); + } + } +} + +export interface Erc20Transfer { + index: number; + address: string; + value: string; // string representation of hex number + decimals?: number; + to: string; + from: string; + symbol?: string; +} diff --git a/src/antelope/wallets/utils/contracts/EvmContractFactory.ts b/src/antelope/wallets/utils/contracts/EvmContractFactory.ts new file mode 100644 index 000000000..8d49bbad0 --- /dev/null +++ b/src/antelope/wallets/utils/contracts/EvmContractFactory.ts @@ -0,0 +1,63 @@ +/* eslint-disable max-len */ +import { erc1155Abi, erc20Abi, erc721Abi } from 'src/antelope/wallets/utils/abi'; +import EvmContract from 'src/antelope/wallets/utils/contracts/EvmContract'; +import { AntelopeError, EvmContractCalldata, EvmContractMetadata, EvmContractFactoryData } from 'src/antelope/types'; + +export default class EvmContractFactory { + buildContract(data: EvmContractFactoryData): EvmContract { + if (!data || !data.address) { + throw new AntelopeError('antelope.contracts.contract_data_required'); + } + + let verified = false; + if (typeof data.abi !== 'undefined' && data.abi.length > 0) { + data.abi = (typeof data.abi === 'string') ? JSON.parse(data.abi) : data.abi; + } else if (typeof data.metadata !== 'undefined' && data.metadata?.length > 0) { + const metadata: EvmContractMetadata = JSON.parse(data.metadata); + data.abi = metadata?.output?.abi; + } + if (data.abi) { + verified = true; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc20')) { + data.abi = erc20Abi; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc721')) { + data.abi = erc721Abi; + } else if (data.supportedInterfaces && data.supportedInterfaces.includes('erc1155')) { + data.abi = erc1155Abi; + } + + const properties: EvmContractCalldata = (data.calldata) ? JSON.parse(data.calldata) : {}; + + if (!data.name) { + if (properties?.name){ + data.name = properties.name; + } else if (data.metadata) { + const metadata: EvmContractMetadata = JSON.parse(data.metadata); + + if(metadata?.settings?.compilationTarget){ + data.name = Object.values(metadata?.settings?.compilationTarget)[0]; + } + } + } + const abi = typeof data.abi === 'string' ? JSON.parse(data.abi) : data.abi; + + return new EvmContract({ + address: data.address, + name: data.name ?? '', + verified: verified, + creationInfo: { + creator: data.creator, + transaction: data.transaction ?? '', + creation_trx: data.transaction ?? '', + block: data.block, + block_num: data.block, + timestamp: data.timestamp ?? '', + abi: data.abi, + }, + supportedInterfaces: data.supportedInterfaces ?? [], + abi, + properties, + manager: data.manager, + }); + } +} diff --git a/src/antelope/wallets/utils/currency-utils.ts b/src/antelope/wallets/utils/currency-utils.ts new file mode 100644 index 000000000..be3f2c673 --- /dev/null +++ b/src/antelope/wallets/utils/currency-utils.ts @@ -0,0 +1,387 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +import { BigNumber } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { formatUnits } from '@ethersproject/units'; +import Decimal from 'decimal.js'; +import { WEI_PRECISION } from 'src/antelope/wallets/utils'; + +/** + * Given a number or string, returns a string representation of the number with up to 18 decimal places + * @param value - number or string to convert to string + * @returns {string} string representation of the number + */ + +export function toStringNumber(value: number | string): string { + if (typeof value === 'string') { + return value; + } else if (typeof value === 'number') { + const num = new Decimal(value); + return num.toFixed(WEI_PRECISION); + } else { + throw new Error('Invalid value type: ' + typeof value); + } +} + + +/** + * Given a locale string, returns the character used to separate integer and decimal portions of a number, + * e.g "." as in 123.456 + * @param locale - standard locale code, such as "en-US" + * @returns {string} decimal separator character + */ +export function getDecimalSeparatorForLocale(locale: string) { + const numberWithDecimalSeparator = 1.1; + const formattedNumber = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator); + return formattedNumber.charAt(1); // Get the character between "1" and "1" +} + +/** + * Given a locale string, returns the character used to separate groups of numbers in a large number, + * e.g "," as in 123,456,789.00 + * + * @param { string } locale - standard locale code, such as "en-US" + * + * @returns {string} large number separator character + */ +export function getLargeNumberSeparatorForLocale(locale: string) { + const largeNumber = 1000000; + const formattedNumber = new Intl.NumberFormat(locale).format(largeNumber); + + const nonDigitCharacters = formattedNumber.match(/\D+/g); + + if (!nonDigitCharacters || nonDigitCharacters.length === 0) { + return ''; + } + + return nonDigitCharacters[0]; +} + +/** + * Given a localized number string, returns a BigNumber + * + * @param {string} formatted - localized number string, e.g. "123,456.78" + * @param {number} decimals - number of decimals the number has, e.g. 2 for 123,456.78 + * @param {string} locale - standard locale code, such as "en-US" + * + * @returns {BigNumber} BigNumber representation of the number + */ +export function getBigNumberFromLocalizedNumberString(formatted: string, decimals: number, locale: string): BigNumber { + const decimalSeparator = getDecimalSeparatorForLocale(locale); + const largeNumberSeparator = getLargeNumberSeparatorForLocale(locale); + const notIntegerOrSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}${largeNumberSeparator}]`, 'g'); + + if (formatted.match(notIntegerOrSeparatorRegex)) { + throw new Error('Invalid number format'); + } + + // if decimals is not a positive integer, throw an error + if (decimals % 1 !== 0 || decimals < 0) { + throw new Error('Invalid decimals value'); + } + + // strip any character which is not an integer or decimal separator + const notIntegerOrDecimalSeparatorRegex = new RegExp(`[^0-9${decimalSeparator}]`, 'g'); + let unformatted = formatted.replace(notIntegerOrDecimalSeparatorRegex, ''); + + // if the decimal separator is anything but a dot, replace it with a dot to allow conversion to number + if (decimalSeparator !== '.') { + unformatted = unformatted.replace(/[^0-9.]/g, '.'); + } + + return parseUnits(unformatted, decimals); +} + + +/* +* Formats a currency amount in a localized way +* +* @param {number|BigNumber} amount - the currency amount +* @param {number} precision - the number of decimals that should be displayed. Ignored if abbreviate is true and the value is over 1000 +* @param {string} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} abbreviate - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency +* @param {boolean?} displayCurrencyAsCode - if currency is defined, controls whether the currency is display as a symbol or code, e.g. $100 or USD 100. Only valid for fiat currencies. +* @param {number?} tokenDecimals - required if amount is BigNumber. The number of decimals a token has, e.g. 18 for TLOS. This option is not used for non-BigNumber amounts +* @param {boolean?} trimZeroes - trim trailing zeroes for decimal values, e.g. '123.000' => '123', '123.45600' => '123.456'. Overrides 'precision' when there are trailing zeroes +* */ +export function prettyPrintCurrency( + amount: number | BigNumber, + precision: number, + locale: string, + abbreviate = false, + currency?: string, + displayCurrencyAsCode?: boolean, + tokenDecimals?: number, + trimZeroes?: boolean, +): string { + if (precision % 1 !== 0 || precision < 0) { + throw new Error('Precision must be a positive integer or zero'); + } + + if (typeof tokenDecimals === 'number' && (tokenDecimals % 1 !== 0 || tokenDecimals < 0)) { + throw new Error('Token decimals must be a positive integer or zero'); + } + + // require token decimals if type is BigNumber + if (typeof amount !== 'number' && typeof tokenDecimals !== 'number') { + throw new Error('Token decimals is required for BigNumber amounts'); + } + + const decimalSeparator = getDecimalSeparatorForLocale(locale); + const trailingZeroesRegex = new RegExp(`(\\d)\\${decimalSeparator}0+(\\D|$)`, 'g'); + + const decimalOptions : Record = { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + minimumIntegerDigits: undefined, + maximumIntegerDigits: undefined, + }; + + const currencyOptions : Record = { + style: currency ? 'currency' : undefined, + currencyDisplay: currency ? (displayCurrencyAsCode ? 'code' : 'symbol') : undefined, + currency, + }; + + if (typeof amount === 'number') { + if (amount < 1 && amount > 0) { + decimalOptions.maximumIntegerDigits = 1; + decimalOptions.minimumIntegerDigits = 1; + } else if (abbreviate) { + const forceFractionDisplay = amount < 1000 && amount > -1000 ; + + decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.maximumIntegerDigits = 3; + } + + let finalFormattedValue = Intl.NumberFormat( + locale, + { + notation: abbreviate ? 'compact' : undefined, + ...currencyOptions, + ...decimalOptions, + }).format(amount); + + if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { + finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, '$1'); + } + + return finalFormattedValue; + } else { + if (amount.lt(1) && amount.gt(0)) { + decimalOptions.maximumIntegerDigits = 1; + decimalOptions.minimumIntegerDigits = 1; + } else if (abbreviate) { + const forceFractionDisplay = amount.lt(1000) && amount.gt(-1000); + + decimalOptions.maximumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.minimumFractionDigits = forceFractionDisplay ? precision : 2; + decimalOptions.maximumIntegerDigits = 3; + } + + // Intl format method only takes number / bigint, and a BigNumber value cannot have a fractional amount, + // and also decimals may be more places than maximum JS precision. + // As such, decimals must be handled specially for BigNumber amounts. + + const amountAsString = formatUnits(amount, tokenDecimals); // amount string, like "1.0" + + const [integerString, decimalString] = amountAsString.split('.'); + + const formattedInteger = Intl.NumberFormat( + locale, + { notation: abbreviate ? 'compact' : undefined }, + ).format(BigInt(integerString)); + + const formattedDecimal = decimalString.slice(0, precision || 1).padEnd(precision, '0'); + + let finalFormattedValue; + + if (abbreviate) { + finalFormattedValue = formattedInteger; // drop decimals for abbreviated amounts + } else { + finalFormattedValue = `${formattedInteger}${decimalSeparator}${formattedDecimal}`; + } + + if ((trimZeroes || precision === 0) && finalFormattedValue.indexOf(decimalSeparator) > -1) { + finalFormattedValue = finalFormattedValue.replace(trailingZeroesRegex, '$1'); + } + + if (precision === 2 && tokenDecimals === 2 && currency) { + // value is a fiat currency with 2 decimals, so add the currency symbol + if (displayCurrencyAsCode) { + finalFormattedValue = `${finalFormattedValue}\u00A0${currency}`; + } else { + const symbol = getCurrencySymbol(locale, currency); + finalFormattedValue = `${symbol}${finalFormattedValue}`; + } + + } else if (currency) { + finalFormattedValue += ` ${currency}`; + } + + return finalFormattedValue; + } +} + + +/** + * Converts a currency amount from one token to another + * + * @param {BigNumber} tokenOneAmount - the amount of token one + * @param {number} tokenOneDecimals - the number of decimals token one has + * @param {number} tokenTwoDecimals - the number of decimals token two has + * @param {string|number} conversionFactor - the conversion rate from token one to token two + * + * @returns {BigNumber} the amount of token two equivalent to the amount of token one + */ +export function convertCurrency(tokenOneAmount: BigNumber, tokenOneDecimals: number, tokenTwoDecimals: number, conversionFactor: string | number): BigNumber { + const conversionRate = toStringNumber(conversionFactor); + const leadingZeroesRegex = /^0+/g; + const trailingZeroesRegex = /0+$/g; + const floatRegex = /^\d+(\.\d+)?$/g; + + if (!Number.isInteger(tokenOneDecimals) || tokenOneDecimals <= 0) { + throw new Error('Token one decimals must be a positive integer or zero'); + } + + if (!Number.isInteger(tokenTwoDecimals) || tokenTwoDecimals <= 0) { + throw new Error('Token two decimals must be a positive integer or zero'); + } + + if (!floatRegex.test(conversionRate) || Number(conversionRate) <= 0) { + throw new Error('Conversion rate must be a positive floating point number or integer'); + } + + if (tokenOneAmount.lt(0)) { + throw new Error('Token one amount must be positive'); + } + + const tenBn = BigNumber.from(10); + + // represents the maximum significant figures of conversion calculations + const precisionCutoffBn = BigNumber.from(256); + + const [rawConversionRateIntegers, rawConversionRateDecimals = ''] = conversionRate.split('.'); + const conversionRateIntegers = rawConversionRateIntegers.replace(leadingZeroesRegex, ''); + const conversionRateDecimals = rawConversionRateDecimals.replace(trailingZeroesRegex, ''); + + const numberOfConversionRateDecimals = conversionRateDecimals.length; + + const conversionRateScalingFactor = BigNumber.from(numberOfConversionRateDecimals).add(precisionCutoffBn); + const conversionRateAsIntegerString = conversionRateIntegers.concat((conversionRateDecimals ?? '')); + + const conversionRateBn = BigNumber.from(conversionRateAsIntegerString); + const scaledConversionRate = conversionRateBn.mul(tenBn.pow(conversionRateScalingFactor)); + + // normalize amount to 256 precision + const normalizedAmount = tokenOneAmount.mul(tenBn.pow((precisionCutoffBn.sub(tokenOneDecimals)))); + + // multiply amount by conversion rate integer + const normalizedScaledAmountTwo = normalizedAmount.mul(scaledConversionRate); + + // denormalize from 256 precision to tokenTwoDecimals + const denormalizedScaledAmountTwo = normalizedScaledAmountTwo.div(tenBn.pow((precisionCutoffBn.sub(tokenTwoDecimals)))); + + // remove conversion rate scaling + return denormalizedScaledAmountTwo.div(tenBn.pow(conversionRateScalingFactor.add(numberOfConversionRateDecimals))); +} + + +/** + * Inverts a floating point number, useful for taking a conversion rate from token A to token B and getting the + * conversion rate from token B to token A + * + * @param {number|string} float - the floating point number to invert + * + * @returns {string} the inverted floating point number rounded to 18 decimal places + */ +export function getFloatReciprocal(float: number | string) { + const floatRegex = /^\d+(\.\d+)?$/g; + const trailingZeroesRegex = /0+$/g; + const trailingDotRegex = /\.$/g; + + if (!floatRegex.test(float.toString())) { + throw new Error('Conversion rate must be a positive floating point number or integer'); + } + + if (parseFloat(float.toString()) === 0) { + throw new Error('Error inverting: cannot divide by zero'); + } + + return new Decimal(1) + .dividedBy(float) + .toFixed(WEI_PRECISION) + .replace(trailingZeroesRegex, '') + .replace(trailingDotRegex, ''); +} + +/** + * Given a locale and currency code, returns the symbol for the currency, e.g. '$' for USD + * @param {string} locale - locale code, e.g. 'en-US' + * @param {string} currencyCode - standard currency code, e.g. 'USD' + */ +export function getCurrencySymbol(locale: string, currencyCode: string) { + const formatter = new Intl.NumberFormat(locale, { + style: 'currency', + currency: currencyCode, + currencyDisplay: 'symbol', + }); + + const parts = formatter.formatToParts(123); + + let symbol; + + for (let i = 0; i < parts.length; i++) { + if (parts[i].type === 'currency') { + symbol = parts[i].value; + break; + } + } + + return symbol; +} + +/** + * Launches a prompt in MetaMask to add a given token as a tracked token, allowing the user to view their balance of + * that token at a glance from MetaMask + * + * @param {string} address - the address of the token contract + * @param {string} symbol - the token's ticker symbol, e.g. 'STLOS' + * @param {string} image - permalink url of the token's icon + * @param {string} type - Ethereum standard of the token; default is 'ERC20' + * @param {number} decimals - the number of decimals constituting the token's precision, default is 18 + * + * @returns {Promise} + */ +export async function promptAddToMetamask( + address: string, + symbol: string, + image: string, + type: string, + decimals: number, +): Promise { + if (!window.ethereum) { + return Promise.reject(); + } + + type MetamaskEthereum = { + request: (args: { method: string, params: Record }) => Promise + }; + + const ethereum = window.ethereum as unknown as MetamaskEthereum; + + return ethereum.request({ + method: 'wallet_watchAsset', + params: { + type, + options: { + address, + symbol, + decimals, + image, + }, + }, + }); +} diff --git a/src/antelope/wallets/utils/date-utils.ts b/src/antelope/wallets/utils/date-utils.ts new file mode 100644 index 000000000..598108120 --- /dev/null +++ b/src/antelope/wallets/utils/date-utils.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +/** + * Useful date-related constants + */ +export const HOUR_SECONDS = 60 * 60; +export const DAY_SECONDS = 24 * HOUR_SECONDS; + + +/** + * Returns true if the given epochMs is less than the given number of minutes ago + * @param epochMs seconds since epoch representing the date to check + * @param minutes number of minutes to check against + * @returns {boolean} true if the given epochMs is less than the given number of minutes ago + */ +export function dateIsWithinXMinutes(epochMs: number, minutes: number) { + if (epochMs <= 0) { + throw new Error('epochMs must be greater than 0'); + } + + if (epochMs % 1 !== 0) { + throw new Error('epochMs must be an integer'); + } + + // make a date object which represents the time X minutes ago + const xMinsAgo = new Date(); + xMinsAgo.setMinutes(xMinsAgo.getMinutes() - minutes); + + // return true if the date is within the defined timeframe + return new Date(epochMs) > xMinsAgo; +} + + +/** + * Translates a number of seconds to a natural language time period using the given translation function. + * + * @param {number|null} seconds number of seconds since epoch representing the date to check + * @param {function} $t translation function. Should accept a string (just the keyname without a path) and return a translated string + * @returns {string} plain english time period + */ +export function formatUnstakePeriod(seconds: number|null, $t: (key:string) => string) { + if (seconds === null) { + return '--'; + } + + let quantity; + let unit; + + if (seconds < HOUR_SECONDS) { + quantity = seconds / 60; + unit = $t('minutes'); + } else if (seconds < DAY_SECONDS) { + quantity = seconds / HOUR_SECONDS; + unit = $t('hours'); + } else { + quantity = seconds / DAY_SECONDS; + unit = $t('days'); + } + + if (!Number.isInteger(quantity)) { + quantity = quantity.toFixed(1); + } + + return `${quantity} ${unit}`; +} + diff --git a/src/antelope/wallets/utils/index.ts b/src/antelope/wallets/utils/index.ts new file mode 100644 index 000000000..06556d983 --- /dev/null +++ b/src/antelope/wallets/utils/index.ts @@ -0,0 +1,307 @@ +/* eslint-disable max-len */ +export * from 'src/antelope/wallets/utils/abi/signature'; +import { BigNumber, ethers } from 'ethers'; +import { formatUnits } from '@ethersproject/units'; +import { EvmABIEntry } from 'src/antelope/types'; +import { fromUnixTime, format } from 'date-fns'; +import { toStringNumber } from 'src/antelope/wallets/utils/currency-utils'; +import { prettyPrintCurrency } from 'src/antelope/wallets/utils/currency-utils'; +import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; + +const REVERT_FUNCTION_SELECTOR = '0x08c379a0'; +const REVERT_PANIC_SELECTOR = '0x4e487b71'; + +export const WEI_PRECISION = 18; + +/** + * divideFloat performs a division of two float numbers represented as strings or native numbers. + * @param a is the numerator expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') + * @param b is the denominator expressed as a number or a string representing a number + * @returns a string representing the result of the division also as a float number + */ +export function divideFloat(a: string | number, b: string | number): string { + const a_str = toStringNumber(a); + const b_str = toStringNumber(b); + const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; + const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; + const decimals = 2 * Math.max(a_decimals, b_decimals); + const A = ethers.utils.parseUnits(a_str, decimals); + const B = ethers.utils.parseUnits(b_str, b_decimals); + const result = A.div(B); + return formatUnits(result.toString(), decimals-b_decimals); +} + +/** + * multiplyFloat performs a multiplication of two float numbers represented as strings or native numbers. + * @param a is the first factor expressed as a number or a string representing a number (e.g. '100000000002', '1.5' or '0.000000000000000001') + * @param b is the second factor expressed as a number or a string representing a number + * @returns a string representing the result of the multiplication also as a float number + */ +export function multiplyFloat(a: string | number, b: string | number): string { + const a_str = toStringNumber(a); + const b_str = toStringNumber(b); + const a_decimals = a_str.split('.')[1] ? a_str.split('.')[1].length : 0; + const b_decimals = b_str.split('.')[1] ? b_str.split('.')[1].length : 0; + const decimals = a_decimals + b_decimals; + const A = ethers.utils.parseUnits(a_str, decimals); + const B = ethers.utils.parseUnits(b_str, decimals); + const result = A.mul(B); + return formatUnits(result.toString(), decimals+decimals); +} + +export function formatWei(bn: string | number | ethers.BigNumber, tokenDecimals: number, displayDecimals = 4): string { + const amount = ethers.BigNumber.from(bn); + const formatted = formatUnits(amount.toString(), tokenDecimals || WEI_PRECISION); + const str = formatted.toString(); + // Use string, do not convert to number so we never lose precision + if (displayDecimals > 0 && str.includes('.')) { + const parts = str.split('.'); + return parts[0] + '.' + parts[1].slice(0, displayDecimals); + } + return str; +} + +export function isValidAddressFormat(ethAddressString: string): boolean { + const pattern = /^0x[a-fA-F0-9]{40}$/; + return pattern.test(ethAddressString); +} + +export function getTopicHash(topic: string): string { + return `0x${topic.substring(topic.length - 40)}`; +} + +export function toChecksumAddress(address: string): string { + if (!address) { + return address; + } + + let addy = address.toLowerCase().replace('0x', ''); + if (addy.length !== 40) { + addy = addy.padStart(40, '0'); + } + + const hash = keccak256(toUtf8Bytes(addy)).replace('0x', ''); + let ret = '0x'; + + for (let i = 0; i < addy.length; i++) { + if (parseInt(hash[i], 16) >= 8) { + ret += addy[i].toUpperCase(); + } else { + ret += addy[i]; + } + } + + return ret; +} + +export function parseErrorMessage(output: string): string { + if (!output) { + return ''; + } + + let message = ''; + if (output.startsWith(REVERT_FUNCTION_SELECTOR)) { + message = parseRevertReason(output); + } + + if (output.startsWith(REVERT_PANIC_SELECTOR)) { + message = parsePanicReason(output); + } + + return message.replace(/[^a-zA-Z0-9 /./'/"/,/@/+/-/_/(/)/[]/g, ''); +} + +export function parseRevertReason(revertOutput: string): string { + if (!revertOutput || revertOutput.length < 138) { + return ''; + } + + let reason = ''; + const trimmedOutput = revertOutput.substr(138); + for (let i = 0; i < trimmedOutput.length; i += 2) { + reason += String.fromCharCode(parseInt(trimmedOutput.substr(i, 2), 16)); + } + return reason; +} + +export function parsePanicReason(revertOutput: string): string { + const trimmedOutput = revertOutput.slice(-2); + let reason; + + switch (trimmedOutput) { + case '01': + reason = 'If you call assert with an argument that evaluates to false.'; + break; + case '11': + reason = 'If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.'; + break; + case '12': + reason = 'If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).'; + break; + case '21': + reason = 'If you convert a value that is too big or negative into an enum type.'; + break; + case '31': + reason = 'If you call .pop() on an empty array.'; + break; + case '32': + reason = 'If you access an array, bytesN or an array slice at an out-of-bounds or negative index ' + + '(i.e. x[i] where i >= x.length or i < 0).'; + break; + case '41': + reason = 'If you allocate too much memory or create an array that is too large.'; + break; + case '51': + reason = 'If you call a zero-initialized variable of internal function type.'; + break; + default: + reason = 'Default panic message'; + } + return reason; +} + +export function sortAbiFunctionsByName(fns: EvmABIEntry[]): EvmABIEntry[] { + return fns.sort( + (entryA, entryB) => { + const upperA = entryA.name.toUpperCase(); + const upperB = entryB.name.toUpperCase(); + return (upperA < upperB) ? -1 : (upperA > upperB) ? 1 : 0; + }, + ); +} + +/** + * Determine whether the user's device is an Apple touch device + * + * @return {boolean} + */ +export function getClientIsApple() { + return [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod', + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); +} + +/** + * Given a Date object, return the pretty-printed timezone offset, e.g. "+05:00" + * + * @param {Date} date + * @return {string} + */ +export function getFormattedUtcOffset(date: Date): string { + const pad = (value: number) => value < 10 ? '0' + value : value; + const sign = (date.getTimezoneOffset() > 0) ? '-' : '+'; + const offset = Math.abs(date.getTimezoneOffset()); + const hours = pad(Math.floor(offset / 60)); + const minutes = pad(offset % 60); + return sign + hours + ':' + minutes; +} + +/** + * Given a unix timestamp, returns a date in the form of Jan 1, 2023 07:45:22 AM + * + * @param epoch + * + * @return string + */ +export function getLongDate(epoch: number): string { + const offset = getFormattedUtcOffset(new Date(epoch)); + return `${format(fromUnixTime(epoch), 'MMM d, yyyy hh:mm:ss a')} (UTC ${offset})`; +} + + +/** + * Given a unix timestamp, returns string with the date in a given format showing UTC offset optionally. + * @param epoch seconds since epoch + * @param timeFormat a string containing the format of the date to be returned (based on date-fns format) + * @param showUtc whether to show the UTC offset + * @returns {string} the formatted date + */ +export function getFormatedDate(epoch: number, timeFormat = 'MMM d, yyyy hh:mm:ss a', showUtc = false): string { + const offset = getFormattedUtcOffset(new Date(epoch)); + const utc = showUtc ? ` (UTC ${offset})` : ''; + return `${format(fromUnixTime(epoch), timeFormat)}${utc}`; +} + +/* +* Determines whether the amount is too large (more than six characters long) to be displayed in full on mobile devices +* +* @param {number} amount - the currency amount +* return {boolean} - true if the amount is too large to be displayed in full on mobile devices +* */ +export function isAmountTooLarge(amount: number | string): boolean { + const primaryAmountIsTooLarge = + (typeof amount === 'number' && amount.toString().length > 6) || + (typeof amount === 'string' && amount.length > 6); + + return primaryAmountIsTooLarge; +} + + + +/* +* Formats a token balance amount in a localized way, using 4 decimals, +* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true +* +* @param {number} amount - the currency amount +* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} symbol - symbol for the currency to be used, e.g. 'TLOS'. If defined, the symbol will be displayed, e.g. 123.00 TLOS. +* return {string} - the formatted amount +* */ +export function prettyPrintBalance(amount: number | string, locale: string, tiny: boolean, symbol = '') { + return ['', ' ' + symbol].join(prettyPrintCurrency(+amount, 4, locale, tiny ? isAmountTooLarge(amount) : false)); +} + +/* +* Formats a fiat balance amount in a localized way, using 2 decimals, +* abbreviating if the amount is too large to be displayed in full on mobile devices only if tiny is true +* +* @param {number} amount - the currency amount +* @param {number} locale - user's locale code, e.g. 'en-US'. Generally gotten from the user store like useUserStore().locale +* @param {boolean} tiny - whether to abbreviate the value, e.g. 123456.78 => 123.46K. Ignored for values under 1000 +* @param {string?} currency - code for the currency to be used, e.g. 'USD'. If defined, either the symbol or code (determined by the param displayCurrencyAsSymbol) will be displayed, e.g. $123.00 . Generally gotten from the user store like useUserStore().currency +* return {string} - the formatted amount +* */ +export function prettyPrintFiatBalance(fiatAmount: number | string, locale: string, tiny: boolean, currency = 'USD') { + return prettyPrintCurrency(+fiatAmount, 2, locale, tiny ? isAmountTooLarge(fiatAmount) : false, currency); +} + +/** + * Converts gas price, which is in its own unit, to TLOS + * + * @param {string} gasUsed - amount of gas used as string representation of a number/hex + * @param {string} gasPrice - gas price in TLOS as string representation of a number/hex + * + * @return {string} gas in TLOS as a number string + */ +export function getGasInTlos(gasUsed: string, gasPrice: string) { + return formatWei( + BigNumber.from(gasPrice) + .mul(gasUsed).toLocaleString(), + WEI_PRECISION, + 5, + ); +} + +/** + * Takes an ethereum hash ('0x...') and returns a shortened version, like '0x0000...0000' + * @param {string} hash - a string beginning with 0x and containing only 0-9, a-f, or A-F + * + * @return {string} shortened hash + */ +export function getShortenedHash(hash: string) { + const textIsAddress = /^0x[0-9a-fA-F]+$/.test(hash); + + if (textIsAddress) { + return hash.slice(0, 6) + '...' + hash.slice(-4); + } else { + throw new Error('Invalid hash ' + hash); + } +} diff --git a/src/antelope/wallets/utils/text-utils.ts b/src/antelope/wallets/utils/text-utils.ts new file mode 100644 index 000000000..1d936809f --- /dev/null +++ b/src/antelope/wallets/utils/text-utils.ts @@ -0,0 +1,48 @@ +/* eslint-disable max-len */ + +/** + * Given some text, ellipsizes the text if it exceeds a specific length + * + * @param text + * @param maxLength + * @returns {string} + */ +export function truncateText(text: string, maxLength = 10): string { + if (text.length <= maxLength) { + return text; + } + + return `${text.slice(0, maxLength)}...`; +} + +/** + * Given an address, returns a shortened version like `0x0000...0000` + * + * @param address + * @param maxLength + * @returns {string} + */ +export function truncateAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +/** + * Given a name and an id, returns the name without the ID. Generally in the UI, NFT name and ID are displayed next to each other; + * this function prevents the ID from duplicated + * @param name + * @param id + * @returns {string} + * @example + * getShapedNftName('SomeNft #1234', '1234') // 'SomeNft' + */ +export function getShapedNftName(name: string, id: string): string { + let shapedName = name; + if (name.includes(id)) { + shapedName = name.replace(id, ''); + + if (shapedName[shapedName.length - 1] === '#') { + shapedName = shapedName.slice(0, -1); + } + } + return shapedName.trim(); +} diff --git a/src/antelope/wallets/utils/trx-utils.ts b/src/antelope/wallets/utils/trx-utils.ts new file mode 100644 index 000000000..cc6b35270 --- /dev/null +++ b/src/antelope/wallets/utils/trx-utils.ts @@ -0,0 +1,36 @@ +import { usePlatformStore } from 'src/antelope'; +import { ethers } from 'ethers'; +import { AccountModel } from 'src/antelope/mocks'; +import { AntelopeError, TransactionResponse } from 'src/antelope/types'; +import { EVMAuthenticator } from 'src/antelope/wallets'; + + +export async function subscribeForTransactionReceipt(account: AccountModel, response: TransactionResponse): Promise<{ + newResponse: TransactionResponse; + receipt: ethers.providers.TransactionReceipt; +}> { + if (account.isNative) { + throw new AntelopeError('Not implemented yet for native'); + } else { + const authenticator = account.authenticator as EVMAuthenticator; + const provider = await authenticator.web3Provider(); + const result = { + newResponse: { ...response } as TransactionResponse, + receipt: {} as ethers.providers.TransactionReceipt, + }; + if (provider) { + const whenConfirmed = provider.waitForTransaction(response.hash); + // we add the wait method to the response, + // so that the caller can subscribe to the confirmation event + result.newResponse.wait = async () => whenConfirmed; + return result; + } else { + if (usePlatformStore().isMobile) { + response.wait = async () => Promise.resolve({} as ethers.providers.TransactionReceipt); + return result; + } else { + throw new AntelopeError('antelope.evm.error_no_provider'); + } + } + } +} diff --git a/src/assets/tokens/telos.png b/src/assets/tokens/telos.png new file mode 100644 index 0000000000000000000000000000000000000000..2be9c2c7c29bd011aae3636553d0a43edf90d17e GIT binary patch literal 71552 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 { - const address = useAccountStore().getAccount(label).account; - setLogin({ address }); - localStorage.setItem(LOGIN_DATA_KEY, JSON.stringify({ - type: LOGIN_EVM, - provider: name, - account: address, - })); - }); + useAccountStore().loginEVM({ authenticator, network, autoLogAccount }, true); emit('hide'); } diff --git a/src/components/TransactionTable.vue b/src/components/TransactionTable.vue index a6e271934..ada7444ad 100644 --- a/src/components/TransactionTable.vue +++ b/src/components/TransactionTable.vue @@ -150,7 +150,6 @@ watch(() => route.query, ); function setPagination(page: number, size: number, desc: boolean) { - console.log('setPagination()', { page, size, desc, initialKey: pagination.value.initialKey }); pagination.value.page = page; pagination.value.rowsPerPage = size; pagination.value.descending = desc; @@ -159,7 +158,6 @@ function setPagination(page: number, size: number, desc: boolean) { // key is page pages away from the initial key const zero_base_page = page - 1; pagination.value.key = pagination.value.initialKey - (zero_base_page * pagination.value.rowsPerPage); - console.log('setPagination() key ->', pagination.value.key); } updateColumns(); parseTransactions(); diff --git a/src/lib/contract/ContractManager.js b/src/lib/contract/ContractManager.js index 814b80382..530701bbe 100644 --- a/src/lib/contract/ContractManager.js +++ b/src/lib/contract/ContractManager.js @@ -51,7 +51,7 @@ export default class ContractManager { return transfers; } async parseContractTransaction(raw, data, contract, transfers) { - if (data === '0x' || data === null || typeof contract === 'undefined') { + if (data === '0x' || !data || !contract) { return false; } if (contract.getInterface()) { diff --git a/src/lib/price.ts b/src/lib/price.ts new file mode 100644 index 000000000..48843f784 --- /dev/null +++ b/src/lib/price.ts @@ -0,0 +1,135 @@ +import axios, { AxiosInstance } from 'axios'; +import { + MarketSourceInfo, + NativeCurrencyAddress, + PriceChartData, + PriceHistory, + PriceStats, + TokenMarketData, + TokenPrice, +} from 'src/antelope/types'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { dateIsWithinXMinutes } from 'src/antelope/stores/utils/date-utils'; + +interface CachedPrice { + lastFetchTime: number | null, + lastPrice: number | null, +} + +const priceCache: { [tokenId: string]: CachedPrice } = {}; + +export const getCoingeckoUsdPrice = async ( + tokenId: string, +): Promise => { + const now = Date.now(); + + if (priceCache[tokenId] && + priceCache[tokenId].lastFetchTime && + now - (priceCache[tokenId].lastFetchTime as number) < 60 * 1000 && + priceCache[tokenId].lastPrice !== null + ) { + // If less than a minute has passed since the last fetch, return the cached price. + return priceCache[tokenId].lastPrice as number; + } + + try { + const stats: PriceStats = await axios.get( + getCoingeckoExchangeStatsUrl(tokenId), + ); + + if (stats && stats.status === 200) { + const price = stats.data[tokenId].usd; + priceCache[tokenId] = { lastFetchTime: now, lastPrice: price }; + return price; + } else { + console.error(`Error: received status code ${stats.status} from Coingecko.`); + return 0; + } + } catch (error) { + console.error('Error: fetching from Coingecko failed.', error); + return 0; + } +}; + +// fetch the fiat price for a token as a number +export async function getFiatPriceFromIndexer( + tokenSymbol: string, + tokenAddress: string, + fiatCode: string, + indexerAxios: AxiosInstance, + chain_settings: EVMChainSettings, +): Promise { + const price = await getTokenPriceDataFromIndexer(tokenSymbol, tokenAddress, fiatCode, indexerAxios, chain_settings); + + if (price) { + return +price.str; + } + + return 0; +} + +// fetch the price data for a particular token from the indexer +export async function getTokenPriceDataFromIndexer( + tokenSymbol: string, + tokenAddress: string, + fiatCode: string, + indexerAxios: AxiosInstance, + chain_settings: EVMChainSettings, +): Promise { + const wrappedSystemAddress = chain_settings.getWrappedSystemToken().address; + const actualTokenAddress = tokenAddress === NativeCurrencyAddress ? wrappedSystemAddress : tokenAddress; + const response = (await indexerAxios.get(`/v1/tokens/marketdata?tokens=${tokenSymbol}&vs=${fiatCode}`)).data as { results: MarketSourceInfo [] }; + + const tokenMarketDataSource = response.results.find( + tokenData => (tokenData.address ?? '').toLowerCase() === actualTokenAddress.toLowerCase(), + ); + + if (!tokenMarketDataSource?.updated || !tokenMarketDataSource.price) { + return null; + } + + const lastPriceUpdated = (new Date(+tokenMarketDataSource.updated)).getTime(); + + // only use indexer data if it is no more than 10 minutes old + if (dateIsWithinXMinutes(lastPriceUpdated, 10)) { + + const marketData = new TokenMarketData(tokenMarketDataSource); + return new TokenPrice(marketData); + } + // if indexer data is stale, return no data + return null; +} + +export const getCoingeckoPriceChartData = async ( + tokenId: string, +): Promise => { + const exchangeStatsUrl = getCoingeckoExchangeStatsUrl(tokenId); + const priceHistoryUrl = `https://api.coingecko.com/api/v3/coins/${tokenId}/market_chart?vs_currency=USD&days=1&interval=hourly`; + + const [priceStats, priceHistory]: [PriceStats, PriceHistory] = + await Promise.all([ + axios.get(exchangeStatsUrl), + axios.get(priceHistoryUrl), + ]); + + return { + lastUpdated: priceStats.data[tokenId].last_updated_at, + tokenPrice: priceStats.data[tokenId].usd, + dayChange: priceStats.data[tokenId].usd_24h_change, + dayVolume: priceStats.data[tokenId].usd_24h_vol, + marketCap: priceStats.data[tokenId].usd_market_cap, + prices: priceHistory.data.prices, + }; +}; + +// eslint-disable-next-line @typescript-eslint/require-await +export const getEmptyPriceChartData = async (): Promise => ({ + lastUpdated: 0, + tokenPrice: 0, + dayChange: 0, + dayVolume: 0, + marketCap: 0, + prices: [], +}); + +const getCoingeckoExchangeStatsUrl = (tokenId: string): string => `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=USD&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true`; From 8bc625e071ff2e1dd71718ca7602916a048896a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viterbo=20Rodr=C3=ADguez?= Date: Thu, 30 May 2024 22:28:12 -0300 Subject: [PATCH 03/26] make current chan dependant of env.NETWORK_EVM_NAME --- src/App.vue | 3 +++ src/antelope/mocks/ChainStore.ts | 14 ++------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/App.vue b/src/App.vue index 5829078c8..4f48d7f87 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,6 +18,9 @@ const $q = useQuasar(); // computed const isNative = computed(() => $store.getters['login/isNative']); + + + onMounted(async () => { const network = useChainStore().currentChain.settings.getNetwork(); if (TELOS_NETWORK_NAMES.includes(network)) { diff --git a/src/antelope/mocks/ChainStore.ts b/src/antelope/mocks/ChainStore.ts index e2c84e80e..80f0b6361 100644 --- a/src/antelope/mocks/ChainStore.ts +++ b/src/antelope/mocks/ChainStore.ts @@ -141,15 +141,5 @@ const ChainStore = { export const useChainStore = () => ChainStore; -/* - -// TODO: put this code somewhere else -setTimeout(() => { - if (process.env.NETWORK === 'mainnet') { - ChainStore.setChain('mainnet', 'telos-evm'); - } else { - ChainStore.setChain('testnet', 'telos-evm-testnet'); - } -}, 1000); - -*/ +// TODO: remove this +ChainStore.setChain('mainnet', process.env.NETWORK_EVM_NAME as string); From 054787987db1be1730c75f737f92ad7774ee4c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viterbo=20Rodr=C3=ADguez?= Date: Mon, 3 Jun 2024 13:48:49 -0300 Subject: [PATCH 04/26] saving wip --- quasar.conf.js | 1 - src/antelope/chains/EVMChainSettings.ts | 10 +++ .../chains/evm/telos-evm-testnet/index.ts | 31 ++++++++ src/antelope/chains/evm/telos-evm/index.ts | 32 +++++++++ src/antelope/mocks/ChainStore.ts | 5 +- src/antelope/wallets/init.ts | 8 +-- src/boot/antelopeApi.js | 71 ------------------- src/boot/api.js | 69 ------------------ src/boot/evm.js | 2 +- src/components/AppSearch.vue | 9 +-- .../ContractTab/FunctionInterface.vue | 16 +++-- src/components/LoginModal.vue | 12 ++-- src/i18n/en-us/index.js | 1 + src/pages/AccountPage.vue | 11 --- 14 files changed, 103 insertions(+), 175 deletions(-) delete mode 100644 src/boot/antelopeApi.js delete mode 100644 src/boot/api.js diff --git a/quasar.conf.js b/quasar.conf.js index df0aa4be3..cc7317161 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -35,7 +35,6 @@ module.exports = function(/* ctx */) { 'ual', 'hyperion', 'i18n', - 'api', 'errorHandling', 'telosApi', 'evm', diff --git a/src/antelope/chains/EVMChainSettings.ts b/src/antelope/chains/EVMChainSettings.ts index 0e454488a..87efbcc71 100644 --- a/src/antelope/chains/EVMChainSettings.ts +++ b/src/antelope/chains/EVMChainSettings.ts @@ -29,6 +29,7 @@ import { createTraceFunction } from 'src/antelope/mocks/FeedbackStore'; import { getAntelope } from 'src/antelope'; import { WEI_PRECISION, PRICE_UPDATE_INTERVAL_IN_MIN } from 'src/antelope/stores/utils'; import { BehaviorSubject, filter } from 'rxjs'; +import { TelosEvmApi } from '@telosnetwork/telosevm-js'; export default abstract class EVMChainSettings implements ChainSettings { @@ -542,4 +543,13 @@ export default abstract class EVMChainSettings implements ChainSettings { return response.result as EvmBlockData; }); } + + // teloscan specific. MUST be overridden by the chain + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getEthAccountByNativeAccount(_: string): Promise { + throw new Error('Feature not supported for this network'); + } + getNativeSupport(): TelosEvmApi | null { + return null; + } } diff --git a/src/antelope/chains/evm/telos-evm-testnet/index.ts b/src/antelope/chains/evm/telos-evm-testnet/index.ts index 933854472..55ae8c89a 100644 --- a/src/antelope/chains/evm/telos-evm-testnet/index.ts +++ b/src/antelope/chains/evm/telos-evm-testnet/index.ts @@ -5,6 +5,9 @@ import { TokenClass, TokenSourceInfo } from 'src/antelope/types'; import { useUserStore } from 'src/antelope'; import { getFiatPriceFromIndexer, getCoingeckoPriceChartData, getCoingeckoUsdPrice } from 'src/lib/price'; +// specific for Telos +import { TelosEvmApi } from '@telosnetwork/telosevm-js'; + const LOGO = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/telos.png'; const CHAIN_ID = '41'; export const NETWORK = 'telos-evm-testnet'; @@ -63,6 +66,20 @@ const CONTRACTS_BUCKET = 'https://verified-evm-contracts-testnet.s3.amazonaws.co declare const fathom: { trackEvent: (eventName: string) => void }; export default class TelosEVMTestnet extends EVMChainSettings { + nativeSupport: TelosEvmApi; + constructor(network: string) { + super(network); + this.nativeSupport = new TelosEvmApi({ + endpoint: this.getHyperionEndpoint(), + chainId: parseInt(this.getChainId()), + ethPrivateKeys: [], + telosContract: this.getEscrowContractAddress(), + telosPrivateKeys: [], + fetch, + }); + console.assert(network === NETWORK, `Network name mismatch: '${network}' !== '${NETWORK}'`); + } + isTestnet() { return true; } @@ -177,4 +194,18 @@ export default class TelosEVMTestnet extends EVMChainSettings { fathom.trackEvent(eventName); } + + // teloscan specific + getNativeSupport(): TelosEvmApi | null { + return this.nativeSupport; + } + + async getEthAccountByNativeAccount(native: string): Promise { + const account = await this.nativeSupport.telos.getEthAccountByTelosAccount(native); + if (account) { + return account.address; + } else { + return ''; + } + } } diff --git a/src/antelope/chains/evm/telos-evm/index.ts b/src/antelope/chains/evm/telos-evm/index.ts index 4f3e49ff7..4f141bae6 100644 --- a/src/antelope/chains/evm/telos-evm/index.ts +++ b/src/antelope/chains/evm/telos-evm/index.ts @@ -5,6 +5,9 @@ import { TokenClass, TokenSourceInfo } from 'src/antelope/types'; import { useUserStore } from 'src/antelope'; import { getFiatPriceFromIndexer, getCoingeckoPriceChartData, getCoingeckoUsdPrice } from 'src/lib/price'; +// specific for Telos +import { TelosEvmApi } from '@telosnetwork/telosevm-js'; + const LOGO = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/telos.png'; const CHAIN_ID = '40'; export const NETWORK = 'telos-evm'; @@ -62,6 +65,20 @@ const CONTRACTS_BUCKET = 'https://verified-evm-contracts.s3.amazonaws.com'; declare const fathom: { trackEvent: (eventName: string) => void }; export default class TelosEVM extends EVMChainSettings { + nativeSupport: TelosEvmApi; + constructor(network: string) { + super(network); + this.nativeSupport = new TelosEvmApi({ + endpoint: this.getHyperionEndpoint(), + chainId: parseInt(this.getChainId()), + ethPrivateKeys: [], + telosContract: this.getEscrowContractAddress(), + telosPrivateKeys: [], + fetch, + }); + console.assert(network === NETWORK, `Network name mismatch: '${network}' !== '${NETWORK}'`); + } + getNetwork(): string { return NETWORK; } @@ -172,4 +189,19 @@ export default class TelosEVM extends EVMChainSettings { fathom.trackEvent(eventName); } + + // teloscan specific + getNativeSupport(): TelosEvmApi | null { + return this.nativeSupport; + } + + async getEthAccountByNativeAccount(native: string): Promise { + const account = await this.nativeSupport.telos.getEthAccountByTelosAccount(native); + if (account) { + return account.address; + } else { + return ''; + } + } + } diff --git a/src/antelope/mocks/ChainStore.ts b/src/antelope/mocks/ChainStore.ts index 80f0b6361..e53932cab 100644 --- a/src/antelope/mocks/ChainStore.ts +++ b/src/antelope/mocks/ChainStore.ts @@ -8,6 +8,7 @@ import { ChainSettings, NativeCurrencyAddress, TokenClass } from 'src/antelope/t import TelosEVM from 'src/antelope/chains/evm/telos-evm'; import TelosEVMTestnet from 'src/antelope/chains/evm/telos-evm-testnet'; import { ethers } from 'ethers'; +import { TelosEvmApi } from '@telosnetwork/telosevm-js'; export interface TeloscanEVMChainSettings { getStakedSystemToken(): TokenClass; @@ -22,6 +23,9 @@ export interface TeloscanEVMChainSettings { getExplorerUrl: () => string; getSmallLogoPath: () => string; getLargeLogoPath: () => string; + // Telos Specific + getEthAccountByNativeAccount: (account: string) => Promise; + getNativeSupport(): TelosEvmApi | null; } export const evmSettings: { [network: string]: TeloscanEVMChainSettings } = { @@ -118,7 +122,6 @@ const ChainStore = { getNetworkSettings: (network: string) => current.settings, getChain: (label: string) => ChainStore.currentChain, setChain: (label: string, network: string) => { - console.error('ChainStore.setChain', label, network); if (network in evmSettings) { // create the chain model if it doesn't exist diff --git a/src/antelope/wallets/init.ts b/src/antelope/wallets/init.ts index 632611e42..99bb8a88d 100644 --- a/src/antelope/wallets/init.ts +++ b/src/antelope/wallets/init.ts @@ -2,8 +2,7 @@ import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'; import { Web3ModalConfig } from '@web3modal/html'; -import { OreIdOptions } from 'oreid-js'; -import { MetamaskAuth, OreIdAuth, SafePalAuth, WalletConnectAuth, BraveAuth } from 'src/antelope/wallets'; +import { MetamaskAuth, SafePalAuth, WalletConnectAuth, BraveAuth } from 'src/antelope/wallets'; import { configureChains, createConfig } from '@wagmi/core'; import { telos, telosTestnet } from '@wagmi/core/chains'; import { getAntelope } from 'src/antelope/mocks/AntelopeConfig'; @@ -14,10 +13,6 @@ import { AntelopeError } from 'src/antelope/types'; * This function is used to register the EVMAuthenticators that will be used by the app. */ export function initAntelope(app: App) { - const oreIdOptions: OreIdOptions = { - appName: process.env.APP_NAME, - appId: process.env.OREID_APP_ID as string, - }; const projectId = process.env.PROJECT_ID || '14ec76c44bae7d461fa0f5fd5f8a9da1'; const chains = [telos, telosTestnet]; @@ -81,7 +76,6 @@ export function initAntelope(app: App) { // set evm authenticators -- ant.wallets.addEVMAuthenticator(new WalletConnectAuth(wagmiOptions, wagmiClient)); - ant.wallets.addEVMAuthenticator(new OreIdAuth(oreIdOptions)); ant.wallets.addEVMAuthenticator(new MetamaskAuth()); ant.wallets.addEVMAuthenticator(new SafePalAuth()); ant.wallets.addEVMAuthenticator(new BraveAuth()); diff --git a/src/boot/antelopeApi.js b/src/boot/antelopeApi.js deleted file mode 100644 index e90dc7d37..000000000 --- a/src/boot/antelopeApi.js +++ /dev/null @@ -1,71 +0,0 @@ -import { boot } from 'quasar/wrappers'; -import { Api, JsonRpc } from 'eosjs'; - -const signTransaction = async function(actions) { - actions.forEach((action) => { - if (!action.authorization || !action.authorization.length) { - action.authorization = [ - { - actor: this.state.account.accountName, - permission: 'active', - }, - ]; - } - }); - let transaction = null; - try { - if (this.$type === 'ual') { - transaction = await this.$ualUser.signTransaction( - { - actions, - }, - { - blocksBehind: 3, - expireSeconds: 30, - }, - ); - } - } catch (e) { - console.error(actions, e.cause.message); - throw e.cause.message; - } - return transaction; -}; - -const getRpc = function () { - return this.$type === 'ual' ? this.$ualUser.rpc : this.$defaultApi.rpc; -}; - -const getTableRows = async function(options) { - const rpc = this.$antelopeApi.getRpc(); - return await rpc.get_table_rows({ - json: true, - ...options, - }); -}; - -const getAccount = async function (accountName) { - const rpc = this.$antelopeApi.getRpc(); - return await rpc.get_account(accountName); -}; - -export default boot(async ({ app, store }) => { - store.$isAntelopeCapable = app.config.globalProperties.isAntelopeCapable = false; - if(process.env.NETWORK_PROTOCOL && process.env.NETWORK_HOST){ - const rpc = new JsonRpc( - `${process.env.NETWORK_PROTOCOL}://${process.env.NETWORK_HOST}:${process.env.NETWORK_PORT}`, - ); - store.$defaultApi = new Api({ - rpc, - textDecoder: new TextDecoder(), - textEncoder: new TextEncoder(), - }); - store.$isAntelopeCapable = app.config.globalProperties.isAntelopeCapable = true; - } - store.$antelopeApi = { - signTransaction: signTransaction.bind(store), - getTableRows: getTableRows.bind(store), - getAccount: getAccount.bind(store), - getRpc: getRpc.bind(store), - }; -}); diff --git a/src/boot/api.js b/src/boot/api.js deleted file mode 100644 index 66ffe3a11..000000000 --- a/src/boot/api.js +++ /dev/null @@ -1,69 +0,0 @@ -import { boot } from 'quasar/wrappers'; -import { Api, JsonRpc } from 'eosjs'; - -const signTransaction = async function(actions) { - actions.forEach((action) => { - if (!action.authorization || !action.authorization.length) { - action.authorization = [ - { - actor: this.state.account.accountName, - permission: 'active', - }, - ]; - } - }); - let transaction = null; - try { - if (this.$type === 'ual') { - transaction = await this.$ualUser.signTransaction( - { - actions, - }, - { - blocksBehind: 3, - expireSeconds: 30, - }, - ); - } - } catch (e) { - console.debug(actions, e.cause.message); - throw e.cause.message; - } - return transaction; -}; - -const getRpc = function () { - return this.$type === 'ual' ? this.$ualUser.rpc : this.$defaultApi.rpc; -}; - -const getTableRows = async function(options) { - const rpc = this.$api.getRpc(); - return await rpc.get_table_rows({ - json: true, - ...options, - }); -}; - -const getAccount = async function (accountName) { - const rpc = this.$api.getRpc(); - return await rpc.get_account(accountName); -}; - -export default boot(async ({ store }) => { - const rpc = new JsonRpc( - `${process.env.NETWORK_PROTOCOL}://${process.env.NETWORK_HOST}:${process.env.NETWORK_PORT}`, - ); - store['$defaultApi'] = new Api({ - rpc, - textDecoder: new TextDecoder(), - textEncoder: new TextEncoder(), - }); - - store['$api'] = { - signTransaction: signTransaction.bind(store), - getTableRows: getTableRows.bind(store), - getAccount: getAccount.bind(store), - getRpc: getRpc.bind(store), - }; - -}); diff --git a/src/boot/evm.js b/src/boot/evm.js index 9ebb6557e..1f09892c7 100644 --- a/src/boot/evm.js +++ b/src/boot/evm.js @@ -48,4 +48,4 @@ export default boot(({ app, store }) => { store.$evmEndpoint = app.config.globalProperties.$evmEndpoint = hyperion; }); -export { evm, providerManager }; +export { providerManager }; diff --git a/src/components/AppSearch.vue b/src/components/AppSearch.vue index 777c008e4..f6f35628a 100644 --- a/src/components/AppSearch.vue +++ b/src/components/AppSearch.vue @@ -1,11 +1,10 @@ - - - - + + + + + diff --git a/src/components/ContractTab/ContractSource.vue b/src/components/ContractTab/ContractSource.vue index 96c107857..fef98f1e1 100644 --- a/src/components/ContractTab/ContractSource.vue +++ b/src/components/ContractTab/ContractSource.vue @@ -14,6 +14,7 @@ import { useRoute } from 'vue-router'; import CopyButton from 'src/components/CopyButton.vue'; import ContractHeader from 'components/ContractHeader.vue'; import { MetaData } from 'src/types/MetaData'; +import { useChainStore } from 'src/antelope'; hljs.registerLanguage('json', json); hljsDefineSolidity(hljs); @@ -104,9 +105,10 @@ watch(expanded.value, () => { onMounted(async () => { let sourceData; try { + // TODO: remove this const checkSumAddress = toChecksumAddress(route.params.address); const response = await axios.get( - `https://${process.env.VERIFIED_CONTRACTS_BUCKET}.s3.amazonaws.com/${checkSumAddress}/source.json`, + `${useChainStore().currentChain.settings.getTrustedContractsBucket()}/${checkSumAddress}/source.json`, ); sourceData = response.data; sources.value = sourceData; diff --git a/src/components/ContractTab/FunctionInterface.vue b/src/components/ContractTab/FunctionInterface.vue index 5fc65526d..dabf50780 100644 --- a/src/components/ContractTab/FunctionInterface.vue +++ b/src/components/ContractTab/FunctionInterface.vue @@ -247,8 +247,12 @@ export default defineComponent({ this.endLoading(); }, async getEthersFunction(provider?: ethers.providers.JsonRpcSigner | ethers.providers.JsonRpcProvider) { - const contractInstance = await this.$contractManager.getContractInstance(this.contract, provider); - return contractInstance[this.functionABI]; + const contractInstance = await useChainStore().currentChain.settings.getContractManager().getContractInstance(this.contract, provider); + if (!contractInstance) { + throw new Error('Contract not found'); + } else { + return contractInstance[this.functionABI]; + } }, runRead() { return this.getEthersFunction() diff --git a/src/components/ContractTab/GenericContractInterface.vue b/src/components/ContractTab/GenericContractInterface.vue index 8e010078a..55cf84e56 100644 --- a/src/components/ContractTab/GenericContractInterface.vue +++ b/src/components/ContractTab/GenericContractInterface.vue @@ -6,7 +6,6 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import 'vue-json-pretty/lib/styles.css'; import ContractFactory from 'src/lib/contract/ContractFactory'; -import { contractManager } from 'src/boot/telosApi'; import { erc721Abi, erc1155Abi } from 'src/lib/abi'; import erc20Abi from 'erc-20-abi'; import { sortAbiFunctionsByName } from 'src/lib/utils'; @@ -14,6 +13,7 @@ import { sortAbiFunctionsByName } from 'src/lib/utils'; import VueJsonPretty from 'vue-json-pretty'; import FunctionInterface from 'components/ContractTab/FunctionInterface.vue'; import AppHeaderWallet from 'src/components/header/AppHeaderWallet.vue'; +import { useChainStore } from 'src/antelope'; const $q = useQuasar(); const route = useRoute(); @@ -148,7 +148,7 @@ const formatAbiFunctionLists = async () => { name: $t('components.contract_tab.unverified_contract'), address: address.value, abi, - manager: contractManager, + manager: useChainStore().currentChain.settings.getContractManager(), }); let read = [] as any[]; let write = [] as any[]; diff --git a/src/components/Health/Monitor.vue b/src/components/Health/Monitor.vue index af1097e89..c2826a623 100644 --- a/src/components/Health/Monitor.vue +++ b/src/components/Health/Monitor.vue @@ -1,8 +1,9 @@ - - - - - + + + + + + diff --git a/src/components/Transaction/ERCTransferList.vue b/src/components/Transaction/ERCTransferList.vue index 00c8b4089..1fc67655b 100644 --- a/src/components/Transaction/ERCTransferList.vue +++ b/src/components/Transaction/ERCTransferList.vue @@ -10,8 +10,8 @@ import AddressField from 'components/AddressField.vue'; import ValueField from 'components/ValueField.vue'; import { ERC721Transfer, ERC1155Transfer, ERC20Transfer, TokenBasicData } from 'src/types'; -import { contractManager } from 'src/boot/telosApi'; import { TRANSFER_SIGNATURES } from 'src/antelope/types'; +import { useChainStore } from 'src/antelope'; const $q = useQuasar(); const { t: $t } = useI18n(); @@ -46,6 +46,8 @@ const loadTransfers = async () => { return; } + const contractManager = useChainStore().currentChain.settings.getContractManager(); + const logs = props.logs as EvmLogs; for (const log of logs) { diff --git a/src/components/Transaction/InternalTxns.vue b/src/components/Transaction/InternalTxns.vue index 13ba02bf6..2b347d07e 100644 --- a/src/components/Transaction/InternalTxns.vue +++ b/src/components/Transaction/InternalTxns.vue @@ -3,6 +3,7 @@ import VueJsonPretty from 'vue-json-pretty'; import 'vue-json-pretty/lib/styles.css'; import FragmentList from 'components/Transaction/FragmentList.vue'; import { getParsedInternalTransactions } from 'src/lib/transaction-utils'; +import { useChainStore } from 'src/antelope'; export default { @@ -25,7 +26,7 @@ export default { methods: { async getContract(address){ try { - return await this.$contractManager.getContract(address); + return await useChainStore().currentChain.settings.getContractManager().getContract(address); } catch (e) { console.error(`Failed to retrieve contract with address ${address}`); // Notify the user diff --git a/src/components/Transaction/LogsViewer.vue b/src/components/Transaction/LogsViewer.vue index 4e04909d2..447be92c5 100644 --- a/src/components/Transaction/LogsViewer.vue +++ b/src/components/Transaction/LogsViewer.vue @@ -3,6 +3,7 @@ import VueJsonPretty from 'vue-json-pretty'; import 'vue-json-pretty/lib/styles.css'; import FragmentList from 'components/Transaction/FragmentList'; import { BigNumber } from 'ethers'; +import { useChainStore } from 'src/antelope'; export default { name: 'LogsViewer', @@ -13,7 +14,7 @@ export default { methods: { async getLogContract(log){ try { - return await this.$contractManager.getContract(log.address); + return await useChainStore().currentChain.settings.getContractManager().getContract(log.address); } catch (e) { console.error(`Failed to retrieve contract with address ${log.address}: ${e}`); this.$q.notify({ @@ -52,7 +53,7 @@ export default { let contract = await this.getLogContract(log); if (contract){ verified = (contract.isVerified()) ? verified + 1: verified; - let parsedLog = await this.$fragmentParser.parseLog(log, contract); + let parsedLog = await useChainStore().currentChain.settings.getFragmentParser().parseLog(log, contract); if(parsedLog){ this.parsedLogs.push(parsedLog); } else { diff --git a/src/components/TransactionOverview.vue b/src/components/TransactionOverview.vue index 9f499741f..c325357cf 100644 --- a/src/components/TransactionOverview.vue +++ b/src/components/TransactionOverview.vue @@ -4,7 +4,6 @@ import { computed, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { BlockData, EvmTransactionExtended } from 'src/types'; import { WEI_PRECISION } from 'src/lib/utils'; -import { indexerApi } from 'src/boot/telosApi'; import AddressField from 'components/AddressField.vue'; import BlockField from 'components/BlockField.vue'; @@ -16,6 +15,7 @@ import TransactionField from 'components/TransactionField.vue'; import TransactionFeeField from 'components/TransactionFeeField.vue'; import ERCTransferList from 'components/Transaction/ERCTransferList.vue'; import TLOSTransferList from 'components/Transaction/TLOSTransferList.vue'; +import { useChainStore } from 'src/antelope'; const { t: $t } = useI18n(); @@ -40,9 +40,10 @@ const showTLOSTransfers = ref(true); const moreDetailsHeight = ref(0); const loadBlockData = async () => { + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); try { if (blockNumber.value) { - const response = await indexerApi.get(`/block/${blockNumber.value}`); + const response = await indexerApi.get(`/v1/block/${blockNumber.value}`); blockData.value = response.data?.results?.[0] as BlockData; } } catch (error) { diff --git a/src/components/TransactionTable.vue b/src/components/TransactionTable.vue index ada7444ad..9c4763b33 100644 --- a/src/components/TransactionTable.vue +++ b/src/components/TransactionTable.vue @@ -6,7 +6,6 @@ import { onBeforeMount, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { getDirection } from 'src/lib/transaction-utils'; -import { contractManager, indexerApi } from 'src/boot/telosApi'; import { WEI_PRECISION } from 'src/lib/utils'; import AddressField from 'components/AddressField.vue'; @@ -20,6 +19,7 @@ import TransactionFeeField from 'components/TransactionFeeField.vue'; import { Pagination, PaginationByKey } from 'src/types'; import { useStore } from 'vuex'; +import { useChainStore } from 'src/antelope'; const $q = useQuasar(); const route = useRoute(); @@ -189,7 +189,7 @@ async function parseTransactions() { try { const path = await getPath(); - let response = await indexerApi.get(path); + let response = await useChainStore().currentChain.settings.getIndexerApi().get(path); totalRows.value = response.data?.total_count; const results = response.data.results; const next = response.data.next; @@ -222,13 +222,13 @@ async function parseTransactions() { addEmptyToCache(response.data.contracts, transaction); - const contract = await contractManager.getContract(transaction.to); + const contract = await useChainStore().currentChain.settings.getContractManager().getContract(transaction.to); if (!contract) { continue; } - const parsedTransaction = await contractManager.parseContractTransaction( + const parsedTransaction = await useChainStore().currentChain.settings.getContractManager().parseContractTransaction( transaction, transaction.input, contract, true, ); if (parsedTransaction) { @@ -281,10 +281,10 @@ function addEmptyToCache(contracts: any, transaction: any){ } } if(found_from === 0){ - contractManager.addContractToCache(transaction.from, { 'address': transaction.from }); + useChainStore().currentChain.settings.getContractManager().addContractToCache(transaction.from, { 'address': transaction.from }); } if(found_to === 0){ - contractManager.addContractToCache(transaction.to, { 'address': transaction.to }); + useChainStore().currentChain.settings.getContractManager().addContractToCache(transaction.to, { 'address': transaction.to }); } } @@ -293,7 +293,7 @@ async function getPath() { const limit = rowsPerPage === 0 ? 50 : Math.max(Math.min(rowsPerPage, props.initialPageSize), 10); let path = ''; if (props.accountAddress) { - path = `address/${props.accountAddress}/transactions?limit=${limit}`; + path = `v1/address/${props.accountAddress}/transactions?limit=${limit}`; path += `&offset=${(page - 1) * rowsPerPage}`; path += `&sort=${descending ? 'desc' : 'asc'}`; path += (pagination.value.rowsNumber === 0) ? '&includePagination=true' : ''; // We only need the count once @@ -301,10 +301,10 @@ async function getPath() { path += `&startBlock=${props.block}&endBlock=${props.block}`; } } else { - path = `transactions?limit=${limit}`; + path = `v1/transactions?limit=${limit}`; if (pagination.value.initialKey === 0) { // in the case of the first query, we need to get the initial key - let response = await indexerApi.get('transactions?includePagination=true&key=0'); + let response = await useChainStore().currentChain.settings.getIndexerApi().get('v1/transactions?includePagination=true&key=0'); const next = response.data.next; pagination.value.initialKey = next + 1; } diff --git a/src/components/header/AppHeaderBottomBar.vue b/src/components/header/AppHeaderBottomBar.vue index 9ed68f3e4..ceff4ad72 100644 --- a/src/components/header/AppHeaderBottomBar.vue +++ b/src/components/header/AppHeaderBottomBar.vue @@ -3,11 +3,10 @@ import { ref, watchEffect } from 'vue'; import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; -import { IS_TESTNET } from 'src/lib/chain-utils'; - import OutlineButton from 'components/OutlineButton.vue'; import AppHeaderWallet from 'components/header/AppHeaderWallet.vue'; import AppHeaderLinks from 'components/header/AppHeaderLinks.vue'; +import { useChainStore } from 'src/antelope'; const $q = useQuasar(); const { t: $t } = useI18n(); @@ -52,7 +51,7 @@ function scrollHandler(info: { direction: string; }) { Teloscan - + Testnet diff --git a/src/components/header/AppHeaderLinks.vue b/src/components/header/AppHeaderLinks.vue index a26245c71..7a5b9898c 100644 --- a/src/components/header/AppHeaderLinks.vue +++ b/src/components/header/AppHeaderLinks.vue @@ -5,14 +5,13 @@ import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import { - IS_MAINNET, - IS_TESTNET, TELOSCAN_MAINNET_URL, TELOSCAN_TESTNET_URL, } from 'src/lib/chain-utils'; import LanguageSwitcherModal from 'components/header/LanguageSwitcherModal.vue'; import OutlineButton from 'components/OutlineButton.vue'; +import { useChainStore } from 'src/antelope'; const $route = useRoute(); const $router = useRouter(); @@ -31,41 +30,40 @@ const blockchainSubmenuItems = [ { name: 'blocks', label: $t('components.header.blocks') }, ]; -const teloscanSwaggerUrl = IS_MAINNET +const teloscanSwaggerUrl = computed(() => !useChainStore().currentChain.settings.isTestnet() ? 'https://api.teloscan.io/v1/docs' - : 'https://api.testnet.teloscan.io/v1/docs'; + : 'https://api.testnet.teloscan.io/v1/docs'); -const telosWalletUrl = IS_MAINNET +const telosWalletUrl = computed(() => !useChainStore().currentChain.settings.isTestnet() ? 'https://wallet.telos.net/' - : 'https://wallet-dev.telos.net/'; + : 'https://wallet-dev.telos.net/'); -const telosBridgeUrl = IS_MAINNET +const telosBridgeUrl = computed(() => !useChainStore().currentChain.settings.isTestnet() ? 'https://bridge.telos.net/bridge' - : 'https://telos-bridge-testnet.netlify.app/bridge'; + : 'https://telos-bridge-testnet.netlify.app/bridge'); -const obeUrl = IS_MAINNET +const obeUrl = computed(() => !useChainStore().currentChain.settings.isTestnet() ? 'https://explorer.telos.net/' - : 'https://explorer-test.telos.net'; + : 'https://explorer-test.telos.net'); -const developersSubmenuItems = [ +const developersSubmenuItems = computed(() => [ { - url: teloscanSwaggerUrl, + url: teloscanSwaggerUrl.value, label: $t('components.header.api_documentation'), }, { url: 'https://sourcify.dev/', label: $t('components.header.verify_contract_sourcify'), }, -]; +]); const telos_walletMenuItem = { - url: telosWalletUrl, + url: telosWalletUrl.value, label: `${$t('components.header.telos_wallet')}/Staking`, }; - const telos_bridgeMenuItem = { - url: telosBridgeUrl, + url: telosBridgeUrl.value, label: $t('components.header.telos_bridge'), }; @@ -80,7 +78,7 @@ const moreSubmenuItems = { label: $t('components.header.telos_ecosystem'), }, { - url: obeUrl, + url: obeUrl.value, label: $t('components.header.telos_zero_explorer'), }, ], @@ -129,11 +127,11 @@ function toggleDarkMode() { function getIsCurrentNetworkMenuItem(url: string) { if (url === TELOSCAN_MAINNET_URL) { - return IS_MAINNET; + return !useChainStore().currentChain.settings.isTestnet(); } if (url === TELOSCAN_TESTNET_URL) { - return IS_TESTNET; + return useChainStore().currentChain.settings.isTestnet(); } return false; diff --git a/src/components/header/AppHeaderTopBar.vue b/src/components/header/AppHeaderTopBar.vue index 97a71706a..667a0c552 100644 --- a/src/components/header/AppHeaderTopBar.vue +++ b/src/components/header/AppHeaderTopBar.vue @@ -10,16 +10,9 @@ import { useStore } from 'vuex'; import { useI18n } from 'vue-i18n'; import { formatUnits } from 'ethers/lib/utils'; import { useRoute } from 'vue-router'; - import { useChainStore } from 'src/antelope'; -import { - IS_MAINNET, - IS_TESTNET, - TELOSCAN_MAINNET_URL, - TELOSCAN_TESTNET_URL, - // BETA_TELOSCAN_MAINNET_URL, - // BETA_TELOSCAN_TESTNET_URL, -} from 'src/lib/chain-utils'; +import { TELOSCAN_MAINNET_URL, TELOSCAN_TESTNET_URL } from 'src/lib/chain-utils'; + import AppHeaderWallet from 'components/header/AppHeaderWallet.vue'; import OutlineButton from 'components/OutlineButton.vue'; @@ -39,7 +32,7 @@ const locale = computed(() => $i18n.locale.value); const hideSearchBar = computed(() => $route.name ==='home'); const systemTokenSymbol = computed(() => chainStore.currentChain.settings.getSystemToken().symbol); const gasPriceInGwei = computed(() => { - if (IS_TESTNET) { + if (chainStore.currentChain.settings.isTestnet()) { return ''; } @@ -54,7 +47,7 @@ const gasPriceInGwei = computed(() => { return gasGweiNoDecimals.toLocaleString(locale.value); }); const tlosPrice = computed(() => { - if (IS_TESTNET) { + if (chainStore.currentChain.settings.isTestnet()) { return ''; } @@ -67,7 +60,7 @@ const tlosPrice = computed(() => { }); onBeforeMount(() => { - if (IS_MAINNET) { + if (!chainStore.currentChain.settings.isTestnet()) { fetchTlosPrice(); fetchGasPrice(); } @@ -95,14 +88,14 @@ function toggleDarkMode() { } function goToTeloscanMainnet() { - if (IS_MAINNET) { + if (!chainStore.currentChain.settings.isTestnet()) { return; } window.open(TELOSCAN_MAINNET_URL, '_blank'); } function goToTeloscanTestnet() { - if (IS_TESTNET) { + if (chainStore.currentChain.settings.isTestnet()) { return; } window.open(TELOSCAN_TESTNET_URL, '_blank'); @@ -113,13 +106,13 @@ function goToTeloscanTestnet() {
-
+
{{ $t('components.header.system_token_price', { token: systemTokenSymbol }) }} ${{ tlosPrice }}
-
+
{{ $t('components.header.gas') }}: @@ -161,7 +154,7 @@ function goToTeloscanTestnet() { @click="goToTeloscanMainnet" @keydown.enter="goToTeloscanMainnet" > - + Telos Mainnet @@ -172,7 +165,7 @@ function goToTeloscanTestnet() { @click="goToTeloscanTestnet" @keydown.enter="goToTeloscanTestnet" > - + Telos Testnet diff --git a/src/components/header/AppHeaderWallet.vue b/src/components/header/AppHeaderWallet.vue index 6d80846e8..5c7cb5d0e 100644 --- a/src/components/header/AppHeaderWallet.vue +++ b/src/components/header/AppHeaderWallet.vue @@ -8,11 +8,9 @@ import { BigNumber } from 'ethers/lib/ethers'; import { formatUnits } from 'ethers/lib/utils'; import { truncateAddress } from 'src/antelope/wallets/utils/text-utils'; -import { useAccountStore } from 'src/antelope'; -import { indexerApi } from 'src/boot/telosApi'; +import { useAccountStore, useChainStore } from 'src/antelope'; import { WEI_PRECISION } from 'src/antelope/wallets/utils'; import { prettyPrintCurrency } from 'src/antelope/wallets/utils/currency-utils'; -import { IS_TESTNET } from 'src/lib/chain-utils'; import LoginModal from 'components/LoginModal.vue'; import OutlineButton from 'components/OutlineButton.vue'; @@ -54,7 +52,7 @@ const prettyIdentity = computed(() => { return truncateAddress(address.value); }); const prettySystemTokenBalanceFiat = computed(() => { - if (IS_TESTNET) { + if (useChainStore().currentChain.settings.isTestnet()) { return ''; } const price = Number($store.getters['chain/tlosPrice']); @@ -103,8 +101,9 @@ function copyAddress() { } async function fetchUserBalance() { + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); const response = await indexerApi.get( - `/account/${address.value}/balances`, + `/v1/account/${address.value}/balances`, ); const tlos = response.data?.results?.find(({ contract }: { contract: string }) => contract === '___NATIVE_CURRENCY___'); userSystemTokenBalanceWei.value = tlos?.balance ?? '0'; diff --git a/src/lib/chain-utils.ts b/src/lib/chain-utils.ts index fca6a1c17..f9847fba0 100644 --- a/src/lib/chain-utils.ts +++ b/src/lib/chain-utils.ts @@ -2,10 +2,3 @@ export const TELOSCAN_MAINNET_URL = 'https://teloscan.io'; export const TELOSCAN_TESTNET_URL = 'https://testnet.teloscan.io'; export const BETA_TELOSCAN_MAINNET_URL = 'https://beta.teloscan.io'; export const BETA_TELOSCAN_TESTNET_URL = 'https://beta.testnet.teloscan.io'; - -export const TELOS_MAINNET_CHAIN_ID = 40; -export const TELOS_TESTNET_CHAIN_ID = 41; - -const currentChainId = Number(process.env.NETWORK_EVM_CHAIN_ID); -export const IS_MAINNET = currentChainId === TELOS_MAINNET_CHAIN_ID; -export const IS_TESTNET = currentChainId === TELOS_TESTNET_CHAIN_ID; diff --git a/src/lib/contract/ContractManager.js b/src/lib/contract/ContractManager.js index 530701bbe..4807d3cdb 100644 --- a/src/lib/contract/ContractManager.js +++ b/src/lib/contract/ContractManager.js @@ -4,6 +4,7 @@ import axios from 'axios'; import { getTopicHash } from 'src/lib/utils'; import { ERC1155_TRANSFER_SIGNATURE, TRANSFER_SIGNATURES } from 'src/lib/abi/signature/transfer_signatures.js'; import { erc1155Abi, erc721MetadataAbi } from 'src/lib/abi'; +import { getAntelope, useChainStore } from 'src/antelope'; const tokenList = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/telosevm.tokenlist.json'; const systemContractList = 'https://raw.githubusercontent.com/telosnetwork/token-list/main/telosevm.systemcontractlist.json'; @@ -17,11 +18,10 @@ export default class ContractManager { this.indexerApi = indexerApi; this.systemContractList = false; this.tokenList = false; - this.ethersProvider = new ethers.providers.JsonRpcProvider(process.env.NETWORK_EVM_RPC); } getEthersProvider() { - return this.ethersProvider; + return getAntelope().wallets.getWeb3Provider(); } async getTransfers(raw) { if(!raw.logs || raw.logs?.length === 0){ @@ -84,7 +84,7 @@ export default class ContractManager { async loadNFTs(contract){ let address = contract.address.toLowerCase(); try { - let response = await this.indexerApi.get(`/contract/${address}/nfts`); + let response = await this.indexerApi.get(`/v1/contract/${address}/nfts`); if(response.data.results?.length > 0){ for(var i = 0; i < response.data.results.length; i++){ let nft = response.data.results[i]; @@ -105,7 +105,7 @@ export default class ContractManager { } try { // TODO: change endpoint based on contract interfaces - let response = await this.indexerApi.get(`/contract/${address}/nfts?tokenId=${tokenId}`); + let response = await this.indexerApi.get(`/v1/contract/${address}/nfts?tokenId=${tokenId}`); if(response.data.results?.length > 0){ this.contracts[address].nfts[tokenId] = response.data.results[0]; return response.data.results[0]; @@ -179,7 +179,7 @@ export default class ContractManager { async loadTokenList() { const results = await axios.get(tokenList); const { tokens } = results.data; - results.data.tokens = (tokens ?? []).filter(({ chainId }) => chainId === process.env.NETWORK_EVM_CHAIN_ID); + results.data.tokens = (tokens ?? []).filter(({ chainId }) => +chainId === +useChainStore().currentChain.settings.getChainId()); this.tokenList = results.data || false; } @@ -191,7 +191,7 @@ export default class ContractManager { const results = await axios.get(systemContractList); const { contracts } = results.data; results.data.contracts = (contracts ?? []).filter( - ({ chainId }) => chainId === process.env.NETWORK_EVM_CHAIN_ID) + ({ chainId }) => +chainId === +useChainStore().currentChain.settings.getChainId()) ; this.systemContractList = results.data || false; this.processing['systemcontractlist'] = false; @@ -253,7 +253,7 @@ export default class ContractManager { this.processing.push(addressLower); let contract = null; try { - let response = await this.indexerApi.get(`/contract/${address}?full=true&includeAbi=true`); + let response = await this.indexerApi.get(`/v1/contract/${address}?full=true&includeAbi=true`); if(response.data?.success && response.data.results.length > 0){ contract = response.data.results[0]; } diff --git a/src/lib/provider.js b/src/lib/provider.js deleted file mode 100644 index 7c5de1256..000000000 --- a/src/lib/provider.js +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-disable */ -const { ethers } = require("ethers"); -const providersError = "More than one provider is active, disable additional providers."; -const unsupportedError ="current EVM wallet provider is not supported."; - -const switchEthereumChain = async () => { - const provider = getProvider(); - - if (provider){ - const chainId = parseInt(process.env.NETWORK_EVM_CHAIN_ID, 10); - const chainIdParam = `0x${chainId.toString(16)}` - const mainnet = chainId === 40; - try { - await provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: chainIdParam }], - }); - return true; - } catch (e) { - if (e.code === 4902) { // "Chain hasn't been added" - try { - await provider.request({ - method: 'wallet_addEthereumChain', - params: [{ - chainId: chainIdParam, - chainName: `Telos EVM ${mainnet ? 'Mainnet' : 'Testnet'}`, - nativeCurrency: { - name: `Telos`, - symbol: `TLOS`, - decimals: 18, - }, - rpcUrls: [`https://${mainnet ? 'mainnet' : 'testnet'}.telos.net/evm`], - blockExplorerUrls: [`https://${mainnet ? '' : 'testnet.'}teloscan.io`] - }], - }); - return true; - } catch (e) { - console.error(e); - } - } - } - }else{ - return false; - } -}; - -const getProvider = () => { - const provider = window.ethereum.isMetaMask || window.ethereum.isCoinbaseWallet ? - window.ethereum : - null - if (!provider){ - console.error(providersError, 'or', unsupportedError); - } - return provider; -} - -const addNetwork = async () => { - const provider = getProvider(); - if (provider) { - const chainId = parseInt(process.env.NETWORK_EVM_CHAIN_ID, 10); - const chainIdParam = `0x${chainId.toString(16)}` - const mainnet = chainId === 40; - try { - await provider.request({ - method: "wallet_addEthereumChain", - params: [{ - chainId: chainIdParam, - chainName: `Telos EVM ${mainnet ? 'Mainnet' : 'Testnet'}`, - nativeCurrency: { - name: `Telos`, - symbol: `TLOS`, - decimals: 18, - }, - rpcUrls: [`https://${mainnet ? 'mainnet' : 'testnet'}.telos.net/evm`], - blockExplorerUrls: [`https://${mainnet ? '' : 'testnet.'}teloscan.io`], - }] - }); - return true; - } catch (error) { - console.error(error); - return false; - } - } else { - return false; - } -} - -const isConnected = async () => { - const provider = getProvider(); - const checkProvider = new ethers.providers.Web3Provider(provider); - const accounts = await checkProvider.listAccounts(); - if (accounts.length > 0){ - const { chainId } = await checkProvider.getNetwork(); - if (chainId !== process.env.NETWORK_EVM_CHAIN_ID){ - await switchEthereumChain(); - } - return accounts[0]; - } - return false; -} - -const requestAccounts = async () => { - const provider = getProvider(); - const accessGranted = await provider.request({ method: 'eth_requestAccounts' }) - return accessGranted > 0 ? accessGranted[0] : false; -} - -const addAccountsListener = () => { - const provider = getProvider(); - provider.on('accountsChanged', (accountsArr) => { - if (!accountsArr.length){ - this.accountConnected = false; - provider.removeAllListeners('accountsChanged'); - } - }); -} - -/* -// TODO: delete this whole file, leaving it behind for reference later -module.exports = { - switchEthereumChain, - addNetwork, - getProvider, - isConnected, - requestAccounts -} - */ diff --git a/src/lib/transaction-utils.ts b/src/lib/transaction-utils.ts index d8fd0b017..55e64f5eb 100644 --- a/src/lib/transaction-utils.ts +++ b/src/lib/transaction-utils.ts @@ -1,14 +1,12 @@ import { ethers } from 'ethers'; -import { indexerApi } from 'src/boot/telosApi'; -import { contractManager } from 'src/boot/telosApi'; - import { EvmTransaction, EvmTransactionLog } from 'src/antelope/types'; import { EvmTransactionExtended, NftTransferData } from 'src/types'; import { TransactionDescription } from 'ethers/lib/utils'; import { WEI_PRECISION, formatWei, parseErrorMessage } from 'src/lib/utils'; import { toChecksumAddress } from 'src/lib/utils'; +import { useChainStore } from 'src/antelope'; export const tryToExtractMethod = (abi: {[hash: string]: string }, input: string) => { if (!abi || !input) { @@ -31,7 +29,8 @@ export const tryToExtractMethod = (abi: {[hash: string]: string }, input: string }; export const loadTransaction = async (hash: string): Promise => { try { - const trxResponse = await indexerApi.get(`/transaction/${hash}?full=true&includeAbi=true`); + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); + const trxResponse = await indexerApi.get(`/v1/transaction/${hash}?full=true&includeAbi=true`); const abi = trxResponse.data.abi; if (trxResponse.data.results.length === 0) { console.error(`Transaction ${hash} not found`); @@ -68,7 +67,9 @@ export const loadTransaction = async (hash: string): Promisestring) => new Promise<{itxs:unknown[], parsedItxs:unknown[]}>((resolve, reject) => { - const query = `/transaction/${hash}/internal?limit=1000&sort=ASC&offset=0&includeAbi=1`; + const query = `/v1/transaction/${hash}/internal?limit=1000&sort=ASC&offset=0&includeAbi=1`; + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); + const contractManager = useChainStore().currentChain.settings.getContractManager(); indexerApi.get(query).then(async (response) => { if(response && response.data?.results?.length > 0) { const dataset = response.data?.results; diff --git a/src/pages/AccountPage.vue b/src/pages/AccountPage.vue index ce46d3361..ab73c6553 100644 --- a/src/pages/AccountPage.vue +++ b/src/pages/AccountPage.vue @@ -3,8 +3,6 @@ import { ref, computed, watch, onMounted } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useStore } from 'vuex'; import { useI18n } from 'vue-i18n'; -import { contractManager } from 'src/boot/telosApi'; -import { indexerApi } from 'src/boot/telosApi'; import { toChecksumAddress } from 'src/lib/utils'; import { getIcon } from 'src/lib/token-utils'; import Contract from 'src/lib/contract/Contract'; @@ -25,6 +23,7 @@ import AddressQR from 'src/components/AddressQR.vue'; import AddressOverview from 'src/components/AddressOverview.vue'; import AddressMoreInfo from 'src/components/AddressMoreInfo.vue'; import ContractMoreInfo from 'src/components/ContractMoreInfo.vue'; +import { useChainStore } from 'src/antelope'; const { t: $t } = useI18n(); @@ -86,11 +85,13 @@ async function loadAccount() { if(!accountAddress.value || accountLoading.value){ return; } + const contractManager = useChainStore().currentChain.settings.getContractManager(); accountLoading.value = true; const tokenList = await contractManager.getTokenList(); try { + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); const response: BalanceQueryResponse = await indexerApi.get( - `/account/${accountAddress.value}/balances?includeAbi=true`, + `/v1/account/${accountAddress.value}/balances?includeAbi=true`, // `/account/${accountAddress.value}/balances?contract=___NATIVE_CURRENCY___&includeAbi=true`, ); //TODO restore original api query when contract param query is fixed diff --git a/src/pages/BlockPage.vue b/src/pages/BlockPage.vue index 729dc4ea6..3275ed2c9 100644 --- a/src/pages/BlockPage.vue +++ b/src/pages/BlockPage.vue @@ -3,10 +3,10 @@ import { ref, watch, onMounted, computed, toRaw } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { indexerApi } from 'src/boot/telosApi'; import TransactionTable from 'components/TransactionTable.vue'; import BlockOverview from 'components/BlockOverview.vue'; import { BlockData } from 'src/types'; +import { useChainStore } from 'src/antelope'; const router = useRouter(); const route = useRoute(); @@ -34,16 +34,18 @@ function nextBlock() { // eslint-disable-next-line @typescript-eslint/no-explicit-any function visitNativeBlockExplorer(extraData: any) { - const explorerLink = process.env.NETWORK_EXPLORER; + // TODO: remove this + const explorerLink = useChainStore().currentChain.settings.getExplorerUrl(); window.open(`${explorerLink}/block/${extraData}`, '_blank'); } const loadBlockData = async () => { + const indexerApi = useChainStore().currentChain.settings.getIndexerApi(); try { if (blockNumber.value <= 0) { return; } - const response = await indexerApi.get(`/block/${blockNumber.value}`); + const response = await indexerApi.get(`/v1/block/${blockNumber.value}`); blockData.value = toRaw(response.data?.results?.[0]) as BlockData; } catch (error) { console.error('Failed to fetch block data:', error); diff --git a/src/pages/ContractVerification.vue b/src/pages/ContractVerification.vue index 289f4d4cb..0d958a44a 100644 --- a/src/pages/ContractVerification.vue +++ b/src/pages/ContractVerification.vue @@ -1,4 +1,5 @@