butterjs-sdk
Version:
Butter Network SDK
533 lines (500 loc) • 16.4 kB
text/typescript
import { BaseCurrency } from '../../entities';
import {
ID_TO_CHAIN_ID,
ID_TO_NEAR_NETWORK,
ID_TO_DEFAULT_RPC_URL,
IS_MAP,
IS_NEAR,
MOS_CONTRACT_ADDRESS_SET,
TOKEN_REGISTER_ADDRESS_SET,
ZERO_ADDRESS,
} from '../../constants';
import {
ButterFee,
ButterFeeDistribution,
ButterFeeRate,
VaultBalance,
} from '../../types/responseTypes';
import { TokenRegister } from '../../libs/TokenRegister';
import { BigNumber, ethers } from 'ethers';
import { getTokenByAddressAndChainId } from '../../utils/tokenUtil';
import { ButterJsonRpcProvider } from '../../types/paramTypes';
import { ID_TO_SUPPORTED_TOKEN } from '../../utils/tokenUtil';
import { asciiToHex, asciiToString, getHexAddress } from '../../utils';
import { VaultToken } from '../../libs/VaultToken';
import { EVMOmnichainService } from '../../libs/mos/EVMOmnichainService';
import MOS_RELAY_METADATA from '../../abis/MAPOmnichainServiceRelay.json';
import MOS_EVM_METADATA from '../../abis/MAPOmnichainService.json';
import { connect } from 'near-api-js';
import { CodeResult } from 'near-api-js/lib/providers/provider';
import { GET_MCS_TOKENS } from '../../constants/near_method_names';
import {
batchGetRelayChainToken,
batchGetToChainToken,
} from '../../utils/batchRequestUtils';
import Web3 from 'web3';
import TokenRegisterMetadata from '../../abis/TokenRegister.json';
import { RelayOmnichainService } from '../../libs/mos/RelayOmnichainService';
import { ButterSwapRoute } from '../../types';
import { assembleCrossChainRouteFromJson } from '../../utils/requestUtils';
import { DEFAULT_SLIPPAGE } from '../../constants/constants';
/**
* get fee for bridging srcToken to targetChain
* @param srcToken
* @param targetChain
* @param amount
* @param mapRpcProvider
*/
export async function getBridgeFee(
srcToken: BaseCurrency,
targetChain: string,
amount: string,
mapRpcProvider: ButterJsonRpcProvider
): Promise<ButterFee> {
const chainId: string = mapRpcProvider.chainId.toString();
const mapChainId: string = mapRpcProvider.chainId.toString();
const mapProvider = new ethers.providers.JsonRpcProvider(
mapRpcProvider.url ? mapRpcProvider.url : ID_TO_DEFAULT_RPC_URL(mapChainId)
);
const tokenRegister = new TokenRegister(
TOKEN_REGISTER_ADDRESS_SET[chainId]!,
mapProvider
);
let feeAmount = '';
let feeRate: ButterFeeRate = { lowest: '0', rate: '0', highest: '0' };
if (IS_MAP(srcToken.chainId)) {
const tokenAddress = srcToken.isNative
? srcToken.wrapped.address
: srcToken.address;
const tokenFeeRate = await tokenRegister.getFeeRate(
tokenAddress,
targetChain
);
feeRate.lowest = tokenFeeRate.lowest.toString();
feeRate.highest = tokenFeeRate.highest.toString();
feeRate.rate = BigNumber.from(tokenFeeRate.rate).div(100).toString();
feeAmount = _getFeeAmount(amount, feeRate);
} else {
const mapTokenAddress = await tokenRegister.getRelayChainToken(
srcToken.chainId.toString(),
srcToken
);
const relayChainAmount = await tokenRegister.getRelayChainAmount(
mapTokenAddress,
srcToken.chainId.toString(),
amount
);
const tokenFeeRate: ButterFeeRate = await tokenRegister.getFeeRate(
mapTokenAddress,
targetChain
);
feeRate.lowest = tokenFeeRate.lowest;
feeRate.highest = tokenFeeRate.highest;
feeRate.rate = BigNumber.from(tokenFeeRate.rate).div(100).toString();
const feeAmountInMappingToken = _getFeeAmount(relayChainAmount, feeRate);
const feeAmountBN = BigNumber.from(feeAmountInMappingToken);
feeRate.lowest = BigNumber.from(feeRate.lowest)
.mul(amount)
.div(relayChainAmount)
.toString();
feeRate.highest = BigNumber.from(feeRate.highest)
.mul(amount)
.div(relayChainAmount)
.toString();
feeAmount = feeAmountBN.mul(amount).div(relayChainAmount).toString();
}
const distribution = await getDistributeRate(mapChainId);
return Promise.resolve({
feeToken: srcToken,
feeRate: feeRate,
amount: feeAmount.toString(),
feeDistribution: distribution,
});
}
/**
* get fee for cross-chain exchange
* @param srcToken source token
* @param targetChain target chain id
* @param amount amount in minimal uint
* @param routeStr cross-chain route in string format
* @param mapRpcProvider map relay chain rpc provider
*/
export async function getSwapFee(
srcToken: BaseCurrency,
targetChain: string,
amount: string,
routeStr: string,
mapRpcProvider: ButterJsonRpcProvider
): Promise<ButterFee> {
const routes = assembleCrossChainRouteFromJson(routeStr, DEFAULT_SLIPPAGE);
const srcRoute = routes.srcChain;
if (
srcRoute === undefined ||
srcRoute.length === 0 ||
srcRoute[0]!.path.length === 0
) {
return await getBridgeFee(srcToken, targetChain, amount, mapRpcProvider);
}
let totalAmountOut: string = '0';
for (let route of routes.mapChain) {
totalAmountOut = BigNumber.from(totalAmountOut)
.add(route.amountOut)
.toString();
}
const tokenOut: BaseCurrency = srcRoute[0]!.tokenOut;
const chainId: string = mapRpcProvider.chainId.toString();
const mapChainId: string = mapRpcProvider.chainId.toString();
const mapProvider = new ethers.providers.JsonRpcProvider(
mapRpcProvider.url ? mapRpcProvider.url : ID_TO_DEFAULT_RPC_URL(mapChainId)
);
const tokenRegister = new TokenRegister(
TOKEN_REGISTER_ADDRESS_SET[chainId]!,
mapProvider
);
let feeAmount = '';
let feeRate: ButterFeeRate = { lowest: '0', rate: '0', highest: '0' };
if (IS_MAP(srcToken.chainId)) {
const tokenAddress = srcToken.isNative
? srcToken.wrapped.address
: srcToken.address;
const tokenFeeRate = await tokenRegister.getFeeRate(
tokenAddress,
targetChain
);
feeRate.lowest = tokenFeeRate.lowest.toString();
feeRate.highest = tokenFeeRate.highest.toString();
feeRate.rate = BigNumber.from(tokenFeeRate.rate).div(100).toString();
feeAmount = _getFeeAmount(amount, feeRate);
} else {
const mapTokenAddress = await tokenRegister.getRelayChainToken(
srcToken.chainId.toString(),
tokenOut
);
const relayChainAmount = await tokenRegister.getRelayChainAmount(
mapTokenAddress,
srcToken.chainId.toString(),
totalAmountOut
);
const tokenFeeRate: ButterFeeRate = await tokenRegister.getFeeRate(
mapTokenAddress,
targetChain
);
feeRate.lowest = tokenFeeRate.lowest;
feeRate.highest = tokenFeeRate.highest;
feeRate.rate = BigNumber.from(tokenFeeRate.rate).div(100).toString();
const feeAmountInMappingToken = _getFeeAmount(relayChainAmount, feeRate);
const feeAmountBN = BigNumber.from(feeAmountInMappingToken);
feeRate.lowest = BigNumber.from(feeRate.lowest)
.mul(totalAmountOut)
.div(relayChainAmount)
.toString();
feeRate.highest = BigNumber.from(feeRate.highest)
.mul(totalAmountOut)
.div(relayChainAmount)
.toString();
feeAmount = feeAmountBN
.mul(totalAmountOut)
.div(relayChainAmount)
.toString();
}
const distribution = await getDistributeRate(mapChainId);
console.log('11123123123123213123');
console.log('distribution', distribution);
return Promise.resolve({
feeToken: getTokenByAddressAndChainId(
getHexAddress(
tokenOut.address,
srcToken.chainId,
!IS_NEAR(srcToken.chainId)
),
srcToken.chainId
),
feeRate: feeRate,
amount: feeAmount.toString(),
feeDistribution: distribution,
});
}
/**
* get vault balance
* @param fromChainId
* @param fromToken
* @param toChainId
* @param rpcProvider
*/
export async function getVaultBalance(
fromChainId: string,
fromToken: BaseCurrency,
toChainId: string,
rpcProvider: ButterJsonRpcProvider
): Promise<VaultBalance> {
if (fromChainId != fromToken.chainId) {
throw new Error("Request Error: chainId and token.chainId doesn't match");
}
const mapChainId: string = rpcProvider.chainId.toString();
const provider = new ethers.providers.JsonRpcProvider(
rpcProvider.url ? rpcProvider.url : ID_TO_DEFAULT_RPC_URL(mapChainId)
);
const tokenRegister = new TokenRegister(
TOKEN_REGISTER_ADDRESS_SET[mapChainId]!,
provider
);
if (fromToken.isNative) {
fromToken = fromToken.wrapped;
}
const mapTokenAddress = IS_MAP(fromChainId)
? fromToken.address
: await tokenRegister.getRelayChainToken(fromChainId.toString(), fromToken);
const vaultAddress = await tokenRegister.getVaultToken(mapTokenAddress);
if (vaultAddress === ZERO_ADDRESS) {
throw new Error(
`getVaultBalance: vault address not found for token: ${mapTokenAddress}`
);
}
const vaultToken = new VaultToken(vaultAddress, provider);
let tokenBalance = await vaultToken.getVaultBalance(toChainId.toString());
let toChainTokenAddress = mapTokenAddress;
if (!IS_MAP(toChainId)) {
toChainTokenAddress = await tokenRegister.getToChainToken(
mapTokenAddress,
toChainId
);
if (toChainTokenAddress === '0x') {
throw new Error(
'Internal Error: Cannot find corresponding target token on target chain'
);
}
const mapToken = getTokenByAddressAndChainId(mapTokenAddress, mapChainId);
const toChainToken = getTokenByAddressAndChainId(
toChainTokenAddress,
toChainId
);
tokenBalance = BigNumber.from(tokenBalance)
.mul(ethers.utils.parseUnits('1', toChainToken.decimals))
.div(ethers.utils.parseUnits('1', mapToken.decimals))
.toString();
}
return Promise.resolve({
token: getTokenByAddressAndChainId(toChainTokenAddress, toChainId),
balance: tokenBalance.toString(),
isMintable: await isTokenMintable(toChainTokenAddress, toChainId),
});
}
/**
* get srcToken mapping token on target chain
* @param srcToken
* @param targetChainId
* @param rpcProvider
*/
export async function getTargetToken(
srcToken: BaseCurrency,
targetChainId: string,
rpcProvider: ButterJsonRpcProvider
): Promise<BaseCurrency> {
const tokenAddress = await getTargetTokenAddress(
srcToken,
targetChainId,
rpcProvider
);
if (tokenAddress === '0x') {
throw new Error('token does not exist');
}
return getTokenByAddressAndChainId(tokenAddress, targetChainId);
}
/**
* get srcToken mapping token on target chain
* @param srcToken
* @param targetChainId
* @param rpcProvider
*/
export async function getTargetTokenAddress(
srcToken: BaseCurrency,
targetChainId: string,
rpcProvider: ButterJsonRpcProvider
): Promise<string> {
const mapChainId: string = rpcProvider.chainId.toString();
const provider = new ethers.providers.JsonRpcProvider(
rpcProvider.url ? rpcProvider.url : ID_TO_DEFAULT_RPC_URL(mapChainId)
);
const tokenRegister = new TokenRegister(
TOKEN_REGISTER_ADDRESS_SET[mapChainId]!,
provider
);
let mapTokenAddress = srcToken.address;
if (!IS_MAP(srcToken.chainId)) {
mapTokenAddress = await tokenRegister.getRelayChainToken(
srcToken.chainId.toString(),
srcToken
);
}
let targetTokenAddress = mapTokenAddress;
if (!IS_MAP(targetChainId)) {
targetTokenAddress = await tokenRegister.getToChainToken(
mapTokenAddress,
targetChainId
);
}
return targetTokenAddress;
}
/**
* get what token can be bridge from src chain to target chain
* @param fromChainId
* @param toChainId
* @param provider
*/
export async function getTokenCandidatesOneByOne(
fromChainId: string,
toChainId: string,
provider: ButterJsonRpcProvider
): Promise<BaseCurrency[]> {
let ret = [];
const fromChainTokenList = ID_TO_SUPPORTED_TOKEN(fromChainId);
for (let i = 0; i < fromChainTokenList.length; i++) {
const token: BaseCurrency = fromChainTokenList[i]!;
const tokenToCheck: BaseCurrency = token.isNative ? token.wrapped : token;
if (
(await getTargetTokenAddress(tokenToCheck, toChainId, provider)) != '0x'
) {
ret.push(token);
}
}
return ret;
}
/**
* get token candidates with one transaction call
* @param fromChainId
* @param toChainId
* @param provider
*/
export async function getTokenCandidates(
fromChainId: string,
toChainId: string,
provider: ButterJsonRpcProvider
): Promise<BaseCurrency[]> {
const mapUrl = provider.url
? provider.url
: ID_TO_DEFAULT_RPC_URL(provider.chainId.toString());
const web3 = new Web3(mapUrl);
const tokenRegisterContract = new web3.eth.Contract(
TokenRegisterMetadata.abi as any,
TOKEN_REGISTER_ADDRESS_SET[provider.chainId.toString()]
);
let tokenArr = ID_TO_SUPPORTED_TOKEN(fromChainId).map(
(token: BaseCurrency) => {
if (IS_NEAR(token.chainId)) {
if (token.isNative) {
return getHexAddress(token.wrapped.address, token.chainId, false);
} else return getHexAddress(token.address, token.chainId, false);
} else {
if (token.isNative) {
return token.wrapped.address;
} else return token.address;
}
}
);
if (!IS_MAP(fromChainId)) {
tokenArr = await batchGetRelayChainToken(
tokenRegisterContract,
fromChainId,
tokenArr,
mapUrl
);
}
if (IS_MAP(toChainId)) {
return ID_TO_SUPPORTED_TOKEN(fromChainId);
}
const toChainTokenList = await batchGetToChainToken(
tokenRegisterContract,
tokenArr,
toChainId,
mapUrl
);
let supportedFromChainTokenArr: BaseCurrency[] = [];
for (let i = 0; i < toChainTokenList.length; i++) {
if (toChainTokenList[i] != null && toChainTokenList[i] != '0x') {
supportedFromChainTokenArr.push(ID_TO_SUPPORTED_TOKEN(fromChainId)[i]!);
}
}
return supportedFromChainTokenArr;
}
/**
* check if a token is mintable on mos
* @param tokenAddress
* @param chainId
*/
export async function isTokenMintable(
tokenAddress: string,
chainId: string
): Promise<boolean> {
const rpcUrl = ID_TO_DEFAULT_RPC_URL(chainId);
const rpcProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
if (IS_MAP(chainId)) {
const tokenRegister = new TokenRegister(
TOKEN_REGISTER_ADDRESS_SET[chainId]!,
rpcProvider
);
return tokenRegister.checkMintable(tokenAddress);
} else if (IS_NEAR(chainId)) {
const accountId = MOS_CONTRACT_ADDRESS_SET[ID_TO_CHAIN_ID(chainId)];
const connectionConfig = {
networkId: ID_TO_NEAR_NETWORK(chainId),
nodeUrl: ID_TO_DEFAULT_RPC_URL(chainId),
};
const near = await connect(connectionConfig);
const response: CodeResult = await near.connection.provider.query({
request_type: 'call_function',
finality: 'final',
account_id: accountId,
method_name: GET_MCS_TOKENS,
args_base64: 'e30=',
});
const mosTokenSet = JSON.parse(asciiToString(response.result));
for (let i = 0; i < mosTokenSet.length; i++) {
if (
getHexAddress(mosTokenSet[i][0].toLowerCase(), chainId, false) ===
tokenAddress.toLowerCase()
) {
return true;
}
}
return false;
} else {
const mos = new EVMOmnichainService(
MOS_CONTRACT_ADDRESS_SET[ID_TO_CHAIN_ID(chainId)],
MOS_EVM_METADATA.abi,
rpcProvider
);
return mos.isMintable(tokenAddress);
}
}
export async function getDistributeRate(
mapChainId: string
): Promise<ButterFeeDistribution> {
const rpcUrl = ID_TO_DEFAULT_RPC_URL(mapChainId);
const rpcProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
if (!IS_MAP(mapChainId)) {
throw new Error('chain id is not MAP');
}
const mos = new ethers.Contract(
MOS_CONTRACT_ADDRESS_SET[ID_TO_CHAIN_ID(mapChainId)],
MOS_RELAY_METADATA.abi,
rpcProvider
);
const lpRate = await mos.distributeRate(0);
const relayerRate = await mos.distributeRate(1);
const protocolRate = await mos.distributeRate(2);
console.log('relay', relayerRate);
return Promise.resolve({
relayer: relayerRate.rate.div(100).toString(),
lp: lpRate.rate.div(100).toString(),
protocol: protocolRate.rate.div(100).toString(),
});
}
function _getFeeAmount(amount: string, feeRate: ButterFeeRate): string {
const feeAmount = BigNumber.from(amount).mul(feeRate.rate).div(10000);
if (feeAmount.gt(feeRate.highest)) {
return feeRate.highest.toString();
} else if (feeAmount.lt(feeRate.lowest)) {
return feeRate.lowest.toString();
}
return feeAmount.toString();
}