@xlink-network/xlink-sdk
Version:
459 lines (426 loc) • 13.4 kB
text/typescript
import * as btc from "@scure/btc-signer"
import { broadcastRevealableTransaction } from "../bitcoinUtils/apiHelpers/broadcastRevealableTransaction"
import { createBitcoinPegInRecipients } from "../bitcoinUtils/apiHelpers/createBitcoinPegInRecipients"
import { createRevealTx } from "../bitcoinUtils/apiHelpers/createRevealTx"
import { bitcoinToSatoshi } from "../bitcoinUtils/bitcoinHelpers"
import { getBTCPegInAddress } from "../bitcoinUtils/btcAddresses"
import { createTransaction } from "../bitcoinUtils/createTransaction"
import { isSupportedBitcoinRoute } from "../bitcoinUtils/peggingHelpers"
import {
BitcoinTransactionPrepareResult,
ReselectSpendableUTXOsFn,
prepareTransaction,
} from "../bitcoinUtils/prepareTransaction"
import {
BridgeSwapRoute_FromBitcoin,
CreateBridgeOrderResult,
createBridgeOrder_BitcoinToEVM,
createBridgeOrder_BitcoinToStacks,
} from "../stacksUtils/createBridgeOrder"
import { validateBridgeOrder } from "../stacksUtils/validateBridgeOrder"
import {
getStacksTokenContractInfo,
numberToStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { range } from "../utils/arrayHelpers"
import { BigNumber } from "../utils/BigNumber"
import {
KnownRoute_FromBitcoin,
KnownRoute_FromBitcoin_ToEVM,
KnownRoute_FromBitcoin_ToStacks,
buildSupportedRoutes,
defineRoute,
} from "../utils/buildSupportedRoutes"
import {
BridgeValidateFailedError,
UnsupportedBridgeRouteError,
} from "../utils/errors"
import { decodeHex } from "../utils/hexHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers"
import {
KnownChainId,
KnownTokenId,
_allKnownEVMMainnetChains,
_allKnownEVMTestnetChains,
} from "../utils/types/knownIds"
import { ChainId, SDKNumber, TokenId } from "./types"
import { SDKGlobalContext } from "./types.internal"
export const supportedRoutes = buildSupportedRoutes(
[
// from mainnet
// to Stacks
...defineRoute(
[[KnownChainId.Bitcoin.Mainnet], [KnownChainId.Stacks.Mainnet]],
[[KnownTokenId.Bitcoin.BTC, KnownTokenId.Stacks.aBTC]],
),
// to EVM
...defineRoute(
[[KnownChainId.Bitcoin.Mainnet], [..._allKnownEVMMainnetChains]],
[
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.WBTC],
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.BTCB],
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.aBTC],
],
),
// from testnet
// to Stacks
...defineRoute(
[[KnownChainId.Bitcoin.Testnet], [KnownChainId.Stacks.Testnet]],
[[KnownTokenId.Bitcoin.BTC, KnownTokenId.Stacks.aBTC]],
),
// to EVM
...defineRoute(
[[KnownChainId.Bitcoin.Testnet], [..._allKnownEVMTestnetChains]],
[
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.WBTC],
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.BTCB],
[KnownTokenId.Bitcoin.BTC, KnownTokenId.EVM.aBTC],
],
),
],
{
isSupported: isSupportedBitcoinRoute,
},
)
export type BridgeFromBitcoinInput_signPsbtFn = (tx: {
psbt: Uint8Array
signInputs: number[]
}) => Promise<{ psbt: Uint8Array }>
export interface BridgeFromBitcoinInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
fromAddress: string
fromAddressScriptPubKey: Uint8Array
toAddress: string
amount: SDKNumber
networkFeeRate: bigint
reselectSpendableUTXOs: ReselectSpendableUTXOsFn
signPsbt: BridgeFromBitcoinInput_signPsbtFn
sendTransaction: (tx: { hex: string }) => Promise<{
txid: string
}>
}
export interface BridgeFromBitcoinOutput {
txid: string
}
export async function bridgeFromBitcoin(
ctx: SDKGlobalContext,
info: BridgeFromBitcoinInput,
): Promise<BridgeFromBitcoinOutput> {
const route = await supportedRoutes.checkRouteValid(ctx, info)
if (KnownChainId.isBitcoinChain(route.fromChain)) {
if (KnownChainId.isStacksChain(route.toChain)) {
if (
KnownTokenId.isBitcoinToken(route.fromToken) &&
KnownTokenId.isStacksToken(route.toToken)
) {
return bridgeFromBitcoin_toStacks(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (KnownChainId.isEVMChain(route.toChain)) {
if (
KnownTokenId.isBitcoinToken(route.fromToken) &&
KnownTokenId.isEVMToken(route.toToken)
) {
return bridgeFromBitcoin_toEVM(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (
KnownChainId.isBRC20Chain(route.toChain) ||
KnownChainId.isRunesChain(route.toChain)
) {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BRC20Chain>())
assertExclude(route.toChain, assertExclude.i<KnownChainId.RunesChain>())
// TODO: bitcoin to brc20/runes is not supported yet
} else {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>())
checkNever(route)
}
} else {
assertExclude(route.fromChain, assertExclude.i<KnownChainId.EVMChain>())
assertExclude(route.fromChain, assertExclude.i<KnownChainId.StacksChain>())
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeFromBitcoin_toStacks(
sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: Omit<
BridgeFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
KnownRoute_FromBitcoin_ToStacks,
): Promise<BridgeFromBitcoinOutput> {
const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain)
const toTokenContractInfo = getStacksTokenContractInfo(
info.toChain,
info.toToken,
)
if (pegInAddress == null || toTokenContractInfo == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const createdOrder = await createBridgeOrder_BitcoinToStacks({
fromChain: info.fromChain,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toChain: info.toChain,
toToken: info.toToken,
toStacksAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount),
swapRoute: [],
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
return broadcastBitcoinTransaction(sdkContext, info, createdOrder)
}
async function bridgeFromBitcoin_toEVM(
sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: Omit<
BridgeFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
KnownRoute_FromBitcoin_ToEVM,
): Promise<BridgeFromBitcoinOutput> {
const createdOrder = await createBridgeOrder_BitcoinToEVM({
fromChain: info.fromChain,
toChain: info.toChain,
toToken: info.toToken,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toEVMAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount),
swapRoute: [],
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
return broadcastBitcoinTransaction(sdkContext, info, createdOrder)
}
async function broadcastBitcoinTransaction(
sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: Omit<
ConstructBitcoinTransactionInput,
"validateBridgeOrder" | "orderData" | "pegInAddress"
> & {
sendTransaction: BridgeFromBitcoinInput["sendTransaction"]
},
createdOrder: CreateBridgeOrderResult,
): Promise<{ txid: string }> {
const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain)
if (pegInAddress == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const tx = await constructBitcoinTransaction(sdkContext, {
...(info as any),
validateBridgeOrder: (btcTx, revealTx, swapRoute) => {
if (revealTx == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
return validateBridgeOrder({
chainId: info.fromChain,
commitTx: btcTx,
revealTx,
terminatingStacksToken: createdOrder.terminatingStacksToken,
swapRoute,
})
},
orderData: createdOrder.data,
pegInAddress,
})
if (tx.revealOutput == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const { txid: apiBroadcastedTxId } = await broadcastRevealableTransaction(
sdkContext,
{
fromChain: info.fromChain,
transactionHex: `0x${tx.hex}`,
orderData: createdOrder.data,
orderOutputIndex: tx.revealOutput.index,
orderOutputSatsAmount: tx.revealOutput.satsAmount,
xlinkPegInAddress: pegInAddress,
},
)
const { txid: delegateBroadcastedTxId } = await info.sendTransaction({
hex: tx.hex,
})
if (apiBroadcastedTxId !== delegateBroadcastedTxId) {
console.warn(
"[xlink-sdk] Transaction id broadcasted by API and delegatee are different:",
`API: ${apiBroadcastedTxId}, `,
`Delegatee: ${delegateBroadcastedTxId}`,
)
}
return { txid: delegateBroadcastedTxId }
}
type ConstructBitcoinTransactionInput = PrepareBitcoinTransactionInput & {
signPsbt: BridgeFromBitcoinInput["signPsbt"]
pegInAddress: {
address: string
scriptPubKey: Uint8Array
}
validateBridgeOrder: (
pegInTx: Uint8Array,
revealTx: undefined | Uint8Array,
swapRoute: BridgeSwapRoute_FromBitcoin,
) => Promise<void>
}
async function constructBitcoinTransaction(
sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: ConstructBitcoinTransactionInput,
): Promise<{
hex: string
revealOutput?: {
index: number
satsAmount: bigint
}
}> {
const txOptions = await prepareBitcoinTransaction(sdkContext, info)
const tx = createTransaction(
txOptions.inputs,
txOptions.recipients.concat({
addressScriptPubKey: info.fromAddressScriptPubKey,
satsAmount: txOptions.changeAmount,
}),
txOptions.revealOutput ? [] : [info.orderData],
)
const { psbt } = await info.signPsbt({
psbt: tx.toPSBT(),
signInputs: range(0, tx.inputsLength),
})
const signedTx = btc.Transaction.fromPSBT(psbt, {
allowUnknownInputs: true,
allowUnknownOutputs: true,
})
if (!signedTx.isFinal) {
signedTx.finalize()
}
let revealTx: undefined | Uint8Array
if (txOptions.revealOutput != null) {
const created = await createRevealTx(sdkContext, {
fromChain: info.fromChain,
txId: signedTx.id,
vout: txOptions.revealOutput.index,
satsAmount: txOptions.revealOutput.satsAmount,
orderData: info.orderData,
xlinkPegInAddress: info.pegInAddress,
})
revealTx = decodeHex(created.txHex)
}
await info
.validateBridgeOrder(signedTx.extract(), revealTx, [])
.catch(err => {
throw new BridgeValidateFailedError(err)
})
return {
hex: signedTx.hex,
revealOutput: txOptions.revealOutput,
}
}
export type PrepareBitcoinTransactionInput = KnownRoute_FromBitcoin & {
fromAddressScriptPubKey: BridgeFromBitcoinInput["fromAddressScriptPubKey"]
fromAddress: BridgeFromBitcoinInput["fromAddress"]
toAddress: BridgeFromBitcoinInput["toAddress"]
amount: BridgeFromBitcoinInput["amount"]
networkFeeRate: BridgeFromBitcoinInput["networkFeeRate"]
reselectSpendableUTXOs: BridgeFromBitcoinInput["reselectSpendableUTXOs"]
orderData: Uint8Array
pegInAddress: {
address: string
scriptPubKey: Uint8Array
}
}
export async function prepareBitcoinTransaction(
sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: PrepareBitcoinTransactionInput,
): Promise<
BitcoinTransactionPrepareResult & {
bitcoinNetwork: typeof btc.NETWORK
revealOutput?: {
index: number
satsAmount: bigint
}
}
> {
const bitcoinNetwork =
info.fromChain === KnownChainId.Bitcoin.Mainnet
? btc.NETWORK
: btc.TEST_NETWORK
const recipient = await createBitcoinPegInRecipients(sdkContext, {
fromChain: info.fromChain,
toChain: info.toChain,
fromToken: info.fromToken,
toToken: info.toToken,
fromAddress: {
address: info.fromAddress,
scriptPubKey: info.fromAddressScriptPubKey,
},
toAddress: info.toAddress,
fromAmount: BigNumber.from(info.amount),
orderData: info.orderData,
feeRate: info.networkFeeRate,
})
const result = await prepareTransaction({
recipients: [
{
addressScriptPubKey: recipient.scriptPubKey,
satsAmount: recipient.satsAmount,
},
{
addressScriptPubKey: info.pegInAddress.scriptPubKey,
satsAmount: bitcoinToSatoshi(info.amount),
},
],
changeAddressScriptPubKey: info.fromAddressScriptPubKey,
feeRate: info.networkFeeRate,
reselectSpendableUTXOs: info.reselectSpendableUTXOs,
})
return {
...result,
bitcoinNetwork,
revealOutput: {
index: 0,
satsAmount: recipient.satsAmount,
},
}
}