mirror of
https://github.com/Brotocol-xyz/bro-sdk.git
synced 2026-01-12 22:25:00 +08:00
feat: add /runesHelpers and update selectUTXOs to support bridgeFromBitcoin's updates
This commit is contained in:
@@ -30,6 +30,11 @@
|
||||
"import": "./lib/bitcoinHelpers.mjs",
|
||||
"require": "./lib/bitcoinHelpers.js"
|
||||
},
|
||||
"./runeHelpers": {
|
||||
"types": "./lib/runeHelpers.d.ts",
|
||||
"import": "./lib/runeHelpers.mjs",
|
||||
"require": "./lib/runeHelpers.js"
|
||||
},
|
||||
"./swapHelpers": {
|
||||
"types": "./lib/swapHelpers.d.ts",
|
||||
"import": "./lib/swapHelpers.mjs",
|
||||
|
||||
130
src/bitcoinUtils/selectRuneUTXOs.ts
Normal file
130
src/bitcoinUtils/selectRuneUTXOs.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { BigNumber } from "../utils/BigNumber"
|
||||
import {
|
||||
RuneIdCombined,
|
||||
SDKNumber,
|
||||
toSDKNumberOrUndefined,
|
||||
} from "../xlinkSdkUtils/types"
|
||||
import { UTXOSpendable } from "./bitcoinHelpers"
|
||||
|
||||
export class NotEnoughRunesError extends Error {
|
||||
remainingAmount: SDKNumber
|
||||
|
||||
constructor(runeId: RuneIdCombined, remainingAmount: BigNumber) {
|
||||
super(
|
||||
`Not enough UTXOs for rune ${runeId}. Missing ${BigNumber.toString(remainingAmount)}`,
|
||||
)
|
||||
this.remainingAmount = toSDKNumberOrUndefined(remainingAmount)
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuneInfo {
|
||||
id: RuneIdCombined
|
||||
divisibility: number
|
||||
}
|
||||
|
||||
export type SpendableRuneUTXO = UTXOSpendable & {
|
||||
runes: Record<
|
||||
RuneIdCombined,
|
||||
{
|
||||
runeInfo: RuneInfo
|
||||
runeAmount: BigNumber
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export interface RuneRecipient {
|
||||
runeId: RuneIdCombined
|
||||
amount: BigNumber
|
||||
}
|
||||
|
||||
export type GetAvailableRuneUTXOFn = (
|
||||
runeId: RuneIdCombined,
|
||||
usedUTXOs: SpendableRuneUTXO[],
|
||||
) => Promise<null | SpendableRuneUTXO>
|
||||
|
||||
export const selectRuneUTXOs = async (
|
||||
runeRecipients: RuneRecipient[],
|
||||
getAvailableRuneUTXO: GetAvailableRuneUTXOFn,
|
||||
): Promise<{
|
||||
inputRuneUTXOs: SpendableRuneUTXO[]
|
||||
changeAmounts: { info: RuneInfo; amount: BigNumber }[]
|
||||
}> => {
|
||||
const usedUTXOs = new Map<`${string}:${number}`, SpendableRuneUTXO>()
|
||||
const changeInfos: Partial<
|
||||
Record<RuneIdCombined, { info: RuneInfo; amount: BigNumber }>
|
||||
> = {}
|
||||
|
||||
for (const recipient of runeRecipients) {
|
||||
const changeInfo = changeInfos[recipient.runeId]
|
||||
if (
|
||||
changeInfo != null &&
|
||||
BigNumber.isGte(changeInfo.amount, recipient.amount)
|
||||
) {
|
||||
changeInfo.amount = BigNumber.minus(changeInfo.amount, recipient.amount)
|
||||
continue
|
||||
}
|
||||
|
||||
let utxo = await getAvailableRuneUTXO(
|
||||
recipient.runeId,
|
||||
Array.from(usedUTXOs.values()),
|
||||
)
|
||||
while (utxo != null) {
|
||||
const utxoPos = `${utxo.txId}:${utxo.index}` as const
|
||||
if (usedUTXOs.has(utxoPos)) {
|
||||
throw new TypeError(
|
||||
`[prepareSendRunesTransaction/selectRuneUTXOs.getAvailableRuneUTXO] UTXO ${utxo.txId}:${utxo.index} already used`,
|
||||
)
|
||||
}
|
||||
usedUTXOs.set(utxoPos, utxo)
|
||||
|
||||
for (const rune of Object.values(utxo.runes)) {
|
||||
changeInfos[rune.runeInfo.id] = {
|
||||
...changeInfos[rune.runeInfo.id],
|
||||
info: rune.runeInfo,
|
||||
amount: BigNumber.sum([
|
||||
changeInfos[rune.runeInfo.id]?.amount ?? 0,
|
||||
rune.runeAmount,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
const changeInfo = changeInfos[recipient.runeId]
|
||||
if (changeInfo == null) {
|
||||
throw new TypeError(
|
||||
"[prepareSendRunesTransaction/selectRuneUTXOs] changeInfo is null, which is not expected",
|
||||
)
|
||||
}
|
||||
|
||||
const changeAmount = BigNumber.minus(changeInfo.amount, recipient.amount)
|
||||
const changeAmountCoveredTransferAmount = BigNumber.isGte(changeAmount, 0)
|
||||
if (changeAmountCoveredTransferAmount) {
|
||||
changeInfo.amount = changeAmount
|
||||
break
|
||||
}
|
||||
|
||||
utxo = await getAvailableRuneUTXO(
|
||||
recipient.runeId,
|
||||
Array.from(usedUTXOs.values()),
|
||||
)
|
||||
|
||||
if (utxo == null) {
|
||||
throw new NotEnoughRunesError(
|
||||
recipient.runeId,
|
||||
BigNumber.minus(recipient.amount, changeInfo.amount),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changeAmounts = Object.entries(changeInfos).flatMap(
|
||||
([_runeId, info]) =>
|
||||
info == null || BigNumber.isZero(info.amount)
|
||||
? []
|
||||
: [{ info: info.info, amount: info.amount }],
|
||||
)
|
||||
|
||||
return {
|
||||
inputRuneUTXOs: Array.from(usedUTXOs.values()),
|
||||
changeAmounts,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { sortBy } from "../utils/arrayHelpers"
|
||||
import { MAX_BIGINT, sum } from "../utils/bigintHelpers"
|
||||
import { decodeHex } from "../utils/hexHelpers"
|
||||
import { isNotNull } from "../utils/typeHelpers"
|
||||
import { ReselectSpendableUTXOsFn_Public } from "../xlinkSdkUtils/bridgeFromBitcoin"
|
||||
import {
|
||||
isSameUTXO,
|
||||
sumUTXO,
|
||||
@@ -18,15 +19,12 @@ export type GetConfirmedSpendableUTXOFn = (
|
||||
export const reselectSpendableUTXOsFactory = (
|
||||
availableUTXOs: UTXOBasic[],
|
||||
getUTXOSpendable: GetConfirmedSpendableUTXOFn,
|
||||
): ReselectSpendableUTXOsFn => {
|
||||
return async (satsToSend, pinnedUTXOs, _lastTimeSelectedUTXOs) => {
|
||||
): ReselectSpendableUTXOsFn_Public => {
|
||||
return async (satsToSend, _lastTimeSelectedUTXOs) => {
|
||||
const lastTimeSelectedUTXOs = await Promise.all(
|
||||
_lastTimeSelectedUTXOs.map(async fetchingUtxo => {
|
||||
const fromPinnedUTXO = pinnedUTXOs.find(pinnedUTXO =>
|
||||
isSameUTXO(pinnedUTXO, fetchingUtxo),
|
||||
)
|
||||
return fromPinnedUTXO ?? getUTXOSpendable(fetchingUtxo)
|
||||
}),
|
||||
_lastTimeSelectedUTXOs.map(fetchingUtxo =>
|
||||
getUTXOSpendable(fetchingUtxo),
|
||||
),
|
||||
).then(utxos => utxos.filter(isNotNull))
|
||||
|
||||
const otherAvailableUTXOs = await Promise.all(
|
||||
@@ -40,11 +38,7 @@ export const reselectSpendableUTXOsFactory = (
|
||||
.map(getUTXOSpendable),
|
||||
).then(utxos => utxos.filter(isNotNull))
|
||||
|
||||
const allUTXOSpendable = [
|
||||
...pinnedUTXOs,
|
||||
...lastTimeSelectedUTXOs,
|
||||
...otherAvailableUTXOs,
|
||||
]
|
||||
const allUTXOSpendable = [...lastTimeSelectedUTXOs, ...otherAvailableUTXOs]
|
||||
const finalSelectedBasicUTXOs = selectUTXOs(
|
||||
satsToSend,
|
||||
lastTimeSelectedUTXOs,
|
||||
@@ -65,8 +59,8 @@ export const reselectSpendableUTXOsWithSafePadFactory = (
|
||||
getUTXOSpendable,
|
||||
)
|
||||
|
||||
return async (satsToSend, pinnedUTXOs, lastTimeSelectedUTXOs) => {
|
||||
const utxos = await reselect(satsToSend, pinnedUTXOs, lastTimeSelectedUTXOs)
|
||||
return async (satsToSend, lastTimeSelectedUTXOs) => {
|
||||
const utxos = await reselect(satsToSend, lastTimeSelectedUTXOs)
|
||||
const selectedAmount = sumUTXO(utxos)
|
||||
|
||||
const difference = satsToSend - selectedAmount
|
||||
|
||||
88
src/runesHelpers.ts
Normal file
88
src/runesHelpers.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { RuneIdCombined, toSDKNumberOrUndefined } from "./xlinkSdkUtils/types"
|
||||
import {
|
||||
GetAvailableRuneUTXOFn as _GetAvailableRuneUTXOFn,
|
||||
RuneInfo,
|
||||
selectRuneUTXOs as _selectRuneUTXOs,
|
||||
} from "./bitcoinUtils/selectRuneUTXOs"
|
||||
import { SDKNumber } from "./xlinkSdkUtils/types"
|
||||
import { UTXOSpendable } from "./bitcoinHelpers"
|
||||
import { BigNumber } from "./utils/BigNumber"
|
||||
|
||||
export type SpendableRuneUTXO = UTXOSpendable & {
|
||||
runes: Record<
|
||||
RuneIdCombined,
|
||||
{
|
||||
runeInfo: RuneInfo
|
||||
runeAmount: SDKNumber
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export interface RuneOutput {
|
||||
runeId: RuneIdCombined
|
||||
amount: SDKNumber
|
||||
}
|
||||
|
||||
export type GetAvailableRuneUTXOFn = (
|
||||
runeId: RuneIdCombined,
|
||||
usedUTXOs: SpendableRuneUTXO[],
|
||||
) => Promise<null | SpendableRuneUTXO>
|
||||
|
||||
export const selectRuneUTXOs = async (
|
||||
runeOutputs: RuneOutput[],
|
||||
getAvailableRuneUTXO: GetAvailableRuneUTXOFn,
|
||||
): Promise<{
|
||||
inputRuneUTXOs: SpendableRuneUTXO[]
|
||||
changeAmounts: { info: RuneInfo; amount: SDKNumber }[]
|
||||
}> => {
|
||||
const _runeOutputs = runeOutputs.map(output => ({
|
||||
...output,
|
||||
amount: BigNumber.from(output.amount),
|
||||
}))
|
||||
|
||||
const _getAvailableRuneUTXO: _GetAvailableRuneUTXOFn = async (
|
||||
runeId,
|
||||
usedUTXOs,
|
||||
) => {
|
||||
const res = await getAvailableRuneUTXO(
|
||||
runeId,
|
||||
usedUTXOs.map(utxo => ({
|
||||
...utxo,
|
||||
runes: Object.fromEntries(
|
||||
Object.entries(utxo.runes).map(([runeId, rune]) => [
|
||||
runeId,
|
||||
{ ...rune, runeAmount: toSDKNumberOrUndefined(rune.runeAmount) },
|
||||
]),
|
||||
),
|
||||
})),
|
||||
)
|
||||
if (res == null) return null
|
||||
return {
|
||||
...res,
|
||||
runes: Object.fromEntries(
|
||||
Object.entries(res.runes).map(([runeId, rune]) => [
|
||||
runeId,
|
||||
{ ...rune, runeAmount: BigNumber.from(rune.runeAmount) },
|
||||
]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const res = await _selectRuneUTXOs(_runeOutputs, _getAvailableRuneUTXO)
|
||||
|
||||
return {
|
||||
inputRuneUTXOs: res.inputRuneUTXOs.map(utxo => ({
|
||||
...utxo,
|
||||
runes: Object.fromEntries(
|
||||
Object.entries(utxo.runes).map(([runeId, rune]) => [
|
||||
runeId,
|
||||
{ ...rune, runeAmount: toSDKNumberOrUndefined(rune.runeAmount) },
|
||||
]),
|
||||
),
|
||||
})),
|
||||
changeAmounts: res.changeAmounts.map(change => ({
|
||||
...change,
|
||||
amount: toSDKNumberOrUndefined(change.amount),
|
||||
})),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user