Skip to content

Commit

Permalink
783/995, add Changelly ccr provider, Refund Address (#773)
Browse files Browse the repository at this point in the history
  • Loading branch information
IDIDOS authored Jan 24, 2025
2 parents 0a32552 + 6b415c6 commit 8559cc6
Show file tree
Hide file tree
Showing 30 changed files with 855 additions and 63 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rubic-sdk",
"version": "5.50.5",
"version": "5.51.0",
"description": "Simplify dApp creation",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/features/common/models/encode-transaction-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ export interface EncodeTransactionOptions {
skipAmountCheck?: boolean;

useCacheData?: boolean;

refundAddress?: string;
}
5 changes: 5 additions & 0 deletions src/features/common/models/swap-transaction-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,9 @@ export interface SwapTransactionOptions {
* Use in case of eip-155
*/
useEip155?: boolean;

/**
* Address for refund assets if error occurs
*/
refundAddress?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SymbiosisCrossChainProvider } from 'src/features/cross-chain/calculatio
import { XyCrossChainProvider } from 'src/features/cross-chain/calculation-manager/providers/xy-provider/xy-cross-chain-provider';

import { AcrossCrossChainProvider } from '../providers/across-provider/across-cross-chain-provider';
import { ChangellyCcrProvider } from '../providers/changelly-provider/changelly-ccr-provider';
import { EddyBridgeProvider } from '../providers/eddy-bridge/eddy-bridge-provider';
import { LayerZeroBridgeProvider } from '../providers/layerzero-bridge/layerzero-bridge-provider';
import { MesonCrossChainProvider } from '../providers/meson-provider/meson-cross-chain-provider';
Expand Down Expand Up @@ -51,6 +52,7 @@ const nonProxyProviders = [
ArbitrumRbcBridgeProvider,
TaikoBridgeProvider,
LayerZeroBridgeProvider,
ChangellyCcrProvider,
SimpleSwapCcrProvider
// MorphBridgeProvider
] as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export const CROSS_CHAIN_TRADE_TYPE = {
RETRO_BRIDGE: 'retro_bridge',
ACROSS: 'across',
UNIZEN: 'unizen',
SIMPLE_SWAP: 'simple_swap'
SIMPLE_SWAP: 'simple_swap',
CHANGELLY: 'changelly'
} as const;

export type CrossChainTradeType =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import BigNumber from 'bignumber.js';
import { MaxAmountError, MinAmountError, NotSupportedTokensError } from 'src/common/errors';
import { PriceToken, PriceTokenAmount } from 'src/common/tokens';
import { compareAddresses } from 'src/common/utils/blockchain';
import { BlockchainName, EvmBlockchainName } from 'src/core/blockchain/models/blockchain-name';
import { Web3PublicSupportedBlockchain } from 'src/core/blockchain/web3-public-service/models/web3-public-storage';
import { EvmEncodeConfig } from 'src/core/blockchain/web3-pure/typed-web3-pure/evm-web3-pure/models/evm-encode-config';
import { getFromWithoutFee } from 'src/features/common/utils/get-from-without-fee';

import { RequiredCrossChainOptions } from '../../models/cross-chain-options';
import { CROSS_CHAIN_TRADE_TYPE } from '../../models/cross-chain-trade-type';
import { CrossChainProvider } from '../common/cross-chain-provider';
import { CalculationResult } from '../common/models/calculation-result';
import { FeeInfo } from '../common/models/fee-info';
import { RubicStep } from '../common/models/rubicStep';
import { ProxyCrossChainEvmTrade } from '../common/proxy-cross-chain-evm-facade/proxy-cross-chain-evm-trade';
import { ChangellyCcrTrade } from './changelly-ccr-trade';
import { changellySpecificChainTickers } from './constants/changelly-specific-chain-ticker';
import { changellySupportedChains } from './constants/changelly-supported-chains';
import { changellyNativeTokensData } from './constants/native-token-data';
import { ChangellyToken } from './models/changelly-token';
import { ChangellyApiService } from './services/changelly-api-service';

export class ChangellyCcrProvider extends CrossChainProvider {
public readonly type = CROSS_CHAIN_TRADE_TYPE.CHANGELLY;

public isSupportedBlockchain(fromBlockchain: BlockchainName): boolean {
return changellySupportedChains.some(chain => chain === fromBlockchain);
}

public async calculate(
from: PriceTokenAmount,
toToken: PriceToken,
options: RequiredCrossChainOptions
): Promise<CalculationResult<EvmEncodeConfig>> {
// provider doesn't support deposit from proxy contract
const useProxy = false;

try {
const feeInfo = await this.getFeeInfo(
from.blockchain as EvmBlockchainName,
options.providerAddress,
from,
useProxy
);

const fromWithoutFee = getFromWithoutFee(
from,
feeInfo.rubicProxy?.platformFee?.percent
);

const changellyTokens = await this.getFromToChangellyTokens(from, toToken);

const response = await ChangellyApiService.getFixedRateEstimation({
from: changellyTokens.fromToken.ticker,
to: changellyTokens.toToken.ticker,
amountFrom: fromWithoutFee.tokenAmount.toFixed()
});

if (!response.result && response.error) {
if (response.error.message.includes('Invalid amount for')) {
const tokenLimits = response.error.data.limits;
const minMaxError = this.checkMinMaxErrors(
from,
tokenLimits.min.from,
tokenLimits.max.from
);

if (minMaxError) {
return {
tradeType: this.type,
trade: this.getEmptyTrade(
from,
toToken,
feeInfo,
options.providerAddress,
changellyTokens
),
error: minMaxError
};
}
}

throw new Error(response.error.message);
}

const quote = response.result[0]!;

const toAmount = new BigNumber(quote.amountTo);

const toAmountMin = toAmount;

const rateId = quote.id;

const to = new PriceTokenAmount({
...toToken.asStruct,
tokenAmount: toAmount
});

const routePath = await this.getRoutePath(from, to);

const trade = new ChangellyCcrTrade({
from,
to,
changellyTokens,
feeInfo,
gasData: null,
priceImpact: from.calculatePriceImpactPercent(to),
providerAddress: options.providerAddress,
useProxy,
toTokenAmountMin: toAmountMin,
routePath,
onChainTrade: null,
rateId
});

return {
trade,
tradeType: this.type
};
} catch (err) {
return {
tradeType: this.type,
trade: null,
error: err
};
}
}

private checkMinMaxErrors(
fromToken: PriceTokenAmount,
minFrom: string,
maxFrom: string
): MinAmountError | MaxAmountError | null {
if (fromToken.tokenAmount.lt(minFrom)) {
return new MinAmountError(new BigNumber(minFrom), fromToken.symbol);
}
if (fromToken.tokenAmount.gt(maxFrom)) {
return new MaxAmountError(new BigNumber(maxFrom), fromToken.symbol);
}

return null;
}

private async getFromToChangellyTokens(
from: PriceTokenAmount,
to: PriceToken
): Promise<{ fromToken: ChangellyToken; toToken: ChangellyToken }> {
const { result: tokenList } = await ChangellyApiService.fetchTokenList();

return {
fromToken: this.getChangellyToken(tokenList, from),
toToken: this.getChangellyToken(tokenList, to)
};
}

private getChangellyToken(tokenList: ChangellyToken[], token: PriceToken): ChangellyToken {
const tokenBlockchain = this.getBlockchain(token.blockchain);

const changellytoken = tokenList.find(
fetchedToken =>
fetchedToken.blockchain.toLowerCase() === tokenBlockchain &&
((fetchedToken.contractAddress &&
compareAddresses(fetchedToken.contractAddress, token.address)) ||
(!fetchedToken.contractAddress && token.isNative) ||
this.isNativeTokenWithAddress(fetchedToken))
);

if (!changellytoken) {
throw new NotSupportedTokensError();
}

return changellytoken;
}

private getBlockchain(blockchain: BlockchainName): string {
const specificChainTicker = changellySpecificChainTickers[blockchain];

return specificChainTicker ? specificChainTicker : blockchain.toLowerCase();
}

private isNativeTokenWithAddress(currency: ChangellyToken): boolean {
return changellyNativeTokensData.some(
nativeTokenData =>
nativeTokenData.ticker === currency.ticker &&
nativeTokenData.blockchain === currency.blockchain.toLowerCase() &&
nativeTokenData.address === currency.contractAddress
);
}

protected async getRoutePath(
from: PriceTokenAmount,
to: PriceTokenAmount
): Promise<RubicStep[]> {
return [
{
type: 'cross-chain',
provider: CROSS_CHAIN_TRADE_TYPE.CHANGELLY,
path: [from, to]
}
];
}

private getEmptyTrade(
from: PriceTokenAmount,
toToken: PriceToken,
feeInfo: FeeInfo,
providerAddress: string,
changellyTokens: { fromToken: ChangellyToken; toToken: ChangellyToken }
): ChangellyCcrTrade {
const to = new PriceTokenAmount({
...toToken.asStruct,
tokenAmount: new BigNumber(0)
});
const trade = new ChangellyCcrTrade({
from,
to,
toTokenAmountMin: new BigNumber(0),
priceImpact: 0,
feeInfo,
changellyTokens,
// rateId: '',
routePath: [],
useProxy: false,
providerAddress,
gasData: null,
onChainTrade: null,
rateId: ''
});

return trade;
}

protected override async getFeeInfo(
fromBlockchain: Web3PublicSupportedBlockchain,
providerAddress: string,
percentFeeToken: PriceTokenAmount,
useProxy: boolean
): Promise<FeeInfo> {
return ProxyCrossChainEvmTrade.getFeeInfo(
fromBlockchain,
providerAddress,
percentFeeToken,
useProxy
);
}
}
Loading

0 comments on commit 8559cc6

Please sign in to comment.