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 3855] Support swap TAO on SimpleSwap - V4 #3939

Merged
merged 9 commits into from
Dec 20, 2024
Merged
5 changes: 5 additions & 0 deletions packages/extension-base/src/background/errors/SwapError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ const defaultErrorMap: Record<SwapErrorType, { message: string, code?: number }>
MAKE_POOL_NOT_ENOUGH_EXISTENTIAL_DEPOSIT: {
message: detectTranslate('Insufficient liquidity to complete the swap. Lower your amount and try again'),
code: undefined
},
NOT_MEET_MIN_EXPECTED: {
// TODO: update message
message: detectTranslate('Unable to process this swap at the moment. Try again later'),
code: undefined
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const branchName = process.env.BRANCH_NAME || 'subwallet-dev';
const fetchDomain = PRODUCTION_BRANCHES.indexOf(branchName) > -1 ? 'https://chain-list-assets.subwallet.app' : 'https://dev.sw-chain-list-assets.pages.dev';
const fetchFile = PRODUCTION_BRANCHES.indexOf(branchName) > -1 ? 'list.json' : 'preview.json';

const ChainListVersion = '0.2.95'; // update this when build chainlist
const ChainListVersion = '0.2.96'; // update this when build chainlist

// todo: move this interface to chainlist
export interface PatchInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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';
Expand Down Expand Up @@ -30,6 +31,7 @@ interface ExchangeSimpleSwapData{
id: string;
trace_id: string;
address_from: string;
amount_to: string;
}

const apiUrl = 'https://api.simpleswap.io';
Expand All @@ -42,6 +44,117 @@ const toBNString = (input: string | number | BigNumber, decimal: number): string
return raw.shiftedBy(decimal).integerValue(BigNumber.ROUND_CEIL).toFixed();
};

const fetchSwapList = async (params: { fromSymbol: string }): Promise<string[]> => {
const swapListParams = new URLSearchParams({
api_key: `${simpleSwapApiKey}`,
fixed: 'false',
symbol: params.fromSymbol
});

const response = await fetch(`${apiUrl}/get_pairs?${swapListParams.toString()}`, {
headers: { accept: 'application/json' }
});

return await response.json() as string[];
};

const fetchRanges = async (params: { fromSymbol: string; toSymbol: string }): Promise<SwapRange> => {
const rangesParams = new URLSearchParams({
api_key: `${simpleSwapApiKey}`,
fixed: 'false',
currency_from: params.fromSymbol,
currency_to: params.toSymbol
});

const response = await fetch(`${apiUrl}/get_ranges?${rangesParams.toString()}`, {
headers: { accept: 'application/json' }
});

return await response.json() as SwapRange;
};

async function getEstimate (request: SwapRequest, fromAsset: _ChainAsset, toAsset: _ChainAsset): Promise<{ toAmount: string; walletFeeAmount: string }> {
const fromSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[fromAsset.slug];
const toSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[toAsset.slug];
const assetDecimals = _getAssetDecimals(fromAsset);

if (!fromSymbol || !toSymbol) {
throw new SwapError(SwapErrorType.ASSET_NOT_SUPPORTED);
}

const formatedAmount = formatNumber(request.fromAmount, assetDecimals, (s) => s);

const params = new URLSearchParams({
api_key: `${simpleSwapApiKey}`,
fixed: 'false',
currency_from: fromSymbol,
currency_to: toSymbol,
amount: formatedAmount
});

try {
const response = await fetch(`${apiUrl}/get_estimated?${params.toString()}`, {
headers: { accept: 'application/json' }
});

if (!response.ok) {
throw new SwapError(SwapErrorType.ERROR_FETCHING_QUOTE);
}

const resToAmount = await response.json() as string;
const toAmount = toBNString(resToAmount, _getAssetDecimals(toAsset));
const bnToAmount = new BigN(toAmount);

const walletFeeRate = 4 / 1000;
const toAmountBeforeFee = bnToAmount.dividedBy(new BigN(1 - walletFeeRate));
const walletFeeAmount = toAmountBeforeFee.multipliedBy(4).dividedBy(1000).toString();

return {
toAmount,
walletFeeAmount
};
} catch (err) {
console.error('Error:', err);
throw new SwapError(SwapErrorType.ERROR_FETCHING_QUOTE);
}
}

