Files
bro-sdk/src/evmUtils/peggingHelpers.ts
2025-04-20 19:57:04 +02:00

653 lines
19 KiB
TypeScript

import { unwrapResponse } from "clarity-codegen"
import { readContract } from "viem/actions"
import { EVM_BARE_PEG_IN_USE_SWAP_CONTRACT } from "../config"
import { getBRC20SupportedRoutes } from "../metaUtils/apiHelpers/getBRC20SupportedRoutes"
import { getRunesSupportedRoutes } from "../metaUtils/apiHelpers/getRunesSupportedRoutes"
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import { StacksContractName } from "../stacksUtils/stxContractAddresses"
import {
executeReadonlyCallXLINK,
getStacksContractCallInfo,
getStacksTokenContractInfo,
numberFromStacksContractNumber,
} from "../stacksUtils/contractHelpers"
import { BigNumber } from "../utils/BigNumber"
import {
getAndCheckTransitStacksTokens,
getSpecialFeeDetailsForSwapRoute,
} from "../utils/SwapRouteHelpers"
import {
IsSupportedFn,
KnownRoute_FromEVM_ToStacks,
KnownRoute_FromStacks_ToEVM,
KnownRoute_ToStacks,
} from "../utils/buildSupportedRoutes"
import { props } from "../utils/promiseHelpers"
import { checkNever } from "../utils/typeHelpers"
import {
TransferProphet,
TransferProphet_Fee_Fixed,
} from "../utils/types/TransferProphet"
import {
_allNoLongerSupportedEVMChains,
KnownChainId,
KnownTokenId,
} from "../utils/types/knownIds"
import {
evmNativeCurrencyAddress,
isStacksContractAddressEqual,
StacksContractAddress,
} from "../sdkUtils/types"
import {
SDKGlobalContext,
withGlobalContextCache,
} from "../sdkUtils/types.internal"
import {
getEVMSupportedRoutes,
getEVMSupportedRoutesByChainType,
} from "./apiHelpers/getEVMSupportedRoutes"
import { BridgeEndpointAbi } from "./contractAbi/bridgeEndpoint"
import { BridgeRegistryAbi } from "./contractAbi/bridgeRegistry"
import {
getEVMContractCallInfo,
getEVMTokenContractInfo,
numberFromSolidityContractNumber,
} from "./contractHelpers"
export const getEvm2StacksFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
return withGlobalContextCache(
ctx.evm.feeRateCache,
withGlobalContextCache.cacheKeyFromRoute(route),
() => _getEvm2StacksFeeInfo(ctx, route),
)
}
const _getEvm2StacksFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromEVM_ToStacks,
): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = EVM_BARE_PEG_IN_USE_SWAP_CONTRACT
? getStacksContractCallInfo(
route.toChain,
StacksContractName.EVMPegInEndpointSwap,
)
: getStacksContractCallInfo(
route.toChain,
StacksContractName.EVMPegInEndpoint,
)
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
const evmTokenContractCallInfo = await getEVMTokenContractInfo(
ctx,
route.fromChain,
route.fromToken,
)
if (
stacksContractCallInfo == null ||
evmContractCallInfo == null ||
evmTokenContractCallInfo == null
) {
return
}
const { client, tokenContractAddress } = evmTokenContractCallInfo
if (tokenContractAddress === evmNativeCurrencyAddress) {
return getEvm2StacksNativeBridgeFeeInfo(ctx, route)
}
const registryAddr =
evmContractCallInfo.registryContractAddress ??
(await readContract(client, {
abi: BridgeEndpointAbi,
address: evmContractCallInfo.bridgeEndpointContractAddress,
functionName: "registry",
}))
const resp = await props({
isApprovedOnEVMSide: 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",
{},
stacksContractCallInfo.executeOptions,
),
}).then(
resp => {
if (ctx.debugLog) {
console.log("[getEvm2StacksFeeInfo]", route, resp)
}
return resp
},
err => {
if (ctx.debugLog) {
console.log("[getEvm2StacksFeeInfo]", route, err)
}
throw err
},
)
if (!resp.isApprovedOnEVMSide) 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,
StacksContractName.EVMPegInEndpoint,
)
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
if (
stacksContractCallInfo == null ||
evmContractCallInfo?.nativeBridgeEndpointContractAddress == null
) {
return
}
const resp = await props({
isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-paused",
{},
stacksContractCallInfo.executeOptions,
),
}).then(
resp => {
if (ctx.debugLog) {
console.log("[getEvm2StacksNativeBridgeFeeInfo]", route, resp)
}
return resp
},
err => {
if (ctx.debugLog) {
console.log("[getEvm2StacksNativeBridgeFeeInfo]", route, err)
}
throw err
},
)
return {
isPaused: resp.isPaused,
bridgeToken: route.fromToken,
fees: [],
minBridgeAmount: null,
maxBridgeAmount: null,
}
}
export const getStacks2EvmFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromStacks_ToEVM,
options: {
toDexAggregator: boolean
/**
* The entry route step that triggered the Stacks transaction.
* It's crucial for correctly calculating fees in multi-step bridging
* processes.
*
* Examples:
*
* * BTC > Runes (`via: ALEX`):
* 1. btc > stacks (initialRoute)
* 2. stacks > runes
* * BTC > Runes (`via: evmDexAggregator`):
* 1. btc > stacks (initialRoute as well, but not what we want)
* 2. stacks > evm
* 3. evm swap
* 4. evm > stacks (initialRoute for this partition)
* 5. stacks > runes
*/
initialRoute: null | KnownRoute_ToStacks
},
): Promise<undefined | TransferProphet> => {
return withGlobalContextCache(
ctx.evm.feeRateCache,
[
withGlobalContextCache.cacheKeyFromRoute(route),
options.toDexAggregator ? "agg" : "",
options.initialRoute == null
? ""
: withGlobalContextCache.cacheKeyFromRoute(options.initialRoute),
].join("#"),
() => _getStacks2EvmFeeInfo(ctx, route, options),
)
}
const _getStacks2EvmFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromStacks_ToEVM,
options: {
toDexAggregator: boolean
/**
* The entry route step that triggered the Stacks transaction.
* It's crucial for correctly calculating fees in multi-step bridging
* processes.
*
* Examples:
*
* * BTC > Runes (`via: ALEX`):
* 1. btc > stacks (initialRoute)
* 2. stacks > runes
* * BTC > Runes (`via: evmDexAggregator`):
* 1. btc > stacks (initialRoute as well, but not what we want)
* 2. stacks > evm
* 3. evm swap
* 4. evm > stacks (initialRoute for this partition)
* 5. stacks > runes
*/
initialRoute: null | KnownRoute_ToStacks
},
): Promise<undefined | TransferProphet> => {
const stacksBaseContractCallInfo = getStacksContractCallInfo(
route.fromChain,
StacksContractName.EVMPegOutEndpoint,
)
const stacksAggContractCallInfo = getStacksContractCallInfo(
route.fromChain,
StacksContractName.EVMPegOutEndpointAggregator,
)
const stacksTokenContractCallInfo = await getStacksTokenContractInfo(
ctx,
route.fromChain,
route.fromToken,
)
const toChainId = contractAssignedChainIdFromKnownChain(route.toChain)
if (
stacksBaseContractCallInfo == null ||
stacksAggContractCallInfo == null ||
stacksTokenContractCallInfo == null
) {
return
}
const stacksContractCallInfo = options.toDexAggregator
? stacksAggContractCallInfo
: stacksBaseContractCallInfo
const terminatingStacksTokenAddress =
(await getTerminatingStacksTokenContractAddress(ctx, {
evmChain: route.toChain,
evmToken: route.toToken,
stacksChain: route.fromChain,
})) ?? stacksTokenContractCallInfo
const specialFeeInfo = await getSpecialFeeDetailsForSwapRoute(ctx, route, {
initialRoute: options.initialRoute,
swapRoute: {
via: options.toDexAggregator ? "evmDexAggregator" : "ALEX",
},
})
if (ctx.debugLog) {
console.log("[getStacks2EvmFeeInfo/specialFeeInfo]", route, specialFeeInfo)
}
const tokenConf = await Promise.all([
executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-approved-pair-or-fail",
{
pair: {
token: `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`,
"chain-id": toChainId,
},
},
stacksContractCallInfo.executeOptions,
),
executeReadonlyCallXLINK(
stacksContractCallInfo.contractName,
"get-paused",
{},
stacksContractCallInfo.executeOptions,
),
]).then(
([resp, isPaused]) => {
if (ctx.debugLog) {
console.log("[getStacks2EvmFeeInfo]", route, resp, isPaused)
}
if (resp.type !== "success") return undefined
return {
...unwrapResponse(resp),
isPaused,
}
},
err => {
if (ctx.debugLog) {
console.log("[getStacks2EvmFeeInfo]", route, err)
}
throw err
},
)
if (tokenConf == null) return undefined
const isPaused = tokenConf.isPaused || tokenConf.approved === false
const reserve = numberFromStacksContractNumber(tokenConf.reserve)
const minAmount = numberFromStacksContractNumber(tokenConf["min-amount"])
const maxAmount = BigNumber.min([
numberFromStacksContractNumber(tokenConf["max-amount"]),
reserve,
])
if (specialFeeInfo != null) {
return {
isPaused,
bridgeToken: route.fromToken,
fees: [
{
type: "rate",
token: route.fromToken,
rate: specialFeeInfo.feeRate,
minimumAmount: specialFeeInfo.minFeeAmount,
},
...(specialFeeInfo.gasFee == null
? []
: [
{
type: "fixed",
token: specialFeeInfo.gasFee.token,
amount: specialFeeInfo.gasFee.amount,
} satisfies TransferProphet_Fee_Fixed,
]),
],
minBridgeAmount: BigNumber.isZero(minAmount)
? specialFeeInfo.minFeeAmount
: BigNumber.max([minAmount, specialFeeInfo.minFeeAmount]),
maxBridgeAmount: maxAmount,
}
}
const feeRate = numberFromStacksContractNumber(tokenConf.fee)
const minFee = numberFromStacksContractNumber(tokenConf["min-fee"])
return {
isPaused,
bridgeToken: route.fromToken,
fees: [
{
type: "rate",
token: route.fromToken,
rate: feeRate,
minimumAmount: minFee,
},
],
minBridgeAmount: BigNumber.isZero(minAmount)
? minFee
: BigNumber.max([minAmount, minFee]),
maxBridgeAmount: maxAmount,
}
}
export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
const { fromChain, fromToken, toChain, toToken } = route
if (fromChain === toChain && fromToken === toToken) {
return false
}
if (!KnownChainId.isKnownChain(toChain)) return false
if (
(KnownChainId.isEVMChain(fromChain) &&
_allNoLongerSupportedEVMChains.includes(fromChain)) ||
(KnownChainId.isEVMChain(toChain) &&
_allNoLongerSupportedEVMChains.includes(toChain))
) {
return false
}
if (
!KnownChainId.isEVMChain(fromChain) ||
!KnownTokenId.isEVMToken(fromToken)
) {
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 === evmNativeCurrencyAddress) {
if (fromContractInfo.bridgeEndpointContractAddress == null) return false
if (!KnownChainId.isBitcoinChain(toChain)) return false
}
const headAndTailStacksTokens = await getAndCheckTransitStacksTokens(ctx, {
...route,
fromChain,
fromToken,
toChain: toChain as any,
toToken: toToken as any,
})
if (headAndTailStacksTokens == null) return false
const { firstStepToStacksToken, lastStepFromStacksToken } =
headAndTailStacksTokens
// evm -> stacks
if (KnownChainId.isStacksChain(toChain)) {
if (!KnownTokenId.isStacksToken(toToken)) return false
const evmRoutes = await getEVMSupportedRoutes(ctx, fromChain)
return evmRoutes.some(
route => route.evmToken === fromToken && route.stacksToken === toToken,
)
}
// evm -> evm
if (KnownChainId.isEVMChain(toChain)) {
if (!KnownTokenId.isEVMToken(toToken)) return false
const fromRoutes = await getEVMSupportedRoutes(ctx, fromChain)
const toRoutes = await getEVMSupportedRoutes(ctx, toChain)
return (
fromRoutes.some(
route =>
route.evmToken === fromToken &&
route.stacksToken === firstStepToStacksToken,
) &&
toRoutes.some(
route =>
route.evmToken === toToken &&
route.stacksToken === lastStepFromStacksToken,
)
)
}
// evm -> btc
if (KnownChainId.isBitcoinChain(toChain)) {
if (!KnownTokenId.isBitcoinToken(toToken)) return false
return toToken === KnownTokenId.Bitcoin.BTC
}
// evm -> brc20
if (KnownChainId.isBRC20Chain(toChain)) {
if (!KnownTokenId.isBRC20Token(toToken)) return false
const fromRoutes = await getEVMSupportedRoutes(ctx, fromChain)
const toRoutes = await getBRC20SupportedRoutes(ctx, toChain)
return (
fromRoutes.some(
route =>
route.evmToken === fromToken &&
route.stacksToken === firstStepToStacksToken,
) &&
toRoutes.some(
route =>
route.stacksToken === lastStepFromStacksToken &&
route.brc20Token === toToken,
)
)
}
// evm -> runes
if (KnownChainId.isRunesChain(toChain)) {
if (!KnownTokenId.isRunesToken(toToken)) return false
const fromRoutes = await getEVMSupportedRoutes(ctx, fromChain)
const toRoutes = await getRunesSupportedRoutes(ctx, toChain)
return (
fromRoutes.some(
route =>
route.evmToken === fromToken &&
route.stacksToken === firstStepToStacksToken,
) &&
toRoutes.some(
route =>
route.stacksToken === lastStepFromStacksToken &&
route.runesToken === toToken,
)
)
}
checkNever(toChain)
return false
}
export async function evmTokenFromCorrespondingStacksToken(
sdkContext: SDKGlobalContext,
toChain: KnownChainId.EVMChain,
fromStacksToken: KnownTokenId.StacksToken,
): Promise<KnownTokenId.EVMToken[]> {
const supportedRoutes = await getEVMSupportedRoutes(sdkContext, toChain)
return supportedRoutes.reduce(
(acc, route) =>
route.stacksToken === fromStacksToken ? [...acc, route.evmToken] : acc,
[] as KnownTokenId.EVMToken[],
)
}
export async function evmTokenToCorrespondingStacksToken(
sdkContext: SDKGlobalContext,
fromChain: KnownChainId.EVMChain,
fromEVMToken: KnownTokenId.EVMToken,
): Promise<undefined | KnownTokenId.StacksToken> {
const supportedRoutes = await getEVMSupportedRoutes(sdkContext, fromChain)
return supportedRoutes.find(route => route.evmToken === fromEVMToken)
?.stacksToken
}
export const getTerminatingStacksTokenContractAddress = async (
sdkContext: SDKGlobalContext,
info: {
evmChain: KnownChainId.EVMChain
evmToken: KnownTokenId.EVMToken
stacksChain: KnownChainId.StacksChain
},
): Promise<undefined | StacksContractAddress> => {
const supportedRoutes = await getEVMSupportedRoutes(sdkContext, info.evmChain)
return (
supportedRoutes.find(r => r.evmToken === info.evmToken)
?.proxyStacksTokenContractAddress ?? undefined
)
}
export const getStacksTokenFromTerminatingStacksTokenContractAddress = async (
sdkContext: SDKGlobalContext,
info: {
stacksChain: KnownChainId.StacksChain
stacksTokenAddress: StacksContractAddress
},
): Promise<undefined | KnownTokenId.StacksToken> => {
const routes = await getEVMSupportedRoutesByChainType(
sdkContext,
info.stacksChain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet",
)
return (
routes.find(r =>
r.proxyStacksTokenContractAddress == null
? false
: isStacksContractAddressEqual(
r.proxyStacksTokenContractAddress,
info.stacksTokenAddress,
),
)?.stacksToken ?? undefined
)
}
export const getEVMTokenFromTerminatingStacksTokenContractAddress = async (
sdkContext: SDKGlobalContext,
info: {
evmChain: KnownChainId.EVMChain
stacksChain: KnownChainId.StacksChain
stacksTokenAddress: StacksContractAddress
},
): Promise<undefined | KnownTokenId.EVMToken> => {
const supportedRoutes = await getEVMSupportedRoutes(sdkContext, info.evmChain)
return (
supportedRoutes.find(r =>
r.proxyStacksTokenContractAddress == null
? false
: isStacksContractAddressEqual(
r.proxyStacksTokenContractAddress,
info.stacksTokenAddress,
),
)?.evmToken ?? undefined
)
}