Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue-4078] Support dynamic swap pair #4079

Open
wants to merge 6 commits into
base: subwallet-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/extension-base/src/koni/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/reques
import { AuthUrls } from '@subwallet/extension-base/services/request-service/types';
import { DEFAULT_AUTO_LOCK_TIME } from '@subwallet/extension-base/services/setting-service/constants';
import { checkLiquidityForPool, estimateTokensForPool, getReserveForPool } from '@subwallet/extension-base/services/swap-service/handler/asset-hub/utils';
import { generateAllDestinations } from '@subwallet/extension-base/services/swap-service/utils';
import { SWPermitTransaction, SWTransaction, SWTransactionInput, SWTransactionResponse, SWTransactionResult, TransactionEmitter, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types';
import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectNamespace } from '@subwallet/extension-base/services/wallet-connect-service/helpers';
import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types';
Expand Down Expand Up @@ -1344,6 +1345,11 @@ export default class KoniExtension {
const warnings: TransactionWarning[] = [];
const chainInfo = this.#koniState.getChainInfo(chain);

// todo: remove test:
const test = generateAllDestinations(this.#koniState.getSubstrateApi(chain), this.#koniState.chainService, transferTokenInfo, 3).map((asset) => asset.slug);

console.log('test', test);

const nativeTokenInfo = this.#koniState.getNativeTokenInfo(chain);
const nativeTokenSlug: string = nativeTokenInfo.slug;
const isTransferNativeToken = nativeTokenSlug === tokenSlug;
Expand Down Expand Up @@ -4064,6 +4070,16 @@ export default class KoniExtension {
}

private async handleSwapRequest (request: SwapRequest): Promise<SwapRequestResult> {
// todo: remove test:
const testSwapRequest = this.#koniState.swapService.handleSwapRequestV2({
address: '1BzDB5n2rfSJwvuCW9deKY9XnUyys8Gy44SoX8tRNDCFBhx',
fromAmount: '1',
fromToken: 'hydradx_main-LOCAL-PINK',
toToken: 'ethereum-NATIVE-ETH'
});

console.log('testSwapRequest', testSwapRequest);

return this.#koniState.swapService.handleSwapRequest(request);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ export function _isAssetFungibleToken (chainAsset: _ChainAsset): boolean {
return ![_AssetType.ERC721, _AssetType.PSP34, _AssetType.UNKNOWN].includes(chainAsset.assetType);
}

export function _getFungibleAssetType () {
return [_AssetType.NATIVE, _AssetType.LOCAL, _AssetType.ERC20, _AssetType.GRC20, _AssetType.TEP74, _AssetType.PSP22, _AssetType.VFT];
}

