diff --git a/README.md b/README.md index 45a39b14d..358e4f6c1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Enkrypt is a web3 wallet built from the ground up to support the multi-chain fut - Ethereum - Bitcoin - Solana +- MultiversX - Acala - Amplitude - Arbitrum diff --git a/packages/extension/package.json b/packages/extension/package.json index de38fc7c4..878a5ce18 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -44,6 +44,8 @@ "@metaplex-foundation/mpl-bubblegum": "^5.0.2", "@metaplex-foundation/umi": "^1.4.1", "@metaplex-foundation/umi-bundle-defaults": "^1.4.1", + "@multiversx/sdk-core": "^15.0.0", + "@multiversx/sdk-transaction-decoder": "^2.0.0", "@polkadot/api": "^16.4.7", "@polkadot/extension-inject": "^0.61.7", "@polkadot/keyring": "^13.5.6", diff --git a/packages/extension/src/libs/background/index.ts b/packages/extension/src/libs/background/index.ts index ca4fdee91..091987315 100644 --- a/packages/extension/src/libs/background/index.ts +++ b/packages/extension/src/libs/background/index.ts @@ -1,33 +1,33 @@ +import DomainState from '@/libs/domain-state'; +import { sendToWindow } from '@/libs/messenger/extension'; +import PersistentEvents from '@/libs/persistent-events'; +import TabInfo from '@/libs/utils/tab-info'; +import Providers from '@/providers'; import { InternalMethods, InternalOnMessageResponse, Message, } from '@/types/messenger'; -import { RPCRequestType, OnMessageResponse } from '@enkryptcom/types'; +import { ProviderName } from '@/types/provider'; +import { OnMessageResponse, RPCRequestType } from '@enkryptcom/types'; import { v4 as randomUUID } from 'uuid'; +import Browser from 'webextension-polyfill'; import { getCustomError } from '../error'; import KeyRingBase from '../keyring/keyring'; -import { sendToWindow } from '@/libs/messenger/extension'; -import { ProviderName } from '@/types/provider'; -import Providers from '@/providers'; -import Browser from 'webextension-polyfill'; -import TabInfo from '@/libs/utils/tab-info'; -import PersistentEvents from '@/libs/persistent-events'; -import DomainState from '@/libs/domain-state'; -import { TabProviderType, ProviderType, ExternalMessageOptions } from './types'; +import SettingsState from '../settings-state'; import { getProviderNetworkByName } from '../utils/networks'; +import { handlePersistentEvents } from './external'; import { - sign, - getEthereumPubKey, - ethereumDecrypt, - unlock, changeNetwork, - sendToTab, - newAccount, + ethereumDecrypt, + getEthereumPubKey, lock, + newAccount, + sendToTab, + sign, + unlock, } from './internal'; -import { handlePersistentEvents } from './external'; -import SettingsState from '../settings-state'; +import { ExternalMessageOptions, ProviderType, TabProviderType } from './types'; class BackgroundHandler { #keyring: KeyRingBase; @@ -48,6 +48,7 @@ class BackgroundHandler { [ProviderName.bitcoin]: {}, [ProviderName.kadena]: {}, [ProviderName.solana]: {}, + [ProviderName.multiversx]: {}, [ProviderName.massa]: {}, }; this.#providers = Providers; diff --git a/packages/extension/src/libs/background/types.ts b/packages/extension/src/libs/background/types.ts index ae2c834cd..82acd660d 100644 --- a/packages/extension/src/libs/background/types.ts +++ b/packages/extension/src/libs/background/types.ts @@ -1,9 +1,10 @@ import BitcoinProvider from '@/providers/bitcoin'; import type EthereumProvider from '@/providers/ethereum'; -import type PolkadotProvider from '@/providers/polkadot'; import type KadenaProvider from '@/providers/kadena'; -import SolanaProvider from '@/providers/solana'; import MassaProvider from '@/providers/massa'; +import MultiversXProvider from '@/providers/multiversx'; +import type PolkadotProvider from '@/providers/polkadot'; +import SolanaProvider from '@/providers/solana'; export interface TabProviderType { [key: string]: Record< @@ -13,6 +14,7 @@ export interface TabProviderType { | BitcoinProvider | KadenaProvider | SolanaProvider + | MultiversXProvider | MassaProvider >; } @@ -23,6 +25,7 @@ export interface ProviderType { | typeof BitcoinProvider | typeof KadenaProvider | typeof SolanaProvider + | typeof MultiversXProvider | typeof MassaProvider; } export interface ExternalMessageOptions { diff --git a/packages/extension/src/libs/tokens-state/mx-icon.webp b/packages/extension/src/libs/tokens-state/mx-icon.webp new file mode 100644 index 000000000..471fd6900 Binary files /dev/null and b/packages/extension/src/libs/tokens-state/mx-icon.webp differ diff --git a/packages/extension/src/libs/utils/initialize-wallet.ts b/packages/extension/src/libs/utils/initialize-wallet.ts index 736d48b39..081006991 100644 --- a/packages/extension/src/libs/utils/initialize-wallet.ts +++ b/packages/extension/src/libs/utils/initialize-wallet.ts @@ -1,12 +1,13 @@ import KeyRing from '@/libs/keyring/keyring'; -import EthereumNetworks from '@/providers/ethereum/networks'; -import PolkadotNetworks from '@/providers/polkadot/networks'; +import { getAccountsByNetworkName } from '@/libs/utils/accounts'; import BitcoinNetworks from '@/providers/bitcoin/networks'; +import EthereumNetworks from '@/providers/ethereum/networks'; import KadenaNetworks from '@/providers/kadena/networks'; -import SolanaNetworks from '@/providers/solana/networks'; import MassaNetworks from '@/providers/massa/networks'; +import MultiversXNetworks from '@/providers/multiversx/networks'; +import PolkadotNetworks from '@/providers/polkadot/networks'; +import SolanaNetworks from '@/providers/solana/networks'; import { NetworkNames, WalletType } from '@enkryptcom/types'; -import { getAccountsByNetworkName } from '@/libs/utils/accounts'; import BackupState from '../backup-state'; export const initAccounts = async (keyring: KeyRing) => { const secp256k1btc = ( @@ -24,6 +25,9 @@ export const initAccounts = async (keyring: KeyRing) => { const ed25519sol = ( await getAccountsByNetworkName(NetworkNames.Solana) ).filter(acc => !acc.isTestWallet); + const ed25519mvx = ( + await getAccountsByNetworkName(NetworkNames.MultiversX) + ).filter(acc => !acc.isTestWallet); const ed25519massa = ( await getAccountsByNetworkName(NetworkNames.Massa) ).filter(acc => !acc.isTestWallet); @@ -62,6 +66,13 @@ export const initAccounts = async (keyring: KeyRing) => { signerType: SolanaNetworks.solana.signer[0], walletType: WalletType.mnemonic, }); + if (ed25519mvx.length == 0) + await keyring.saveNewAccount({ + basePath: MultiversXNetworks.multiversx.basePath, + name: 'MultiversX Account 1', + signerType: MultiversXNetworks.multiversx.signer[0], + walletType: WalletType.mnemonic, + }); if (ed25519massa.length == 0) await keyring.saveNewAccount({ basePath: MassaNetworks.Massa.basePath, diff --git a/packages/extension/src/libs/utils/networks.ts b/packages/extension/src/libs/utils/networks.ts index 2eccad1da..c1814a100 100644 --- a/packages/extension/src/libs/utils/networks.ts +++ b/packages/extension/src/libs/utils/networks.ts @@ -1,20 +1,22 @@ -import { ProviderName } from '@/types/provider'; -import { NetworkNames } from '@enkryptcom/types'; -import EthereumNetworks from '@/providers/ethereum/networks'; -import PolkadotNetworks from '@/providers/polkadot/networks'; import BitcoinNetworks from '@/providers/bitcoin/networks'; -import KadenaNetworks from '@/providers/kadena/networks'; -import SolanaNetworks from '@/providers/solana/networks'; -import { BaseNetwork } from '@/types/base-network'; -import CustomNetworksState from '../custom-networks-state'; -import { CustomEvmNetwork } from '@/providers/ethereum/types/custom-evm-network'; -import Ethereum from '@/providers/ethereum/networks/eth'; -import Polkadot from '@/providers/polkadot/networks/polkadot'; import Bitcoin from '@/providers/bitcoin/networks/bitcoin'; +import EthereumNetworks from '@/providers/ethereum/networks'; +import Ethereum from '@/providers/ethereum/networks/eth'; +import { CustomEvmNetwork } from '@/providers/ethereum/types/custom-evm-network'; +import KadenaNetworks from '@/providers/kadena/networks'; import Kadena from '@/providers/kadena/networks/kadena'; -import Solana from '@/providers/solana/networks/solana'; import MassaNetworks from '@/providers/massa/networks'; import Massa from '@/providers/massa/networks/mainnet'; +import MultiversXNetworks from '@/providers/multiversx/networks'; +import MultiversX from '@/providers/multiversx/networks/multiversx'; +import PolkadotNetworks from '@/providers/polkadot/networks'; +import Polkadot from '@/providers/polkadot/networks/polkadot'; +import SolanaNetworks from '@/providers/solana/networks'; +import Solana from '@/providers/solana/networks/solana'; +import { BaseNetwork } from '@/types/base-network'; +import { ProviderName } from '@/types/provider'; +import { NetworkNames } from '@enkryptcom/types'; +import CustomNetworksState from '../custom-networks-state'; const providerNetworks: Record> = { [ProviderName.ethereum]: EthereumNetworks, @@ -22,6 +24,7 @@ const providerNetworks: Record> = { [ProviderName.bitcoin]: BitcoinNetworks, [ProviderName.kadena]: KadenaNetworks, [ProviderName.solana]: SolanaNetworks, + [ProviderName.multiversx]: MultiversXNetworks, [ProviderName.massa]: MassaNetworks, [ProviderName.enkrypt]: {}, }; @@ -38,6 +41,7 @@ const getAllNetworks = async ( .concat(Object.values(BitcoinNetworks) as BaseNetwork[]) .concat(Object.values(KadenaNetworks) as BaseNetwork[]) .concat(Object.values(SolanaNetworks) as BaseNetwork[]) + .concat(Object.values(MultiversXNetworks) as BaseNetwork[]) .concat(Object.values(MassaNetworks) as BaseNetwork[]); if (!includeCustom) { @@ -72,6 +76,7 @@ const DEFAULT_SUBSTRATE_NETWORK_NAME = NetworkNames.Polkadot; const DEFAULT_BTC_NETWORK_NAME = NetworkNames.Bitcoin; const DEFAULT_KADENA_NETWORK_NAME = NetworkNames.Kadena; const DEFAULT_SOLANA_NETWORK_NAME = NetworkNames.Solana; +const DEFAULT_MULTIVERSX_NETWORK_NAME = NetworkNames.MultiversX; const DEFAULT_MASSA_NETWORK_NAME = NetworkNames.Massa; const DEFAULT_EVM_NETWORK = Ethereum; @@ -79,6 +84,7 @@ const DEFAULT_SUBSTRATE_NETWORK = Polkadot; const DEFAULT_BTC_NETWORK = Bitcoin; const DEFAULT_KADENA_NETWORK = Kadena; const DEFAULT_SOLANA_NETWORK = Solana; +const DEFAULT_MULTIVERSX_NETWORK = MultiversX; const DEFAULT_MASSA_NETWORK = Massa; const POPULAR_NAMES = [ @@ -92,22 +98,25 @@ const POPULAR_NAMES = [ NetworkNames.Rootstock, NetworkNames.Optimism, NetworkNames.Arbitrum, + NetworkNames.MultiversX, ]; export { - getAllNetworks, - getNetworkByName, - getProviderNetworkByName, - DEFAULT_EVM_NETWORK_NAME, - DEFAULT_SUBSTRATE_NETWORK_NAME, + DEFAULT_BTC_NETWORK, DEFAULT_BTC_NETWORK_NAME, - POPULAR_NAMES, DEFAULT_EVM_NETWORK, - DEFAULT_SUBSTRATE_NETWORK, - DEFAULT_BTC_NETWORK, + DEFAULT_EVM_NETWORK_NAME, DEFAULT_KADENA_NETWORK, DEFAULT_KADENA_NETWORK_NAME, - DEFAULT_SOLANA_NETWORK, - DEFAULT_SOLANA_NETWORK_NAME, DEFAULT_MASSA_NETWORK, DEFAULT_MASSA_NETWORK_NAME, + DEFAULT_MULTIVERSX_NETWORK, + DEFAULT_MULTIVERSX_NETWORK_NAME, + DEFAULT_SOLANA_NETWORK, + DEFAULT_SOLANA_NETWORK_NAME, + DEFAULT_SUBSTRATE_NETWORK, + DEFAULT_SUBSTRATE_NETWORK_NAME, + getAllNetworks, + getNetworkByName, + getProviderNetworkByName, + POPULAR_NAMES, }; diff --git a/packages/extension/src/providers/index.ts b/packages/extension/src/providers/index.ts index 90498059e..f5cef944c 100644 --- a/packages/extension/src/providers/index.ts +++ b/packages/extension/src/providers/index.ts @@ -1,9 +1,10 @@ -import EthereumProvider from '@/providers/ethereum'; -import PolkadotProvider from '@/providers/polkadot'; import BitcoinProvider from '@/providers/bitcoin'; +import EthereumProvider from '@/providers/ethereum'; import KadenaProvider from '@/providers/kadena'; -import SolanaProvider from '@/providers/solana'; import MassaProvider from '@/providers/massa'; +import MultiversXProvider from '@/providers/multiversx'; +import PolkadotProvider from '@/providers/polkadot'; +import SolanaProvider from '@/providers/solana'; import { ProviderName } from '@/types/provider'; export default { @@ -12,5 +13,6 @@ export default { [ProviderName.bitcoin]: BitcoinProvider, [ProviderName.kadena]: KadenaProvider, [ProviderName.solana]: SolanaProvider, + [ProviderName.multiversx]: MultiversXProvider, [ProviderName.massa]: MassaProvider, }; diff --git a/packages/extension/src/providers/multiversx/index.ts b/packages/extension/src/providers/multiversx/index.ts new file mode 100644 index 000000000..4a0f4e2c6 --- /dev/null +++ b/packages/extension/src/providers/multiversx/index.ts @@ -0,0 +1,83 @@ +import PublicKeyRing from '@/libs/keyring/public-keyring'; + +import getUiPath from '@/libs/utils/get-ui-path'; +import { + BackgroundProviderInterface, + ProviderName, + ProviderRPCRequest, +} from '@/types/provider'; +import getRequestProvider from '@enkryptcom/request'; +import { MiddlewareFunction, OnMessageResponse } from '@enkryptcom/types'; +import EventEmitter from 'events'; +import Middlewares from './methods'; +import Networks from './networks/index'; +import { MultiversXNetwork } from './types/mvx-network'; +import UIRoutes from './ui/routes/names'; + +class MultiversXProvider + extends EventEmitter + implements BackgroundProviderInterface +{ + override listeners(event: string | symbol): ((...args: any[]) => void)[] { + // Cast each Function to the expected type + return super.listeners(event) as ((...args: any[]) => void)[]; + } + + UIRoutes = UIRoutes; + toWindow: (message: string) => void; + network: MultiversXNetwork; + requestProvider: any; + middlewares: MiddlewareFunction[] = []; + namespace: ProviderName; + KeyRing: PublicKeyRing; + + constructor( + toWindow: (message: string) => void, + network: MultiversXNetwork = Networks.multiversx, + ) { + super(); + this.network = network; + this.toWindow = toWindow; + this.setMiddleWares(); + this.requestProvider = getRequestProvider('', this.middlewares); + this.requestProvider.on('notification', (notif: any) => { + this.sendNotification(JSON.stringify(notif)); + }); + this.namespace = ProviderName.multiversx; + this.KeyRing = new PublicKeyRing(); + } + + private setMiddleWares(): void { + this.middlewares = Middlewares.map(mw => mw.bind(this)); + } + + setRequestProvider(network: MultiversXNetwork): void { + this.network = network; + this.requestProvider.changeNetwork(network.node); + } + + request(request: ProviderRPCRequest): Promise { + return this.requestProvider + .request(request) + .then((res: any) => ({ + result: JSON.stringify(res), + })) + .catch((e: { message: any }) => ({ + error: JSON.stringify(e.message), + })); + } + + async sendNotification(notif: string): Promise { + return this.toWindow(notif); + } + + async isPersistentEvent(): Promise { + return false; + } + + getUIPath(page: string): string { + return getUiPath(page, this.namespace); + } +} + +export default MultiversXProvider; diff --git a/packages/extension/src/providers/multiversx/inject.ts b/packages/extension/src/providers/multiversx/inject.ts new file mode 100644 index 000000000..1dfe4da52 --- /dev/null +++ b/packages/extension/src/providers/multiversx/inject.ts @@ -0,0 +1,57 @@ +import { EthereumRequest, EthereumResponse } from '@/providers/ethereum/types'; +import { EnkryptWindow } from '@/types/globals'; +import { + ProviderInterface, + ProviderName, + ProviderOptions, + ProviderType, + SendMessageHandler, +} from '@/types/provider'; +import EventEmitter from 'eventemitter3'; + +import { handleIncomingMessage } from './libs/message-handler'; +import { MultiversXNetworks } from './types'; + +export class Provider extends EventEmitter implements ProviderInterface { + connected: boolean; + name: ProviderName; + type: ProviderType; + version = __VERSION__; + networks: typeof MultiversXNetworks; + sendMessageHandler: SendMessageHandler; + + constructor(options: ProviderOptions) { + super(); + this.connected = true; + this.name = options.name; + this.type = options.type; + this.networks = MultiversXNetworks; + this.sendMessageHandler = options.sendMessageHandler; + } + + async request(request: EthereumRequest): Promise { + const res = (await this.sendMessageHandler( + this.name, + JSON.stringify(request), + )) as EthereumResponse; + return res; + } + + isConnected(): boolean { + return this.connected; + } + + handleMessage(msg: string): void { + handleIncomingMessage(this, msg); + } +} + +const injectDocument = ( + document: EnkryptWindow | Window, + options: ProviderOptions, +): void => { + const provider = new Provider(options); + document['enkrypt']['providers'][options.name] = provider; +}; + +export default injectDocument; diff --git a/packages/extension/src/providers/multiversx/libs/account-state/index.ts b/packages/extension/src/providers/multiversx/libs/account-state/index.ts new file mode 100644 index 000000000..0c0f3f2dd --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/account-state/index.ts @@ -0,0 +1,69 @@ +import BrowserStorage from '@/libs/common/browser-storage'; +import { InternalStorageNamespace } from '@/types/provider'; +import { IState, StorageKeys } from './types'; + +class AccountState { + #storage: BrowserStorage; + constructor() { + this.#storage = new BrowserStorage( + InternalStorageNamespace.multiversxAccountsState, + ); + } + + async addApprovedDomain(domain: string): Promise { + const state = await this.getStateByDomain(domain); + state.isApproved = true; + await this.setState(state, domain); + } + + async removeApprovedDomain(domain: string): Promise { + const state = await this.getStateByDomain(domain); + state.isApproved = false; + await this.setState(state, domain); + } + + async isApproved(domain: string): Promise { + const state = await this.getStateByDomain(domain); + return state.isApproved; + } + + async deleteState(domain: string): Promise { + const allStates = await this.getAllStates(); + if (allStates[domain]) { + delete allStates[domain]; + await this.#storage.set(StorageKeys.accountsState, allStates); + } + } + + async isConnected(domain: string): Promise { + return this.getStateByDomain(domain).then(res => res.isApproved); + } + + async deleteAllStates(): Promise { + return await this.#storage.remove(StorageKeys.accountsState); + } + + async setState(state: IState, domain: string): Promise { + const allStates = await this.getAllStates(); + allStates[domain] = state; + await this.#storage.set(StorageKeys.accountsState, allStates); + } + + async getStateByDomain(domain: string): Promise { + const allStates: Record = await this.getAllStates(); + if (!allStates[domain]) + return { + isApproved: false, + }; + else return allStates[domain]; + } + + async getAllStates(): Promise> { + const allStates: Record = await this.#storage.get( + StorageKeys.accountsState, + ); + if (!allStates) return {}; + return allStates; + } +} +export default AccountState; diff --git a/packages/extension/src/providers/multiversx/libs/account-state/types.ts b/packages/extension/src/providers/multiversx/libs/account-state/types.ts new file mode 100644 index 000000000..60f696118 --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/account-state/types.ts @@ -0,0 +1,6 @@ +export enum StorageKeys { + accountsState = 'multiversx-accounts-state', +} +export interface IState { + isApproved: boolean; +} diff --git a/packages/extension/src/providers/multiversx/libs/activity-handlers/index.ts b/packages/extension/src/providers/multiversx/libs/activity-handlers/index.ts new file mode 100644 index 000000000..0ec4ecf19 --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/activity-handlers/index.ts @@ -0,0 +1,3 @@ +import { multiversxScanActivity, nftHandler } from './multiversx'; + +export { multiversxScanActivity, nftHandler }; diff --git a/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/configs.ts b/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/configs.ts new file mode 100644 index 000000000..c0e2ae5cd --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/configs.ts @@ -0,0 +1,11 @@ +import { NetworkNames } from '@enkryptcom/types'; + +const NetworkEndpoint = { + [NetworkNames.MultiversX]: 'https://api.multiversx.com', +}; + +const NetworkTtl = { + [NetworkNames.MultiversX]: 0, +}; + +export { NetworkEndpoint, NetworkTtl }; diff --git a/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/index.ts b/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/index.ts new file mode 100644 index 000000000..d240e9362 --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/activity-handlers/multiversx/index.ts @@ -0,0 +1,241 @@ +import cacheFetch from '@/libs/cache-fetch'; +import MarketData from '@/libs/market-data'; +import { Activity, ActivityStatus, ActivityType } from '@/types/activity'; +import { BaseNetwork } from '@/types/base-network'; +import { NFTCollection, NFTItem, NFTType } from '@/types/nft'; +import { TokenComputer, TransactionStatus } from '@multiversx/sdk-core'; +import { TransactionDecoder } from '@multiversx/sdk-transaction-decoder/lib/src/transaction.decoder'; +import { NetworkEndpoint, NetworkTtl } from './configs'; + +const getAddressActivity = async ( + address: string, + endpoint: string, + ttl: number, +): Promise => { + const url = `${endpoint}/accounts/${address}/transactions?size=50`; + return cacheFetch({ url }, ttl) + .then(res => { + return res ? res : []; + }) + .catch(error => { + console.error('Failed to fetch activity:', error); + return []; + }); +}; + +export const multiversxScanActivity = async ( + network: BaseNetwork, + address: string, +): Promise => { + const networkName = network.name as keyof typeof NetworkEndpoint; + const enpoint = NetworkEndpoint[networkName]; + const ttl = NetworkTtl[networkName]; + const activities = await getAddressActivity(address, enpoint, ttl); + const tokenComputer = new TokenComputer(); + const decoder = new TransactionDecoder(); + + let egldPrice = '0'; + + if (network.coingeckoID) { + const marketData = new MarketData(); + await marketData + .getTokenPrice(network.coingeckoID) + .then(mdata => (egldPrice = mdata || '0')); + } + + const allActivities: Activity[] = []; + + await Promise.all( + activities.map(async (activity: any) => { + let status = ActivityStatus.success; + const txStatus = new TransactionStatus(activity.status); + + if (txStatus.isPending()) { + status = ActivityStatus.pending; + } else if (txStatus.isFailed()) { + status = ActivityStatus.failed; + } + + const metadata = decoder.getTransactionMetadata({ + sender: activity.sender, + receiver: activity.receiver, + data: activity.data, + value: activity.value, + }); + + if (metadata.transfers?.length) { + for (let i = 0; i < metadata.transfers.length; i++) { + const tokenIdentifier = + metadata.transfers[i].properties!.identifier || + metadata.transfers[i].properties!.token || + ''; + + const tokenNonce = + tokenComputer.extractNonceFromExtendedIdentifier(tokenIdentifier); + let tokenDetails: any; + let icon: string = ''; + let price: string = '0'; + + if (tokenNonce === 0) { + tokenDetails = await getFungibleTokenDetails( + enpoint, + tokenIdentifier, + ttl, + ); + + icon = tokenDetails.assets?.pngUrl; + price = tokenDetails.price?.toString(); + } else { + tokenDetails = await getNFTTokenDetails( + enpoint, + tokenIdentifier, + ttl, + ); + + icon = tokenDetails.media?.[0]?.thumbnailUrl || ''; + } + + const name = tokenDetails.name; + const symbol = tokenDetails.identifier; + allActivities.push({ + nonce: (activity.nonce || 0).toString(), + from: metadata.sender, + to: metadata.receiver, + isIncoming: metadata.sender !== address, + network: network.name, + rawInfo: activity, + status, + timestamp: activity.timestampMs, + value: metadata.transfers[i].value + ? metadata.transfers[i].value.toString() + : '', + transactionHash: activity.txHash, + type: ActivityType.transaction, + token: { + decimals: tokenDetails.decimals || 0, + icon: icon, + name: name, + symbol: symbol, + price: price, + }, + }); + } + } else { + allActivities.push({ + nonce: (activity.nonce || 0).toString(), + from: metadata.sender, + to: metadata.receiver, + isIncoming: metadata.sender !== address, + network: network.name, + rawInfo: activity, + status, + timestamp: activity.timestampMs, + value: metadata.value ? metadata.value.toString() : '', + transactionHash: activity.txHash, + type: ActivityType.transaction, + token: { + decimals: network.decimals, + icon: network.icon, + name: network.currencyNameLong, + symbol: network.currencyName, + price: egldPrice, + }, + }); + } + }), + ); + + allActivities.sort((a, b) => b.timestamp - a.timestamp); + return allActivities; +}; + +const getFungibleTokenDetails = async ( + endpoint: string, + token: string, + ttl: number, +): Promise => { + const url = `${endpoint}/tokens/${token}`; + return cacheFetch({ url }, ttl) + .then(res => { + return res ? res : []; + }) + .catch(error => { + console.error('Failed to fetch fungible tokens:', error); + return []; + }); +}; + +const getNFTTokenDetails = async ( + endpoint: string, + token: string, + ttl: number, +): Promise => { + const url = `${endpoint}/nfts/${token}`; + return cacheFetch({ url }, ttl) + .then(res => { + return res ? res : []; + }) + .catch(error => { + console.error('Failed to fetch NFT:', error); + return []; + }); +}; + +const getAddressNfts = async ( + address: string, + endpoint: string, + ttl: number, +): Promise => { + const url = `${endpoint}/accounts/${address}/nfts?excludeMetaESDT=true&size=10000`; + return cacheFetch({ url }, ttl) + .then(res => { + return res ? res : []; + }) + .catch(error => { + console.error('Failed to fetch NFTs:', error); + return []; + }); +}; + +export const nftHandler = async ( + network: BaseNetwork, + address: string, +): Promise => { + const networkName = network.name as keyof typeof NetworkEndpoint; + const enpoint = NetworkEndpoint[networkName]; + const ttl = NetworkTtl[networkName]; + + const nfts = await getAddressNfts(address, enpoint, ttl); + + // Group NFTs by collection + const collectionsMap: Record = {}; + + nfts.forEach(nft => { + const item: NFTItem = { + name: nft.name, + id: nft.identifier, + contract: '', + image: nft.media?.[0]?.url ?? '', + type: NFTType.MultiversXNFT, + url: nft.url, + }; + const collectionId = nft.collection; + if (!collectionsMap[collectionId]) { + collectionsMap[collectionId] = []; + } + collectionsMap[collectionId].push(item); + }); + + // Create NFTCollection objects + const nftCollections: NFTCollection[] = Object.entries(collectionsMap).map( + ([collectionId, items]) => ({ + name: collectionId, + items, + image: items[0]?.image ?? '', + description: '', + contract: '', + }), + ); + + return nftCollections; +}; diff --git a/packages/extension/src/providers/multiversx/libs/api.ts b/packages/extension/src/providers/multiversx/libs/api.ts new file mode 100644 index 000000000..ee3c62502 --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/api.ts @@ -0,0 +1,76 @@ +import { MultiversXRawInfo } from '@/types/activity'; +import { ProviderAPIInterface } from '@/types/provider'; +import { Address, ApiNetworkProvider, Transaction } from '@multiversx/sdk-core'; +import BigNumber from 'bignumber.js'; +import { numberToHex } from 'web3-utils'; + +/** MultiversX API wrapper */ +class API implements ProviderAPIInterface { + node: string; + networkProvider: ApiNetworkProvider; + + constructor(node: string) { + this.node = node; + this.networkProvider = new ApiNetworkProvider(node); + } + + async init(): Promise {} + + public get api() { + return this; + } + + async getBalance(pubkey: string): Promise { + const balance = (await this.networkProvider.getAccount(new Address(pubkey))) + .balance; + return numberToHex(new BigNumber(balance).toFixed()); + } + + async getChainId(): Promise { + return (await this.networkProvider.getNetworkConfig()).chainID; + } + + async getAccountNonce(address: Address): Promise { + const account = await this.networkProvider.getAccount(address); + return account.nonce; + } + + async sendTransaction(transaction: Transaction): Promise { + return await this.networkProvider.sendTransaction(transaction); + } + + async getTransactionStatus(hash: string): Promise { + let rawTx: Record; + + try { + rawTx = (await this.networkProvider.getTransaction(hash)).raw; + } catch { + // Transaction can't be fetched + return null; + } + + if (!rawTx) { + return null; + } + + const returnValue: MultiversXRawInfo = { + transactionHash: rawTx.hash, + timestamp: rawTx.timestampMs, + gasLimit: rawTx.gasLimit, + gasPrice: rawTx.gasPrice, + from: rawTx.sender, + to: rawTx.receiver, + value: rawTx.value, + nonce: rawTx.nonce, + data: rawTx.data, + status: rawTx.status, + }; + return returnValue; + } + + async broadcastTx(_rawtx: string): Promise { + throw new Error('Method not implemented.'); + } +} + +export default API; diff --git a/packages/extension/src/providers/multiversx/libs/message-handler.ts b/packages/extension/src/providers/multiversx/libs/message-handler.ts new file mode 100644 index 000000000..4d7f301f1 --- /dev/null +++ b/packages/extension/src/providers/multiversx/libs/message-handler.ts @@ -0,0 +1,29 @@ +import { + EmitEvent, + MessageMethod, + ProviderMessage, +} from '@/providers/multiversx/types'; + +import { handleIncomingMessage as handleIncomingMessageType } from '@/types/provider'; +import MultiversXProvider from '..'; + +const handleIncomingMessage: handleIncomingMessageType = ( + provider, + message, +): void => { + try { + const _provider = provider as unknown as MultiversXProvider; + const jsonMsg = JSON.parse(message) as ProviderMessage; + + if (jsonMsg.method === MessageMethod.changeAddress) { + const address = jsonMsg.params[0] as string; + _provider.emit(EmitEvent.accountsChanged, [address]); + } else { + console.error(`Unable to process message: ${message}`); + } + } catch (e) { + console.error(e); + } +}; + +export { handleIncomingMessage }; diff --git a/packages/extension/src/providers/multiversx/methods/index.ts b/packages/extension/src/providers/multiversx/methods/index.ts new file mode 100644 index 000000000..95ea1673c --- /dev/null +++ b/packages/extension/src/providers/multiversx/methods/index.ts @@ -0,0 +1,3 @@ +import mvxSignTransaction from './mvx_signTransaction'; + +export default [mvxSignTransaction]; diff --git a/packages/extension/src/providers/multiversx/methods/mvx_signTransaction.ts b/packages/extension/src/providers/multiversx/methods/mvx_signTransaction.ts new file mode 100644 index 000000000..d64db93e7 --- /dev/null +++ b/packages/extension/src/providers/multiversx/methods/mvx_signTransaction.ts @@ -0,0 +1,42 @@ +import { getCustomError } from '@/libs/error'; +import { WindowPromise } from '@/libs/window-promise'; +import { ProviderRPCRequest } from '@/types/provider'; +import { MiddlewareFunction } from '@enkryptcom/types'; +import MultiversXProvider from '..'; +const method: MiddlewareFunction = function ( + this: MultiversXProvider, + payload: ProviderRPCRequest, + res, + next, +): void { + if (payload.method !== 'mvx_signTransaction') return next(); + else { + if (!payload.params?.length) { + return res(getCustomError('Missing Params: mvx_signTransaction')); + } + + const reqPayload = payload.params[0]; + + this.KeyRing.getAccount(reqPayload.address) + .then(account => { + const windowPromise = new WindowPromise(); + + windowPromise + .getResponse( + this.getUIPath(this.UIRoutes.mvxSignMessage.path), + JSON.stringify({ + ...payload, + params: [reqPayload, account], + }), + true, + ) + .then(({ error, result }) => { + if (error) return res(error); + res(null, result as string); + }); + }) + .catch(res); + } +}; + +export default method; diff --git a/packages/extension/src/providers/multiversx/networks/icons/multiversx.webp b/packages/extension/src/providers/multiversx/networks/icons/multiversx.webp new file mode 100644 index 000000000..471fd6900 Binary files /dev/null and b/packages/extension/src/providers/multiversx/networks/icons/multiversx.webp differ diff --git a/packages/extension/src/providers/multiversx/networks/index.ts b/packages/extension/src/providers/multiversx/networks/index.ts new file mode 100644 index 000000000..093aaaea8 --- /dev/null +++ b/packages/extension/src/providers/multiversx/networks/index.ts @@ -0,0 +1,3 @@ +import multiversx from './multiversx'; + +export default { multiversx }; diff --git a/packages/extension/src/providers/multiversx/networks/multiversx.ts b/packages/extension/src/providers/multiversx/networks/multiversx.ts new file mode 100644 index 000000000..4825b8539 --- /dev/null +++ b/packages/extension/src/providers/multiversx/networks/multiversx.ts @@ -0,0 +1,34 @@ +import wrapActivityHandler from '@/libs/activity-state/wrap-activity-handler'; +import { CoingeckoPlatform, NetworkNames } from '@enkryptcom/types'; +import { multiversxScanActivity, nftHandler } from '../libs/activity-handlers'; +import { + MultiversXNetwork, + MultiversXNetworkOptions, + isValidAddress, +} from '../types/mvx-network'; +import icon from './icons/multiversx.webp'; + +const multiversxOptions: MultiversXNetworkOptions = { + name: NetworkNames.MultiversX, + name_long: 'MultiversX', + homePage: 'https://multiversx.com/', + blockExplorerTX: 'https://explorer.multiversx.com/transactions/[[txHash]]', + blockExplorerAddr: 'https://explorer.multiversx.com/accounts/[[address]]', + isTestNetwork: false, + currencyName: 'EGLD', + currencyNameLong: 'EGLD', + icon, + decimals: 18, + node: 'https://api.multiversx.com', + coingeckoID: 'elrond-erd-2', + coingeckoPlatform: CoingeckoPlatform.MultiversX, + activityHandler: wrapActivityHandler(multiversxScanActivity), + basePath: "m/44'/508'", + isAddress: isValidAddress, + buyLink: 'https://buy.multiversx.com/', + NFTHandler: nftHandler, +}; + +const multiversx = new MultiversXNetwork(multiversxOptions); + +export default multiversx; diff --git a/packages/extension/src/providers/multiversx/types/index.ts b/packages/extension/src/providers/multiversx/types/index.ts new file mode 100644 index 000000000..c1d56aaca --- /dev/null +++ b/packages/extension/src/providers/multiversx/types/index.ts @@ -0,0 +1,21 @@ +import { NetworkNames } from '@enkryptcom/types'; +import type { Provider as InjectedProvider } from '../inject'; + +export const MultiversXNetworks = { + MVX: NetworkNames.MultiversX, +}; + +export interface ProviderMessage { + method: MessageMethod; + params: Array; +} + +export enum MessageMethod { + changeAddress = 'changeAddress', +} + +export enum EmitEvent { + accountsChanged = 'accountsChanged', +} + +export { InjectedProvider }; diff --git a/packages/extension/src/providers/multiversx/types/mvx-network.ts b/packages/extension/src/providers/multiversx/types/mvx-network.ts new file mode 100644 index 000000000..011c9df1e --- /dev/null +++ b/packages/extension/src/providers/multiversx/types/mvx-network.ts @@ -0,0 +1,262 @@ +import MarketData from '@/libs/market-data'; +import { CoinGeckoTokenMarket } from '@/libs/market-data/types'; +import Sparkline from '@/libs/sparkline'; +import { formatFloatingPointValue } from '@/libs/utils/number-formatter'; +import createIcon from '@/providers/ethereum/libs/blockies'; +import { Activity } from '@/types/activity'; +import { BaseNetwork, BaseNetworkOptions } from '@/types/base-network'; +import { BaseToken } from '@/types/base-token'; +import { NFTCollection } from '@/types/nft'; +import { AssetsType, ProviderName } from '@/types/provider'; +import { CoingeckoPlatform, NetworkNames, SignerType } from '@enkryptcom/types'; +import { fromBase, numberToHex } from '@enkryptcom/utils'; +import { Address, ApiNetworkProvider } from '@multiversx/sdk-core'; +import BigNumber from 'bignumber.js'; +import API from '../libs/api'; +import { MVXToken, MvxTokenOptions } from './mvx-token'; + +export interface MultiversXNetworkOptions { + name: NetworkNames; + name_long: string; + homePage: string; + blockExplorerTX: string; + blockExplorerAddr: string; + isTestNetwork: boolean; + currencyName: string; + currencyNameLong: string; + icon: string; + decimals: number; + node: string; + coingeckoID?: string; + coingeckoPlatform: CoingeckoPlatform; + basePath: string; + buyLink: string | undefined; + activityHandler: ( + network: BaseNetwork, + address: string, + ) => Promise; + isAddress: (address: string) => boolean; + NFTHandler?: ( + network: BaseNetwork, + address: string, + ) => Promise; + assetsInfoHandler?: ( + network: BaseNetwork, + address: string, + ) => Promise; +} + +export const isValidAddress = (address: string) => { + try { + Address.newFromBech32(address); + return true; + } catch { + return false; + } +}; + +export const getAddress = (pubkey: string) => { + return new Address(pubkey).toBech32(); +}; + +export class MultiversXNetwork extends BaseNetwork { + public options: MultiversXNetworkOptions; + + private activityHandler: ( + network: BaseNetwork, + address: string, + ) => Promise; + + assetsInfoHandler?: ( + network: BaseNetwork, + address: string, + ) => Promise; + + NFTHandler?: ( + network: BaseNetwork, + address: string, + ) => Promise; + + isAddress: (address: string) => boolean; + + constructor(options: MultiversXNetworkOptions) { + const api = async () => { + const api = new API(options.node); + await api.init(); + return api as API; + }; + + const baseOptions: BaseNetworkOptions = { + identicon: createIcon, + signer: [SignerType.ed25519mvx], + provider: ProviderName.multiversx, + displayAddress: getAddress, + api, + ...options, + }; + super(baseOptions); + this.options = options; + this.activityHandler = options.activityHandler; + this.isAddress = options.isAddress; + this.assetsInfoHandler = options.assetsInfoHandler; + this.NFTHandler = options.NFTHandler; + } + + public async getAllTokens(pubkey: string): Promise { + const native = await this.getNativeTokenInfo(pubkey); + const assets = [ + new MVXToken({ + decimals: native.decimals, + icon: native.icon, + name: native.name, + symbol: native.symbol, + balance: native.balance, + price: native.value, + coingeckoID: this.coingeckoID, + type: 'native', + }), + ]; + + const fungibleTokens = await this.getFungibleTokens(pubkey); + fungibleTokens.map(token => { + const bTokenOptions: MvxTokenOptions = { + decimals: token.decimals, + icon: token.assets.pngUrl ? token.assets.pngUrl : '', + name: token.name, + symbol: token.identifier, + balance: token.balance, + price: new BigNumber(token.price).toString(), + coingeckoID: '', + type: token.type, + }; + assets.push(new MVXToken(bTokenOptions)); + }); + + const nonFungibleTokens = await this.getNonFungibleTokens(pubkey); + nonFungibleTokens.map(token => { + const icon = token.media?.[0]?.thumbnailUrl + ? token.media[0].thumbnailUrl + : ''; + const bTokenOptions: MvxTokenOptions = { + decimals: token?.decimals ?? 0, + icon: icon, + name: token.name, + symbol: token.identifier, + balance: token.balance, + coingeckoID: '', + type: token.type, + }; + assets.push(new MVXToken(bTokenOptions)); + }); + + return assets; + } + + public async getAllTokenInfo(pubkey: string): Promise { + const assets: AssetsType[] = []; + assets.push(await this.getNativeTokenInfo(pubkey)); + + const fungibleTokens = await this.getFungibleTokens(pubkey); + fungibleTokens.map(token => { + const balance = numberToHex(new BigNumber(token.balance).toFixed()); + const userBalance = fromBase(balance, token.decimals); + + const asset: AssetsType = { + balance: balance, + balancef: formatFloatingPointValue(userBalance).value, + balanceUSD: token.valueUsd.toNumber(), + balanceUSDf: token.valueUsd.toString(), + icon: token.assets.pngUrl ? token.assets.pngUrl : '', + name: token.name, + symbol: token.identifier, + value: token.price.toString(), + valuef: token.price.toString(), + decimals: token.decimals, + sparkline: new Sparkline([], 25).dataValues, + priceChangePercentage: 0, + }; + assets.push(asset); + }); + + const nonFungibleTokens = await this.getNonFungibleTokens(pubkey); + nonFungibleTokens.map(token => { + const icon = token.media?.[0]?.thumbnailUrl + ? token.media[0].thumbnailUrl + : ''; + const asset: AssetsType = { + balance: token.balance, + balancef: formatFloatingPointValue(token.balance).value, + balanceUSD: 0, + balanceUSDf: '', + icon: icon, + name: token.name, + symbol: token.identifier, + value: '', + valuef: '', + decimals: token?.decimals ?? 0, + sparkline: '', + priceChangePercentage: 0, + }; + assets.push(asset); + }); + + return assets; + } + + private async getNativeTokenInfo(pubkey: string): Promise { + const balance = await (await this.api()).getBalance(pubkey); + + let marketData: (CoinGeckoTokenMarket | null)[] = []; + if (this.coingeckoID) { + const market = new MarketData(); + marketData = await market.getMarketData([this.coingeckoID]); + } + + const currentPrice = marketData.length + ? marketData[0]!.current_price || 0 + : 0; + const userBalance = fromBase(balance, this.decimals); + const usdBalance = new BigNumber(userBalance).times(currentPrice); + const nativeAsset: AssetsType = { + balance: balance, + balancef: formatFloatingPointValue(userBalance).value, + balanceUSD: usdBalance.toNumber(), + balanceUSDf: usdBalance.toString(), + icon: this.icon, + name: this.name_long, + symbol: this.currencyName, + value: marketData.length ? currentPrice.toString() : '0', + valuef: marketData.length ? currentPrice.toString() : '0', + decimals: this.decimals, + sparkline: marketData.length + ? new Sparkline(marketData[0]!.sparkline_in_24h.price, 25).dataValues + : '', + priceChangePercentage: marketData.length + ? marketData[0]!.price_change_percentage_24h_in_currency + : 0, + }; + return nativeAsset; + } + + private async getFungibleTokens(pubkey: string): Promise { + const address = Address.newFromBech32(pubkey); + const api = new ApiNetworkProvider(this.options.node); + + return await api.doGetGeneric( + `accounts/${address.toBech32()}/tokens?from=0&size=10000`, + ); + } + + private async getNonFungibleTokens(pubkey: string): Promise { + const address = Address.newFromBech32(pubkey); + const api = new ApiNetworkProvider(this.options.node); + + return await api.doGetGeneric( + `accounts/${address.toBech32()}/nfts?from=0&size=10000`, + ); + } + + public getAllActivity(address: string): Promise { + return this.activityHandler(this, address); + } +} diff --git a/packages/extension/src/providers/multiversx/types/mvx-token.ts b/packages/extension/src/providers/multiversx/types/mvx-token.ts new file mode 100644 index 000000000..8c8981d6d --- /dev/null +++ b/packages/extension/src/providers/multiversx/types/mvx-token.ts @@ -0,0 +1,123 @@ +import MultiversXAPI from '@/providers/multiversx/libs/api'; +import { BaseToken, BaseTokenOptions } from '@/types/base-token'; +import { EnkryptAccount } from '@enkryptcom/types'; +import { bufferToHex } from '@enkryptcom/utils'; +import { + Address, + GasLimitEstimator, + Token, + TokenComputer, + TokenTransfer, + Transaction, + TransactionComputer, + TransactionsFactoryConfig, + TransferTransactionsFactory, +} from '@multiversx/sdk-core'; +import { TransactionSigner } from '../ui/libs/signer'; +import { MultiversXNetwork } from './mvx-network'; + +export interface MvxTokenOptions extends BaseTokenOptions { + type: string; +} + +export class MVXToken extends BaseToken { + public type: string; + + constructor(options: MvxTokenOptions) { + super(options); + this.type = options.type; + } + + public async getLatestUserBalance( + api: MultiversXAPI, + pubkey: string, + ): Promise { + return api.getBalance(pubkey); + } + + public async getBalance(api: MultiversXAPI, pubkey: string): Promise { + return this.getLatestUserBalance(api, pubkey); + } + + public async send(): Promise { + throw new Error('send is not implemented here'); + } + + public async buildTransaction( + to: string, + from: EnkryptAccount | any, + token: MVXToken, + amount: string, + network: MultiversXNetwork, + ): Promise { + to = network.displayAddress(to); + + const api = (await network.api()) as MultiversXAPI; + const chainID = await api.getChainId(); + + const factoryConfig = new TransactionsFactoryConfig({ chainID }); + const gasEstimator = new GasLimitEstimator({ + networkProvider: api.networkProvider, + gasMultiplier: 1.1, + }); + const transferFactory = new TransferTransactionsFactory({ + config: factoryConfig, + gasLimitEstimator: gasEstimator, + }); + + const sender = Address.newFromBech32(from.address); + const receiver = Address.newFromBech32(to); + + let transaction: Transaction; + + if (token.type === 'native') { + transaction = await transferFactory.createTransactionForTransfer(sender, { + receiver, + nativeAmount: BigInt(amount), + }); + } else { + const tokenComputer = new TokenComputer(); + const identifier = tokenComputer.extractIdentifierFromExtendedIdentifier( + token.symbol, + ); + const nonce = tokenComputer.extractNonceFromExtendedIdentifier( + token.symbol, + ); + + const tokenObj = new Token({ + identifier: identifier, + nonce: BigInt(nonce.toString()), + }); + + const transfer = new TokenTransfer({ + token: tokenObj, + amount: BigInt(amount), + }); + + transaction = await transferFactory.createTransactionForESDTTokenTransfer( + sender, + { + receiver, + tokenTransfers: [transfer], + }, + ); + } + + transaction.nonce = await api.getAccountNonce(sender); + + const txComputer = new TransactionComputer(); + txComputer.applyOptionsForHashSigning(transaction); + + const signature = await TransactionSigner({ + account: from, + network: network, + payload: bufferToHex(txComputer.computeHashForSigning(transaction)), + }).then(res => { + if (res.error) return Promise.reject(res.error); + else return res.result?.replace('0x', '') as string; + }); + + transaction.signature = Buffer.from(signature, 'hex'); + return transaction; + } +} diff --git a/packages/extension/src/providers/multiversx/ui/libs/signer.ts b/packages/extension/src/providers/multiversx/ui/libs/signer.ts new file mode 100644 index 000000000..72a29bb6a --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/libs/signer.ts @@ -0,0 +1,27 @@ +import { getCustomError } from '@/libs/error'; +import sendUsingInternalMessengers from '@/libs/messenger/internal-messenger'; +import { InternalMethods, InternalOnMessageResponse } from '@/types/messenger'; +import { SignerTransactionOptions } from '../types'; + +const TransactionSigner = ( + options: SignerTransactionOptions, +): Promise => { + const { account, payload } = options; + if (account.isHardware) { + return new Promise((resolve, reject) => { + reject(getCustomError('NOT_IMPLEMENTED')); + }); + } else { + return sendUsingInternalMessengers({ + method: InternalMethods.sign, + params: [payload, account], + }).then(res => { + if (res.error) return res; + return { + result: JSON.parse(res.result as string), + }; + }); + } +}; + +export { TransactionSigner }; diff --git a/packages/extension/src/providers/multiversx/ui/mvx-accounts.vue b/packages/extension/src/providers/multiversx/ui/mvx-accounts.vue new file mode 100644 index 000000000..3eb4f1054 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/mvx-accounts.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/mvx-sign-message.vue b/packages/extension/src/providers/multiversx/ui/mvx-sign-message.vue new file mode 100644 index 000000000..75e637db0 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/mvx-sign-message.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/routes/index.ts b/packages/extension/src/providers/multiversx/ui/routes/index.ts new file mode 100644 index 000000000..8588f513a --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/routes/index.ts @@ -0,0 +1,16 @@ +import { RouteRecordRaw } from 'vue-router'; +import mvxAccounts from '../mvx-accounts.vue'; +import mvxSignMessage from '../mvx-sign-message.vue'; +import RouteNames from './names'; + +const routes = Object.assign({}, RouteNames); +routes.mvxAccounts.component = mvxAccounts; +routes.mvxSignMessage.component = mvxSignMessage; + +export default (namespace: string): RouteRecordRaw[] => { + return Object.values(routes).map(route => { + route.path = `/${namespace}/${route.path}`; + route.name = `${namespace}-${String(route.name)}`; + return route; + }); +}; diff --git a/packages/extension/src/providers/multiversx/ui/routes/names.ts b/packages/extension/src/providers/multiversx/ui/routes/names.ts new file mode 100644 index 000000000..60ce0efec --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/routes/names.ts @@ -0,0 +1,12 @@ +export default { + mvxSignMessage: { + path: 'mvx-sign', + name: 'mvxSign', + component: {}, + }, + mvxAccounts: { + path: 'mvx-send-transaction', + name: 'mvxSendTransaction', + component: {}, + }, +}; diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-address-input.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-address-input.vue new file mode 100644 index 000000000..d1c8924cb --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-address-input.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-alert.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-alert.vue new file mode 100644 index 000000000..ff3565591 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-alert.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-display.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-display.vue new file mode 100644 index 000000000..c1cd080d8 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-display.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-select.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-select.vue new file mode 100644 index 000000000..d8af5f46f --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-fee-select.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-item.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-item.vue new file mode 100644 index 000000000..176e3bd8f --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-item.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-select.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-select.vue new file mode 100644 index 000000000..574add6c6 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/components/send-token-select.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/index.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/index.vue new file mode 100644 index 000000000..681293d8b --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/index.vue @@ -0,0 +1,708 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/send-transaction/verify-transaction/index.vue b/packages/extension/src/providers/multiversx/ui/send-transaction/verify-transaction/index.vue new file mode 100644 index 000000000..cec53c804 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/send-transaction/verify-transaction/index.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/packages/extension/src/providers/multiversx/ui/types/index.ts b/packages/extension/src/providers/multiversx/ui/types/index.ts new file mode 100644 index 000000000..32a0024e7 --- /dev/null +++ b/packages/extension/src/providers/multiversx/ui/types/index.ts @@ -0,0 +1,33 @@ +import { BaseNetwork } from '@/types/base-network'; +import { ToTokenData } from '@/ui/action/types/token'; +import { EnkryptAccount } from '@enkryptcom/types'; + +export interface TxFeeInfo { + nativeValue: string; + fiatValue: string; + nativeSymbol: string; + fiatSymbol: string; +} + +export interface SendTransactionDataType { + from: string; + value: string; + to: string; +} + +export interface VerifyTransactionParams { + fromAddress: string; + fromAddressName: string; + chainId: string; + toAddress: string; + toToken: ToTokenData; + tokenType: string; + txFee: TxFeeInfo; + TransactionData: SendTransactionDataType; +} + +export interface SignerTransactionOptions { + payload: string; + network: BaseNetwork; + account: EnkryptAccount; +} diff --git a/packages/extension/src/scripts/inject.ts b/packages/extension/src/scripts/inject.ts index 5fed1666d..70f9725ea 100644 --- a/packages/extension/src/scripts/inject.ts +++ b/packages/extension/src/scripts/inject.ts @@ -1,14 +1,15 @@ import { + providerSendMessage, setWindowNamespace, windowOnMessage, - providerSendMessage, } from '@/libs/messenger/window'; -import { ProviderName, ProviderType } from '@/types/provider'; -import EthereumProvider from '@/providers/ethereum/inject'; -import PolkadotProvider from '@/providers/polkadot/inject'; import BitcoinProvider from '@/providers/bitcoin/inject'; +import EthereumProvider from '@/providers/ethereum/inject'; import KadenaProvider from '@/providers/kadena/inject'; +import MultiversXProvider from '@/providers/multiversx/inject'; +import PolkadotProvider from '@/providers/polkadot/inject'; import SolanaProvider from '@/providers/solana/inject'; +import { ProviderName, ProviderType } from '@/types/provider'; import { InternalMethods } from '@/types/messenger'; @@ -43,6 +44,11 @@ const loadInjectedProviders = () => { type: ProviderType.solana, sendMessageHandler: providerSendMessage, }); + MultiversXProvider(window, { + name: ProviderName.multiversx, + type: ProviderType.multiversx, + sendMessageHandler: providerSendMessage, + }); }; loadInjectedProviders(); diff --git a/packages/extension/src/types/activity.ts b/packages/extension/src/types/activity.ts index b13efcab1..67937c947 100644 --- a/packages/extension/src/types/activity.ts +++ b/packages/extension/src/types/activity.ts @@ -1,12 +1,12 @@ -import { NetworkNames } from '@enkryptcom/types'; -import { BaseTokenOptions } from './base-token'; import { + StatusOptionsResponse, TokenType, TokenTypeTo, - StatusOptionsResponse, } from '@enkryptcom/swap'; +import { NetworkNames } from '@enkryptcom/types'; import { ICommandResult } from '@kadena/client'; import { OperationStatus } from '@massalabs/massa-web3'; +import { BaseTokenOptions } from './base-token'; interface BTCIns { address: string; @@ -24,6 +24,19 @@ interface SOLRawInfo { status: boolean; } +interface MultiversXRawInfo { + transactionHash: string; + timestamp: number; + gasLimit: number; + gasPrice: number; + from: string; + to: string; + value: string; + nonce: number; + data: string; + status: string; +} + interface BTCRawInfo { blockNumber: number; transactionHash: string; @@ -132,20 +145,22 @@ interface Activity { | SwapRawInfo | KadenaRawInfo | SOLRawInfo + | MultiversXRawInfo | MassaRawInfo; } export { - EthereumRawInfo, - SubstrateRawInfo, Activity, ActivityStatus, ActivityType, - SubscanExtrinsicInfo, BTCRawInfo, - SwapRawInfo, - KadenaRawInfo, + EthereumRawInfo, KadenaDBInfo, - SOLRawInfo, + KadenaRawInfo, MassaRawInfo, + MultiversXRawInfo, + SOLRawInfo, + SubscanExtrinsicInfo, + SubstrateRawInfo, + SwapRawInfo, }; diff --git a/packages/extension/src/types/base-network.ts b/packages/extension/src/types/base-network.ts index 3ca1af7a2..8bed54c3e 100644 --- a/packages/extension/src/types/base-network.ts +++ b/packages/extension/src/types/base-network.ts @@ -1,14 +1,15 @@ -import EvmAPI from '@/providers/ethereum/libs/api'; -import SubstrateAPI from '@/providers/polkadot/libs/api'; import BitcoinAPI from '@/providers/bitcoin/libs/api'; +import { BNType } from '@/providers/common/types'; +import EvmAPI from '@/providers/ethereum/libs/api'; import KadenaAPI from '@/providers/kadena/libs/api'; +import MultiversXAPI from '@/providers/multiversx/libs/api'; +import SubstrateAPI from '@/providers/polkadot/libs/api'; import SolanaAPI from '@/providers/solana/libs/api'; import { AssetsType, ProviderName } from '@/types/provider'; -import { CoingeckoPlatform, SignerType, NetworkNames } from '@enkryptcom/types'; +import { CoingeckoPlatform, NetworkNames, SignerType } from '@enkryptcom/types'; +import MassaAPI from '../providers/massa/libs/api'; import { Activity } from './activity'; import { BaseToken } from './base-token'; -import { BNType } from '@/providers/common/types'; -import MassaAPI from '../providers/massa/libs/api'; export interface SubNetworkOptions { id: string; @@ -40,6 +41,7 @@ export interface BaseNetworkOptions { | Promise | Promise | Promise + | Promise | Promise; customTokens?: boolean; } @@ -83,6 +85,7 @@ export abstract class BaseNetwork { | Promise | Promise | Promise + | Promise | Promise; public customTokens: boolean; diff --git a/packages/extension/src/types/base-token.ts b/packages/extension/src/types/base-token.ts index 264523689..194910c21 100644 --- a/packages/extension/src/types/base-token.ts +++ b/packages/extension/src/types/base-token.ts @@ -1,9 +1,10 @@ -import EvmAPI from '@/providers/ethereum/libs/api'; import MarketData from '@/libs/market-data'; -import { ApiPromise } from '@polkadot/api'; import BitcoinAPI from '@/providers/bitcoin/libs/api'; -import KadenaAPI from '@/providers/kadena/libs/api'; import { BNType } from '@/providers/common/types'; +import EvmAPI from '@/providers/ethereum/libs/api'; +import KadenaAPI from '@/providers/kadena/libs/api'; +import MultiversXAPI from '@/providers/multiversx/libs/api'; +import { ApiPromise } from '@polkadot/api'; export type TransferType = 'keepAlive' | 'all' | 'allKeepAlive' | 'transfer'; @@ -60,7 +61,7 @@ export abstract class BaseToken { } public abstract getLatestUserBalance( - api: EvmAPI | ApiPromise | BitcoinAPI | KadenaAPI, + api: EvmAPI | ApiPromise | BitcoinAPI | KadenaAPI | MultiversXAPI, address: string, ): Promise; diff --git a/packages/extension/src/types/nft.ts b/packages/extension/src/types/nft.ts index eb48bbae7..e141a7034 100644 --- a/packages/extension/src/types/nft.ts +++ b/packages/extension/src/types/nft.ts @@ -4,6 +4,7 @@ export enum NFTType { Ordinals = 'ORDINALS', SolanaBGUM = 'SOLANABGUM', SolanaToken = 'SOLANATOKEN', + MultiversXNFT = 'MULTIVERSXNFT', } export interface NFTItem { diff --git a/packages/extension/src/types/provider.ts b/packages/extension/src/types/provider.ts index b43819ba4..403a8b19a 100644 --- a/packages/extension/src/types/provider.ts +++ b/packages/extension/src/types/provider.ts @@ -1,9 +1,9 @@ -import type { InjectedProvider as EthereumProvider } from '../providers/ethereum/types'; -import type { InjectedProvider as PolkadotProvider } from '@/providers/polkadot/types'; +import PublicKeyRing from '@/libs/keyring/public-keyring'; import type { InjectedProvider as BitcoinProvider } from '@/providers/bitcoin/types'; import type { InjectedProvider as KadenaProvider } from '@/providers/kadena/types'; +import type { InjectedProvider as MultiversXProvider } from '@/providers/multiversx/types'; +import type { InjectedProvider as PolkadotProvider } from '@/providers/polkadot/types'; import type { InjectedProvider as SolanaProvider } from '@/providers/solana/types'; -import EventEmitter from 'eventemitter3'; import { MiddlewareFunction, NetworkNames, @@ -11,20 +11,22 @@ import { RPCRequestType, SignerType, } from '@enkryptcom/types'; +import EventEmitter from 'eventemitter3'; import { RouteRecordRaw } from 'vue-router'; -import PublicKeyRing from '@/libs/keyring/public-keyring'; -import { RoutesType } from './ui'; -import { NFTCollection } from './nft'; -import { BaseNetwork } from './base-network'; -import { BaseToken } from './base-token'; +import type { InjectedProvider as EthereumProvider } from '../providers/ethereum/types'; import { BTCRawInfo, EthereumRawInfo, - SubscanExtrinsicInfo, KadenaRawInfo, - SOLRawInfo, MassaRawInfo, + MultiversXRawInfo, + SOLRawInfo, + SubscanExtrinsicInfo, } from './activity'; +import { BaseNetwork } from './base-network'; +import { BaseToken } from './base-token'; +import { NFTCollection } from './nft'; +import { RoutesType } from './ui'; export enum ProviderName { enkrypt = 'enkrypt', @@ -33,6 +35,7 @@ export enum ProviderName { polkadot = 'polkadot', kadena = 'kadena', solana = 'solana', + multiversx = 'multiversx', massa = 'massa', } export enum InternalStorageNamespace { @@ -44,6 +47,7 @@ export enum InternalStorageNamespace { bitcoinAccountsState = 'BitcoinAccountsState', kadenaAccountsState = 'KadenaAccountsState', solanaAccountsState = 'SolanaAccountsState', + multiversxAccountsState = 'MultiversxAccountsState', activityState = 'ActivityState', marketData = 'MarketData', cacheFetch = 'CacheFetch', @@ -70,6 +74,7 @@ export enum ProviderType { bitcoin, kadena, solana, + multiversx, massa, } @@ -147,6 +152,7 @@ export abstract class ProviderAPIInterface { | BTCRawInfo | KadenaRawInfo | SOLRawInfo + | MultiversXRawInfo | MassaRawInfo | null >; @@ -162,10 +168,11 @@ export type handleOutgoingMessage = ( message: string, ) => Promise; export { - EthereumProvider, - PolkadotProvider, BitcoinProvider, + EthereumProvider, KadenaProvider, + MultiversXProvider, + PolkadotProvider, SolanaProvider, }; export type Provider = @@ -173,7 +180,8 @@ export type Provider = | PolkadotProvider | BitcoinProvider | KadenaProvider - | SolanaProvider; + | SolanaProvider + | MultiversXProvider; export interface ProviderRequestOptions { url: string; diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index a0efffa29..ecf1448a9 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -82,52 +82,53 @@