@xlink-network/xlink-sdk
Version:
487 lines (428 loc) • 14.5 kB
text/typescript
import { callReadOnlyFunction } from "@stacks/transactions"
import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen"
import { readContract } from "viem/actions"
import {
getBRC20SupportedRoutes,
getRunesSupportedRoutes,
} from "../metaUtils/xlinkContractHelpers"
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import {
getTerminatingStacksTokenContractAddress,
stxTokenContractAddresses,
} from "../stacksUtils/stxContractAddresses"
import {
executeReadonlyCallXLINK,
getStacksContractCallInfo,
getStacksTokenContractInfo,
numberFromStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import {
IsSupportedFn,
KnownRoute_FromEVM_ToStacks,
KnownRoute_FromStacks_ToEVM,
} from "../utils/buildSupportedRoutes"
import { props } from "../utils/promiseHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers"
import { TransferProphet } from "../utils/types/TransferProphet"
import {
_allNoLongerSupportedEVMChains,
KnownChainId,
KnownTokenId,
} from "../utils/types/knownIds"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { nativeCurrencyAddress } from "./addressHelpers"
import { BridgeEndpointAbi } from "./contractAbi/bridgeEndpoint"
import { BridgeRegistryAbi } from "./contractAbi/bridgeRegistry"
import {
getEVMContractCallInfo,
getEVMTokenContractInfo,
numberFromSolidityContractNumber,
} from "./xlinkContractHelpers"
export const getEvm2StacksFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo(
route.toChain,
"cross-peg-in-endpoint-v2-04",
)
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
const evmTokenContractCallInfo = await getEVMTokenContractInfo(
ctx,
route.fromChain,
route.fromToken,
)
if (
stacksContractCallInfo == null ||
evmContractCallInfo == null ||
evmTokenContractCallInfo == null
) {
return
}
if (evmTokenContractCallInfo.tokenContractAddress === nativeCurrencyAddress) {
return getEvm2StacksNativeBridgeFeeInfo(ctx, route)
}
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const { client, tokenContractAddress } = evmTokenContractCallInfo
const registryAddr =
evmContractCallInfo.registryContractAddress ??
(await readContract(client, {
abi: BridgeEndpointAbi,
address: evmContractCallInfo.bridgeEndpointContractAddress,
functionName: "registry",
}))
const resp = await props({
isApproved: readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "APPROVED_TOKEN",
}).then(key =>
readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "hasRole",
args: [key, tokenContractAddress],
}),
),
feeRate: readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "feePctPerToken",
args: [tokenContractAddress],
}).then(numberFromSolidityContractNumber),
minFeeAmount: readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "minFeePerToken",
args: [tokenContractAddress],
}).then(numberFromSolidityContractNumber),
minAmount: readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "minAmountPerToken",
args: [tokenContractAddress],
}).then(numberFromSolidityContractNumber),
maxAmount: readContract(client, {
abi: BridgeRegistryAbi,
address: registryAddr,
functionName: "maxAmountPerToken",
args: [tokenContractAddress],
}).then(numberFromSolidityContractNumber),
isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-paused",
{},
executeOptions,
),
})
if (!resp.isApproved) return undefined
const minAmount = BigNumber.max([resp.minAmount, resp.minFeeAmount])
const maxAmount = BigNumber.min([resp.maxAmount])
return {
isPaused: resp.isPaused,
bridgeToken: route.fromToken,
fees: [
{
type: "rate",
token: route.fromToken,
rate: resp.feeRate,
minimumAmount: resp.minFeeAmount,
},
],
minBridgeAmount: BigNumber.isZero(minAmount) ? null : minAmount,
maxBridgeAmount: BigNumber.isZero(maxAmount) ? null : maxAmount,
}
}
const getEvm2StacksNativeBridgeFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo(
route.toChain,
"cross-peg-in-endpoint-v2-04",
)
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
if (
stacksContractCallInfo == null ||
evmContractCallInfo?.nativeBridgeEndpointContractAddress == null
) {
return
}
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const resp = await props({
isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-paused",
{},
executeOptions,
),
})
return {
isPaused: resp.isPaused,
bridgeToken: route.fromToken,
fees: [],
minBridgeAmount: null,
maxBridgeAmount: null,
}
}
export const getStacks2EvmFeeInfo = async (
route: KnownRoute_FromStacks_ToEVM,
): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo(
route.fromChain,
"cross-peg-out-endpoint-v2-01",
)
const stacksTokenContractCallInfo = getStacksTokenContractInfo(
route.fromChain,
route.fromToken,
)
const toChainId = contractAssignedChainIdFromKnownChain(route.toChain)
if (stacksContractCallInfo == null || stacksTokenContractCallInfo == null) {
return
}
const terminatingStacksTokenAddress =
getTerminatingStacksTokenContractAddress(route) ??
stacksTokenContractCallInfo
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const tokenConf = await Promise.all([
executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-approved-pair-or-fail",
{
pair: {
token: `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`,
"chain-id": toChainId,
},
},
executeOptions,
),
executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-paused",
{},
executeOptions,
),
]).then(([resp, isPaused]) => {
if (resp.type !== "success") return undefined
return {
...unwrapResponse(resp),
isPaused,
}
})
if (tokenConf == null) return undefined
const feeRate = numberFromStacksContractNumber(tokenConf.fee)
const minFee = numberFromStacksContractNumber(tokenConf["min-fee"])
const reserve = numberFromStacksContractNumber(tokenConf.reserve)
const minAmount = BigNumber.max([
numberFromStacksContractNumber(tokenConf["min-amount"]),
minFee,
])
const maxAmount = BigNumber.min([
numberFromStacksContractNumber(tokenConf["max-amount"]),
reserve,
])
return {
isPaused: tokenConf.isPaused || tokenConf.approved === false,
bridgeToken: route.fromToken,
fees: [
{
type: "rate",
token: route.fromToken,
rate: feeRate,
minimumAmount: minFee,
},
],
minBridgeAmount: BigNumber.isZero(minAmount) ? null : minAmount,
maxBridgeAmount: BigNumber.isZero(maxAmount) ? null : maxAmount,
}
}
export async function fromCorrespondingStacksToken(
toChain: KnownChainId.EVMChain,
stacksToken: KnownTokenId.StacksToken,
): Promise<KnownTokenId.EVMToken[]> {
const EVMToken = KnownTokenId.EVM
const StacksToken = KnownTokenId.Stacks
const restEVMTokenPossibilities = assertExclude.i<KnownTokenId.EVMToken>()
if (stacksToken === StacksToken.sLUNR) {
return [EVMToken.LUNR]
}
assertExclude(restEVMTokenPossibilities, EVMToken.LUNR)
if (stacksToken === StacksToken.ALEX) {
return [EVMToken.ALEX]
}
assertExclude(restEVMTokenPossibilities, EVMToken.ALEX)
if (stacksToken === StacksToken.sSKO) {
return [EVMToken.SKO]
}
assertExclude(restEVMTokenPossibilities, EVMToken.SKO)
if (stacksToken === StacksToken.vLiSTX) {
return [EVMToken.vLiSTX]
}
assertExclude(restEVMTokenPossibilities, EVMToken.vLiSTX)
if (stacksToken === StacksToken.vLiALEX) {
return [EVMToken.vLiALEX]
}
assertExclude(restEVMTokenPossibilities, EVMToken.vLiALEX)
if (stacksToken === StacksToken.sUSDT) {
return [EVMToken.sUSDT, EVMToken.USDT, EVMToken.USDC]
}
assertExclude(restEVMTokenPossibilities, EVMToken.sUSDT)
assertExclude(restEVMTokenPossibilities, EVMToken.USDT)
assertExclude(restEVMTokenPossibilities, EVMToken.USDC)
if (stacksToken === StacksToken.aBTC) {
return [EVMToken.aBTC, EVMToken.WBTC, EVMToken.BTCB, EVMToken.cbBTC]
}
assertExclude(restEVMTokenPossibilities, EVMToken.aBTC)
assertExclude(restEVMTokenPossibilities, EVMToken.WBTC)
assertExclude(restEVMTokenPossibilities, EVMToken.BTCB)
assertExclude(restEVMTokenPossibilities, EVMToken.cbBTC)
if (stacksToken === StacksToken.uBTC) {
return [EVMToken.uBTC, EVMToken.wuBTC]
}
assertExclude(restEVMTokenPossibilities, EVMToken.uBTC)
assertExclude(restEVMTokenPossibilities, EVMToken.wuBTC)
if (stacksToken === StacksToken.DB20) {
return [EVMToken.DB20]
}
assertExclude(restEVMTokenPossibilities, EVMToken.DB20)
if (stacksToken === StacksToken.DOG) {
return [EVMToken.DOG]
}
assertExclude(restEVMTokenPossibilities, EVMToken.DOG)
checkNever(restEVMTokenPossibilities)
return []
}
export async function toCorrespondingStacksToken(
evmToken: KnownTokenId.EVMToken,
): Promise<undefined | KnownTokenId.StacksToken> {
const EVMToken = KnownTokenId.EVM
const StacksToken = KnownTokenId.Stacks
switch (evmToken) {
case EVMToken.LUNR:
return StacksToken.sLUNR
case EVMToken.ALEX:
return StacksToken.ALEX
case EVMToken.SKO:
return StacksToken.sSKO
case EVMToken.vLiSTX:
return StacksToken.vLiSTX
case EVMToken.vLiALEX:
return StacksToken.vLiALEX
case EVMToken.sUSDT:
case EVMToken.USDT:
case EVMToken.USDC:
return StacksToken.sUSDT
case EVMToken.aBTC:
case EVMToken.BTCB:
case EVMToken.WBTC:
case EVMToken.cbBTC:
return StacksToken.aBTC
case EVMToken.uBTC:
case EVMToken.wuBTC:
return StacksToken.uBTC
case EVMToken.DB20:
return StacksToken.DB20
case EVMToken.DOG:
return StacksToken.DOG
default:
checkNever(evmToken)
return
}
}
export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
const { fromChain, fromToken, toChain, toToken } = route
if (fromChain === toChain && fromToken === toToken) {
return false
}
if (
KnownChainId.isEVMChain(fromChain) &&
_allNoLongerSupportedEVMChains.includes(fromChain)
) {
return false
}
if (
KnownChainId.isEVMChain(toChain) &&
_allNoLongerSupportedEVMChains.includes(toChain)
) {
return false
}
if (
!KnownChainId.isEVMChain(fromChain) ||
!KnownTokenId.isEVMToken(fromToken)
) {
return false
}
if (!KnownChainId.isKnownChain(toChain)) return false
const fromContractInfo = await getEVMContractCallInfo(ctx, fromChain)
if (fromContractInfo == null) return false
const fromTokenInfo = await getEVMTokenContractInfo(ctx, fromChain, fromToken)
if (fromTokenInfo == null) return false
if (fromTokenInfo.tokenContractAddress === nativeCurrencyAddress) {
if (fromContractInfo.bridgeEndpointContractAddress == null) return false
if (!KnownChainId.isBitcoinChain(toChain)) return false
}
if (KnownChainId.isStacksChain(toChain)) {
if (!KnownTokenId.isStacksToken(toToken)) return false
const stacksToken = await toCorrespondingStacksToken(fromToken)
if (stacksToken == null) return false
if (stxTokenContractAddresses[stacksToken]?.[toChain] == null) {
return false
}
return stacksToken === toToken
}
if (KnownChainId.isEVMChain(toChain)) {
if (!KnownTokenId.isEVMToken(toToken)) return false
const toTokenInfo = await getEVMTokenContractInfo(ctx, toChain, toToken)
if (toTokenInfo == null) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false
const toEVMTokens = await fromCorrespondingStacksToken(
toChain,
transitStacksToken,
)
return toEVMTokens.includes(toToken)
}
if (KnownChainId.isBitcoinChain(toChain)) {
if (!KnownTokenId.isBitcoinToken(toToken)) return false
const stacksToken = await toCorrespondingStacksToken(fromToken)
return stacksToken === KnownTokenId.Stacks.aBTC
}
if (KnownChainId.isRunesChain(toChain)) {
if (!KnownTokenId.isRunesToken(toToken)) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false
const runesRoutes = await getRunesSupportedRoutes(ctx, toChain)
return runesRoutes.some(route => route.stacksToken === transitStacksToken)
}
if (KnownChainId.isBRC20Chain(toChain)) {
if (!KnownTokenId.isBRC20Token(toToken)) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false
const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain)
return brc20Routes.some(route => route.stacksToken === transitStacksToken)
}
checkNever(toChain)
return false
}