From a7098b4200b010a7ea92ad275bca46f03ba1e737 Mon Sep 17 00:00:00 2001 From: tunghp2002 Date: Tue, 26 Nov 2024 18:32:18 +0700 Subject: [PATCH 01/64] [Add] Extension - Intergrate Simple Swap] --- .env.example | 1 + .github/workflows/push-koni-dev.yml | 1 + .github/workflows/push-master.yml | 1 + .github/workflows/push-web-runner.yml | 1 + .github/workflows/push-webapp.yml | 1 + .../src/core/logic-validation/swap.ts | 29 +- .../handler/simpleswap-handler.ts | 411 ++++++++++++++++++ .../src/services/swap-service/index.ts | 13 +- .../src/services/swap-service/utils.ts | 10 +- .../src/services/transaction-service/utils.ts | 10 +- .../extension-base/src/types/swap/index.ts | 16 +- .../src/Popup/Home/History/Detail/index.tsx | 13 +- .../src/Popup/Home/History/Detail/index.tsx | 8 +- packages/web-runner/webpack.config.cjs | 3 +- packages/webapp/webpack.config.cjs | 3 +- 15 files changed, 508 insertions(+), 13 deletions(-) create mode 100644 packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts diff --git a/.env.example b/.env.example index 0d84f2a1e6..cdb6bb3958 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ BITTENSOR_API_KEY_3=abafdbad3 BITTENSOR_API_KEY_4=abafdbad4 BITTENSOR_API_KEY_5=abafdbad5 BITTENSOR_API_KEY_6=abafdbad6 +SIMPLE_SWAP_API_KEY=abacasdf diff --git a/.github/workflows/push-koni-dev.yml b/.github/workflows/push-koni-dev.yml index 1b32aefbf3..b0bf8f2f20 100644 --- a/.github/workflows/push-koni-dev.yml +++ b/.github/workflows/push-koni-dev.yml @@ -53,6 +53,7 @@ jobs: BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} BITTENSOR_API_KEY_5: ${{ secrets.BITTENSOR_API_KEY_5 }} BITTENSOR_API_KEY_6: ${{ secrets.BITTENSOR_API_KEY_6 }} + SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} GH_RELEASE_FILES: master-build.zip,master-src.zip COMMIT_MESSAGE: ${{ github.event.head_commit.message }} REF_NAME: ${{ github.ref_name }} diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index ae535ffbeb..ffd13e7259 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -34,6 +34,7 @@ jobs: BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} BITTENSOR_API_KEY_5: ${{ secrets.BITTENSOR_API_KEY_5 }} BITTENSOR_API_KEY_6: ${{ secrets.BITTENSOR_API_KEY_6 }} + SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-web-runner.yml b/.github/workflows/push-web-runner.yml index 63f9426744..cbb151ab59 100644 --- a/.github/workflows/push-web-runner.yml +++ b/.github/workflows/push-web-runner.yml @@ -38,6 +38,7 @@ jobs: BITTENSOR_API_KEY_4: ${{ secrets.BITTENSOR_API_KEY_4 }} BITTENSOR_API_KEY_5: ${{ secrets.BITTENSOR_API_KEY_5 }} BITTENSOR_API_KEY_6: ${{ secrets.BITTENSOR_API_KEY_6 }} + SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} BRANCH_NAME: master run: | yarn install --immutable | grep -v 'YN0013' diff --git a/.github/workflows/push-webapp.yml b/.github/workflows/push-webapp.yml index 01dd6f4460..9827f97c0e 100644 --- a/.github/workflows/push-webapp.yml +++ b/.github/workflows/push-webapp.yml @@ -34,6 +34,7 @@ jobs: CHAINFLIP_BROKER_API: ${{ secrets.CHAINFLIP_BROKER_API }} BITTENSOR_API_KEY_1: ${{ secrets.BITTENSOR_API_KEY_1 }} BITTENSOR_API_KEY_2: ${{ secrets.BITTENSOR_API_KEY_2 }} + SIMPLE_SWAP_API_KEY: ${{ secrets.SIMPLE_SWAP_API_KEY }} BRANCH_NAME: ${{ github.ref_name }} run: | yarn install --immutable | grep -v 'YN0013' diff --git a/packages/extension-base/src/core/logic-validation/swap.ts b/packages/extension-base/src/core/logic-validation/swap.ts index a9af3f6359..a749f5710f 100644 --- a/packages/extension-base/src/core/logic-validation/swap.ts +++ b/packages/extension-base/src/core/logic-validation/swap.ts @@ -6,7 +6,7 @@ import { SwapError } from '@subwallet/extension-base/background/errors/SwapError import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { _getAssetDecimals, _getTokenMinAmount, _isChainEvmCompatible, _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; import { BasicTxErrorType } from '@subwallet/extension-base/types'; -import { AssetHubPreValidationMetadata, ChainflipPreValidationMetadata, HydradxPreValidationMetadata, SwapErrorType } from '@subwallet/extension-base/types/swap'; +import { AssetHubPreValidationMetadata, ChainflipPreValidationMetadata, HydradxPreValidationMetadata, SimpleSwapValidationMetadata, SwapErrorType } from '@subwallet/extension-base/types/swap'; import { formatNumber } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; @@ -163,3 +163,30 @@ export function _getEarlyAssetHubValidationError (error: SwapErrorType, metadata return new SwapError(error); } } + +export function _getSimpleSwapEarlyValidationError (error: SwapErrorType, metadata: SimpleSwapValidationMetadata): SwapError { // todo: support more providers + switch (error) { + case SwapErrorType.NOT_MEET_MIN_SWAP: { + const message = `Amount too low. Increase your amount above ${metadata.minSwap.value} ${metadata.minSwap.symbol} and try again`; + + return new SwapError(error, message); + } + + case SwapErrorType.SWAP_EXCEED_ALLOWANCE: { + if (metadata.maxSwap) { + return new SwapError(error, `Amount too high. Lower your amount below ${metadata.maxSwap.value} ${metadata.maxSwap.symbol} and try again`); + } else { + return new SwapError(error, 'Amount too high. Lower your amount and try again'); + } + } + + case SwapErrorType.ASSET_NOT_SUPPORTED: + return new SwapError(error, 'This swap pair is not supported'); + case SwapErrorType.UNKNOWN: + return new SwapError(error, `Undefined error. Check your Internet and ${metadata.chain.slug} connection or contact support`); + case SwapErrorType.ERROR_FETCHING_QUOTE: + return new SwapError(error, 'No swap quote found. Adjust your amount or try again later.'); + default: + return new SwapError(error); + } +} diff --git a/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts new file mode 100644 index 0000000000..a994ca4ada --- /dev/null +++ b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts @@ -0,0 +1,411 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainAsset } from '@subwallet/chain-list/types'; +import { SwapError } from '@subwallet/extension-base/background/errors/SwapError'; +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; +import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { _getSimpleSwapEarlyValidationError } from '@subwallet/extension-base/core/logic-validation/swap'; +import { _getAssetSymbol, _getChainNativeTokenSlug, _getContractAddressOfToken, _isChainSubstrateCompatible, _isNativeToken, _isSmartContractToken } from '@subwallet/extension-base/services/chain-service/utils'; +import { BaseStepDetail, BasicTxErrorType, CommonFeeComponent, CommonOptimalPath, CommonStepFeeInfo, CommonStepType, OptimalSwapPathParams, SimpleSwapTxData, SimpleSwapValidationMetadata, SwapEarlyValidation, SwapErrorType, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapStepType, SwapSubmitParams, SwapSubmitStepData, TransactionData, ValidateSwapProcessParams } from '@subwallet/extension-base/types'; +import { formatNumber, toBNString } from '@subwallet/extension-base/utils'; +import BigNumber from 'bignumber.js'; + +import { SubmittableExtrinsic } from '@polkadot/api/types'; + +import { BalanceService } from '../../balance-service'; +import { getERC20TransactionObject, getEVMTransactionObject } from '../../balance-service/transfer/smart-contract'; +import { createTransferExtrinsic } from '../../balance-service/transfer/token'; +import { ChainService } from '../../chain-service'; +import { calculateSwapRate, SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING, SWAP_QUOTE_TIMEOUT_MAP } from '../utils'; +import { SwapBaseHandler, SwapBaseInterface } from './base-handler'; + +interface SwapRange { + min: string; + max: string; +} + +interface ExchangeSimpleSwapData{ + id: string; + trace_id: string; + address_from: string; +} + +const apiUrl = 'https://api.simpleswap.io'; + +export const simpleSwapApiKey = process.env.SIMPLE_SWAP_API_KEY || ''; + +export class SimpleSwapHandler implements SwapBaseInterface { + private swapBaseHandler: SwapBaseHandler; + providerSlug: SwapProviderId; + + constructor (chainService: ChainService, balanceService: BalanceService) { + this.swapBaseHandler = new SwapBaseHandler({ + chainService, + balanceService, + providerName: 'SimpleSwap', + providerSlug: SwapProviderId.SIMPLE_SWAP + }); + this.providerSlug = SwapProviderId.SIMPLE_SWAP; + } + + public validateSwapProcess (params: ValidateSwapProcessParams): Promise { + const amount = params.selectedQuote.fromAmount; + const bnAmount = new BigNumber(amount); + + if (bnAmount.lte(0)) { + return Promise.resolve([new TransactionError(BasicTxErrorType.INVALID_PARAMS, 'Amount must be greater than 0')]); + } + + return Promise.resolve([]); + } + + get chainService () { + return this.swapBaseHandler.chainService; + } + + get balanceService () { + return this.swapBaseHandler.balanceService; + } + + get providerInfo () { + return this.swapBaseHandler.providerInfo; + } + + get name () { + return this.swapBaseHandler.name; + } + + get slug () { + return this.swapBaseHandler.slug; + } + + public async getSwapQuote (request: SwapRequest): Promise { + try { + const fromAsset = this.chainService.getAssetBySlug(request.pair.from); + const toAsset = this.chainService.getAssetBySlug(request.pair.to); + const fromSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[fromAsset.slug]; + const toSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[toAsset.slug]; + + if (!fromAsset || !toAsset) { + return new SwapError(SwapErrorType.UNKNOWN); + } + + const earlyValidation = await this.validateSwapRequest(request); + + const metadata = earlyValidation.metadata as SimpleSwapValidationMetadata; + + if (earlyValidation.error) { + return _getSimpleSwapEarlyValidationError(earlyValidation.error, metadata); + } + + const params = new URLSearchParams({ + api_key: `${simpleSwapApiKey}`, + fixed: 'false', + currency_from: fromSymbol, + currency_to: toSymbol, + amount: formatNumber(request.fromAmount, fromAsset.decimals || 0) + }); + + const response = await fetch(`${apiUrl}/get_estimated?${params.toString()}`, { + headers: { accept: 'application/json' } + }); + + if (!response.ok) { + return new SwapError(SwapErrorType.ERROR_FETCHING_QUOTE); + } + + const resToAmount = await response.json() as string; + const toAmount = toBNString(resToAmount, toAsset.decimals || 0); + + const rate = calculateSwapRate(request.fromAmount, toAmount, fromAsset, toAsset); + + const fromChain = this.chainService.getChainInfoByKey(fromAsset.originChain); + const fromChainNativeTokenSlug = _getChainNativeTokenSlug(fromChain); + const defaultFeeToken = _isNativeToken(fromAsset) ? fromAsset.slug : fromChainNativeTokenSlug; + + const feeComponent: CommonFeeComponent[] = [ + { + tokenSlug: fromAsset.slug, + amount: '0', + feeType: SwapFeeType.NETWORK_FEE + } + ]; + + return { + pair: request.pair, + fromAmount: request.fromAmount, + toAmount, + rate, + provider: this.providerInfo, + aliveUntil: +Date.now() + (SWAP_QUOTE_TIMEOUT_MAP[this.slug] || SWAP_QUOTE_TIMEOUT_MAP.default), + minSwap: toBNString(metadata.minSwap.value, fromAsset.decimals || 0), + maxSwap: metadata.maxSwap?.value, + estimatedArrivalTime: 0, + isLowLiquidity: false, + feeInfo: { + feeComponent, + defaultFeeToken, + feeOptions: [defaultFeeToken] + }, + route: { + path: [fromAsset.slug, toAsset.slug] + } + } as SwapQuote; + } catch (e) { + return new SwapError(SwapErrorType.UNKNOWN); + } + } + + generateOptimalProcess (params: OptimalSwapPathParams): Promise { + return this.swapBaseHandler.generateOptimalProcess(params, [ + this.getSubmitStep + ]); + } + + async getSubmitStep (params: OptimalSwapPathParams): Promise<[BaseStepDetail, CommonStepFeeInfo] | undefined> { + if (params.selectedQuote) { + const submitStep = { + name: 'Swap', + type: SwapStepType.SWAP + }; + + return Promise.resolve([submitStep, params.selectedQuote.feeInfo]); + } + + return Promise.resolve(undefined); + } + + public async validateSwapRequest (request: SwapRequest): Promise { + try { + const fromAsset = this.chainService.getAssetBySlug(request.pair.from); + const toAsset = this.chainService.getAssetBySlug(request.pair.to); + + if (!fromAsset || !toAsset) { + return { error: SwapErrorType.ASSET_NOT_SUPPORTED }; + } + + const fromSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[fromAsset.slug]; + const toSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[toAsset.slug]; + + if (!fromSymbol || !toSymbol) { + return { error: SwapErrorType.ASSET_NOT_SUPPORTED }; + } + + console.log('Hmm', simpleSwapApiKey); + const swapListParams = new URLSearchParams({ + api_key: `${simpleSwapApiKey}`, + fixed: 'false', + symbol: fromSymbol + }); + + const swapListResponse = await fetch(`${apiUrl}/get_pairs?${swapListParams.toString()}`, { + headers: { accept: 'application/json' } + }); + + if (!swapListResponse.ok) { + return { error: SwapErrorType.UNKNOWN }; + } + + const swapList = await swapListResponse.json() as string[]; + + console.log('Hmm', swapList); + + if (!swapList.includes(toAsset.symbol.toLowerCase())) { + return { error: SwapErrorType.ASSET_NOT_SUPPORTED }; + } + + const rangesParams = new URLSearchParams({ + api_key: `${simpleSwapApiKey}`, + fixed: 'false', + currency_from: fromSymbol, + currency_to: toSymbol + }); + + const rangesResponse = await fetch(`${apiUrl}/get_ranges?${rangesParams.toString()}`, { + headers: { accept: 'application/json' } + }); + + if (!rangesResponse.ok) { + return { error: SwapErrorType.UNKNOWN }; + } + + const ranges = await rangesResponse.json() as SwapRange; + const { max, min } = ranges; + + const bnAmount = new BigNumber(request.fromAmount); + const parsedbnAmount = formatNumber(bnAmount.toString(), fromAsset.decimals || 0); + + if (parsedbnAmount < min) { + return { + error: SwapErrorType.NOT_MEET_MIN_SWAP, + metadata: { + minSwap: { + value: min, + symbol: fromAsset.symbol + }, + maxSwap: max + ? { + value: max, + symbol: fromAsset.symbol + } + : undefined, + chain: this.chainService.getChainInfoByKey(fromAsset.originChain) + } as SimpleSwapValidationMetadata + }; + } + + if (max && parsedbnAmount > max) { + return { + error: SwapErrorType.SWAP_EXCEED_ALLOWANCE, + metadata: { + minSwap: { + value: min, + symbol: fromAsset.symbol + }, + maxSwap: { + value: max, + symbol: fromAsset.symbol + }, + chain: this.chainService.getChainInfoByKey(fromAsset.originChain) + } as SimpleSwapValidationMetadata + }; + } + + return { + metadata: { + minSwap: { + value: min, + symbol: fromAsset.symbol + }, + maxSwap: max + ? { + value: max, + symbol: fromAsset.symbol + } + : undefined, + chain: this.chainService.getChainInfoByKey(fromAsset.originChain) + } as SimpleSwapValidationMetadata + }; + } catch (e) { + return { error: SwapErrorType.UNKNOWN }; + } + } + + public async handleSwapProcess (params: SwapSubmitParams): Promise { + const { currentStep, process } = params; + const type = process.steps[currentStep].type; + + switch (type) { + case CommonStepType.DEFAULT: + return Promise.reject(new TransactionError(BasicTxErrorType.UNSUPPORTED)); + case SwapStepType.SWAP: + return this.handleSubmitStep(params); + default: + return this.handleSubmitStep(params); + } + } + + public async handleSubmitStep (params: SwapSubmitParams): Promise { + const { address, quote, recipient } = params; + + const pair = quote.pair; + + const fromAsset = this.chainService.getAssetBySlug(pair.from); + const toAsset = this.chainService.getAssetBySlug(pair.to); + const chainInfo = this.chainService.getChainInfoByKey(fromAsset.originChain); + const chainType = _isChainSubstrateCompatible(chainInfo) ? ChainType.SUBSTRATE : ChainType.EVM; + const receiver = recipient ?? address; + + const fromSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[fromAsset.slug]; + const toSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[toAsset.slug]; + + const requestBody = { + fixed: false, + currency_from: fromSymbol, + currency_to: toSymbol, + amount: quote.fromAmount, + address_to: receiver, + extra_id_to: '', + user_refund_address: address, + user_refund_extra_id: '' + }; + + const response = await fetch( + `${apiUrl}/create_exchange?api_key=${simpleSwapApiKey}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify(requestBody) + } + ); + + const depositAddressResponse = await response.json() as ExchangeSimpleSwapData; + + const txData: SimpleSwapTxData = { + id: depositAddressResponse.id, + address, + provider: this.providerInfo, + quote: params.quote, + slippage: params.slippage, + recipient, + process: params.process + }; + + let extrinsic: TransactionData; + + if (chainType === ChainType.SUBSTRATE) { + const chainApi = this.chainService.getSubstrateApi(chainInfo.slug); + const substrateApi = await chainApi.isReady; + + const [submittableExtrinsic] = await createTransferExtrinsic({ + from: address, + networkKey: chainInfo.slug, + substrateApi, + to: depositAddressResponse.address_from, + tokenInfo: fromAsset, + transferAll: false, + value: quote.fromAmount + }); + + extrinsic = submittableExtrinsic as SubmittableExtrinsic<'promise'>; + } else { + if (_isNativeToken(fromAsset)) { + const [transactionConfig] = await getEVMTransactionObject( + chainInfo, + address, + depositAddressResponse.address_from, + quote.fromAmount, + false, + this.chainService.getEvmApi(chainInfo.slug) + ); + + extrinsic = transactionConfig; + } else { + const [transactionConfig] = await getERC20TransactionObject( + _getContractAddressOfToken(fromAsset), + chainInfo, + address, + depositAddressResponse.address_from, + quote.fromAmount, + false, + this.chainService.getEvmApi(chainInfo.slug) + ); + + extrinsic = transactionConfig; + } + } + + return { + txChain: fromAsset.originChain, + txData, + extrinsic, + transferNativeAmount: _isNativeToken(fromAsset) ? quote.fromAmount : '0', + extrinsicType: ExtrinsicType.SWAP, + chainType + } as SwapSubmitStepData; + } +} diff --git a/packages/extension-base/src/services/swap-service/index.ts b/packages/extension-base/src/services/swap-service/index.ts index b7da4e3e6f..541b3f288a 100644 --- a/packages/extension-base/src/services/swap-service/index.ts +++ b/packages/extension-base/src/services/swap-service/index.ts @@ -18,6 +18,14 @@ import { _SUPPORTED_SWAP_PROVIDERS, OptimalSwapPathParams, QuoteAskResponse, Swa import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils'; import { BehaviorSubject } from 'rxjs'; +import { SimpleSwapHandler } from './handler/simpleswap-handler'; + +export const _isChainSupportedByProvider = (providerSlug: SwapProviderId, chain: string) => { + const supportedChains = _PROVIDER_TO_SUPPORTED_PAIR_MAP[providerSlug]; + + return supportedChains ? supportedChains.includes(chain) : false; +}; + export class SwapService implements ServiceWithProcessInterface, StoppableServiceInterface { protected readonly state: KoniState; private eventService: EventService; @@ -41,7 +49,7 @@ export class SwapService implements ServiceWithProcessInterface, StoppableServic await Promise.all(Object.values(this.handlers).map(async (handler) => { // temporary solution to reduce number of requests to providers, will work as long as there's only 1 provider for 1 chain - if (!_PROVIDER_TO_SUPPORTED_PAIR_MAP[handler.providerSlug].includes(swappingSrcChain)) { + if (!_isChainSupportedByProvider(handler.providerSlug, swappingSrcChain)) { return; } @@ -182,6 +190,9 @@ export class SwapService implements ServiceWithProcessInterface, StoppableServic case SwapProviderId.ROCOCO_ASSET_HUB: this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, 'rococo_assethub'); break; + case SwapProviderId.SIMPLE_SWAP: + this.handlers[providerId] = new SimpleSwapHandler(this.chainService, this.state.balanceService); + break; default: throw new Error('Unsupported provider'); diff --git a/packages/extension-base/src/services/swap-service/utils.ts b/packages/extension-base/src/services/swap-service/utils.ts index 6dd416ab0f..903fb3d8c3 100644 --- a/packages/extension-base/src/services/swap-service/utils.ts +++ b/packages/extension-base/src/services/swap-service/utils.ts @@ -12,6 +12,8 @@ import BigN from 'bignumber.js'; export const CHAIN_FLIP_TESTNET_EXPLORER = 'https://blocks-perseverance.chainflip.io'; export const CHAIN_FLIP_MAINNET_EXPLORER = 'https://scan.chainflip.io'; +export const SIMPLE_SWAP_EXPLORER = 'https://simpleswap.io'; + export const CHAIN_FLIP_SUPPORTED_MAINNET_MAPPING: Record = { [COMMON_CHAIN_SLUGS.POLKADOT]: Chains.Polkadot, [COMMON_CHAIN_SLUGS.ETHEREUM]: Chains.Ethereum, @@ -36,6 +38,11 @@ export const CHAIN_FLIP_SUPPORTED_TESTNET_ASSET_MAPPING: Record = [COMMON_ASSETS.USDC_SEPOLIA]: Assets.USDC }; +export const SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING: Record = { + 'bittensor-NATIVE-TAO': 'tao', + [COMMON_ASSETS.ETH]: 'eth' +}; + export const SWAP_QUOTE_TIMEOUT_MAP: Record = { // in milliseconds default: 30000, [SwapProviderId.CHAIN_FLIP_TESTNET]: 30000, @@ -49,7 +56,8 @@ export const _PROVIDER_TO_SUPPORTED_PAIR_MAP: Record = { [SwapProviderId.CHAIN_FLIP_TESTNET]: [COMMON_CHAIN_SLUGS.CHAINFLIP_POLKADOT, COMMON_CHAIN_SLUGS.ETHEREUM_SEPOLIA], [SwapProviderId.POLKADOT_ASSET_HUB]: [COMMON_CHAIN_SLUGS.POLKADOT_ASSET_HUB], [SwapProviderId.KUSAMA_ASSET_HUB]: [COMMON_CHAIN_SLUGS.KUSAMA_ASSET_HUB], - [SwapProviderId.ROCOCO_ASSET_HUB]: [COMMON_CHAIN_SLUGS.ROCOCO_ASSET_HUB] + [SwapProviderId.ROCOCO_ASSET_HUB]: [COMMON_CHAIN_SLUGS.ROCOCO_ASSET_HUB], + [SwapProviderId.SIMPLE_SWAP]: ['bittensor', 'bittensor_testnet', COMMON_CHAIN_SLUGS.ETHEREUM] }; export function getSwapAlternativeAsset (swapPair: SwapPair): string | undefined { diff --git a/packages/extension-base/src/services/transaction-service/utils.ts b/packages/extension-base/src/services/transaction-service/utils.ts index 72a5dcfa16..4b7b42a32f 100644 --- a/packages/extension-base/src/services/transaction-service/utils.ts +++ b/packages/extension-base/src/services/transaction-service/utils.ts @@ -4,8 +4,8 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { ExtrinsicDataTypeMap, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { _getBlockExplorerFromChain, _isChainTestNet, _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; -import { CHAIN_FLIP_MAINNET_EXPLORER, CHAIN_FLIP_TESTNET_EXPLORER } from '@subwallet/extension-base/services/swap-service/utils'; -import { ChainflipSwapTxData } from '@subwallet/extension-base/types/swap'; +import { CHAIN_FLIP_MAINNET_EXPLORER, CHAIN_FLIP_TESTNET_EXPLORER, SIMPLE_SWAP_EXPLORER } from '@subwallet/extension-base/services/swap-service/utils'; +import { ChainflipSwapTxData, SimpleSwapTxData } from '@subwallet/extension-base/types/swap'; // @ts-ignore export function parseTransactionData (data: unknown): ExtrinsicDataTypeMap[T] { @@ -97,3 +97,9 @@ export function getChainflipExplorerLink (data: ChainflipSwapTxData, chainInfo: return `${chainflipDomain}/channels/${data.depositChannelId}`; } + +export function getSimpleSwapExplorerLink (data: SimpleSwapTxData) { + const simpleswapDomain = SIMPLE_SWAP_EXPLORER; + + return `${simpleswapDomain}/exchange?id=${data.id}`; +} diff --git a/packages/extension-base/src/types/swap/index.ts b/packages/extension-base/src/types/swap/index.ts index 129dc7c6f5..9122cfa7c2 100644 --- a/packages/extension-base/src/types/swap/index.ts +++ b/packages/extension-base/src/types/swap/index.ts @@ -67,6 +67,7 @@ export enum SwapProviderId { POLKADOT_ASSET_HUB = 'POLKADOT_ASSET_HUB', KUSAMA_ASSET_HUB = 'KUSAMA_ASSET_HUB', ROCOCO_ASSET_HUB = 'ROCOCO_ASSET_HUB', + SIMPLE_SWAP = 'SIMPLE_SWAP' } export const _SUPPORTED_SWAP_PROVIDERS: SwapProviderId[] = [ @@ -76,7 +77,8 @@ export const _SUPPORTED_SWAP_PROVIDERS: SwapProviderId[] = [ SwapProviderId.HYDRADX_TESTNET, SwapProviderId.POLKADOT_ASSET_HUB, SwapProviderId.KUSAMA_ASSET_HUB, - SwapProviderId.ROCOCO_ASSET_HUB + SwapProviderId.ROCOCO_ASSET_HUB, + SwapProviderId.SIMPLE_SWAP ]; export interface SwapProvider { @@ -93,7 +95,7 @@ export enum SwapFeeType { WALLET_FEE = 'WALLET_FEE' } -export type SwapTxData = ChainflipSwapTxData | HydradxSwapTxData; // todo: will be more +export type SwapTxData = ChainflipSwapTxData | HydradxSwapTxData | SimpleSwapTxData; // todo: will be more export interface SwapBaseTxData { provider: SwapProvider; @@ -110,6 +112,10 @@ export interface ChainflipSwapTxData extends SwapBaseTxData { estimatedDepositChannelExpiryTime?: number; } +export interface SimpleSwapTxData extends SwapBaseTxData { + id: string; +} + export interface HydradxSwapTxData extends SwapBaseTxData { txHex: string; } @@ -135,6 +141,12 @@ export interface AssetHubPreValidationMetadata { priceImpactPct?: string; } +export interface SimpleSwapValidationMetadata{ + minSwap: AmountData; + maxSwap: AmountData; + chain: _ChainInfo; +} + export interface QuoteAskResponse { quote?: SwapQuote; error?: SwapError; diff --git a/packages/extension-koni-ui/src/Popup/Home/History/Detail/index.tsx b/packages/extension-koni-ui/src/Popup/Home/History/Detail/index.tsx index b47ff9b18f..a9ffe30994 100644 --- a/packages/extension-koni-ui/src/Popup/Home/History/Detail/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/History/Detail/index.tsx @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { ExtrinsicType, TransactionAdditionalInfo } from '@subwallet/extension-base/background/KoniTypes'; -import { getExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; +import { getExplorerLink, getSimpleSwapExplorerLink } from '@subwallet/extension-base/services/transaction-service/utils'; +import { SimpleSwapTxData, SwapProviderId, SwapTxData } from '@subwallet/extension-base/types'; import { InfoItemBase } from '@subwallet/extension-koni-ui/components'; import { HISTORY_DETAIL_MODAL } from '@subwallet/extension-koni-ui/constants'; import { RootState } from '@subwallet/extension-koni-ui/stores'; @@ -55,7 +56,15 @@ function Component ({ className = '', data, onCancel }: Props): React.ReactEleme originChainInfo = chainInfoMap[additionalInfo.originalChain] || chainInfo; } - const link = (data.extrinsicHash && data.extrinsicHash !== '') && getExplorerLink(originChainInfo, data.extrinsicHash, 'tx'); + let link = (data.extrinsicHash && data.extrinsicHash !== '') && getExplorerLink(originChainInfo, data.extrinsicHash, 'tx'); + + if (extrinsicType === ExtrinsicType.SWAP) { + const additionalInfo = data.additionalInfo as SwapTxData; + + if ([SwapProviderId.SIMPLE_SWAP].includes(additionalInfo.provider.id)) { + link = getSimpleSwapExplorerLink(additionalInfo as SimpleSwapTxData); + } + } return (