export const _isAssetAutoEnable = (chainAsset: _ChainAsset): boolean => {
return chainAsset.metadata ? !!chainAsset.metadata.autoEnable : false;
};
Expand Down
101 changes: 99 additions & 2 deletions packages/extension-base/src/services/swap-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { AssetHubSwapHandler } from '@subwallet/extension-base/services/swap-ser
import { SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler';
import { ChainflipSwapHandler } from '@subwallet/extension-base/services/swap-service/handler/chainflip-handler';
import { HydradxHandler } from '@subwallet/extension-base/services/swap-service/handler/hydradx-handler';
import { _PROVIDER_TO_SUPPORTED_PAIR_MAP, getSwapAltToken, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils';
import { BasicTxErrorType } from '@subwallet/extension-base/types';
import { _PROVIDER_TO_SUPPORTED_PAIR_MAP, DynamicSwapAction, findSwapDestinations, findXcmDestinations, getInitStep, getSwapAltToken, getSwapStep, getXcmStep, isEquiValentAsset, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils';
import { BasicTxErrorType, SwapRequestV2 } from '@subwallet/extension-base/types';
import { CommonOptimalPath, DEFAULT_FIRST_STEP, MOCK_STEP_FEE } from '@subwallet/extension-base/types/service-base';
import { _SUPPORTED_SWAP_PROVIDERS, OptimalSwapPathParams, QuoteAskResponse, SwapErrorType, SwapPair, SwapProviderId, SwapQuote, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap';
import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils';
Expand Down Expand Up @@ -131,6 +131,103 @@ export class SwapService implements ServiceWithProcessInterface, StoppableServic
} as SwapRequestResult;
}

public handleSwapRequestV2 (request: SwapRequestV2): DynamicSwapAction[][] {
const { fromToken, toToken } = request;
const fromTokenInfo = this.chainService.getAssetBySlug(fromToken);
const toTokenInfo = this.chainService.getAssetBySlug(toToken);
const fromChain = fromTokenInfo.originChain;
const toChain = toTokenInfo.originChain;

const firstSwapDes = findSwapDestinations(this.chainService, fromTokenInfo);
const firstXcmDes = findXcmDestinations(this.chainService, fromTokenInfo);

if (!firstSwapDes.length && !firstXcmDes.length) {
return [];
}

const routes: DynamicSwapAction[][] = [];
let steps: DynamicSwapAction[] = [];

// try find swap
if (fromChain === toChain) {
if (firstSwapDes.includes(toTokenInfo)) {
steps.push(getInitStep(fromToken));
steps.push(getSwapStep(toToken));
routes.push(steps);

return routes;
}
}

// try find xcm
if (isEquiValentAsset(fromTokenInfo, toTokenInfo)) {
if (firstXcmDes.includes(toTokenInfo)) {
steps.push(getInitStep(fromToken));
steps.push(getXcmStep(toToken));
routes.push(steps);

return routes;
}
}

// brute force to find nested routes
for (const xcmAsset of firstXcmDes) {
const swapDesChild = findSwapDestinations(this.chainService, xcmAsset);

if (!swapDesChild.length) {
/* empty */
} else if (swapDesChild.includes(toTokenInfo)) {
steps = [];
steps.push(getInitStep(fromToken));
steps.push(getXcmStep(xcmAsset.slug));
steps.push(getSwapStep(toToken));
routes.push(steps);
} else {
for (const swapAsset of swapDesChild) {
const xcmDesChild = findXcmDestinations(this.chainService, swapAsset);

if (xcmDesChild.includes(toTokenInfo)) {
steps = [];
steps.push(getInitStep(fromToken));
steps.push(getXcmStep(xcmAsset.slug));
steps.push(getSwapStep(swapAsset.slug));
steps.push(getXcmStep(toToken));
routes.push(steps);
}
}
}
}

for (const swapAsset of firstSwapDes) {
const xcmDesChild = findXcmDestinations(this.chainService, swapAsset);

if (!xcmDesChild.length) {
/* empty */
} else if (xcmDesChild.includes(toTokenInfo)) {
steps = [];
steps.push(getInitStep(fromToken));
steps.push(getSwapStep(swapAsset.slug));
steps.push(getXcmStep(toToken));
routes.push(steps);
} else {
for (const xcmAsset of xcmDesChild) {
const swapDesChild = findSwapDestinations(this.chainService, xcmAsset);

if (swapDesChild.includes(toTokenInfo)) {
steps = [];
steps.push(getInitStep(fromToken));
steps.push(getSwapStep(swapAsset.slug));
steps.push(getXcmStep(xcmAsset.slug));
steps.push(getSwapStep(toToken));
routes.push(steps);
}
}
}
}

return routes;
}

public async getLatestQuotes (request: SwapRequest): Promise<SwapQuoteResponse> {
request.pair.metadata = this.getSwapPairMetadata(request.pair.slug); // todo: improve this
const quoteAskResponses = await this.askProvidersForQuote(request);
Expand Down
174 changes: 172 additions & 2 deletions packages/extension-base/src/services/swap-service/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

import { COMMON_ASSETS, COMMON_CHAIN_SLUGS } from '@subwallet/chain-list';
import { _ChainAsset } from '@subwallet/chain-list/types';
import { _getAssetDecimals } from '@subwallet/extension-base/services/chain-service/utils';
import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types';
import { ChainService } from '@subwallet/extension-base/services/chain-service';
import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types';
import { _getAssetDecimals, _getAssetPriceId, _getAssetSymbol, _getFungibleAssetType, _getMultiChainAsset } from '@subwallet/extension-base/services/chain-service/utils';
import { CHAINFLIP_BROKER_API } from '@subwallet/extension-base/services/swap-service/handler/chainflip-handler';
import { SwapPair, SwapProviderId } from '@subwallet/extension-base/types/swap';
import BigN from 'bignumber.js';
Expand Down Expand Up @@ -100,3 +102,171 @@ export function getChainflipSwap (isTestnet: boolean) {
return `https://chainflip-broker.io/swap?apikey=${CHAINFLIP_BROKER_API}`;
}
}

export function generateAllDestinations (substrateApi: _SubstrateApi, chainService: ChainService, fromAsset: _ChainAsset, maxPathLength = 1) {
if (maxPathLength < 1) {
return [];
}

let currentTargets: _ChainAsset[] = [];
let newTargets: _ChainAsset[] = [];
let currentStep = 1;

// step 1:
newTargets = findDestinations(chainService, fromAsset);
currentTargets = mergeWithoutDuplicate<_ChainAsset>(currentTargets, newTargets);
currentStep += 1;

// step 2 - n
while (currentStep <= maxPathLength) { // todo: improve by stop when nothing new
newTargets = Array.from(newTargets.reduce((farChildTargets, currentTarget) => {
const farChildTargetsLocal = findDestinations(chainService, currentTarget);

return new Set([...farChildTargets, ...farChildTargetsLocal]);
}, new Set<_ChainAsset>()));
currentTargets = mergeWithoutDuplicate<_ChainAsset>(currentTargets, newTargets);
currentStep += 1;
}

return currentTargets;
}

function findDestinations (chainService: ChainService, chainAsset: _ChainAsset) {
const xcmTargets = findXcmDestinations(chainService, chainAsset);
const swapTargets = findSwapDestinations(chainService, chainAsset);

return mergeWithoutDuplicate<_ChainAsset>(xcmTargets, swapTargets);
}

export function findXcmDestinations (chainService: ChainService, chainAsset: _ChainAsset) {
const xcmTargets: _ChainAsset[] = [];
const multichainAssetSlug = _getMultiChainAsset(chainAsset);

if (!multichainAssetSlug) {
return xcmTargets;
}

const assetRegistry = chainService.getAssetRegistry();

for (const asset of Object.values(assetRegistry)) {
if (multichainAssetSlug === _getMultiChainAsset(asset)) {
xcmTargets.push(asset);
}
}

return xcmTargets.filter((candidate) => candidate.slug !== chainAsset.slug);
}

export function findSwapDestinations (chainService: ChainService, chainAsset: _ChainAsset) {
const chain = chainAsset.originChain;
const swapTargets: _ChainAsset[] = [];

const availableChains = Object.values(_PROVIDER_TO_SUPPORTED_PAIR_MAP).reduce((remainChains, currentChains) => {
if (currentChains.includes(chain)) {
currentChains.forEach((candidate) => {
remainChains.add(candidate);
});
}

return remainChains;
}, new Set<string>());

availableChains.forEach((candidate) => {
const assets = chainService.getAssetByChainAndType(candidate, _getFungibleAssetType()); // todo: recheck assetType

swapTargets.push(...Object.values(assets));
});

return swapTargets.filter((candidate) => candidate.slug !== chainAsset.slug);
}

// @ts-ignore
export function findSwapDestinationsV2 (chainService: ChainService, chainAsset: _ChainAsset) {
const chain = chainAsset.originChain;
const swapTargets: _ChainAsset[] = [];

// Convert to Set once at the start
const availableChains = new Set<string>(
Object.values(_PROVIDER_TO_SUPPORTED_PAIR_MAP)
.filter((chains) => chains.includes(chain))
.flat()
);

// Use Set for O(1) lookup instead of includes()
for (const candidate of availableChains) {
const assets = chainService.getAssetByChainAndType(
candidate,
_getFungibleAssetType()
);

swapTargets.push(...Object.values(assets));
}

return swapTargets;
}

// @ts-ignore
async function isHasXcmChannelSubstrate (substrateApi: _SubstrateApi, fromChain: _ChainInfo, toChain: _ChainInfo) {
const channel = await substrateApi.api.query.hrmp.hrmpChainnels(fromChain.substrateInfo?.paraId, toChain.substrateInfo?.paraId);

return !!channel.toPrimitive();
}

// @ts-ignore
export async function getAllXcmChannelSubstrate (substrateApi: _SubstrateApi) {
const channels = await substrateApi.api.query.hrmp?.hrmpChannels?.keys();
const allKeys = [];

for (const key of channels) {
allKeys.push(key.args[0].toPrimitive());
}

return allKeys;
}

// todo: improve
// function isHasBridgeChanel ()

function mergeWithoutDuplicate<T> (arr1: T[], arr2: T[]): T[] {
return Array.from(new Set([...arr1, ...arr2]));
}

export enum DynamicSwapType {
INIT = 'INIT',
SWAP = 'SWAP',
XCM = 'XCM' // todo: rename XCM to a better name to describe cross chain transfer action;
}

export interface DynamicSwapAction {
action: DynamicSwapType;
toToken: string;
}

export function getInitStep (tokenSlug: string): DynamicSwapAction {
return {
action: DynamicSwapType.INIT,
toToken: tokenSlug
};
}

export function getXcmStep (tokenSlug: string): DynamicSwapAction {
return {
action: DynamicSwapType.XCM,
toToken: tokenSlug
};
}

export function getSwapStep (tokenSlug: string): DynamicSwapAction {
return {
action: DynamicSwapType.SWAP,
toToken: tokenSlug
};
}

export function isEquiValentAsset (from: _ChainAsset, to: _ChainAsset) {
return (
_getMultiChainAsset(from) === _getMultiChainAsset(to) ||
_getAssetPriceId(from) === _getAssetPriceId(to) ||
_getAssetSymbol(from) === _getAssetSymbol(to)
);
}
7 changes: 7 additions & 0 deletions packages/extension-base/src/types/swap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ export interface SwapRequest {
currentQuote?: SwapProvider
}

export interface SwapRequestV2 {
address: string;
fromAmount: string;
fromToken: string;
toToken: string;
}

export interface SwapRequestResult {
process: CommonOptimalPath;
quote: SwapQuoteResponse;
Expand Down
Loading