const createSwapRequest = async (params: {fromSymbol: string; toSymbol: string; fromAmount: string; fromAsset: _ChainAsset; receiver: string; sender: string; toAsset: _ChainAsset;}) => {
const fromDecimals = _getAssetDecimals(params.fromAsset);
const toDecimals = _getAssetDecimals(params.toAsset);
const formatedAmount = formatNumber(params.fromAmount, fromDecimals, (s) => s);
const requestBody = {
fixed: false,
currency_from: params.fromSymbol,
currency_to: params.toSymbol,
amount: formatedAmount, // Convert to small number due to require of api
address_to: params.receiver,
extra_id_to: '',
user_refund_address: params.sender,
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;

return {
id: depositAddressResponse.id,
addressFrom: depositAddressResponse.address_from,
amountTo: toBNString(depositAddressResponse.amount_to, toDecimals)
};
};

export class SimpleSwapHandler implements SwapBaseInterface {
private swapBaseHandler: SwapBaseHandler;
providerSlug: SwapProviderId;
Expand Down Expand Up @@ -119,13 +232,6 @@ export class SimpleSwapHandler implements SwapBaseInterface {
return new SwapError(SwapErrorType.UNKNOWN);
}

const fromSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[fromAsset.slug];
const toSymbol = SIMPLE_SWAP_SUPPORTED_TESTNET_ASSET_MAPPING[toAsset.slug];

if (!fromSymbol || !toSymbol) {
return new SwapError(SwapErrorType.ASSET_NOT_SUPPORTED);
}

const earlyValidation = await this.validateSwapRequest(request);

const metadata = earlyValidation.metadata as SimpleSwapValidationMetadata;
Expand All @@ -134,35 +240,8 @@ export class SimpleSwapHandler implements SwapBaseInterface {
return _getSimpleSwapEarlyValidationError(earlyValidation.error, metadata);
}

const params = new URLSearchParams({
api_key: `${simpleSwapApiKey}`,
fixed: 'false',
currency_from: fromSymbol,
currency_to: toSymbol,
amount: formatNumber(request.fromAmount, _getAssetDecimals(fromAsset))
});

let resToAmount: string;

try {
const response = await fetch(`${apiUrl}/get_estimated?${params.toString()}`, {
headers: { accept: 'application/json' }
});

if (!response.ok) {
return new SwapError(SwapErrorType.ERROR_FETCHING_QUOTE);
}

resToAmount = await response.json() as string;
} catch (err) {
console.error('Error:', err);

return new SwapError(SwapErrorType.ERROR_FETCHING_QUOTE);
}

const toAmount = toBNString(resToAmount, _getAssetDecimals(toAsset));
const { toAmount, walletFeeAmount } = await getEstimate(request, fromAsset, toAsset);
const fromAmount = request.fromAmount;
const bnToAmount = new BigN(toAmount);

const rate = calculateSwapRate(request.fromAmount, toAmount, fromAsset, toAsset);

Expand All @@ -188,12 +267,9 @@ export class SimpleSwapHandler implements SwapBaseInterface {
feeType: SwapFeeType.NETWORK_FEE
};

const walletFeeRate = 4 / 1000;
const toAmountBeforeFee = bnToAmount.dividedBy(new BigN(1 - walletFeeRate));

const walletFee: CommonFeeComponent = {
tokenSlug: toAsset.slug,
amount: toAmountBeforeFee.multipliedBy(4).dividedBy(1000).toString(),
amount: walletFeeAmount,
feeType: SwapFeeType.WALLET_FEE
};

Expand Down Expand Up @@ -257,22 +333,8 @@ export class SimpleSwapHandler implements SwapBaseInterface {
return { error: SwapErrorType.ASSET_NOT_SUPPORTED };
}

const swapListParams = new URLSearchParams({
api_key: `${simpleSwapApiKey}`,
fixed: 'false',
symbol: fromSymbol
});

try {
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[];
const swapList = await fetchSwapList({ fromSymbol });

if (!swapList.includes(toSymbol)) {
return { error: SwapErrorType.ASSET_NOT_SUPPORTED };
Expand All @@ -281,22 +343,7 @@ export class SimpleSwapHandler implements SwapBaseInterface {
console.error('Error:', err);
}

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 ranges = await fetchRanges({ fromSymbol, toSymbol }) as unknown as SwapRange;
const { max, min } = ranges;
const bnMin = toBNString(min, _getAssetDecimals(fromAsset));
const bnAmount = BigInt(request.fromAmount);
Expand Down Expand Up @@ -383,38 +430,26 @@ export class SimpleSwapHandler implements SwapBaseInterface {
const chainInfo = this.chainService.getChainInfoByKey(fromAsset.originChain);
const toChainInfo = this.chainService.getChainInfoByKey(toAsset.originChain);
const chainType = _isChainSubstrateCompatible(chainInfo) ? ChainType.SUBSTRATE : ChainType.EVM;
const receiver = _reformatAddressWithChain(recipient ?? address, toChainInfo);
const sender = _reformatAddressWithChain(address, chainInfo);
const receiver = _reformatAddressWithChain(recipient ?? sender, toChainInfo);

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 { fromAmount } = quote;
const { addressFrom, amountTo, id } = await createSwapRequest({ fromSymbol, toSymbol, fromAmount, fromAsset, receiver, sender, toAsset });

const response = await fetch(
`${apiUrl}/create_exchange?api_key=${simpleSwapApiKey}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(requestBody)
}
);
// Validate the amount to be swapped
const rate = BigN(amountTo).div(BigN(quote.toAmount)).multipliedBy(100);

if (rate.lt(95)) {
throw new SwapError(SwapErrorType.NOT_MEET_MIN_EXPECTED);
}

const depositAddressResponse = await response.json() as ExchangeSimpleSwapData;
// Can modify quote.toAmount to amountTo after confirm real amount received

const txData: SimpleSwapTxData = {
id: depositAddressResponse.id,
id: id,
address,
provider: this.providerInfo,
quote: params.quote,
Expand All @@ -433,7 +468,7 @@ export class SimpleSwapHandler implements SwapBaseInterface {
from: address,
networkKey: chainInfo.slug,
substrateApi,
to: depositAddressResponse.address_from,
to: addressFrom,
tokenInfo: fromAsset,
transferAll: false,
value: quote.fromAmount
Expand All @@ -445,7 +480,7 @@ export class SimpleSwapHandler implements SwapBaseInterface {
const [transactionConfig] = await getEVMTransactionObject(
chainInfo,
address,
depositAddressResponse.address_from,
addressFrom,
quote.fromAmount,
false,
this.chainService.getEvmApi(chainInfo.slug)
Expand All @@ -457,7 +492,7 @@ export class SimpleSwapHandler implements SwapBaseInterface {
_getContractAddressOfToken(fromAsset),
chainInfo,
address,
depositAddressResponse.address_from,
addressFrom,
quote.fromAmount,
false,
this.chainService.getEvmApi(chainInfo.slug)
Expand Down
1 change: 1 addition & 0 deletions packages/extension-base/src/types/swap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export enum SwapErrorType {
NOT_ENOUGH_LIQUIDITY = 'NOT_ENOUGH_LIQUIDITY',
MAKE_POOL_NOT_ENOUGH_EXISTENTIAL_DEPOSIT = 'MAKE_POOL_NOT_ENOUGH_EXISTENTIAL_DEPOSIT',
AMOUNT_CANNOT_BE_ZERO = 'AMOUNT_CANNOT_BE_ZERO',
NOT_MEET_MIN_EXPECTED = 'NOT_MEET_MIN_EXPECTED',
}

export enum SwapStepType {
Expand Down
9 changes: 9 additions & 0 deletions packages/extension-base/src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,15 @@ export const PREDEFINED_FORMATTER: Record<string, NumberFormatter> = {
balance: balanceFormatter
};

/** @function formatNumber
* Convert number to a formatted string by dividing by 10^decimal
* @param {string | number | BigNumber} input - Input number
* @param {number} decimal - Decimal number
* @param {NumberFormatter} [formatter] - Formatter function
* - Default: balanceFormatter: With number > 1, show decimal with 2 numbers,
* with number < 1, show decimal with 6 (default) number
* @param {Record<string, number>} [metadata] - Metadata for formatter
*/
export const formatNumber = (
input: string | number | BigNumber,
decimal: number,
Expand Down
Loading