diff --git a/packages/extension/package.json b/packages/extension/package.json index 46709466e..78d50be2d 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@enkryptcom/extension", - "version": "1.17.1", + "version": "1.18.0", "private": true, "scripts": { "zip": "cd dist; zip -r release.zip *;", diff --git a/packages/extension/src/providers/ethereum/libs/accounts-state/index.ts b/packages/extension/src/providers/ethereum/libs/accounts-state/index.ts index facf2626d..e3ca57ee8 100644 --- a/packages/extension/src/providers/ethereum/libs/accounts-state/index.ts +++ b/packages/extension/src/providers/ethereum/libs/accounts-state/index.ts @@ -30,7 +30,12 @@ class AccountState { } async getApprovedAddresses(domain: string): Promise { const state = await this.getStateByDomain(domain); - if (state.approvedAccounts) return state.approvedAccounts; + if (state.approvedAccounts) { + for (const acc of state.approvedAccounts) { + if (acc.length !== 42) await this.removeApprovedAddress(acc, domain); // remove after a while, bug due to getting btc accounts added to evm + } + return state.approvedAccounts.filter((acc) => acc.length === 42); + } return []; } async deleteState(domain: string): Promise { diff --git a/packages/extension/src/providers/ethereum/networks/index.ts b/packages/extension/src/providers/ethereum/networks/index.ts index aab793449..f6b4dc324 100644 --- a/packages/extension/src/providers/ethereum/networks/index.ts +++ b/packages/extension/src/providers/ethereum/networks/index.ts @@ -1,8 +1,5 @@ import ethNode from "./eth"; import goerliNode from "./goerli"; -import kovanNode from "./kov"; -import ropstenNode from "./rop"; -import rinkebyNode from "./rin"; import etcNode from "./etc"; import maticNode from "./matic"; import bscNode from "./bsc"; @@ -32,9 +29,6 @@ import puppyNode from "./puppy"; export default { goerli: goerliNode, ethereum: ethNode, - kovan: kovanNode, - ropsten: ropstenNode, - rinkeby: rinkebyNode, etc: etcNode, matic: maticNode, bsc: bscNode, diff --git a/packages/extension/src/providers/ethereum/networks/kov.ts b/packages/extension/src/providers/ethereum/networks/kov.ts deleted file mode 100644 index 3d123379c..000000000 --- a/packages/extension/src/providers/ethereum/networks/kov.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NetworkNames } from "@enkryptcom/types"; -import { EvmNetwork, EvmNetworkOptions } from "../types/evm-network"; -import { EtherscanActivity } from "../libs/activity-handlers"; -import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; - -const kovOptions: EvmNetworkOptions = { - name: NetworkNames.Kovan, - name_long: "Kovan", - homePage: "https://github.com/kovan-testnet", - blockExplorerTX: "https://kovan.etherscan.io/tx/[[txHash]]", - blockExplorerAddr: "https://kovan.etherscan.io/address/[[address]]", - chainID: "0x2a", - isTestNetwork: true, - currencyName: "KOV", - currencyNameLong: "Kovan", - node: "wss://nodes.mewapi.io/ws/kovan", - icon: require("./icons/eth.svg"), - gradient: "linear-gradient(180deg, #C549FF 0%, #684CFF 100%)", - activityHandler: wrapActivityHandler(EtherscanActivity), -}; - -const kov = new EvmNetwork(kovOptions); - -export default kov; diff --git a/packages/extension/src/providers/ethereum/networks/rin.ts b/packages/extension/src/providers/ethereum/networks/rin.ts deleted file mode 100644 index 47a96dcf8..000000000 --- a/packages/extension/src/providers/ethereum/networks/rin.ts +++ /dev/null @@ -1,24 +0,0 @@ -import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; -import { NetworkNames } from "@enkryptcom/types"; -import { RivetActivity } from "../libs/activity-handlers"; -import { EvmNetwork, EvmNetworkOptions } from "../types/evm-network"; - -const rinOptions: EvmNetworkOptions = { - name: NetworkNames.Rinkeby, - name_long: "Rinkeby", - homePage: "https://www.rinkeby.io/", - blockExplorerTX: "https://rinkeby.etherscan.io/tx/[[txHash]]", - blockExplorerAddr: "https://rinkeby.etherscan.io/address/[[address]]", - chainID: "0x4", - isTestNetwork: true, - currencyName: "RIN", - currencyNameLong: "Rinkeby", - node: "wss://nodes.mewapi.io/ws/rinkeby", - icon: require("./icons/eth.svg"), - gradient: "linear-gradient(180deg, #C549FF 0%, #684CFF 100%)", - activityHandler: wrapActivityHandler(RivetActivity), -}; - -const rin = new EvmNetwork(rinOptions); - -export default rin; diff --git a/packages/extension/src/providers/ethereum/networks/rop.ts b/packages/extension/src/providers/ethereum/networks/rop.ts deleted file mode 100644 index bd64a3e4c..000000000 --- a/packages/extension/src/providers/ethereum/networks/rop.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NetworkNames } from "@enkryptcom/types"; -import { EvmNetwork, EvmNetworkOptions } from "../types/evm-network"; -import { RivetActivity } from "../libs/activity-handlers"; -import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; - -const ropOptions: EvmNetworkOptions = { - name: NetworkNames.Ropsten, - name_long: "Ropsten", - homePage: "https://github.com/ethereum/ropsten", - blockExplorerTX: "https://ropsten.etherscan.io/tx/[[txHash]]", - blockExplorerAddr: "https://ropsten.etherscan.io/address/[[address]]", - chainID: "0x3", - isTestNetwork: true, - currencyName: "ROP", - currencyNameLong: "Ropsten", - node: "wss://nodes.mewapi.io/ws/rop", - icon: require("./icons/eth.svg"), - basePath: "m/44'/1'/0'/0", - gradient: "linear-gradient(180deg, #C549FF 0%, #684CFF 100%)", - activityHandler: wrapActivityHandler(RivetActivity), -}; - -const rop = new EvmNetwork(ropOptions); - -export default rop; diff --git a/packages/extension/src/providers/polkadot/libs/activity-handlers/providers/subscan/configs.ts b/packages/extension/src/providers/polkadot/libs/activity-handlers/providers/subscan/configs.ts index 47e2a426a..78f2d0bac 100644 --- a/packages/extension/src/providers/polkadot/libs/activity-handlers/providers/subscan/configs.ts +++ b/packages/extension/src/providers/polkadot/libs/activity-handlers/providers/subscan/configs.ts @@ -11,6 +11,8 @@ const NetworkEndpoints = { [NetworkNames.Bifrost]: "https://bifrost.api.subscan.io/", [NetworkNames.BifrostKusama]: "https://bifrost-kusama.api.subscan.io/", [NetworkNames.Edgeware]: "https://edgeware.api.subscan.io/", + [NetworkNames.Quartz]: "https://quartz.api.subscan.io/", + [NetworkNames.Unique]: "https://unique.api.subscan.io/", }; export { NetworkEndpoints }; diff --git a/packages/extension/src/providers/polkadot/networks/icons/interlay.svg b/packages/extension/src/providers/polkadot/networks/icons/interlay.svg new file mode 100644 index 000000000..570c73cf8 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/icons/interlay.svg @@ -0,0 +1,104 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/extension/src/providers/polkadot/networks/icons/kintsugi.svg b/packages/extension/src/providers/polkadot/networks/icons/kintsugi.svg new file mode 100644 index 000000000..10488c6e9 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/icons/kintsugi.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/extension/src/providers/polkadot/networks/icons/opal.svg b/packages/extension/src/providers/polkadot/networks/icons/opal.svg new file mode 100644 index 000000000..c3d1b9357 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/icons/opal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/extension/src/providers/polkadot/networks/icons/quartz.svg b/packages/extension/src/providers/polkadot/networks/icons/quartz.svg new file mode 100644 index 000000000..9b68cdde3 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/icons/quartz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/extension/src/providers/polkadot/networks/icons/unique.svg b/packages/extension/src/providers/polkadot/networks/icons/unique.svg new file mode 100644 index 000000000..1963c5e3d --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/icons/unique.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/extension/src/providers/polkadot/networks/index.ts b/packages/extension/src/providers/polkadot/networks/index.ts index 198c52aa7..5d41bb54f 100644 --- a/packages/extension/src/providers/polkadot/networks/index.ts +++ b/packages/extension/src/providers/polkadot/networks/index.ts @@ -8,6 +8,11 @@ import sdnNode from "./astar/shiden"; import bncNode from "./bifrost/polkadot"; import bncKusamaNode from "./bifrost/kusama"; import edgNode from "./edgeware"; +import opalNode from "./unique/opal"; +import quartzNode from "./unique/quartz"; +import uniqueNode from "./unique/unique"; +// import interlayNode from "./interlay/interlay"; +// import kintsugiNode from "./interlay/kintsugi"; export default { acala: acaNode, @@ -20,4 +25,9 @@ export default { bifrost: bncNode, bifrostKusama: bncKusamaNode, edgeware: edgNode, + opal: opalNode, + quartz: quartzNode, + unique: uniqueNode, + // interlay: interlayNode, + // kintsugi: kintsugiNode, }; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/DOT.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/DOT.png new file mode 100755 index 000000000..bb0e3ad3b Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/DOT.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/IBTC.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/IBTC.png new file mode 100644 index 000000000..e0a9cf428 Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/IBTC.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/INTR.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/INTR.png new file mode 100644 index 000000000..40d48f892 Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/INTR.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KBTC.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KBTC.png new file mode 100644 index 000000000..cc8cee742 Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KBTC.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KINT.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KINT.png new file mode 100644 index 000000000..53d1ff84d Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KINT.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KSM.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KSM.png new file mode 100644 index 000000000..00dd41637 Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/KSM.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LDOT.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LDOT.png new file mode 100644 index 000000000..dff99e44d Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LDOT.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LKSM.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LKSM.png new file mode 100644 index 000000000..c90daa65b Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/LKSM.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDC.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDC.png new file mode 100644 index 000000000..3c9ea9f32 Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDC.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDT.png b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDT.png new file mode 100644 index 000000000..b5733e33e Binary files /dev/null and b/packages/extension/src/providers/polkadot/networks/interlay/assets/icons/USDT.png differ diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/interlay-assets.ts b/packages/extension/src/providers/polkadot/networks/interlay/assets/interlay-assets.ts new file mode 100644 index 000000000..460dbbd06 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/assets/interlay-assets.ts @@ -0,0 +1,30 @@ +import { KnownTokenDisplay } from "@/providers/polkadot/types"; + +const assets: KnownTokenDisplay[] = [ + { + name: "Interlay", + symbol: "INTR", + coingeckoID: "interlay", + icon: require("./icons/INTR.png"), + }, + { + name: "Polkadot", + symbol: "DOT", + coingeckoID: "polkadot", + icon: require("../../icons/polkadot.svg"), + }, + { + name: "USDT", + symbol: "USDT", + icon: require("./icons/USDT.png"), + coingeckoID: "usdt", + }, + { + name: "IBTC", + symbol: "IBTC", + icon: require("./icons/IBTC.png"), + coingeckoID: "interbtc", + }, +]; + +export default assets; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/assets/kintsugi-assets.ts b/packages/extension/src/providers/polkadot/networks/interlay/assets/kintsugi-assets.ts new file mode 100644 index 000000000..6050413fa --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/assets/kintsugi-assets.ts @@ -0,0 +1,36 @@ +import { KnownTokenDisplay } from "@/providers/polkadot/types"; + +const assets: KnownTokenDisplay[] = [ + { + name: "Interlay", + symbol: "INTR", + coingeckoID: "interlay", + icon: require("./icons/INTR.png"), + }, + { + name: "Kusama", + symbol: "KSM", + + icon: require("./icons/KSM.png"), + coingeckoID: "kusama", + }, + { + name: "Liquid KSM", + symbol: "LKSM", + icon: require("./icons/LKSM.png"), + }, + { + name: "USDT", + symbol: "USDT", + icon: require("./icons/USDT.png"), + coingeckoID: "usdt", + }, + { + name: "KBTC", + symbol: "KBTC", + icon: require("./icons/KBTC.png"), + coingeckoID: "kintsugi-btc", + }, +]; + +export default assets; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/interlay.ts b/packages/extension/src/providers/polkadot/networks/interlay/interlay.ts new file mode 100644 index 000000000..15b998392 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/interlay.ts @@ -0,0 +1,39 @@ +import assetHandler from "./libs/assetinfo-orml"; +import { CoingeckoPlatform, NetworkNames } from "@enkryptcom/types"; +import assets from "./assets/interlay-assets"; +import { + SubstrateNetwork, + SubstrateNetworkOptions, +} from "../../types/substrate-network"; +import { subscanActivity } from "../../libs/activity-handlers"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { toBN } from "web3-utils"; + +const interlayOptions: SubstrateNetworkOptions = { + name: NetworkNames.Interlay, + name_long: "Interlay", + homePage: "https://interlay.io/", + blockExplorerTX: "https://interlay.subscan.io/extrinsic/[[txHash]]", + blockExplorerAddr: "https://interlay.subscan.io/account/[[address]]", + isTestNetwork: false, + currencyName: "INTR", + currencyNameLong: "Interlay", + icon: require("../icons/interlay.svg"), + decimals: 12, + prefix: 2032, + gradient: + "linear-gradient(326.87deg, #1A0A2D 12.53%, #1A0A2D 50.89%, #1A0A2D 89.24%)", + node: "wss://api.interlay.io:443/parachain", + coingeckoID: "interlay", + coingeckoPlatform: CoingeckoPlatform.Interlay, + genesisHash: + "0xbf88efe70e9e0e916416e8bed61f2b45717f517d7f3523e33c7b001e5ffcbc72", + activityHandler: wrapActivityHandler(subscanActivity), + assetHandler: assetHandler, + knownTokens: assets, + existentialDeposit: toBN("0"), +}; + +const interlay = new SubstrateNetwork(interlayOptions); + +export default interlay; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/kintsugi.ts b/packages/extension/src/providers/polkadot/networks/interlay/kintsugi.ts new file mode 100644 index 000000000..aeb1976c3 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/kintsugi.ts @@ -0,0 +1,39 @@ +import assetHandler from "./libs/assetinfo-orml"; +import { CoingeckoPlatform, NetworkNames } from "@enkryptcom/types"; +import assets from "./assets/kintsugi-assets"; +import { + SubstrateNetwork, + SubstrateNetworkOptions, +} from "../../types/substrate-network"; +import { subscanActivity } from "../../libs/activity-handlers"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { toBN } from "web3-utils"; + +const kintsugiOptions: SubstrateNetworkOptions = { + name: NetworkNames.Kintsugi, + name_long: "Kintsugi", + homePage: "https://interlay.io/kintsugi", + blockExplorerTX: "https://kintsugi.subscan.io/extrinsic/[[txHash]]", + blockExplorerAddr: "https://kintsugi.subscan.io/account/[[address]]", + isTestNetwork: false, + currencyName: "KINT", + currencyNameLong: "Kintsugi", + icon: require("../icons/kintsugi.svg"), + decimals: 12, + prefix: 2092, + gradient: + "linear-gradient(326.87deg, #041333 12.53%, #041333 50.89%, #041333 89.24%)", + node: "wss://api-kusama.interlay.io:443/parachain", + coingeckoID: "kintsugi", + coingeckoPlatform: CoingeckoPlatform.Kintsugi, + genesisHash: + "0x9af9a64e6e4da8e3073901c3ff0cc4c3aad9563786d89daf6ad820b6e14a0b8b", + activityHandler: wrapActivityHandler(subscanActivity), + assetHandler: assetHandler, + knownTokens: assets, + existentialDeposit: toBN("0"), +}; + +const kintsugi = new SubstrateNetwork(kintsugiOptions); + +export default kintsugi; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/libs/assetinfo-orml.ts b/packages/extension/src/providers/polkadot/networks/interlay/libs/assetinfo-orml.ts new file mode 100644 index 000000000..5f81774eb --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/libs/assetinfo-orml.ts @@ -0,0 +1,160 @@ +import API from "@/providers/polkadot/libs/api"; +import { SubstrateNetwork } from "@/providers/polkadot/types/substrate-network"; +import { hexToString, hexToBn } from "@polkadot/util"; +import { + InterlayOrmlAsset, + InterlayOrmlAssetOptions, + OrmlAssetType, +} from "../types/interlay-orml-asset"; +import { OrmlTokensAccountData } from "../../acala/types/acala-orml-asset"; +import { toBN } from "web3-utils"; +import { KnownTokenDisplay } from "@/providers/polkadot/types"; +import { SubstrateNativeToken } from "@/providers/polkadot/types/substrate-native-token"; + +type AssetMetadata = { + name: `0x${string}`; + symbol: `0x${string}`; + decimals: number; + minimalBalance: number | `0x${string}`; +}; + +type NativeAsset = Record; + +type ForeignAsset = string; + +type StableAsset = string; + +type Erc20 = string; + +enum AssetIds { + NATIVE_ASSET = "NativeAssetId", + FOREIGN_ASSET = "ForeignAssetId", + STABLE_ASSET = "StableAssetId", + ERC20_ASSET = "Erc20", +} + +type AssetKey = Record< + AssetIds, + NativeAsset | ForeignAsset | StableAsset | Erc20 +>; + +export default async ( + network: SubstrateNetwork, + address: string | null, + knownTokens?: KnownTokenDisplay[] +) => { + const api = (await network.api()) as API; + + const apiPromise = api.api; + + const metadata = + await apiPromise.query.assetRegistry.assetMetadatas.entries(); + + const assets = metadata + .map(([key, value]) => { + const assetKey = (key.toHuman() as [AssetKey])[0]; + const assetMetadata = value.toJSON() as AssetMetadata; + const decimals = assetMetadata.decimals; + const minimalBalance = + typeof assetMetadata.minimalBalance === "string" + ? hexToBn(assetMetadata.minimalBalance) + : toBN(assetMetadata.minimalBalance); + + let assetLookupId: OrmlAssetType | null = null; + let assetLookupValue: string | null = null; + + if (assetKey[AssetIds.FOREIGN_ASSET]) { + assetLookupId = "foreignAsset"; + assetLookupValue = assetKey[AssetIds.FOREIGN_ASSET] as string; + } else if (assetKey[AssetIds.NATIVE_ASSET]) { + assetLookupId = Object.keys( + assetKey[AssetIds.NATIVE_ASSET] + )[0] as "token"; + + assetLookupValue = (assetKey[AssetIds.NATIVE_ASSET] as NativeAsset)[ + assetLookupId + ] as string; + } else if (assetKey[AssetIds.STABLE_ASSET]) { + assetLookupId = "stableAssetPoolToken"; + assetLookupValue = assetKey[AssetIds.STABLE_ASSET] as string; + } else if (assetKey[AssetIds.ERC20_ASSET]) { + // TODO add Erc20 support, required special RPC call + } + + if (assetLookupId && assetLookupValue) { + const assetInfo = { + name: assetMetadata.name, + symbol: assetMetadata.symbol, + decimals, + minimalBalance, + assetLookupId, + assetLookupValue, + }; + + return assetInfo; + } else { + // Unhandled token types, right now just Erc20 + return null; + } + }) + .filter((asset) => asset !== null); + + const tokenOptions: InterlayOrmlAssetOptions[] = assets + .map((asset) => { + const ormlOptions: InterlayOrmlAssetOptions = { + name: hexToString(asset!.name), + symbol: hexToString(asset!.symbol), + existentialDeposit: asset!.minimalBalance, + assetType: asset!.assetLookupId, + lookupValue: asset!.assetLookupValue, + icon: network.icon, + decimals: asset!.decimals, + }; + + return ormlOptions; + }) + .map((tokenOptions) => { + if (knownTokens) { + const knownToken = knownTokens.find( + (knownToken) => + knownToken.name === tokenOptions.name && + knownToken.symbol === tokenOptions.symbol + ); + + if (knownToken) { + tokenOptions.coingeckoID = knownToken.coingeckoID; + tokenOptions.icon = knownToken.icon; + } + } + return tokenOptions; + }); + + const nativeAsset = new SubstrateNativeToken({ + name: network.currencyNameLong, + symbol: network.name, + decimals: network.decimals, + existentialDeposit: network.existentialDeposit, + icon: network.icon, + coingeckoID: network.coingeckoID, + }); + + if (address) { + await nativeAsset.getLatestUserBalance(apiPromise, address); + const queries = tokenOptions.map((asset) => { + const token = { [asset.assetType]: asset.lookupValue }; + const query = [address, token]; + + return query; + }); + + const tokenBalances = await apiPromise.query.tokens.accounts.multi(queries); + + tokenBalances.forEach((tokenData, index) => { + const data = tokenData as unknown as OrmlTokensAccountData; + + tokenOptions[index].balance = data.free.toString(); + }); + } + + return [nativeAsset, ...tokenOptions.map((o) => new InterlayOrmlAsset(o))]; +}; diff --git a/packages/extension/src/providers/polkadot/networks/interlay/types/interlay-orml-asset.ts b/packages/extension/src/providers/polkadot/networks/interlay/types/interlay-orml-asset.ts new file mode 100644 index 000000000..a48f6604f --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/interlay/types/interlay-orml-asset.ts @@ -0,0 +1,54 @@ +import { ApiPromise } from "@polkadot/api"; +import { OrmlTokensAccountData } from "../../acala/types/acala-orml-asset"; +import { SubmittableExtrinsic } from "@polkadot/api/types"; +import { ISubmittableResult } from "@polkadot/types/types"; +import { SubstrateToken } from "@/providers/polkadot/types/substrate-token"; +import { BaseTokenOptions } from "@/types/base-token"; + +export type OrmlAssetType = + | "token" + | "foreignAsset" + | "stableAssetPoolToken" + | "liquidCrowdloan"; + +export interface InterlayOrmlAssetOptions extends BaseTokenOptions { + assetType: OrmlAssetType; + lookupValue: string | number; +} + +export class InterlayOrmlAsset extends SubstrateToken { + public assetType: OrmlAssetType; + public lookupValue: string | number; + + constructor(options: InterlayOrmlAssetOptions) { + super(options); + + this.assetType = options.assetType; + this.lookupValue = options.lookupValue; + } + + public async getLatestUserBalance( + api: ApiPromise, + address: any + ): Promise { + const tokenLookup: Record = {}; + tokenLookup[this.assetType] = this.lookupValue; + + return api.query.tokens.accounts(address, tokenLookup).then((res) => { + const balance = (res as unknown as OrmlTokensAccountData).free.toString(); + this.balance = balance; + return balance; + }); + } + + public async send( + api: ApiPromise, + to: string, + amount: string + ): Promise> { + const currencyId: Record = {}; + currencyId[this.assetType] = this.lookupValue; + + return (api as ApiPromise).tx.currencies.transfer(to, currencyId, amount); + } +} diff --git a/packages/extension/src/providers/polkadot/networks/unique/libs/activity-handler.ts b/packages/extension/src/providers/polkadot/networks/unique/libs/activity-handler.ts new file mode 100644 index 000000000..e9dd1470e --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/unique/libs/activity-handler.ts @@ -0,0 +1,130 @@ +import { ActivityHandlerType } from "@/libs/activity-state/types"; +import cacheFetch from "@/libs/cache-fetch"; +import { BaseNetwork } from "@/types/base-network"; +import { Activity, ActivityStatus, ActivityType } from "@/types/activity"; + +const TTL = 30000; + +type ExtrinsicData = { + amount: number; + block_index: string; + block_number: string; + fee: number; + from_owner: string; + from_owner_normalized: string; + hash: string; + method: string; + section: string; + success: boolean; + timestamp: number; + to_owner: string; + to_owner_normalized: string; +}; + +const query = ` + query getLastTransfers($orderBy: ExtrinsicOrderByParams = {}, $where: ExtrinsicWhereParams = {}) { + extrinsics(limit: 50, offset: 0, order_by: $orderBy, where: $where) { + data { + block_number + block_index + amount + fee + from_owner_normalized + hash + success + timestamp + to_owner_normalized + } + timestamp + } + } +`.replace(/[\n ]+/g, " "); + +const getVariables = (address: string) => ({ + orderBy: { timestamp: "desc" }, + where: { + _and: [ + { amount: { _neq: 0 } }, + { + method: { + _in: ["transfer", "transfer_all", "transfer_keep_alive"], + }, + }, + { section: { _eq: "Balances" } }, + { + _or: [ + { from_owner_normalized: { _eq: address } }, + { to_owner_normalized: { _eq: address } }, + ], + }, + ], + }, +}); + +function getLastTransfersByAddress( + graphqlEndpoint: string, + address: string +): Promise { + const queryParams = new URLSearchParams({ + query, + variables: JSON.stringify(getVariables(address)), + }); + + const url = `${graphqlEndpoint}?${queryParams.toString()}`; + + return cacheFetch({ url }, TTL) + .then((response) => { + return response.data.extrinsics.data; + }) + .catch((reason) => { + console.error("Failed to fetch activity", reason); + + return []; + }); +} + +const transform = ( + address: string, + network: BaseNetwork, + activity: ExtrinsicData +): Activity => ({ + from: activity.from_owner_normalized, + to: activity.from_owner_normalized, + isIncoming: activity.from_owner_normalized !== address, + network: network.name, + rawInfo: { + from: activity.from_owner_normalized, + to: activity.to_owner_normalized, + success: activity.success, + hash: activity.hash, + block_num: parseInt(activity.block_number), + block_timestamp: activity.timestamp, + module: activity.section, + amount: activity.amount.toString(), + fee: activity.fee.toString(), + nonce: 0, + asset_symbol: network.currencyName, + asset_type: "", + }, + status: activity.success ? ActivityStatus.success : ActivityStatus.failed, + timestamp: activity.timestamp * 1000, + value: activity.amount + "".padStart(network.decimals, "0"), + transactionHash: activity.block_index, + type: ActivityType.transaction, + token: { + decimals: network.decimals, + icon: network.icon, + name: network.currencyNameLong, + symbol: network.currencyName, + }, +}); + +export const getActivityHandler = ( + graphqlEndpoint: string +): ActivityHandlerType => { + return async (network: BaseNetwork, address: string) => { + const transfers = await getLastTransfersByAddress(graphqlEndpoint, address); + + return transfers.map((transfer) => transform(address, network, transfer)); + }; +}; diff --git a/packages/extension/src/providers/polkadot/networks/unique/opal.ts b/packages/extension/src/providers/polkadot/networks/unique/opal.ts new file mode 100644 index 000000000..710c542f7 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/unique/opal.ts @@ -0,0 +1,31 @@ +import { NetworkNames } from "@enkryptcom/types"; +import { + SubstrateNetwork, + SubstrateNetworkOptions, +} from "../../types/substrate-network"; +import { getActivityHandler } from "./libs/activity-handler"; + +const GRAPHQL_ENDPOINT = "https://api-opal.uniquescan.io/v1/graphql"; + +const opalOptions: SubstrateNetworkOptions = { + name: NetworkNames.Opal, + name_long: "Opal", + homePage: "https://unique.network/", + blockExplorerTX: "https://uniquescan.io/opal/extrinsic/[[txHash]]", + blockExplorerAddr: "https://uniquescan.io/opal/account/[[address]]", + isTestNetwork: true, + currencyName: "OPL", + currencyNameLong: "Opal", + icon: require("../icons/opal.svg"), + decimals: 18, + prefix: 42, + gradient: "#0CB6B8", + node: "wss://ws-opal.unique.network", + genesisHash: + "0xc87870ef90a438d574b8e320f17db372c50f62beb52e479c8ff6ee5b460670b9", + activityHandler: getActivityHandler(GRAPHQL_ENDPOINT), +}; + +const opal = new SubstrateNetwork(opalOptions); + +export default opal; diff --git a/packages/extension/src/providers/polkadot/networks/unique/quartz.ts b/packages/extension/src/providers/polkadot/networks/unique/quartz.ts new file mode 100644 index 000000000..131cad1b6 --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/unique/quartz.ts @@ -0,0 +1,32 @@ +import { CoingeckoPlatform, NetworkNames } from "@enkryptcom/types"; +import { subscanActivity } from "../../libs/activity-handlers"; +import { + SubstrateNetwork, + SubstrateNetworkOptions, +} from "../../types/substrate-network"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; + +const quartzOptions: SubstrateNetworkOptions = { + name: NetworkNames.Quartz, + name_long: "Quartz", + homePage: "https://unique.network/", + blockExplorerTX: "https://quartz.subscan.io/extrinsic/[[txHash]]", + blockExplorerAddr: "https://quartz.subscan.io/account/[[address]]", + isTestNetwork: false, + currencyName: "QTZ", + currencyNameLong: "Quartz", + icon: require("../icons/quartz.svg"), + decimals: 18, + prefix: 255, + gradient: "#FF4D6A", + node: "wss://ws-quartz.unique.network", + coingeckoID: "quartz", + coingeckoPlatform: CoingeckoPlatform.Quartz, + genesisHash: + "0xcd4d732201ebe5d6b014edda071c4203e16867305332301dc8d092044b28e554", + activityHandler: wrapActivityHandler(subscanActivity), +}; + +const quartz = new SubstrateNetwork(quartzOptions); + +export default quartz; diff --git a/packages/extension/src/providers/polkadot/networks/unique/unique.ts b/packages/extension/src/providers/polkadot/networks/unique/unique.ts new file mode 100644 index 000000000..f7c4b0a8c --- /dev/null +++ b/packages/extension/src/providers/polkadot/networks/unique/unique.ts @@ -0,0 +1,32 @@ +import { CoingeckoPlatform, NetworkNames } from "@enkryptcom/types"; +import { subscanActivity } from "../../libs/activity-handlers"; +import { + SubstrateNetwork, + SubstrateNetworkOptions, +} from "../../types/substrate-network"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; + +const uniqueOptions: SubstrateNetworkOptions = { + name: NetworkNames.Unique, + name_long: "Unique", + homePage: "https://unique.network/", + blockExplorerTX: "https://unique.subscan.io/extrinsic/[[txHash]]", + blockExplorerAddr: "https://unique.subscan.io/account/[[address]]", + isTestNetwork: false, + currencyName: "UNQ", + currencyNameLong: "Unique", + icon: require("../icons/unique.svg"), + decimals: 18, + prefix: 7391, + gradient: "#00BFFF", + node: "wss://ws.unique.network", + coingeckoID: "unique-network", + coingeckoPlatform: CoingeckoPlatform.Unique, + genesisHash: + "0x84322d9cddbf35088f1e54e9a85c967a41a56a4f43445768125e61af166c7d31", + activityHandler: wrapActivityHandler(subscanActivity), +}; + +const unique = new SubstrateNetwork(uniqueOptions); + +export default unique; diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 4b6f887a2..adf6a4edd 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -117,6 +117,7 @@ import { fromBase } from "@enkryptcom/utils"; import { EnkryptAccount } from "@enkryptcom/types"; import Browser from "webextension-polyfill"; import EVMAccountState from "@/providers/ethereum/libs/accounts-state"; +import BTCAccountState from "@/providers/bitcoin/libs/accounts-state"; import { ProviderName } from "@/types/provider"; import { onClickOutside } from "@vueuse/core"; import RateState from "@/libs/rate-state"; @@ -304,9 +305,12 @@ const onSelectedAddressChanged = async (newAccount: EnkryptAccount) => { currentNetwork.value.provider === ProviderName.ethereum || currentNetwork.value.provider === ProviderName.bitcoin ) { - const evmAccountState = new EVMAccountState(); + const AccountState = + currentNetwork.value.provider === ProviderName.ethereum + ? new EVMAccountState() + : new BTCAccountState(); const domain = await domainState.getCurrentDomain(); - evmAccountState.addApprovedAddress(newAccount.address, domain); + AccountState.addApprovedAddress(newAccount.address, domain); } await domainState.setSelectedAddress(newAccount.address); await sendToBackgroundFromAction({ diff --git a/packages/extension/src/ui/action/views/settings/views/settings-start/index.vue b/packages/extension/src/ui/action/views/settings/views/settings-start/index.vue index 4e3195d57..3c75d1f87 100644 --- a/packages/extension/src/ui/action/views/settings/views/settings-start/index.vue +++ b/packages/extension/src/ui/action/views/settings/views/settings-start/index.vue @@ -36,7 +36,7 @@ { props.network .getAllTokenInfo(props.accountInfo.selectedAccount?.address as string) .then(async (tokens) => { - console.log(tokens); await swap.initPromise; let swapFromTokens = await swap.getFromTokens(); const tokensWithBalance: Record = {}; diff --git a/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-list.vue b/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-list.vue index 248239f96..a56f5ad56 100644 --- a/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-list.vue +++ b/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-list.vue @@ -26,9 +26,9 @@ interface IProps { } const getReadable = (idx: number) => { - return new SwapToken(props.toToken).toReadable( - props.trades[idx].toTokenAmount - ); + return new SwapToken(props.toToken) + .toReadable(props.trades[idx].toTokenAmount) + .substring(0, 20); }; const props = defineProps(); diff --git a/packages/swap/src/index.ts b/packages/swap/src/index.ts index da5247e8f..71196c462 100644 --- a/packages/swap/src/index.ts +++ b/packages/swap/src/index.ts @@ -3,7 +3,9 @@ import { merge } from "lodash"; import EventEmitter from "eventemitter3"; import { TOKEN_LISTS, TOP_TOKEN_INFO_LIST } from "./configs"; import OneInch from "./providers/oneInch"; +import Paraswap from "./providers/paraswap"; import Changelly from "./providers/changelly"; +import ZeroX from "./providers/zerox"; import NetworkDetails, { isSupportedNetwork, getSupportedNetworks, @@ -48,9 +50,14 @@ class Swap extends EventEmitter { initPromise: Promise; - private providerClasses: (typeof OneInch | typeof Changelly)[]; + private providerClasses: ( + | typeof OneInch + | typeof Changelly + | typeof Paraswap + | typeof ZeroX + )[]; - private providers: (OneInch | Changelly)[]; + private providers: (OneInch | Changelly | Paraswap | ZeroX)[]; private tokenList: FromTokenType; @@ -72,7 +79,7 @@ class Swap extends EventEmitter { }; this.api = options.api; this.walletId = options.walletIdentifier; - this.providerClasses = [OneInch, Changelly]; + this.providerClasses = [OneInch, Paraswap, Changelly, ZeroX]; this.topTokenInfo = { contractsToId: {}, topTokens: {}, diff --git a/packages/swap/src/providers/paraswap/index.ts b/packages/swap/src/providers/paraswap/index.ts new file mode 100644 index 000000000..ae1db09bf --- /dev/null +++ b/packages/swap/src/providers/paraswap/index.ts @@ -0,0 +1,348 @@ +import type Web3Eth from "web3-eth"; +import { numberToHex, toBN } from "web3-utils"; +import { + EVMTransaction, + getQuoteOptions, + MinMaxResponse, + ProviderClass, + ProviderFromTokenResponse, + ProviderName, + ProviderQuoteResponse, + ProviderSwapResponse, + ProviderToTokenResponse, + QuoteMetaOptions, + StatusOptions, + StatusOptionsResponse, + SupportedNetworkName, + SwapQuote, + TokenType, + TransactionStatus, + TransactionType, +} from "../../types"; +import { + DEFAULT_SLIPPAGE, + FEE_CONFIGS, + GAS_LIMITS, + NATIVE_TOKEN_ADDRESS, +} from "../../configs"; +import { + ParaSwapSwapResponse, + ParaswapResponseType, + ParaswpQuoteResponse, +} from "./types"; +import { + getAllowanceTransactions, + TOKEN_AMOUNT_INFINITY_AND_BEYOND, +} from "../../utils/approvals"; +import estimateGasList from "../../common/estimateGasList"; +import { isEVMAddress } from "../../utils/common"; + +export const PARASWAP_APPROVAL_ADDRESS = + "0x216b4b4ba9f3e719726886d34a177484278bfcae"; + +const supportedNetworks: { + [key in SupportedNetworkName]?: { approvalAddress: string; chainId: string }; +} = { + [SupportedNetworkName.Ethereum]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "1", + }, + [SupportedNetworkName.Binance]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "56", + }, + [SupportedNetworkName.Matic]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "137", + }, + [SupportedNetworkName.Avalanche]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "43114", + }, + [SupportedNetworkName.Fantom]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "250", + }, + [SupportedNetworkName.Arbitrum]: { + approvalAddress: PARASWAP_APPROVAL_ADDRESS, + chainId: "42161", + }, +}; + +const BASE_URL = "https://apiv5.paraswap.io/"; + +class ParaSwap extends ProviderClass { + tokenList: TokenType[]; + + network: SupportedNetworkName; + + web3eth: Web3Eth; + + name: ProviderName; + + fromTokens: ProviderFromTokenResponse; + + toTokens: ProviderToTokenResponse; + + constructor(web3eth: Web3Eth, network: SupportedNetworkName) { + super(web3eth, network); + this.network = network; + this.tokenList = []; + this.web3eth = web3eth; + this.name = ProviderName.paraswap; + this.fromTokens = {}; + this.toTokens = {}; + } + + init(tokenList: TokenType[]): Promise { + if (!ParaSwap.isSupported(this.network)) return; + tokenList.forEach((t) => { + this.fromTokens[t.address] = t; + if (!this.toTokens[this.network]) this.toTokens[this.network] = {}; + this.toTokens[this.network][t.address] = { + ...t, + networkInfo: { + name: this.network, + isAddress: (address: string) => + Promise.resolve(isEVMAddress(address)), + }, + }; + }); + } + + static isSupported(network: SupportedNetworkName) { + return Object.keys(supportedNetworks).includes( + network as unknown as string + ); + } + + getFromTokens() { + return this.fromTokens; + } + + getToTokens() { + return this.toTokens; + } + + getMinMaxAmount(): Promise { + return Promise.resolve({ + minimumFrom: toBN("1"), + maximumFrom: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND), + minimumTo: toBN("1"), + maximumTo: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND), + }); + } + + private getParaswapSwap( + options: getQuoteOptions, + meta: QuoteMetaOptions, + accurateEstimate: boolean + ): Promise { + if ( + !ParaSwap.isSupported( + options.toToken.networkInfo.name as SupportedNetworkName + ) || + this.network !== options.toToken.networkInfo.name + ) + return Promise.resolve(null); + const feeConfig = FEE_CONFIGS[this.name][meta.walletIdentifier]; + const params = new URLSearchParams({ + ignoreChecks: "true", + ignoreGasEstimate: "true", + onlyParams: "false", + }); + const body = JSON.stringify({ + srcToken: options.fromToken.address, + srcDecimals: options.fromToken.decimals.toString(), + destToken: options.toToken.address, + destDecimals: options.toToken.decimals.toString(), + srcAmount: options.amount.toString(), + priceRoute: meta.priceRoute!, + userAddress: options.fromAddress, + txOrigin: options.fromAddress, + receiver: options.toAddress, + slippage: parseInt( + ( + parseFloat(meta.slippage ? meta.slippage : DEFAULT_SLIPPAGE) * 10 + ).toString(), + 10 + ).toString(), + deadline: Math.floor(Date.now() / 1000) + 300, + partnerAddress: feeConfig ? feeConfig.referrer : "", + partnerFeeBps: feeConfig + ? parseInt((feeConfig.fee * 10000).toString(), 10).toString() + : "0", + }); + return fetch( + `${BASE_URL}transactions/${ + supportedNetworks[this.network].chainId + }?${params.toString()}`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body, + } + ) + .then((res) => res.json()) + .then(async (response: ParaswapResponseType) => { + if (response.error) { + console.error(response.error); + return Promise.resolve(null); + } + const transactions: EVMTransaction[] = []; + + if (options.fromToken.address !== NATIVE_TOKEN_ADDRESS) { + const approvalTxs = await getAllowanceTransactions({ + infinityApproval: meta.infiniteApproval, + spender: supportedNetworks[this.network].approvalAddress, + web3eth: this.web3eth, + amount: options.amount, + fromAddress: options.fromAddress, + fromToken: options.fromToken, + }); + transactions.push(...approvalTxs); + } + transactions.push({ + from: options.fromAddress, + gasLimit: GAS_LIMITS.swap, + to: response.to, + value: numberToHex(response.value), + data: response.data, + type: TransactionType.evm, + }); + if (accurateEstimate) { + const accurateGasEstimate = await estimateGasList( + transactions, + this.network + ); + if (accurateGasEstimate) { + if (accurateGasEstimate.isError) return null; + transactions.forEach((tx, idx) => { + tx.gasLimit = accurateGasEstimate.result[idx]; + }); + } + } + return { + transactions, + toTokenAmount: toBN( + (meta.priceRoute as ParaswpQuoteResponse).destAmount + ), + fromTokenAmount: toBN( + (meta.priceRoute as ParaswpQuoteResponse).srcAmount + ), + }; + }); + } + + getQuote( + options: getQuoteOptions, + meta: QuoteMetaOptions + ): Promise { + if ( + !ParaSwap.isSupported( + options.toToken.networkInfo.name as SupportedNetworkName + ) || + this.network !== options.toToken.networkInfo.name + ) + return Promise.resolve(null); + const feeConfig = FEE_CONFIGS[this.name][meta.walletIdentifier]; + const params = new URLSearchParams({ + srcToken: options.fromToken.address, + srcDecimals: options.fromToken.decimals.toString(), + destToken: options.toToken.address, + destDecimals: options.toToken.decimals.toString(), + amount: options.amount.toString(), + side: "SELL", + network: supportedNetworks[this.network].chainId, + userAddress: options.fromAddress, + receiver: options.toAddress, + partnerAddress: feeConfig ? feeConfig.referrer : "", + partnerFeeBps: feeConfig + ? parseInt((feeConfig.fee * 10000).toString(), 10).toString() + : "0", + }); + return fetch(`${BASE_URL}prices?${params.toString()}`) + .then((j) => j.json()) + .then(async (jsonRes) => { + if (!jsonRes) return null; + const res: ParaswpQuoteResponse = jsonRes.priceRoute; + const transactions: EVMTransaction[] = []; + if (options.fromToken.address !== NATIVE_TOKEN_ADDRESS) { + const approvalTxs = await getAllowanceTransactions({ + infinityApproval: meta.infiniteApproval, + spender: supportedNetworks[this.network].approvalAddress, + web3eth: this.web3eth, + amount: options.amount, + fromAddress: options.fromAddress, + fromToken: options.fromToken, + }); + transactions.push(...approvalTxs); + } + const response: ProviderQuoteResponse = { + fromTokenAmount: toBN(res.srcAmount), + toTokenAmount: toBN(res.destAmount), + provider: this.name, + quote: { + meta: { + ...meta, + priceRoute: res, + }, + options, + provider: this.name, + }, + totalGaslimit: + transactions.reduce( + (total: number, curVal: EVMTransaction) => + total + toBN(curVal.gasLimit).toNumber(), + 0 + ) + toBN(GAS_LIMITS.swap).toNumber(), + minMax: await this.getMinMaxAmount(), + }; + return response; + }); + } + + getSwap(quote: SwapQuote): Promise { + return this.getParaswapSwap(quote.options, quote.meta, true).then((res) => { + if (!res) return null; + const feeConfig = + FEE_CONFIGS[this.name][quote.meta.walletIdentifier].fee || 0; + const response: ProviderSwapResponse = { + fromTokenAmount: res.fromTokenAmount, + provider: this.name, + toTokenAmount: res.toTokenAmount, + transactions: res.transactions, + slippage: quote.meta.slippage || DEFAULT_SLIPPAGE, + fee: feeConfig * 100, + getStatusObject: async ( + options: StatusOptions + ): Promise => ({ + options, + provider: this.name, + }), + }; + return response; + }); + } + + getStatus(options: StatusOptions): Promise { + const promises = options.transactionHashes.map((hash) => + this.web3eth.getTransactionReceipt(hash) + ); + return Promise.all(promises).then((receipts) => { + // eslint-disable-next-line no-restricted-syntax + for (const receipt of receipts) { + if (!receipt || (receipt && !receipt.blockNumber)) { + return TransactionStatus.pending; + } + if (receipt && !receipt.status) return TransactionStatus.failed; + } + return TransactionStatus.success; + }); + } +} + +export default ParaSwap; diff --git a/packages/swap/src/providers/paraswap/types.ts b/packages/swap/src/providers/paraswap/types.ts new file mode 100644 index 000000000..9d4c1a79e --- /dev/null +++ b/packages/swap/src/providers/paraswap/types.ts @@ -0,0 +1,28 @@ +import { BN, EVMTransaction } from "../../types"; + +export interface ParaswapResponseType { + error?: string; + from: string; + to: string; + data: string; + value: string; +} +export interface ParaSwapSwapResponse { + transactions: EVMTransaction[]; + toTokenAmount: BN; + fromTokenAmount: BN; +} + +export interface ParaswpQuoteResponse { + blockNumber: number; + network: number; + srcToken: string; + srcDecimals: number; + srcAmount: string; + destToken: string; + destDecimals: number; + destAmount: string; + tokenTransferProxy: string; + contractAddress: string; + bestRoute: unknown; +} diff --git a/packages/swap/src/providers/zerox/index.ts b/packages/swap/src/providers/zerox/index.ts new file mode 100644 index 000000000..39c183d95 --- /dev/null +++ b/packages/swap/src/providers/zerox/index.ts @@ -0,0 +1,281 @@ +import type Web3Eth from "web3-eth"; +import { numberToHex, toBN } from "web3-utils"; +import { + EVMTransaction, + getQuoteOptions, + MinMaxResponse, + ProviderClass, + ProviderFromTokenResponse, + ProviderName, + ProviderQuoteResponse, + ProviderSwapResponse, + ProviderToTokenResponse, + QuoteMetaOptions, + StatusOptions, + StatusOptionsResponse, + SupportedNetworkName, + SwapQuote, + TokenType, + TransactionStatus, + TransactionType, +} from "../../types"; +import { + DEFAULT_SLIPPAGE, + FEE_CONFIGS, + GAS_LIMITS, + NATIVE_TOKEN_ADDRESS, +} from "../../configs"; +import { ZeroXResponseType, ZeroXSwapResponse } from "./types"; +import { + getAllowanceTransactions, + TOKEN_AMOUNT_INFINITY_AND_BEYOND, +} from "../../utils/approvals"; +import estimateGasList from "../../common/estimateGasList"; +import { isEVMAddress } from "../../utils/common"; + +const supportedNetworks: { + [key in SupportedNetworkName]?: { approvalAddress: string; chainId: string }; +} = { + [SupportedNetworkName.Ethereum]: { + approvalAddress: "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + chainId: "1", + }, + [SupportedNetworkName.Binance]: { + approvalAddress: "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + chainId: "56", + }, + [SupportedNetworkName.Matic]: { + approvalAddress: "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + chainId: "137", + }, + [SupportedNetworkName.Optimism]: { + approvalAddress: "0xdef1abe32c034e558cdd535791643c58a13acc10", + chainId: "10", + }, + [SupportedNetworkName.Avalanche]: { + approvalAddress: "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + chainId: "43114", + }, + [SupportedNetworkName.Fantom]: { + approvalAddress: "0xdef189deaef76e379df891899eb5a00a94cbc250", + chainId: "250", + }, + [SupportedNetworkName.Arbitrum]: { + approvalAddress: "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + chainId: "42161", + }, +}; + +const BASE_URL = "https://partners.mewapi.io/zerox/"; + +class ZeroX extends ProviderClass { + tokenList: TokenType[]; + + network: SupportedNetworkName; + + web3eth: Web3Eth; + + name: ProviderName; + + fromTokens: ProviderFromTokenResponse; + + toTokens: ProviderToTokenResponse; + + constructor(web3eth: Web3Eth, network: SupportedNetworkName) { + super(web3eth, network); + this.network = network; + this.tokenList = []; + this.web3eth = web3eth; + this.name = ProviderName.zerox; + this.fromTokens = {}; + this.toTokens = {}; + } + + init(tokenList: TokenType[]): Promise { + if (!ZeroX.isSupported(this.network)) return; + tokenList.forEach((t) => { + this.fromTokens[t.address] = t; + if (!this.toTokens[this.network]) this.toTokens[this.network] = {}; + this.toTokens[this.network][t.address] = { + ...t, + networkInfo: { + name: this.network, + isAddress: (address: string) => + Promise.resolve(isEVMAddress(address)), + }, + }; + }); + } + + static isSupported(network: SupportedNetworkName) { + return Object.keys(supportedNetworks).includes( + network as unknown as string + ); + } + + getFromTokens() { + return this.fromTokens; + } + + getToTokens() { + return this.toTokens; + } + + getMinMaxAmount(): Promise { + return Promise.resolve({ + minimumFrom: toBN("1"), + maximumFrom: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND), + minimumTo: toBN("1"), + maximumTo: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND), + }); + } + + private getZeroXSwap( + options: getQuoteOptions, + meta: QuoteMetaOptions, + accurateEstimate: boolean + ): Promise { + if ( + !ZeroX.isSupported( + options.toToken.networkInfo.name as SupportedNetworkName + ) || + this.network !== options.toToken.networkInfo.name + ) + return Promise.resolve(null); + if (options.fromAddress.toLowerCase() !== options.toAddress.toLowerCase()) + // zerox doesnt allow different to address + return Promise.resolve(null); + const feeConfig = FEE_CONFIGS[this.name][meta.walletIdentifier]; + const params = new URLSearchParams({ + sellToken: options.fromToken.address, + buyToken: options.toToken.address, + sellAmount: options.amount.toString(), + takerAddress: options.fromAddress, + slippagePercentage: ( + parseFloat(meta.slippage ? meta.slippage : DEFAULT_SLIPPAGE) / 100 + ).toString(), + buyTokenPercentageFee: feeConfig ? feeConfig.fee.toString() : "0", + feeRecipient: feeConfig ? feeConfig.referrer : "", + skipValidation: "true", + enableSlippageProtection: "false", + affiliateAddress: feeConfig ? feeConfig.referrer : "", + }); + return fetch( + `${BASE_URL}${ + supportedNetworks[this.network].chainId + }/swap/v1/quote?${params.toString()}` + ) + .then((res) => res.json()) + .then(async (response: ZeroXResponseType) => { + if (response.code) { + console.error(response.code, response.reason); + return Promise.resolve(null); + } + const transactions: EVMTransaction[] = []; + + if (options.fromToken.address !== NATIVE_TOKEN_ADDRESS) { + const approvalTxs = await getAllowanceTransactions({ + infinityApproval: meta.infiniteApproval, + spender: supportedNetworks[this.network].approvalAddress, + web3eth: this.web3eth, + amount: options.amount, + fromAddress: options.fromAddress, + fromToken: options.fromToken, + }); + transactions.push(...approvalTxs); + } + transactions.push({ + from: options.fromAddress, + gasLimit: GAS_LIMITS.swap, + to: response.to, + value: numberToHex(response.value), + data: response.data, + type: TransactionType.evm, + }); + if (accurateEstimate) { + const accurateGasEstimate = await estimateGasList( + transactions, + this.network + ); + if (accurateGasEstimate) { + if (accurateGasEstimate.isError) return null; + transactions.forEach((tx, idx) => { + tx.gasLimit = accurateGasEstimate.result[idx]; + }); + } + } + return { + transactions, + toTokenAmount: toBN(response.buyAmount), + fromTokenAmount: toBN(response.sellAmount), + }; + }); + } + + getQuote( + options: getQuoteOptions, + meta: QuoteMetaOptions + ): Promise { + return this.getZeroXSwap(options, meta, false).then(async (res) => { + if (!res) return null; + const response: ProviderQuoteResponse = { + fromTokenAmount: res.fromTokenAmount, + toTokenAmount: res.toTokenAmount, + provider: this.name, + quote: { + meta, + options, + provider: this.name, + }, + totalGaslimit: res.transactions.reduce( + (total: number, curVal: EVMTransaction) => + total + toBN(curVal.gasLimit).toNumber(), + 0 + ), + minMax: await this.getMinMaxAmount(), + }; + return response; + }); + } + + getSwap(quote: SwapQuote): Promise { + return this.getZeroXSwap(quote.options, quote.meta, true).then((res) => { + if (!res) return null; + const feeConfig = + FEE_CONFIGS[this.name][quote.meta.walletIdentifier].fee || 0; + const response: ProviderSwapResponse = { + fromTokenAmount: res.fromTokenAmount, + provider: this.name, + toTokenAmount: res.toTokenAmount, + transactions: res.transactions, + slippage: quote.meta.slippage || DEFAULT_SLIPPAGE, + fee: feeConfig * 100, + getStatusObject: async ( + options: StatusOptions + ): Promise => ({ + options, + provider: this.name, + }), + }; + return response; + }); + } + + getStatus(options: StatusOptions): Promise { + const promises = options.transactionHashes.map((hash) => + this.web3eth.getTransactionReceipt(hash) + ); + return Promise.all(promises).then((receipts) => { + // eslint-disable-next-line no-restricted-syntax + for (const receipt of receipts) { + if (!receipt || (receipt && !receipt.blockNumber)) { + return TransactionStatus.pending; + } + if (receipt && !receipt.status) return TransactionStatus.failed; + } + return TransactionStatus.success; + }); + } +} + +export default ZeroX; diff --git a/packages/swap/src/providers/zerox/types.ts b/packages/swap/src/providers/zerox/types.ts new file mode 100644 index 000000000..9bd5c7b5f --- /dev/null +++ b/packages/swap/src/providers/zerox/types.ts @@ -0,0 +1,18 @@ +import { BN, EVMTransaction } from "../../types"; + +export interface ZeroXResponseType { + code?: number; + reason?: string; + buyTokenAddress: string; + sellTokenAddress: string; + buyAmount: string; + sellAmount: string; + to: string; + data: string; + value: string; +} +export interface ZeroXSwapResponse { + transactions: EVMTransaction[]; + toTokenAmount: BN; + fromTokenAmount: BN; +} diff --git a/packages/swap/src/types/index.ts b/packages/swap/src/types/index.ts index 0696acfb2..7fe9a18ea 100644 --- a/packages/swap/src/types/index.ts +++ b/packages/swap/src/types/index.ts @@ -75,9 +75,9 @@ export interface FromTokenType { } export interface ToTokenType { - top: Record; - trending: Record; - all: Record; + top: Record | Record; + trending: Record | Record; + all: Record | Record; } export interface EvmOptions { @@ -97,6 +97,7 @@ export interface QuoteMetaOptions { walletIdentifier: WalletIdentifier; slippage?: string; changellyQuoteId?: string; + priceRoute?: unknown; } export interface SwapOptions { @@ -196,10 +197,9 @@ export interface ProviderSwapResponse { export type ProviderFromTokenResponse = Record; -export type ProviderToTokenResponse = Record< - SupportedNetworkName, - Record ->; +export type ProviderToTokenResponse = + | Record> + | Record; export interface TopTokenInfo { trendingTokens: Record; diff --git a/packages/swap/tests/fixtures/mainnet/configs.ts b/packages/swap/tests/fixtures/mainnet/configs.ts index 955c242ed..07279342b 100644 --- a/packages/swap/tests/fixtures/mainnet/configs.ts +++ b/packages/swap/tests/fixtures/mainnet/configs.ts @@ -19,6 +19,22 @@ const fromToken: TokenType = { type: NetworkType.EVM, }; +const toTokenWETH: TokenTypeTo = { + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + decimals: 18, + logoURI: + "https://assets.coingecko.com/coins/images/2518/thumb/weth.png?1628852295", + name: "WETH", + symbol: "WETH", + rank: 100, + cgId: "ethereum", + type: NetworkType.EVM, + networkInfo: { + isAddress: async (address: string) => isAddress(address), + name: NetworkNames.Ethereum, + }, +}; + const toToken: TokenTypeTo = { address: "0x111111111117dc0aa78b770fa6a738034120c302", decimals: 18, @@ -35,4 +51,12 @@ const toToken: TokenTypeTo = { }, }; -export { fromToken, toToken, amount, fromAddress, toAddress, nodeURL }; +export { + fromToken, + toToken, + toTokenWETH, + amount, + fromAddress, + toAddress, + nodeURL, +}; diff --git a/packages/swap/tests/paraswap.test.ts b/packages/swap/tests/paraswap.test.ts new file mode 100644 index 000000000..141dc5319 --- /dev/null +++ b/packages/swap/tests/paraswap.test.ts @@ -0,0 +1,84 @@ +import { expect } from "chai"; +import Web3Eth from "web3-eth"; +import { numberToHex } from "web3-utils"; +import Parawap, { PARASWAP_APPROVAL_ADDRESS } from "../src/providers/paraswap"; +import { + EVMTransaction, + ProviderName, + SupportedNetworkName, + WalletIdentifier, +} from "../src/types"; +import { TOKEN_AMOUNT_INFINITY_AND_BEYOND } from "../src/utils/approvals"; +import { + fromToken, + toTokenWETH as toToken, + amount, + fromAddress, + toAddress, + nodeURL, +} from "./fixtures/mainnet/configs"; + +describe("Paraswap Provider", () => { + // @ts-ignore + const web3eth = new Web3Eth(nodeURL); + const paraSwap = new Parawap(web3eth, SupportedNetworkName.Ethereum); + it("it should return a quote infinity approval", async () => { + const quote = await paraSwap.getQuote( + { + amount, + fromAddress, + fromToken, + toToken, + toAddress, + }, + { infiniteApproval: true, walletIdentifier: WalletIdentifier.enkrypt } + ); + expect(quote?.provider).to.be.eq(ProviderName.paraswap); + expect(quote?.quote.meta.infiniteApproval).to.be.eq(true); + expect(quote?.quote.meta.walletIdentifier).to.be.eq( + WalletIdentifier.enkrypt + ); + expect(quote?.fromTokenAmount.toString()).to.be.eq(amount.toString()); + expect(quote?.toTokenAmount.gtn(0)).to.be.eq(true); + + const swap = await paraSwap.getSwap(quote!.quote); + expect(swap?.transactions.length).to.be.eq(2); + expect(swap?.transactions[0].to).to.be.eq(fromToken.address); + expect((swap?.transactions[0] as EVMTransaction).data).to.be.eq( + `0x095ea7b3000000000000000000000000${PARASWAP_APPROVAL_ADDRESS.replace( + "0x", + "" + )}${TOKEN_AMOUNT_INFINITY_AND_BEYOND.replace("0x", "")}` + ); + expect(swap?.transactions[1].to).to.be.eq( + "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57" + ); + }).timeout(5000); + + it("it should return a quote non infinity approval", async () => { + const quote = await paraSwap.getQuote( + { + amount, + fromAddress, + fromToken, + toToken, + toAddress, + }, + { infiniteApproval: false, walletIdentifier: WalletIdentifier.enkrypt } + ); + expect(quote?.quote.meta.infiniteApproval).to.be.eq(false); + const swap = await paraSwap.getSwap(quote!.quote); + expect(swap?.transactions.length).to.be.eq(2); + expect((swap?.transactions[0] as EVMTransaction).data).to.be.eq( + `0x095ea7b3000000000000000000000000${PARASWAP_APPROVAL_ADDRESS.replace( + "0x", + "" + )}000000000000000000000000000000000000000000000000${numberToHex( + amount + ).replace("0x", "")}` + ); + expect(swap?.transactions[1].to).to.be.eq( + "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57" + ); + }).timeout(5000); +}); diff --git a/packages/swap/tests/swap.test.ts b/packages/swap/tests/swap.test.ts index 60b4d0774..426b7cddf 100644 --- a/packages/swap/tests/swap.test.ts +++ b/packages/swap/tests/swap.test.ts @@ -52,7 +52,7 @@ describe("Swap", () => { ); }); - it("it should get quote and swap", async () => { + it("it should get quote and swap for different destination", async () => { await enkryptSwap.initPromise; const quotes = await enkryptSwap.getQuotes({ amount, @@ -61,19 +61,51 @@ describe("Swap", () => { toToken, toAddress, }); - expect(quotes?.length).to.be.eq(2); + expect(quotes?.length).to.be.eq(3); const oneInceQuote = quotes.find( (q) => q.provider === ProviderName.oneInch ); + const paraswapQuote = quotes.find( + (q) => q.provider === ProviderName.paraswap + ); const changellyQuote = quotes.find( (q) => q.provider === ProviderName.changelly ); + const zeroxQuote = quotes.find((q) => q.provider === ProviderName.zerox); + expect(zeroxQuote).to.be.eq(undefined); expect(changellyQuote!.provider).to.be.eq(ProviderName.changelly); expect(oneInceQuote!.provider).to.be.eq(ProviderName.oneInch); + expect(paraswapQuote!.provider).to.be.eq(ProviderName.paraswap); const swapOneInch = await enkryptSwap.getSwap(oneInceQuote!.quote); expect(swapOneInch?.fromTokenAmount.toString()).to.be.eq(amount.toString()); expect(swapOneInch?.transactions.length).to.be.eq(2); const swapChangelly = await enkryptSwap.getSwap(changellyQuote!.quote); if (swapChangelly) expect(swapChangelly?.transactions.length).to.be.eq(1); }).timeout(10000); + + it("it should get quote and swap for same destination", async () => { + await enkryptSwap.initPromise; + const quotes = await enkryptSwap.getQuotes({ + amount, + fromAddress, + fromToken, + toToken, + toAddress: fromAddress, + }); + expect(quotes?.length).to.be.eq(4); + const oneInceQuote = quotes.find( + (q) => q.provider === ProviderName.oneInch + ); + const paraswapQuote = quotes.find( + (q) => q.provider === ProviderName.paraswap + ); + const changellyQuote = quotes.find( + (q) => q.provider === ProviderName.changelly + ); + const zeroxQuote = quotes.find((q) => q.provider === ProviderName.zerox); + expect(zeroxQuote!.provider).to.be.eq(ProviderName.zerox); + expect(changellyQuote!.provider).to.be.eq(ProviderName.changelly); + expect(oneInceQuote!.provider).to.be.eq(ProviderName.oneInch); + expect(paraswapQuote!.provider).to.be.eq(ProviderName.paraswap); + }).timeout(10000); }); diff --git a/packages/swap/tests/zerox.test.ts b/packages/swap/tests/zerox.test.ts new file mode 100644 index 000000000..772cf68aa --- /dev/null +++ b/packages/swap/tests/zerox.test.ts @@ -0,0 +1,80 @@ +import { expect } from "chai"; +import Web3Eth from "web3-eth"; +import { numberToHex } from "web3-utils"; +import Zerox from "../src/providers/zerox"; +import { + EVMTransaction, + ProviderName, + SupportedNetworkName, + WalletIdentifier, +} from "../src/types"; +import { TOKEN_AMOUNT_INFINITY_AND_BEYOND } from "../src/utils/approvals"; +import { + fromToken, + toToken, + amount, + fromAddress, + nodeURL, +} from "./fixtures/mainnet/configs"; + +describe("Zerox Provider", () => { + // @ts-ignore + const web3eth = new Web3Eth(nodeURL); + const zerox = new Zerox(web3eth, SupportedNetworkName.Ethereum); + const ZEROX_APPROVAL = "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; + it("it should return a quote infinity approval", async () => { + const quote = await zerox.getQuote( + { + amount, + fromAddress, + fromToken, + toToken, + toAddress: fromAddress, + }, + { infiniteApproval: true, walletIdentifier: WalletIdentifier.enkrypt } + ); + expect(quote?.provider).to.be.eq(ProviderName.zerox); + expect(quote?.quote.meta.infiniteApproval).to.be.eq(true); + expect(quote?.quote.meta.walletIdentifier).to.be.eq( + WalletIdentifier.enkrypt + ); + expect(quote?.fromTokenAmount.toString()).to.be.eq(amount.toString()); + expect(quote?.toTokenAmount.gtn(0)).to.be.eq(true); + + const swap = await zerox.getSwap(quote!.quote); + expect(swap?.transactions.length).to.be.eq(2); + expect(swap?.transactions[0].to).to.be.eq(fromToken.address); + expect((swap?.transactions[0] as EVMTransaction).data).to.be.eq( + `0x095ea7b3000000000000000000000000${ZEROX_APPROVAL.replace( + "0x", + "" + )}${TOKEN_AMOUNT_INFINITY_AND_BEYOND.replace("0x", "")}` + ); + expect(swap?.transactions[1].to).to.be.eq(ZEROX_APPROVAL); + }).timeout(5000); + + it("it should return a quote non infinity approval", async () => { + const quote = await zerox.getQuote( + { + amount, + fromAddress, + fromToken, + toToken, + toAddress: fromAddress, + }, + { infiniteApproval: false, walletIdentifier: WalletIdentifier.enkrypt } + ); + expect(quote?.quote.meta.infiniteApproval).to.be.eq(false); + const swap = await zerox.getSwap(quote!.quote); + expect(swap?.transactions.length).to.be.eq(2); + expect((swap?.transactions[0] as EVMTransaction).data).to.be.eq( + `0x095ea7b3000000000000000000000000${ZEROX_APPROVAL.replace( + "0x", + "" + )}000000000000000000000000000000000000000000000000${numberToHex( + amount + ).replace("0x", "")}` + ); + expect(swap?.transactions[1].to).to.be.eq(ZEROX_APPROVAL); + }).timeout(5000); +}); diff --git a/packages/types/src/networks.ts b/packages/types/src/networks.ts index a2cfa08b4..29d92e42e 100644 --- a/packages/types/src/networks.ts +++ b/packages/types/src/networks.ts @@ -50,6 +50,11 @@ export enum NetworkNames { Klaytn = "KLAY", Aurora = "AURORA", PuppyNet = "puppyNet", + Opal = "OPL", + Quartz = "QTZ", + Unique = "UNQ", + Interlay = "INTR", + Kintsugi = "KINT", } export enum CoingeckoPlatform { @@ -83,4 +88,8 @@ export enum CoingeckoPlatform { Klaytn = "klay-token", Aurora = "aurora", Zksync = "zksync", + Quartz = "quartz", + Unique = "unique-network", + Interlay = "interlay", + Kintsugi = "kintsugi", }