diff --git a/README.md b/README.md index ce4ea61ad..8980e7647 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Enkrypt is a web3 wallet built from the ground up to support the multi-chain fut * Edgeware * Acala * Karura +* TomoChain * More coming soon! Looking to add your project? [Contact us!](https://mewwallet.typeform.com/enkrypt-inquiry?typeform-source=www.enkrypt.com) diff --git a/package.json b/package.json index 0d23da215..c06aa372a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "packages/storage", "packages/request", "packages/hw-wallets", + "packages/swap", "packages/name-resolution" ], "scripts": { diff --git a/packages/extension/package.json b/packages/extension/package.json index 733999ef7..105fd7dca 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@enkryptcom/extension", - "version": "1.15.0", + "version": "1.16.0", "private": true, "scripts": { "zip": "cd dist; zip -r release.zip *;", @@ -19,13 +19,14 @@ }, "dependencies": { "@acala-network/api": "^4.1.8-2.3", - "@enkryptcom/extension-bridge": "^0.0.2", - "@enkryptcom/hw-wallets": "^0.0.2", - "@enkryptcom/keyring": "^0.0.2", - "@enkryptcom/request": "^0.0.1", - "@enkryptcom/storage": "^0.0.2", - "@enkryptcom/types": "^0.0.1", - "@enkryptcom/utils": "^0.0.3", + "@enkryptcom/extension-bridge": "workspace:^", + "@enkryptcom/hw-wallets": "workspace:^", + "@enkryptcom/keyring": "workspace:^", + "@enkryptcom/request": "workspace:^", + "@enkryptcom/storage": "workspace:^", + "@enkryptcom/swap": "workspace:^", + "@enkryptcom/types": "workspace:^", + "@enkryptcom/utils": "workspace:^", "@ethereumjs/common": "^3.1.1", "@ethereumjs/tx": "^4.1.1", "@ledgerhq/hw-transport-webusb": "^6.27.12", diff --git a/packages/extension/src/libs/activity-state/index.ts b/packages/extension/src/libs/activity-state/index.ts index 7f7108748..c6a3377fd 100644 --- a/packages/extension/src/libs/activity-state/index.ts +++ b/packages/extension/src/libs/activity-state/index.ts @@ -30,7 +30,12 @@ class ActivityState { const cleanArr: Activity[] = []; const currentTime = new Date().getTime(); const minedNonces = cleanArr - .filter((item) => item.status === ActivityStatus.success && item.nonce) + .filter( + (item) => + (item.status === ActivityStatus.success || + item.status === ActivityStatus.failed) && + item.nonce + ) .map((item) => item.nonce); for (let i = 0; i < combined.length; i++) { if (!existingHashes.includes(combined[i].transactionHash)) { diff --git a/packages/extension/src/libs/activity-state/wrap-activity-handler.ts b/packages/extension/src/libs/activity-state/wrap-activity-handler.ts index 3584fc2dc..41bb0062d 100644 --- a/packages/extension/src/libs/activity-state/wrap-activity-handler.ts +++ b/packages/extension/src/libs/activity-state/wrap-activity-handler.ts @@ -1,4 +1,3 @@ -import { ActivityStatus } from "@/types/activity"; import ActivityState from "."; import { ActivityHandlerType } from "./types"; const CACHE_TTL = 1000 * 60 * 5; // 5 mins @@ -20,17 +19,7 @@ export default (activityHandler: ActivityHandlerType): ActivityHandlerType => { await activityState.setCacheTime(options); return liveActivities; } else { - const pendingActivities = activities.filter( - (act) => act.status === ActivityStatus.pending - ); - const liveActivityHashes = liveActivities.map( - (act) => act.transactionHash - ); - const stillPendingActivities = pendingActivities.filter( - (act) => !liveActivityHashes.includes(act.transactionHash) - ); - const newSet = stillPendingActivities.concat(liveActivities); - await activityState.addActivities(newSet, options); + await activityState.addActivities(liveActivities, options); await activityState.setCacheTime(options); return activityState.getAllActivities(options); } diff --git a/packages/extension/src/libs/dapp-list/index.ts b/packages/extension/src/libs/dapp-list/index.ts index 8e749c84b..2cf16c213 100644 --- a/packages/extension/src/libs/dapp-list/index.ts +++ b/packages/extension/src/libs/dapp-list/index.ts @@ -47,6 +47,18 @@ const lists: Partial> = { "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/zksync.json", [NetworkNames.Rootstock]: "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/rootstock.json", + [NetworkNames.TomoChain]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/tomo.json", + [NetworkNames.Arbitrum]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/arb.json", + [NetworkNames.Avalanche]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/avax.json", + [NetworkNames.Fantom]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/ftm.json", + [NetworkNames.Klaytn]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/klay.json", + [NetworkNames.Aurora]: + "https://raw.githubusercontent.com/enkryptcom/dynamic-data/main/dapps/aurora.json", }; export default lists; diff --git a/packages/extension/src/libs/market-data/ethvm.ts b/packages/extension/src/libs/market-data/ethvm.ts new file mode 100644 index 000000000..eb43dd39f --- /dev/null +++ b/packages/extension/src/libs/market-data/ethvm.ts @@ -0,0 +1,80 @@ +import { + CoinGeckoToken, + CoinGeckoTokenMarket, + CoingeckPlatforms, +} from "./types"; + +interface getCoinGeckoTokenInfoAllType { + data: { + getCoinGeckoTokenInfoAll: { + id: string; + symbol: string; + name: string; + platforms: { + platform: string; + address: string; + }[]; + }[]; + }; +} +const ETHVM_BASE = `https://api-v2.ethvm.dev/`; + +const ethvmPost = (requestData: string): Promise => { + return fetch(ETHVM_BASE, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: requestData, + }).then((res) => res.json()); +}; + +export const getAllPlatformData = (): Promise => { + return ethvmPost( + '{"operationName":null,"variables":{},"query":"{\\n getCoinGeckoTokenInfoAll {\\n id\\n symbol\\n name\\n platforms {\\n platform\\n address\\n }\\n }\\n}\\n"}' + ).then((json: getCoinGeckoTokenInfoAllType) => { + const retResponse: CoinGeckoToken[] = []; + json.data.getCoinGeckoTokenInfoAll.forEach((item) => { + const { id, name, symbol, platforms } = item; + const cgPlatforms: CoingeckPlatforms = {}; + platforms.forEach((p) => { + cgPlatforms[p.platform] = p.address; + }); + const token: CoinGeckoToken = { + id, + name, + symbol, + platforms: cgPlatforms, + }; + retResponse.push(token); + }); + return retResponse; + }); +}; + +export const getUSDPriceById = (id: string): Promise => { + return ethvmPost( + '{"operationName":null,"variables":{},"query":"{\\n getCoinGeckoTokenMarketDataByIds(coinGeckoTokenIds: [\\"' + + id + + '\\"]) {\\n current_price\\n }\\n}\\n"}' + ) + .then((json) => { + return json.data.getCoinGeckoTokenMarketDataByIds[0] + ? json.data.getCoinGeckoTokenMarketDataByIds[0].current_price.toString() + : null; + }) + .catch(() => null); +}; + +export const getMarketInfoByIDs = ( + ids: string[] +): Promise> => { + const params = ids.map((i) => '\\"' + i + '\\"').join(", "); + return ethvmPost( + '{"operationName":null,"variables":{},"query":"{\\n getCoinGeckoTokenMarketDataByIds(coinGeckoTokenIds: [' + + params + + ']) {\\n id\\n symbol\\n name\\n image\\n market_cap\\n market_cap_rank\\n high_24h\\n low_24h\\n price_change_24h\\n price_change_percentage_24h\\n sparkline_in_7d {\\n price\\n }\\n price_change_percentage_7d_in_currency\\n current_price\\n }\\n}\\n"}' + ).then((json) => { + return json.data.getCoinGeckoTokenMarketDataByIds as CoinGeckoTokenMarket[]; + }); +}; diff --git a/packages/extension/src/libs/market-data/index.ts b/packages/extension/src/libs/market-data/index.ts index c22d490dc..1a53a0820 100644 --- a/packages/extension/src/libs/market-data/index.ts +++ b/packages/extension/src/libs/market-data/index.ts @@ -7,9 +7,12 @@ import { FiatMarket, } from "./types"; import BigNumber from "bignumber.js"; -import cacheFetch from "../cache-fetch"; import { CoingeckoPlatform } from "@enkryptcom/types"; -const COINGECKO_ENDPOINT = "https://partners.mewapi.io/coingecko/api/v3/"; +import { + getAllPlatformData, + getMarketInfoByIDs, + getUSDPriceById, +} from "./ethvm"; const FIAT_EXCHANGE_RATE_ENDPOINT = "https://mainnet.mewwallet.dev/v2/prices/exchange-rates"; const REFRESH_DELAY = 1000 * 60 * 5; @@ -36,25 +39,8 @@ class MarketData { } return "0"; } - async getTokenPrice( - coingeckoID: string, - currency = "usd" - ): Promise { - const urlParams = new URLSearchParams(); - urlParams.append("ids", coingeckoID); - urlParams.append("vs_currencies", currency); - - return cacheFetch( - { - url: `${COINGECKO_ENDPOINT}simple/price?include_market_cap=false&include_24hr_vol=false&include_24hr_change=false&include_last_updated_at=false&${urlParams.toString()}`, - }, - REFRESH_DELAY - ).then((json) => { - if (json[coingeckoID] && json[coingeckoID][currency] !== undefined) { - return json[coingeckoID][currency].toString(); - } - return null; - }); + async getTokenPrice(coingeckoID: string): Promise { + return getUSDPriceById(coingeckoID); } async getMarketInfoByContracts( contracts: string[], @@ -77,6 +63,7 @@ class MarketData { return false; }) .map((token) => token.id); + if (!tokenIds.length) return requested; const marketData = await this.getMarketData(tokenIds); Object.keys(contractTokenMap).forEach((contract) => { if (contractTokenMap[contract]) { @@ -92,21 +79,9 @@ class MarketData { async getMarketData( coingeckoIDs: string[] ): Promise> { - return await cacheFetch( - { - url: `${COINGECKO_ENDPOINT}coins/markets?vs_currency=usd&order=market_cap_desc&price_change_percentage=7d&per_page=250&page=1&sparkline=true&ids=${coingeckoIDs.join( - "," - )}`, - }, - REFRESH_DELAY - ).then((json) => { - const markets = json as CoinGeckoTokenMarket[]; - const retMarkets: Array = []; - coingeckoIDs.forEach((id) => { - retMarkets.push(markets.find((m) => m.id === id) || null); - }); - return retMarkets; - }); + return getMarketInfoByIDs(coingeckoIDs).catch(() => + coingeckoIDs.map(() => null) + ); } async getFiatValue(symbol: string): Promise { await this.setMarketInfo(); @@ -137,19 +112,15 @@ class MarketData { const lastTimestamp = await this.#getLastTimestamp(); if (lastTimestamp && lastTimestamp >= new Date().getTime() - REFRESH_DELAY) return; - const allCoins = await fetch( - `${COINGECKO_ENDPOINT}coins/list?include_platform=true` - ) - .then((res) => res.json()) - .then((json) => { - console.log(json); - const allTokens = json as CoinGeckoToken[]; - const tokens: Record = {}; - allTokens.forEach((token) => { - tokens[token.id] = token; - }); - return tokens; + + const allCoins = await getAllPlatformData().then((json) => { + const allTokens = json as CoinGeckoToken[]; + const tokens: Record = {}; + allTokens.forEach((token) => { + tokens[token.id] = token; }); + return tokens; + }); await this.#setAllTokens(allCoins); const fiatMarketData = await fetch(`${FIAT_EXCHANGE_RATE_ENDPOINT}`) .then((res) => res.json()) diff --git a/packages/extension/src/libs/nft-handlers/simplehash.ts b/packages/extension/src/libs/nft-handlers/simplehash.ts index 39da37dce..d20067ccd 100644 --- a/packages/extension/src/libs/nft-handlers/simplehash.ts +++ b/packages/extension/src/libs/nft-handlers/simplehash.ts @@ -12,6 +12,9 @@ export default async ( const supportedNetworks = { [NetworkNames.Optimism]: "optimism", [NetworkNames.Binance]: "bsc", + [NetworkNames.Arbitrum]: "arbitrum", + [NetworkNames.Gnosis]: "gnosis", + [NetworkNames.Avalanche]: "avalanche", }; if (!Object.keys(supportedNetworks).includes(network.name)) throw new Error("Simplehash: network not supported"); diff --git a/packages/extension/src/providers/bitcoin/libs/api.ts b/packages/extension/src/providers/bitcoin/libs/api.ts index 499e7dd87..924f37df1 100644 --- a/packages/extension/src/providers/bitcoin/libs/api.ts +++ b/packages/extension/src/providers/bitcoin/libs/api.ts @@ -82,7 +82,7 @@ class API implements ProviderAPIInterface { }); } async getUTXOs(pubkey: string): Promise { - const address = this.getAddress(pubkey); + const address = pubkey.length < 64 ? pubkey : this.getAddress(pubkey); return fetch(`${this.node}address/${address}/unspent`) .then((res) => res.json()) .then((utxos: HaskoinUnspentType[]) => { diff --git a/packages/extension/src/providers/bitcoin/libs/btc-fee-handler.ts b/packages/extension/src/providers/bitcoin/libs/btc-fee-handler.ts new file mode 100644 index 000000000..07d8de534 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/libs/btc-fee-handler.ts @@ -0,0 +1,22 @@ +import { GasPriceTypes } from "@/providers/common/types"; + +const BTCFeeHandler = async (): Promise> => { + return fetch(`https://bitcoiner.live/api/fees/estimates/latest`) + .then((res) => res.json()) + .then((json) => { + return { + [GasPriceTypes.FASTEST]: Math.ceil(json.estimates["30"].sat_per_vbyte), + [GasPriceTypes.FAST]: Math.ceil(json.estimates["60"].sat_per_vbyte), + [GasPriceTypes.REGULAR]: Math.ceil(json.estimates["120"].sat_per_vbyte), + [GasPriceTypes.ECONOMY]: Math.ceil(json.estimates["180"].sat_per_vbyte), + }; + }) + .catch(() => ({ + [GasPriceTypes.FASTEST]: 25, + [GasPriceTypes.FAST]: 20, + [GasPriceTypes.REGULAR]: 10, + [GasPriceTypes.ECONOMY]: 5, + })); +}; + +export default BTCFeeHandler; diff --git a/packages/extension/src/providers/bitcoin/libs/utils.ts b/packages/extension/src/providers/bitcoin/libs/utils.ts index 18ce1bc5f..0c72aee8b 100644 --- a/packages/extension/src/providers/bitcoin/libs/utils.ts +++ b/packages/extension/src/providers/bitcoin/libs/utils.ts @@ -1,8 +1,9 @@ import { BitcoinNetworkInfo } from "../types"; import { address as BTCAddress } from "bitcoinjs-lib"; import { GasPriceTypes } from "@/providers/common/types"; -import { fromBase } from "@/libs/utils/units"; +import { fromBase } from "@enkryptcom/utils"; import BigNumber from "bignumber.js"; +import { BitcoinNetwork } from "../types/bitcoin-network"; const isAddress = (address: string, network: BitcoinNetworkInfo): boolean => { try { @@ -12,17 +13,19 @@ const isAddress = (address: string, network: BitcoinNetworkInfo): boolean => { return false; } }; -const getGasCostValues = ( +const getGasCostValues = async ( + network: BitcoinNetwork, byteSize: number, nativeVal = "0", decimals: number, currencyName: string ) => { + const fees = await network.feeHandler(); const gasVals = { - [GasPriceTypes.FASTEST]: (byteSize * 25).toString(), - [GasPriceTypes.FAST]: (byteSize * 20).toString(), - [GasPriceTypes.REGULAR]: (byteSize * 10).toString(), - [GasPriceTypes.ECONOMY]: (byteSize * 5).toString(), + [GasPriceTypes.FASTEST]: (byteSize * fees.FASTEST).toString(), + [GasPriceTypes.FAST]: (byteSize * fees.FAST).toString(), + [GasPriceTypes.REGULAR]: (byteSize * fees.REGULAR).toString(), + [GasPriceTypes.ECONOMY]: (byteSize * fees.ECONOMY).toString(), }; const getConvertedVal = (type: GasPriceTypes) => fromBase(gasVals[type], decimals); diff --git a/packages/extension/src/providers/bitcoin/networks/bitcoin-testnet.ts b/packages/extension/src/providers/bitcoin/networks/bitcoin-testnet.ts index 08b3cf8cd..6235735dd 100644 --- a/packages/extension/src/providers/bitcoin/networks/bitcoin-testnet.ts +++ b/packages/extension/src/providers/bitcoin/networks/bitcoin-testnet.ts @@ -5,6 +5,7 @@ import { } from "../types/bitcoin-network"; import { haskoinHandler } from "../libs/activity-handlers"; import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { GasPriceTypes } from "@/providers/common/types"; const bitcoinOptions: BitcoinNetworkOptions = { name: NetworkNames.BitcoinTest, @@ -23,6 +24,13 @@ const bitcoinOptions: BitcoinNetworkOptions = { activityHandler: wrapActivityHandler(haskoinHandler), basePath: "m/49'/1'/0'/0", coingeckoID: "bitcoin", + feeHandler: () => + Promise.resolve({ + [GasPriceTypes.FASTEST]: 25, + [GasPriceTypes.FAST]: 20, + [GasPriceTypes.REGULAR]: 10, + [GasPriceTypes.ECONOMY]: 5, + }), networkInfo: { messagePrefix: "\x18Bitcoin Signed Message:\n", bech32: "tb", diff --git a/packages/extension/src/providers/bitcoin/networks/bitcoin.ts b/packages/extension/src/providers/bitcoin/networks/bitcoin.ts index 5ebf98cf3..ca1b40e7e 100644 --- a/packages/extension/src/providers/bitcoin/networks/bitcoin.ts +++ b/packages/extension/src/providers/bitcoin/networks/bitcoin.ts @@ -5,6 +5,7 @@ import { } from "../types/bitcoin-network"; import { haskoinHandler } from "../libs/activity-handlers"; import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import BTCFeeHandler from "../libs/btc-fee-handler"; const bitcoinOptions: BitcoinNetworkOptions = { name: NetworkNames.Bitcoin, @@ -22,6 +23,7 @@ const bitcoinOptions: BitcoinNetworkOptions = { coingeckoID: "bitcoin", activityHandler: wrapActivityHandler(haskoinHandler), basePath: "m/49'/0'/0'/0", + feeHandler: BTCFeeHandler, networkInfo: { messagePrefix: "\x18Bitcoin Signed Message:\n", bech32: "bc", diff --git a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts index f6b7a2e3b..b4a4b325b 100644 --- a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts +++ b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts @@ -8,17 +8,17 @@ import createIcon from "../libs/blockies"; import { Activity } from "@/types/activity"; import { BitcoinNetworkInfo } from "."; import { payments } from "bitcoinjs-lib"; -import { hexToBuffer } from "@enkryptcom/utils"; +import { hexToBuffer, fromBase } from "@enkryptcom/utils"; import { formatFiatValue, formatFloatingPointValue, } from "@/libs/utils/number-formatter"; -import { fromBase } from "@/libs/utils/units"; import MarketData from "@/libs/market-data"; import BigNumber from "bignumber.js"; import { CoinGeckoTokenMarket } from "@/libs/market-data/types"; import Sparkline from "@/libs/sparkline"; import { BTCToken } from "./btc-token"; +import { GasPriceTypes } from "@/providers/common/types"; export interface BitcoinNetworkOptions { name: NetworkNames; @@ -36,6 +36,7 @@ export interface BitcoinNetworkOptions { coingeckoID?: string; basePath: string; networkInfo: BitcoinNetworkInfo; + feeHandler: () => Promise>; activityHandler: ( network: BaseNetwork, address: string @@ -49,6 +50,7 @@ export class BitcoinNetwork extends BaseNetwork { network: BaseNetwork, address: string ) => Promise; + feeHandler: () => Promise>; constructor(options: BitcoinNetworkOptions) { const api = async () => { const api = new BitcoinAPI(options.node, options.networkInfo); @@ -74,6 +76,7 @@ export class BitcoinNetwork extends BaseNetwork { super(baseOptions); this.activityHandler = options.activityHandler; this.networkInfo = options.networkInfo; + this.feeHandler = options.feeHandler; } public async getAllTokens(pubkey: string): Promise { diff --git a/packages/extension/src/providers/bitcoin/ui/btc-connect-dapp.vue b/packages/extension/src/providers/bitcoin/ui/btc-connect-dapp.vue index 269f77c5e..0dd5ec9b8 100644 --- a/packages/extension/src/providers/bitcoin/ui/btc-connect-dapp.vue +++ b/packages/extension/src/providers/bitcoin/ui/btc-connect-dapp.vue @@ -77,7 +77,7 @@ import { WindowPromiseHandler } from "@/libs/window-promise"; import { BitcoinNetwork } from "../types/bitcoin-network"; import { ProviderRequestOptions } from "@/types/provider"; import PublicKeyRing from "@/libs/keyring/public-keyring"; -import { fromBase } from "@/libs/utils/units"; +import { fromBase } from "@enkryptcom/utils"; import { getError } from "@/libs/error"; import { ErrorCodes } from "@/providers/ethereum/types"; import AccountState from "../libs/accounts-state"; diff --git a/packages/extension/src/providers/bitcoin/ui/btc-verify-transaction.vue b/packages/extension/src/providers/bitcoin/ui/btc-verify-transaction.vue index 876656845..b7a3a248b 100644 --- a/packages/extension/src/providers/bitcoin/ui/btc-verify-transaction.vue +++ b/packages/extension/src/providers/bitcoin/ui/btc-verify-transaction.vue @@ -130,7 +130,7 @@ import { getCustomError, getError } from "@/libs/error"; import { ErrorCodes } from "@/providers/ethereum/types"; import { WindowPromiseHandler } from "@/libs/window-promise"; import { DEFAULT_BTC_NETWORK, getNetworkByName } from "@/libs/utils/networks"; -import { fromBase, toBase } from "@/libs/utils/units"; +import { fromBase, toBase } from "@enkryptcom/utils"; import { ProviderRequestOptions } from "@/types/provider"; import { GasFeeType } from "./types"; import MarketData from "@/libs/market-data"; @@ -216,12 +216,13 @@ const hasEnoughBalance = computed(() => { }); const setTransactionFees = (byteSize: number) => { - gasCostValues.value = getGasCostValues( + getGasCostValues( + network.value as BitcoinNetwork, byteSize, nativePrice.value, network.value.decimals, network.value.currencyName - ); + ).then((val) => (gasCostValues.value = val)); }; const setBaseCosts = () => { diff --git a/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-token-select.vue b/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-token-select.vue index 7ddf62995..b86d1f202 100644 --- a/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-token-select.vue +++ b/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-token-select.vue @@ -15,7 +15,7 @@ diff --git a/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list-item.vue b/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list-item.vue new file mode 100644 index 000000000..9864c1db5 --- /dev/null +++ b/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list-item.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list.vue b/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list.vue new file mode 100644 index 000000000..986f6200d --- /dev/null +++ b/packages/extension/src/ui/action/views/swap/components/swap-network-select/network-select-list.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/extension/src/ui/action/views/swap/components/swap-token-amount-input/index.vue b/packages/extension/src/ui/action/views/swap/components/swap-token-amount-input/index.vue index 2a9323d89..06df9e800 100644 --- a/packages/extension/src/ui/action/views/swap/components/swap-token-amount-input/index.vue +++ b/packages/extension/src/ui/action/views/swap/components/swap-token-amount-input/index.vue @@ -5,34 +5,24 @@ Max -
- {{ - currentInputError === "MAX" - ? `Maximum swap amount is ${ - $filters.formatFloatingPointValue(max).value - }` - : currentInputError === "MIN" - ? `Minimum swap amount is ${ - $filters.formatFloatingPointValue(min).value - }` - : "Insufficient Balance" - }} +
+ {{ errorMessage }}
≈ ${{ $filters.formatFiatValue(tokenPrice).value }} @@ -44,80 +34,42 @@ import { computed, ref } from "vue"; import SwapTokenSelect from "../swap-token-select/index.vue"; import SwapTokenAmountInput from "./components/swap-token-amount-input.vue"; -import { BaseToken } from "@/types/base-token"; -import BigNumber from "bignumber.js"; -import { fromBase } from "@/libs/utils/units"; import { NATIVE_TOKEN_ADDRESS } from "@/providers/ethereum/libs/common"; +import { TokenType, SwapToken } from "@enkryptcom/swap"; -const emit = defineEmits<{ +defineEmits<{ (e: "update:inputMax"): void; - (e: "input:changed", amount: string, isInvalid: boolean): void; }>(); interface IProps { - tokenAmount: string; - token: BaseToken | null; + value: string; + token: TokenType; autofocus: boolean; - min?: string; - max?: string; + errorMessage?: string; } const props = defineProps(); const isFocus = ref(false); -const currentInputError = computed(() => { - return inputError(props.tokenAmount.toString()); -}); -const inputError = (value: string) => { - if (value && value !== "" && Number(value) !== 0) { - const fromBn = new BigNumber(value); - if ( - props.token && - props.token.balance && - fromBn.gt(fromBase(props.token.balance, props.token.decimals)) - ) { - return "INSUFFICIENT"; - } else if (props.max && fromBn.gt(props.max)) { - return "MAX"; - } else if (props.min && fromBn.lt(props.min)) { - return "MIN"; - } - } - return null; -}; - const tokenPrice = computed(() => { - if (props.token?.price && props.tokenAmount !== "") { - return new BigNumber(props.tokenAmount) - .times(new BigNumber(props.token.price)) - .toFixed(); + if (props.value !== "") { + const Token = new SwapToken(props.token); + return Token.getReadableToFiat(props.value); } - return null; + return 0; }); -const amountChanged = (newVal: string) => { - emit("input:changed", newVal, inputError(newVal) !== null); -}; - const changeFocus = (newVal: boolean) => { isFocus.value = newVal; }; - -const inputMax = () => { - if (props.token && props.token.balance) { - const tokenBalance = fromBase(props.token.balance, props.token.decimals); - amountChanged(tokenBalance); - } - emit("update:inputMax"); -}; diff --git a/packages/extension/src/ui/action/views/swap/components/swap-token-fast-list/components/swap-token-fast-item.vue b/packages/extension/src/ui/action/views/swap/components/swap-token-fast-list/components/swap-token-fast-item.vue index 4d21efdf7..459fdd02f 100644 --- a/packages/extension/src/ui/action/views/swap/components/swap-token-fast-list/components/swap-token-fast-item.vue +++ b/packages/extension/src/ui/action/views/swap/components/swap-token-fast-list/components/swap-token-fast-item.vue @@ -1,19 +1,19 @@ diff --git a/packages/extension/src/ui/action/views/swap/components/swap-token-to-amount/index.vue b/packages/extension/src/ui/action/views/swap/components/swap-token-to-amount/index.vue index 7c905e0ff..bddef5d37 100644 --- a/packages/extension/src/ui/action/views/swap/components/swap-token-to-amount/index.vue +++ b/packages/extension/src/ui/action/views/swap/components/swap-token-to-amount/index.vue @@ -6,6 +6,7 @@ v-show="!!token" :value="amount" :is-finding-rate="isFindingRate" + :no-providers="noProviders" /> , + type: Object as PropType, default: () => { return null; }, }, amount: { type: String, - default: () => "0.0", + default: () => "", }, isFindingRate: { type: Boolean, default: () => false, }, fastList: { - type: Object as PropType, + type: Object as PropType, default: () => null, }, totalTokens: { type: Number, default: () => null, }, + noProviders: Boolean, }); const isFocus = ref(false); const tokenPrice = computed(() => { - if (props.token?.price && props.amount !== "Searching") { - return new BigNumber(props.amount) - .times(new BigNumber(props.token.price)) - .toFixed(); + if (props.token?.price && props.amount) { + return new SwapToken(props.token).getReadableToFiat(props.amount); } - - return null; + return 0; }); @@ -78,7 +76,7 @@ const tokenPrice = computed(() => { @import "~@action/styles/theme.less"; .swap-token-input { width: 100%; - min-height: 148px; + min-height: 136px; border: 1px solid rgba(95, 99, 104, 0.2); box-sizing: border-box; border-radius: 10px; diff --git a/packages/extension/src/ui/action/views/swap/index.vue b/packages/extension/src/ui/action/views/swap/index.vue index 1d6dce876..b49b93cca 100644 --- a/packages/extension/src/ui/action/views/swap/index.vue +++ b/packages/extension/src/ui/action/views/swap/index.vue @@ -11,28 +11,31 @@
+ + @@ -40,10 +43,10 @@ @@ -52,7 +55,8 @@ :show-accounts="isOpenSelectContact" :accounts="toAccounts" :address="address" - :network="network" + :display-address="toAddressInputMeta.displayAddress" + :identicon="toAddressInputMeta.identicon" @selected:account="selectAccount" @update:paste-from-clipboard="addressInput.pasteFromClipboard()" @close="toggleSelectContact" @@ -69,7 +73,7 @@
@@ -78,8 +82,15 @@
+ + - + ; const router = useRouter(); const route = useRoute(); -const swap = new Swap(); const props = defineProps({ network: { @@ -150,314 +184,370 @@ const props = defineProps({ }, }); -const address = ref(""); -const addressIsValid = ref(false); -const addressIsLoading = ref(false); -const isOpenSelectContact = ref(false); -const addressInput = ref(); -const toAccounts = ref(props.accountInfo.activeAccounts); - const selected: string = route.params.id as string; -const network = ref(); -const fromTokens = ref(); -const fromToken = ref( - new UnknownToken({ name: "Loading", symbol: "", decimals: 18, icon: "" }) -); +const fromTokens = ref(); +const fromToken = ref({ + name: "Loading", + symbol: "", + decimals: 18, + address: "", + logoURI: props.network.icon, + type: "" as any, +}); const fromAmount = ref(null); - -const toTokens = ref(); -const toToken = ref(null); -const fetchingTokens = ref(true); - -const featuredTokens = ref(); - -const fromSelectOpened = ref(false); -const toSelectOpened = ref(false); - -const isLooking = ref(false); - -const rates = ref(); -const minFrom = ref(); -const maxFrom = ref(); -const inputError = ref(false); -const addressInputTimeout = ref(); - +const toNetworks = ref([]); +const toNetwork = ref(null); +const toNetworkOpen = ref(false); +const toToken = ref(null); +const toTokens = ref([]); +const toAmount = ref(""); +const trendingToTokens = ref([]); const swapError = ref(); const showSwapError = ref(false); - -const swapMax = ref(false); - -const toTokensFiltered = computed(() => { - if (toTokens.value) { - return toTokens.value.filter((token) => { - const toTokenAddress = (token as any).contract - ? (token as any).contract.toUpperCase() - : undefined; - - const fromTokenAddress = (fromToken.value as any).contract - ? (fromToken.value as any).contract.toUpperCase() - : undefined; - - if ( - fromTokenAddress && - toTokenAddress && - toTokenAddress === fromTokenAddress - ) - return false; - - return true; - }); - } - - return []; -}); - -const featuredTokensFiltered = computed(() => { - if (featuredTokens.value) { - return featuredTokens.value.filter((token) => { - const featuredTokenAddress = (token as any).contract - ? (token as any).contract.toUpperCase() - : undefined; - - const fromTokenAddress = (fromToken.value as any).contract - ? (fromToken.value as any).contract.toUpperCase() - : undefined; - - if ( - fromTokenAddress && - featuredTokenAddress && - featuredTokenAddress === fromTokenAddress - ) - return false; - - return true; - }); - } - - return []; +const LoadingType = ref(SWAP_LOADING.LOADING); +const isFindingRate = ref(false); +const toAddressInputMeta = ref({ + displayAddress: (address: string) => address, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + identicon: (address: string) => "" as string, + networkName: "", }); - -const isFindingRate = computed(() => { - if (rates.value) { - return false; - } else { - return true; - } +const errors = ref({ + inputAmount: "", + noProviders: false, }); +const bestProviderQuotes = ref([]); -const getEstimateRate = computed(() => { - if (rates.value) { - if (rates.value.length === 1) { - return () => new BigNumber(rates.value![0].rate); - } - - const x = rates.value.map(({ amount }) => new BigNumber(amount)); - const sumX = x.reduce((x1, x2) => x1.plus(x2), new BigNumber(0)); - const avgX = sumX.div(x.length); - - const xDifferencesToAverage = x.map((value) => avgX.minus(value)); - const xDifferencesToAverageSquared = xDifferencesToAverage.map((value) => - value.pow(2) - ); - const SSxx = xDifferencesToAverageSquared.reduce( - (prev, curr) => prev.plus(curr), - new BigNumber(0) - ); - - const y = rates.value.map(({ rate }) => new BigNumber(rate)); - const sumY = y.reduce((y1, y2) => y1.plus(y2), new BigNumber(0)); - const avgY = sumY.div(y.length); - const yDifferencesToAverage = y.map((value) => avgY.minus(value)); - const xAndYDifferencesMultiplied = xDifferencesToAverage.map( - (curr, index) => curr.times(yDifferencesToAverage[index]) - ); - const SSxy = xAndYDifferencesMultiplied.reduce( - (prev, curr) => prev.plus(curr), - new BigNumber(0) - ); - - const slope = SSxy.div(SSxx); - const intercept = avgY.minus(slope.times(avgX)); +const address = ref(""); +const addressIsValid = ref(true); +const isOpenSelectContact = ref(false); +const addressInput = ref(); +const toAccounts = ref([]); - return (x: number) => intercept.plus(slope.times(x)); - } +const fetchingTokens = ref(true); - return () => new BigNumber(0); -}); +const fromSelectOpened = ref(false); +const toSelectOpened = ref(false); -const toAmount = computed(() => { - const estimate = getEstimateRate - .value(Number(fromAmount.value) ?? 0) - .times(fromAmount.value ?? 0); +const isLooking = ref(false); - if (estimate.isZero()) return "0"; +const swapMax = ref(false); - return estimate.toFixed(); +const swap = new EnkryptSwap({ + api: new Web3Eth(props.network.node), + network: props.network.name as unknown as SupportedNetworkName, + walletIdentifier: WalletIdentifier.enkrypt, + evmOptions: { + infiniteApproval: true, + }, }); - +const keyring = new PublicKeyRing(); onMounted(async () => { - network.value = (await getNetworkByName(selected))!; - const supportedNetworks = swap.getSupportedNetworks(); - if (!supportedNetworks.includes(props.network.name)) { + if ( + !isSupportedNetwork(props.network.name as unknown as SupportedNetworkName) + ) { swapError.value = SwapError.NETWORK_NOT_SUPPORTED; toggleSwapError(); return; } - + isLooking.value = true; props.network - .getAllTokens(props.accountInfo.selectedAccount?.address as string) + .getAllTokenInfo(props.accountInfo.selectedAccount?.address as string) .then(async (tokens) => { - const api = await props.network.api(); - - const balancePromises = tokens.map((token) => { - if (token.balance) { - return Promise.resolve(token.balance); - } - - return token.getLatestUserBalance( - api.api, - props.accountInfo.selectedAccount?.address ?? "" - ); + console.log(tokens); + await swap.initPromise; + let swapFromTokens = await swap.getFromTokens(); + const tokensWithBalance: Record = {}; + tokens.forEach((t) => { + if ( + toBN(t.balance).gtn(0) || + t.contract === NATIVE_TOKEN_ADDRESS || + !t.contract + ) + tokensWithBalance[t.contract || NATIVE_TOKEN_ADDRESS] = t.balance; }); - const pricePromises = tokens.map((token) => { - if (token.price && token.price !== "0") { - return Promise.resolve(token.price); + swapFromTokens = { + all: swapFromTokens.all.filter((t) => { + if (tokensWithBalance[t.address]) { + t.balance = toBN(tokensWithBalance[t.address]); + return true; + } + return false; + }), + top: swapFromTokens.top.filter((t) => { + if (tokensWithBalance[t.address]) { + t.balance = toBN(tokensWithBalance[t.address]); + return true; + } + return false; + }), + trending: swapFromTokens.trending.filter((t) => { + if (tokensWithBalance[t.address]) { + t.balance = toBN(tokensWithBalance[t.address]); + return true; + } + return false; + }), + }; + fromTokens.value = swapFromTokens.all; + if (fromTokens.value.length) fromToken.value = fromTokens.value[0]; + + const swapToTokens = swap.getToTokens(); + const supportedNetworks = Object.keys(swapToTokens.all); + let thisNetwork: NetworkInfo; + supportedNetworks.forEach((net) => { + const netInfo = getNetworkInfoByName(net as SupportedNetworkName); + if (props.network.name === net) { + thisNetwork = + swapToTokens.all[net as unknown as SupportedNetworkName].length === + 1 + ? getNetworkInfoByName( + NetworkNames.Ethereum as unknown as SupportedNetworkName + ) + : netInfo; } - return token.getLatestPrice(); + toNetworks.value.push(netInfo); }); - await Promise.all([...balancePromises, ...pricePromises].flat()); - - fromTokens.value = tokens; - if (tokens.length > 0) { - fromToken.value = tokens[0]; - } + toNetworks.value.sort(sortByRank); + await initToNetworkInfo(thisNetwork!); + setToTokens(); + isLooking.value = false; }); - - swap.getAllTokens(props.network.name).then(({ tokens, featured, error }) => { - if (tokens.length === 0 && featured.length === 0 && error) { - swapError.value = SwapError.NO_TOKENS; - toggleSwapError(); - } else if (error) { - swapError.value = SwapError.SOME_TOKENS; - toggleSwapError(); - } - - featuredTokens.value = featured.length > 0 ? featured : tokens.slice(0, 5); - toTokens.value = tokens; - fetchingTokens.value = false; - }); }); +const defaultBNVals: Record = {}; +const setToTokens = () => { + const swapToTokens = swap.getToTokens(); + trendingToTokens.value = []; + toToken.value = null; + const MAX_TRENDING = 5; + toTokens.value = swapToTokens.all[toNetwork.value!.id].filter((val) => { + if (!defaultBNVals[val.decimals]) + defaultBNVals[val.decimals] = toBN(toBase("1", val.decimals)); + val.balance = defaultBNVals[val.decimals]; + return ( + (toNetwork.value!.id as string) !== (props.network.name as string) || + val.address !== fromToken.value?.address + ); + }); -watch([fromToken, toToken], () => { - if (fromToken.value && toToken.value) { - rates.value = undefined; - swap - .getTradePreview(props.network.name, fromToken.value, toToken.value) - .then((preview) => { - if (preview) { - rates.value = preview.rates; - minFrom.value = preview.min; - maxFrom.value = preview.max; - } - }); + if (swapToTokens.trending[toNetwork.value!.id]) + trendingToTokens.value.push( + ...swapToTokens.trending[toNetwork.value!.id].slice(0, MAX_TRENDING) + ); + if ( + swapToTokens.top[toNetwork.value!.id] && + trendingToTokens.value.length < MAX_TRENDING + ) { + trendingToTokens.value.push( + ...swapToTokens.top[toNetwork.value!.id].slice( + 0, + MAX_TRENDING - trendingToTokens.value.length + ) + ); } -}); - -watch(toToken, async () => { - if (toToken.value) { - let toNetwork: BaseNetwork | undefined = undefined; - - if ((toToken.value as any).contract) { - toNetwork = await getNetworkByName("ETH"); - } else { - switch (toToken.value.symbol.toUpperCase()) { - case "DOT": - toNetwork = await getNetworkByName("DOT"); - break; - case "KSM": - toNetwork = await getNetworkByName("KSM"); - break; - case "ETH": - toNetwork = await getNetworkByName("ETH"); - break; - case "MATIC": - toNetwork = await getNetworkByName("MATIC"); - break; - case "BNB": - toNetwork = await getNetworkByName("BSC"); - break; - case "BTC": - toNetwork = await getNetworkByName("BTC"); - break; - } + const existingtrending: string[] = []; + trendingToTokens.value = trendingToTokens.value.filter((val) => { + if (!defaultBNVals[val.decimals]) + defaultBNVals[val.decimals] = toBN(toBase("1", val.decimals)); + val.balance = defaultBNVals[val.decimals]; + const isInTrending = existingtrending.includes(val.address); + existingtrending.push(val.address); + return ( + ((toNetwork.value!.id as string) !== (props.network.name as string) || + val.address !== fromToken.value?.address) && + !isInTrending + ); + }); + if (toTokens.value.length === 1) toToken.value = toTokens.value[0]; + keyring.getAccounts(toNetwork.value?.signerType).then((accounts) => { + toAccounts.value = accounts; + const currentAccount = accounts.find( + (a) => a.address === props.accountInfo.selectedAccount!.address + ); + if (currentAccount) { + address.value = currentAccount.address; + isValidToAddress(); + } else if (accounts.length) { + address.value = accounts[0].address; + isValidToAddress(); } + }); +}; - if (toNetwork) { - const accounts = await getAccountsByNetworkName(toNetwork.name); +const setMax = () => { + fromAmount.value = new SwapToken(fromToken.value!).getBalanceReadable(); +}; - const currentAccount = accounts.find( - (account) => - account.publicKey === props.accountInfo.selectedAccount?.publicKey - ); +const inputAddress = (text: string) => { + try { + address.value = toAddressInputMeta.value.displayAddress(text); + } catch { + address.value = text; + } + isValidToAddress(); +}; - if (currentAccount) { - address.value = currentAccount.address; - } else { - address.value = accounts.length ? accounts[0].address : ""; - } +const toggleSelectContact = () => { + isOpenSelectContact.value = !isOpenSelectContact.value; +}; - network.value = toNetwork; - toAccounts.value = accounts; +const isValidToAddress = debounce(() => { + addressIsValid.value = true; + if (!address.value) addressIsValid.value = false; + else { + try { + const converted = toAddressInputMeta.value.displayAddress(address.value); + toToken.value?.networkInfo.isAddress(converted).then((res) => { + addressIsValid.value = res; + if (res) updateQuote(); + }); + } catch (e) { + addressIsValid.value = false; } } -}); +}, 200); + +const pickBestQuote = (fromAmountBN: BN, quotes: ProviderQuoteResponse[]) => { + errors.value.inputAmount = ""; + if (!quotes.length) return; + const token = new SwapToken(fromToken.value!); + const filteredQuotes = quotes.filter((q) => { + return ( + q.minMax.minimumFrom.lte(fromAmountBN) && + q.minMax.maximumFrom.gte(fromAmountBN) + ); + }); + if (!filteredQuotes.length) { + let lowestMinimum: BN = quotes[0].minMax.minimumFrom; + let highestMaximum: BN = quotes[0].minMax.maximumFrom; + quotes.forEach((q) => { + if (q.minMax.minimumFrom.lt(lowestMinimum)) + lowestMinimum = q.minMax.minimumFrom; + if (q.minMax.maximumFrom.gt(highestMaximum)) + highestMaximum = q.minMax.maximumFrom; + }); + if (fromAmountBN.lt(lowestMinimum)) + errors.value.inputAmount = `Minimum amount: ${token.toReadable( + lowestMinimum + )}`; + if (fromAmountBN.gt(highestMaximum)) + errors.value.inputAmount = `Maximum amount: ${token.toReadable( + highestMaximum + )}`; + return; + } + const fromT = new SwapToken(fromToken.value!); + if (fromT.getBalanceRaw().lt(fromAmountBN)) { + errors.value.inputAmount = "Insufficient funds"; + } + filteredQuotes.sort((a, b) => (b.toTokenAmount.gt(a.toTokenAmount) ? 1 : -1)); + toAmount.value = new SwapToken(toToken.value!).toReadable( + filteredQuotes[0].toTokenAmount + ); + bestProviderQuotes.value = filteredQuotes; + isFindingRate.value = false; +}; -watch(address, async () => { - addressIsLoading.value = true; +const updateQuote = () => { + isFindingRate.value = true; + toAmount.value = ""; + bestProviderQuotes.value = []; + errors.value.noProviders = false; + const token = new SwapToken(fromToken.value!); + const fromRawAmount = token.toRaw(fromAmount.value!); + swap + .getQuotes({ + amount: fromRawAmount, + fromAddress: props.network.displayAddress( + props.accountInfo.selectedAccount!.address + ), + fromToken: fromToken.value!, + toToken: toToken.value!, + toAddress: toAddressInputMeta.value.displayAddress(address.value), + }) + .then((quotes) => { + if (quotes.length) pickBestQuote(fromRawAmount, quotes); + else { + isFindingRate.value = false; + errors.value.noProviders = true; + } + }); +}; - clearTimeout(addressInputTimeout.value); +watch( + [fromToken, toToken, fromAmount], + debounce(async () => { + if ( + fromToken.value && + toToken.value && + fromAmount.value && + !isNaN(((fromAmount.value || "") as any) && Number(fromAmount.value) > 0) + ) { + updateQuote(); + } else { + isFindingRate.value = false; + toAmount.value = ""; + } + }, 300) +); - // Prevents multiple API calls every time a user types something - addressInputTimeout.value = setTimeout(async () => { - if (toToken.value) { - let displayAddress = address.value; +const selectTokenFrom = (token: TokenType | TokenTypeTo) => { + fromToken.value = token as TokenType; + fromAmount.value = ""; + errors.value.inputAmount = ""; + toggleFromToken(); + setToTokens(); +}; - try { - displayAddress = network.value!.displayAddress(address.value); - } catch { - displayAddress = address.value; - } - swap - .isValidAddress(displayAddress, toToken.value) - .then((isValid) => { - addressIsValid.value = isValid; - }) - .catch(() => { - addressIsValid.value = false; - }) - .finally(() => { - addressIsLoading.value = false; - }); +const initToNetworkInfo = async (network: NetworkInfo) => { + await getNetworkByName(network.id).then((net) => { + if (net) { + toAddressInputMeta.value = { + displayAddress: net.displayAddress, + identicon: net.identicon, + networkName: network.name, + }; + } else { + toAddressInputMeta.value = { + displayAddress: (address: string) => address, + identicon: () => "", + networkName: network.name, + }; } - }, 500) as any; -}); + }); + toNetwork.value = network; + inputAddress(""); +}; -const selectTokenFrom = (token: BaseToken) => { - fromToken.value = token; - fromSelectOpened.value = false; +const selectToNetwork = (network: NetworkInfo) => { + toAccounts.value = []; + address.value = ""; + toggleToNetwork(); + initToNetworkInfo(network).then(() => { + setToTokens(); + }); }; -const selectTokenTo = (token: BaseToken) => { - toToken.value = token; +const selectTokenTo = (token: TokenTypeTo | TokenType) => { + toToken.value = token as TokenTypeTo; + isValidToAddress(); toSelectOpened.value = false; }; -const inputAmountFrom = async (newVal: string, isInvalid: boolean) => { - inputError.value = isInvalid; +const inputAmountFrom = async (newVal: string) => { fromAmount.value = newVal; swapMax.value = false; }; +const selectAccount = (account: string) => { + address.value = account; + isValidToAddress(); + isOpenSelectContact.value = false; +}; + +const toggleToNetwork = () => { + toNetworkOpen.value = !toNetworkOpen.value; +}; const toggleFromToken = () => { fromSelectOpened.value = !fromSelectOpened.value; @@ -473,7 +563,6 @@ const toggleLooking = () => { const toggleSwapError = () => { showSwapError.value = !showSwapError.value; - if ( swapError.value === SwapError.NETWORK_NOT_SUPPORTED && !showSwapError.value @@ -482,83 +571,84 @@ const toggleSwapError = () => { } }; -const setMax = () => { - swapMax.value = true; -}; - -const sendButtonTitle = () => { - let title = "Select token"; - - if (!!fromToken.value && !!toToken.value) { - title = "Preview swap"; - } +const sendButtonTitle = computed(() => { + if (!fromAmount.value || fromAmount.value === "0" || errors.value.inputAmount) + return "Enter valid amount"; + if (!toToken.value) return "Select To Token"; + if (!address.value || !addressIsValid.value) return "Enter address"; + return "Preview swap"; +}); - return title; -}; const isDisabled = computed(() => { - let _isDisabled = true; - if ( - !!fromToken.value && - !!toToken.value && - Number(fromAmount.value) > 0 && - Number(toAmount.value) > 0 && - addressIsValid.value && - !inputError.value - ) { - _isDisabled = false; - } - return _isDisabled; + if (!fromAmount.value || fromAmount.value === "0" || errors.value.inputAmount) + return true; + if (!toToken.value) return true; + if (!address.value || !addressIsValid.value) return true; + if (!bestProviderQuotes.value.length) return true; + return false; }); const sendAction = async () => { toggleLooking(); - - let fromAddress = props.accountInfo.selectedAccount!.address; - - if ((props.network as unknown as SubstrateNetwork).prefix !== undefined) { - fromAddress = (props.network as unknown as SubstrateNetwork).displayAddress( - fromAddress - ); - } - - const priceDifference = new BigNumber(fromAmount.value!) - .times(fromToken.value?.price ?? 0) - .div(new BigNumber(toAmount.value).times(toToken.value?.price ?? 0)) + const marketData = new MarketData(); + const fromPrice = fromToken.value!.cgId + ? await marketData + .getMarketData([fromToken.value!.cgId]) + .then((res) => res[0]!.current_price) + : 0; + const toPrice = toToken.value!.cgId + ? await marketData + .getMarketData([toToken.value!.cgId]) + .then((res) => res[0]!.current_price) + : 0; + const localFromToken = { ...fromToken.value! }; + const localToToken = { ...toToken.value! }; + localFromToken.price = fromPrice; + localToToken.price = toPrice; + localFromToken.balance = bestProviderQuotes.value[0]!.fromTokenAmount; + localToToken.balance = bestProviderQuotes.value[0]!.toTokenAmount; + const swapToToken = new SwapToken(localToToken); + const swapFromToken = new SwapToken(localFromToken); + const priceDifference = BigNumber(swapFromToken.getFiatTotal()) + .div(swapToToken.getFiatTotal()) .toString(); - const trades = await swap.getTrade( - props.network.name, - fromAddress, - network.value!.displayAddress(address.value), - fromToken.value!, - toToken.value!, - fromAmount.value!, - swapMax.value + const tradePromises = bestProviderQuotes.value.map((q) => + swap.getSwap(q.quote) ); - if ( - trades.length === 0 || - trades.flatMap((trade) => { - return trade.txs; - }).length === 0 - ) { + const trades: (ProviderResponseWithStatus | null)[] = await Promise.all( + tradePromises + ).then((responses) => responses.filter((r) => !!r)); + + const tradeStatusOptions = trades.map((t) => + t!.getStatusObject({ + transactionHashes: [], + }) + ); + const statusObjects = await Promise.all(tradeStatusOptions); + trades.forEach((t, idx) => (t!.status = statusObjects[idx])); + if (!trades.length) { swapError.value = SwapError.NO_TRADES; toggleLooking(); toggleSwapError(); return; } - - const swapData = { - trades, - toToken: toToken.value, - fromToken: fromToken.value, - fromAmount: fromAmount.value, - toAddress: address.value, + const nativeToken = fromTokens.value?.find( + (ft) => ft.address === NATIVE_TOKEN_ADDRESS + ); + const swapData: SwapData = { + trades: trades as ProviderResponseWithStatus[], + fromToken: localFromToken, + toToken: localToToken, priceDifference: priceDifference, - swapMax: swapMax.value, + nativeBalance: nativeToken!.balance || toBN("0"), + nativePrice: nativeToken!.price || 0, + existentialDeposit: + (props.network as SubstrateNetwork).existentialDeposit || toBN("0"), fromAddress: props.accountInfo.selectedAccount!.address, + toAddress: address.value, }; - const routedRoute = router.resolve({ name: RouterNames.swapBestOffer.name, query: { @@ -587,25 +677,6 @@ const sendAction = async () => { router.push(routedRoute); } }; - -const inputAddress = (text: string) => { - try { - if (network.value) { - address.value = network.value!.displayAddress(text); - } else { - address.value = text; - } - } catch { - address.value = text; - } -}; -const toggleSelectContact = (open: boolean) => { - isOpenSelectContact.value = open; -}; -const selectAccount = (account: string) => { - address.value = account; - isOpenSelectContact.value = false; -};