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

958 lines
26 KiB
TypeScript

import { getOutputDustThreshold } from "@c4/btc-utils"
import * as btc from "@scure/btc-signer"
import { equalBytes } from "@scure/btc-signer/utils"
import { broadcastRevealableTransaction } from "../bitcoinUtils/apiHelpers/broadcastRevealableTransaction"
import { createBitcoinPegInRecipients } from "../bitcoinUtils/apiHelpers/createBitcoinPegInRecipients"
import { createRevealTx } from "../bitcoinUtils/apiHelpers/createRevealTx"
import {
UTXOSpendable,
addressToScriptPubKey,
bitcoinToSatoshi,
} from "../bitcoinUtils/bitcoinHelpers"
import {
BitcoinAddress,
getBitcoinHardLinkageAddress,
} from "../bitcoinUtils/btcAddresses"
import { BITCOIN_OUTPUT_MINIMUM_AMOUNT } from "../bitcoinUtils/constants"
import { createTransaction } from "../bitcoinUtils/createTransaction"
import {
BitcoinTransactionPrepareResult,
prepareTransaction,
} from "../bitcoinUtils/prepareTransaction"
import { getMetaPegInAddress } from "../metaUtils/btcAddresses"
import { isSupportedRunesRoute } from "../metaUtils/peggingHelpers"
import { runesTokenToId } from "../metaUtils/tokenAddresses"
import { CreateBridgeOrderResult } from "../stacksUtils/createBridgeOrderFromBitcoin"
import {
createBridgeOrder_MetaToBitcoin,
createBridgeOrder_MetaToEVM,
createBridgeOrder_MetaToMeta,
createBridgeOrder_MetaToStacks,
} from "../stacksUtils/createBridgeOrderFromMeta"
import { validateBridgeOrderFromMeta } from "../stacksUtils/validateBridgeOrderFromMeta"
import { getStacksTokenContractInfo } from "../stacksUtils/contractHelpers"
import { range } from "../utils/arrayHelpers"
import { BigNumber } from "../utils/BigNumber"
import {
KnownRoute_FromRunes,
KnownRoute_FromRunes_ToBRC20,
KnownRoute_FromRunes_ToBitcoin,
KnownRoute_FromRunes_ToEVM,
KnownRoute_FromRunes_ToRunes,
KnownRoute_FromRunes_ToStacks,
checkRouteValid,
} from "../utils/buildSupportedRoutes"
import {
BridgeValidateFailedError,
InvalidMethodParametersError,
UnsupportedBridgeRouteError,
} from "../utils/errors"
import { decodeHex } from "../utils/hexHelpers"
import { toBitcoinOpReturnScript } from "../utils/RunesProtocol/RunesBitcoinScript"
import {
SwapRouteViaALEX,
SwapRouteViaALEX_WithMinimumAmountsToReceive_Public,
SwapRouteViaEVMDexAggregator,
SwapRouteViaEVMDexAggregator_WithMinimumAmountsToReceive_Public,
SwapRoute_WithMinimumAmountsToReceive_Public,
} from "../utils/SwapRouteHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers"
import {
KnownChainId,
KnownTokenId,
_knownChainIdToErrorMessagePart,
getChainIdNetworkType,
} from "../utils/types/knownIds"
import {
ReselectSpendableUTXOsFn_Public,
reselectSpendableUTXOsFactory,
} from "./bridgeFromBitcoin"
import { getBridgeFeeOutput } from "./bridgeFromBRC20"
import {
ChainId,
RuneIdCombined,
SDKNumber,
TokenId,
isEVMAddress,
} from "./types"
import { SDKGlobalContext } from "./types.internal"
export type BridgeFromRunesInput_signPsbtFn = (tx: {
psbt: Uint8Array
signBitcoinInputs: number[]
signRunesInputs: number[]
}) => Promise<{ psbt: Uint8Array }>
export type BridgeFromRunesInput_reselectSpendableNetworkFeeUTXOs =
ReselectSpendableUTXOsFn_Public
export type RunesUTXOSpendable = UTXOSpendable & {
runes: {
runeId: RuneIdCombined
runeDivisibility: number
runeAmount: bigint
}[]
}
export interface BridgeFromRunesInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
fromAddress: string
fromAddressScriptPubKey: Uint8Array
toAddress: string
/**
* **Required** when `toChain` is one of bitcoin chains
*/
toAddressScriptPubKey?: Uint8Array
amount: SDKNumber
inputRuneUTXOs: RunesUTXOSpendable[]
swapRoute?: SwapRoute_WithMinimumAmountsToReceive_Public
networkFeeRate: bigint
networkFeeChangeAddress: string
networkFeeChangeAddressScriptPubKey: Uint8Array
reselectSpendableNetworkFeeUTXOs: BridgeFromRunesInput_reselectSpendableNetworkFeeUTXOs
signPsbt: BridgeFromRunesInput_signPsbtFn
sendTransaction: (tx: {
hex: string
pegInOrderOutput: {
index: number
amount: bigint
orderData: Uint8Array
}
}) => Promise<{
txid: string
}>
}
export interface BridgeFromRunesOutput {
txid: string
}
export async function bridgeFromRunes(
ctx: SDKGlobalContext,
info: BridgeFromRunesInput,
): Promise<BridgeFromRunesOutput> {
const route = await checkRouteValid(ctx, isSupportedRunesRoute, info)
if (
!equalBytes(
info.fromAddressScriptPubKey,
addressToScriptPubKey(
info.fromChain === KnownChainId.Bitcoin.Mainnet
? btc.NETWORK
: btc.TEST_NETWORK,
info.fromAddress,
),
)
) {
throw new InvalidMethodParametersError(
["XLinkSDK", "bridgeFromRunes"],
[
{
name: "fromAddressScriptPubKey",
expected: "the scriptPubKey of the fromAddress",
received: "invalid scriptPubKey",
},
],
)
}
if (info.toAddressScriptPubKey != null) {
if (
!equalBytes(
info.toAddressScriptPubKey,
addressToScriptPubKey(
info.fromChain === KnownChainId.Bitcoin.Mainnet
? btc.NETWORK
: btc.TEST_NETWORK,
info.toAddress,
),
)
) {
throw new InvalidMethodParametersError(
["XLinkSDK", "bridgeFromRunes"],
[
{
name: "toAddressScriptPubKey",
expected: "the scriptPubKey of the toAddress",
received: "invalid scriptPubKey",
},
],
)
}
}
if (KnownChainId.isRunesChain(route.fromChain)) {
if (KnownChainId.isStacksChain(route.toChain)) {
if (
KnownTokenId.isRunesToken(route.fromToken) &&
KnownTokenId.isStacksToken(route.toToken)
) {
return bridgeFromRunes_toStacks(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (KnownChainId.isEVMChain(route.toChain)) {
if (
KnownTokenId.isRunesToken(route.fromToken) &&
KnownTokenId.isEVMToken(route.toToken)
) {
return bridgeFromRunes_toEVM(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (KnownChainId.isBitcoinChain(route.toChain)) {
if (
KnownTokenId.isRunesToken(route.fromToken) &&
KnownTokenId.isBitcoinToken(route.toToken)
) {
return bridgeFromRunes_toBitcoin(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (KnownChainId.isBRC20Chain(route.toChain)) {
if (
KnownTokenId.isRunesToken(route.fromToken) &&
KnownTokenId.isBRC20Token(route.toToken)
) {
return bridgeFromRunes_toMeta(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} else if (KnownChainId.isRunesChain(route.toChain)) {
if (
KnownTokenId.isRunesToken(route.fromToken) &&
KnownTokenId.isRunesToken(route.toToken)
) {
return bridgeFromRunes_toMeta(ctx, {
...info,
fromChain: route.fromChain,
toChain: route.toChain,
fromToken: route.fromToken,
toToken: route.toToken,
})
}
} 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>())
assertExclude(route.fromChain, assertExclude.i<KnownChainId.BitcoinChain>())
assertExclude(route.fromChain, assertExclude.i<KnownChainId.BRC20Chain>())
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeFromRunes_toStacks(
sdkContext: SDKGlobalContext,
info: Omit<
BridgeFromRunesInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
KnownRoute_FromRunes_ToStacks,
): Promise<BridgeFromRunesOutput> {
const swapRoute = info.swapRoute
const pegInAddress = getMetaPegInAddress(info.fromChain, info.toChain)
const toTokenContractInfo = await getStacksTokenContractInfo(
sdkContext,
info.toChain,
info.toToken,
)
if (pegInAddress == null || toTokenContractInfo == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const createdOrder = await createBridgeOrder_MetaToStacks(sdkContext, {
fromChain: info.fromChain,
fromToken: info.fromToken,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toChain: info.toChain,
toToken: info.toToken,
toStacksAddress: info.toAddress,
swap:
swapRoute == null
? undefined
: {
...swapRoute,
minimumAmountsToReceive: BigNumber.from(
swapRoute.minimumAmountsToReceive,
),
},
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const bridgeFeeOutput = await getBridgeFeeOutput(sdkContext, info)
return broadcastRunesTransaction(
sdkContext,
{
...info,
withHardLinkageOutput: false,
bridgeFeeOutput,
swapRoute: info.swapRoute,
},
createdOrder,
)
}
async function bridgeFromRunes_toEVM(
sdkContext: SDKGlobalContext,
info: Omit<
BridgeFromRunesInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
KnownRoute_FromRunes_ToEVM,
): Promise<BridgeFromRunesOutput> {
const swapRoute = info.swapRoute
const createdOrder = !isEVMAddress(info.toAddress)
? null
: await createBridgeOrder_MetaToEVM(sdkContext, {
...info,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toEVMAddress: info.toAddress,
swap:
swapRoute == null
? undefined
: {
...swapRoute,
minimumAmountsToReceive: BigNumber.from(
swapRoute.minimumAmountsToReceive,
),
},
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const bridgeFeeOutput = await getBridgeFeeOutput(sdkContext, info)
return broadcastRunesTransaction(
sdkContext,
{
...info,
withHardLinkageOutput: false,
bridgeFeeOutput,
swapRoute: info.swapRoute,
},
createdOrder,
)
}
async function bridgeFromRunes_toBitcoin(
sdkContext: SDKGlobalContext,
info: Omit<
BridgeFromRunesInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
KnownRoute_FromRunes_ToBitcoin,
): Promise<BridgeFromRunesOutput> {
if (info.toAddressScriptPubKey == null) {
throw new InvalidMethodParametersError(
[
"XLinkSDK",
`bridgeFromRunes (to ${_knownChainIdToErrorMessagePart(info.toChain)})`,
],
[
{
name: "toAddressScriptPubKey",
expected: "Uint8Array",
received: "undefined",
},
],
)
}
const swapRoute = info.swapRoute
const createdOrder = await createBridgeOrder_MetaToBitcoin(sdkContext, {
...info,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toBitcoinScriptPubKey: info.toAddressScriptPubKey,
swap:
swapRoute == null
? undefined
: {
...swapRoute,
minimumAmountsToReceive: BigNumber.from(
swapRoute.minimumAmountsToReceive,
),
},
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const bridgeFeeOutput = await getBridgeFeeOutput(sdkContext, info)
return broadcastRunesTransaction(
sdkContext,
{
...info,
withHardLinkageOutput: true,
bridgeFeeOutput,
swapRoute: info.swapRoute,
},
createdOrder,
)
}
async function bridgeFromRunes_toMeta(
sdkContext: SDKGlobalContext,
info: Omit<
BridgeFromRunesInput,
"fromChain" | "toChain" | "fromToken" | "toToken"
> &
(KnownRoute_FromRunes_ToBRC20 | KnownRoute_FromRunes_ToRunes),
): Promise<BridgeFromRunesOutput> {
if (info.toAddressScriptPubKey == null) {
throw new InvalidMethodParametersError(
[
"XLinkSDK",
`bridgeFromRunes (to ${_knownChainIdToErrorMessagePart(info.toChain)})`,
],
[
{
name: "toAddressScriptPubKey",
expected: "Uint8Array",
received: "undefined",
},
],
)
}
const swapRoute = info.swapRoute
const createdOrder = await createBridgeOrder_MetaToMeta(sdkContext, {
...info,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toBitcoinScriptPubKey: info.toAddressScriptPubKey,
swap:
swapRoute == null
? undefined
: {
...swapRoute,
minimumAmountsToReceive: BigNumber.from(
swapRoute.minimumAmountsToReceive,
),
},
})
if (createdOrder == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const bridgeFeeOutput = await getBridgeFeeOutput(sdkContext, info)
return broadcastRunesTransaction(
sdkContext,
{
...info,
withHardLinkageOutput: true,
bridgeFeeOutput,
swapRoute: info.swapRoute,
},
createdOrder,
)
}
async function broadcastRunesTransaction(
sdkContext: SDKGlobalContext,
info: Omit<
ConstructRunesTransactionInput,
"validateBridgeOrder" | "orderData" | "pegInAddress" | "hardLinkageOutput"
> & {
withHardLinkageOutput: boolean
sendTransaction: BridgeFromRunesInput["sendTransaction"]
},
createdOrder: CreateBridgeOrderResult,
): Promise<{ txid: string }> {
const pegInAddress = getMetaPegInAddress(info.fromChain, info.toChain)
if (pegInAddress == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const route: KnownRoute_FromRunes = {
fromChain: info.fromChain,
fromToken: info.fromToken,
toChain: info.toChain as any,
toToken: info.toToken as any,
}
const tx = await constructRunesTransaction(sdkContext, {
...info,
...route,
validateBridgeOrder: async (btcTx, revealTx, extra) => {
if (revealTx == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
/**
* due to contract limit, we are unable to validate this tx before the
* commit tx be confirmed, so we will skip it and fix it in the future
*/
void validateBridgeOrderFromMeta({
chainId: info.fromChain,
commitTx: btcTx,
revealTx,
terminatingStacksToken: createdOrder.tokenOutTrait,
transferOutputIndex: extra.transferOutputIndex,
bridgeFeeOutputIndex: extra.bridgeFeeOutputIndex,
swapRoute: extra.swapRoute,
})
},
orderData: createdOrder.data,
pegInAddress,
hardLinkageOutput: info.withHardLinkageOutput
? ((await getBitcoinHardLinkageAddress(info.fromChain, info.toChain)) ??
null)
: null,
})
const { txid: apiBroadcastedTxId } = await broadcastRevealableTransaction(
sdkContext,
{
fromChain: info.fromChain,
transactionHex: `0x${tx.hex}`,
orderData: createdOrder.data,
orderOutputIndex: tx.revealOutput.index,
orderOutputSatsAmount: tx.revealOutput.satsAmount,
pegInAddress: pegInAddress,
},
)
const { txid: delegateBroadcastedTxId } = await info.sendTransaction({
hex: tx.hex,
pegInOrderOutput: {
index: tx.revealOutput.index,
amount: tx.revealOutput.satsAmount,
orderData: createdOrder.data,
},
})
if (apiBroadcastedTxId !== delegateBroadcastedTxId) {
console.warn(
"[bro-sdk] Transaction id broadcasted by API and delegatee are different:",
`API: ${apiBroadcastedTxId}, `,
`Delegatee: ${delegateBroadcastedTxId}`,
)
}
return { txid: delegateBroadcastedTxId }
}
type ConstructRunesTransactionInput = PrepareRunesTransactionInput & {
swapRoute:
| undefined
| SwapRouteViaALEX_WithMinimumAmountsToReceive_Public
| SwapRouteViaEVMDexAggregator_WithMinimumAmountsToReceive_Public
signPsbt: BridgeFromRunesInput["signPsbt"]
pegInAddress: BitcoinAddress
validateBridgeOrder: (
pegInTx: Uint8Array,
revealTx: undefined | Uint8Array,
info: {
transferOutputIndex: number
bridgeFeeOutputIndex: undefined | number
swapRoute: undefined | SwapRouteViaALEX | SwapRouteViaEVMDexAggregator
},
) => Promise<void>
}
async function constructRunesTransaction(
sdkContext: SDKGlobalContext,
info: ConstructRunesTransactionInput,
): Promise<{
hex: string
revealOutput: {
index: number
satsAmount: bigint
}
}> {
const txOptions = await prepareRunesTransaction(
sdkContext,
"bridgeFromRunes",
info,
)
const tx = createTransaction(
txOptions.inputs,
txOptions.recipients.concat({
addressScriptPubKey: info.networkFeeChangeAddressScriptPubKey,
satsAmount: txOptions.changeAmount,
}),
txOptions.opReturnScripts ?? [],
)
const { psbt } = await info.signPsbt({
psbt: tx.toPSBT(),
signRunesInputs: range(0, info.inputRuneUTXOs.length),
signBitcoinInputs: range(info.inputRuneUTXOs.length, tx.inputsLength),
})
const signedTx = btc.Transaction.fromPSBT(psbt, {
allowUnknownInputs: true,
allowUnknownOutputs: true,
})
if (!signedTx.isFinal) {
signedTx.finalize()
}
const revealTx = await createRevealTx(sdkContext, {
fromChain: info.fromChain,
txId: signedTx.id,
vout: txOptions.revealOutput.index,
satsAmount: txOptions.revealOutput.satsAmount,
orderData: info.orderData,
pegInAddress: info.pegInAddress,
})
await info
.validateBridgeOrder(signedTx.extract(), decodeHex(revealTx.txHex), {
transferOutputIndex: txOptions.transferOutput.index,
bridgeFeeOutputIndex: txOptions.bridgeFeeOutput?.index,
swapRoute: info.swapRoute,
})
.catch(err => {
if (sdkContext.brc20.ignoreValidateResult) {
console.error(
"Bridge tx validation failed, but ignoreValidateResult is true, so we ignore the error",
err,
)
} else {
throw new BridgeValidateFailedError(err)
}
})
return {
hex: signedTx.hex,
revealOutput: txOptions.revealOutput,
}
}
export type PrepareRunesTransactionInput = KnownRoute_FromRunes & {
fromAddressScriptPubKey: BridgeFromRunesInput["fromAddressScriptPubKey"]
fromAddress: BridgeFromRunesInput["fromAddress"]
toAddress: BridgeFromRunesInput["toAddress"]
amount: BridgeFromRunesInput["amount"]
inputRuneUTXOs: BridgeFromRunesInput["inputRuneUTXOs"]
networkFeeRate: BridgeFromRunesInput["networkFeeRate"]
networkFeeChangeAddress: string
networkFeeChangeAddressScriptPubKey: Uint8Array
reselectSpendableNetworkFeeUTXOs: BridgeFromRunesInput["reselectSpendableNetworkFeeUTXOs"]
pegInAddress: BitcoinAddress
orderData: Uint8Array
bridgeFeeOutput: null | {
address: string
scriptPubKey: Uint8Array
satsAmount: BigNumber
}
hardLinkageOutput: null | BitcoinAddress
}
/**
* Bitcoin Tx Structure:
*
* * Inputs: ...
* * Outputs:
* * Runes change
* * Peg-in order data
* * Bridge fee (optional)
* * Hard linkage (optional)
* * Peg-in Rune tokens
* * BTC change (optional)
* * Runestone
*
* (with bridge fee example tx) https://mempool.space/testnet/tx/db5518a5e785c55a8b53ca6c8e7a2c21cb11913addd972fe9de4322dfcbaf723
* (with hard linkage example tx) https://mempool.space/tx/f1ac518ab087924d17dffcc9cefb4d0d59ba15c04b75be567e1edf59bc0d7bf1#vout=2
*/
export async function prepareRunesTransaction(
sdkContext: SDKGlobalContext,
methodName: string,
info: PrepareRunesTransactionInput,
): Promise<
BitcoinTransactionPrepareResult & {
bitcoinNetwork: typeof btc.NETWORK
transferOutput: {
index: number
}
revealOutput: {
index: number
satsAmount: bigint
}
bridgeFeeOutput?: {
index: number
satsAmount: bigint
}
hardLinkageOutput?: {
index: number
satsAmount: bigint
}
}
> {
const bitcoinNetwork =
getChainIdNetworkType(info.fromChain) === "mainnet"
? btc.NETWORK
: btc.TEST_NETWORK
const runeId = await runesTokenToId(
sdkContext,
info.fromChain,
info.fromToken,
)
if (runeId == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const runeIdCombined: RuneIdCombined = `${Number(runeId.id.blockHeight)}:${Number(runeId.id.txIndex)}`
const runeDivisibilityAry = info.inputRuneUTXOs.flatMap(u =>
u.runes.flatMap(r =>
r.runeId === runeIdCombined ? [r.runeDivisibility] : [],
),
)
const runeDivisibility = runeDivisibilityAry[0]
if (runeDivisibility == null) {
throw new InvalidMethodParametersError(
[
"XLinkSDK",
`${methodName} (to ${_knownChainIdToErrorMessagePart(info.toChain)})`,
],
[
{
name: "inputRuneUTXOs",
expected: `contains rune with id ${runeIdCombined}`,
received: "undefined",
},
],
)
}
const runeRawAmountToPegIn = BigNumber.toBigInt(
{ roundingMode: BigNumber.roundUp },
BigNumber.rightMoveDecimals(runeDivisibility, info.amount),
)
const runeAmountsInTotal = sumRuneUTXOs(info.inputRuneUTXOs)
const runeRawAmountToSend = runeAmountsInTotal[runeIdCombined] ?? 0n
if (runeRawAmountToSend < runeRawAmountToPegIn) {
throw new InvalidMethodParametersError(
[
"XLinkSDK",
`${methodName} (to ${_knownChainIdToErrorMessagePart(info.toChain)})`,
],
[
{
name: "inputRuneUTXOs",
expected: `contains enough rune with id ${runeIdCombined}`,
received: String(runeAmountsInTotal[runeIdCombined] ?? 0n),
},
],
)
}
const pegInOrderRecipient = await createBitcoinPegInRecipients(sdkContext, {
fromChain: info.fromChain,
fromToken: info.fromToken,
toChain: info.toChain,
toToken: info.toToken,
fromAddress: {
address: info.fromAddress,
scriptPubKey: info.fromAddressScriptPubKey,
},
toAddress: info.toAddress,
orderData: info.orderData,
feeRate: info.networkFeeRate,
})
const runesChangeCausedOffset = 1
const pegInOrderDataCausedOffset = runesChangeCausedOffset + 1
const bridgeFeeCausedOffset =
pegInOrderDataCausedOffset + (info.bridgeFeeOutput == null ? 0 : 1)
const hardLinkageCausedOffset =
bridgeFeeCausedOffset + (info.hardLinkageOutput == null ? 0 : 1)
const pegInRuneTokensCausedOffset = hardLinkageCausedOffset + 1
const runesOpReturnScript = toBitcoinOpReturnScript({
edicts: [
{
id: runeId.id,
amount: runeRawAmountToPegIn,
output: BigInt(pegInRuneTokensCausedOffset - 1),
},
],
// collect all remaining runes to the change address output
pointer: 0n,
})
const result = await prepareTransaction({
pinnedUTXOs: info.inputRuneUTXOs,
recipients: [
// runes change
{
addressScriptPubKey: info.fromAddressScriptPubKey,
satsAmount: BigNumber.toBigInt(
{ roundingMode: BigNumber.roundUp },
getOutputDustThreshold({
scriptPubKey: info.fromAddressScriptPubKey,
}),
),
},
// peg in order data
{
addressScriptPubKey: pegInOrderRecipient.scriptPubKey,
satsAmount: pegInOrderRecipient.satsAmount,
},
// bridge fee
...(info.bridgeFeeOutput == null
? []
: [
{
addressScriptPubKey: info.pegInAddress.scriptPubKey,
satsAmount: BigNumber.toBigInt(
{ roundingMode: BigNumber.roundUp },
info.bridgeFeeOutput.satsAmount,
),
},
]),
// hard linkage
...(info.hardLinkageOutput == null
? []
: [
{
addressScriptPubKey: info.hardLinkageOutput.scriptPubKey,
satsAmount: BITCOIN_OUTPUT_MINIMUM_AMOUNT,
},
]),
// peg in rune tokens
{
addressScriptPubKey: info.pegInAddress.scriptPubKey,
satsAmount: BigNumber.toBigInt(
{ roundingMode: BigNumber.roundUp },
getOutputDustThreshold({
scriptPubKey: info.pegInAddress.scriptPubKey,
}),
),
},
],
changeAddressScriptPubKey: info.networkFeeChangeAddressScriptPubKey,
feeRate: info.networkFeeRate,
opReturnScripts: [runesOpReturnScript],
reselectSpendableUTXOs: reselectSpendableUTXOsFactory(
info.reselectSpendableNetworkFeeUTXOs,
),
})
return {
...result,
bitcoinNetwork,
transferOutput: {
index: pegInRuneTokensCausedOffset - 1,
},
revealOutput: {
index: pegInOrderDataCausedOffset - 1,
satsAmount: pegInOrderRecipient.satsAmount,
},
bridgeFeeOutput:
info.bridgeFeeOutput == null
? undefined
: {
index: bridgeFeeCausedOffset - 1,
satsAmount: bitcoinToSatoshi(
BigNumber.toString(info.bridgeFeeOutput.satsAmount),
),
},
hardLinkageOutput:
info.hardLinkageOutput == null
? undefined
: {
index: hardLinkageCausedOffset - 1,
satsAmount: BITCOIN_OUTPUT_MINIMUM_AMOUNT,
},
}
}
const sumRuneUTXOs = (
runeUTXOs: RunesUTXOSpendable[],
): Partial<Record<RuneIdCombined, bigint>> => {
return runeUTXOs.reduce(
(acc, runeUTXO) => {
runeUTXO.runes.forEach(rune => {
acc[rune.runeId] = (acc[rune.runeId] ?? 0n) + rune.runeAmount
})
return acc
},
{} as Record<RuneIdCombined, bigint>,
)
}