[Client] Add vtxo expiration details to balance & Fix coin selection

* add expiry details in balance command

* coin selection: sort vtxos by olderFirst

* rename type

* balance: add next expiration

* add next expiration in offchain_balance json

* print duration in nextExpiration

* fix dust coin selection

* refactor sort
This commit is contained in:
Louis Singer
2024-02-20 17:10:18 +01:00
committed by GitHub
parent 10c19dd7d9
commit b8e0914ba9
10 changed files with 378 additions and 140 deletions

View File

@@ -1,18 +1,31 @@
package main package main
import ( import (
"fmt"
"math"
"sync" "sync"
"time"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var expiryDetailsFlag = cli.BoolFlag{
Name: "expiry-details",
Usage: "show cumulative balance by expiry time",
Value: false,
Required: false,
}
var balanceCommand = cli.Command{ var balanceCommand = cli.Command{
Name: "balance", Name: "balance",
Usage: "Print balance of the Ark wallet", Usage: "Print balance of the Ark wallet",
Action: balanceAction, Action: balanceAction,
Flags: []cli.Flag{&expiryDetailsFlag},
} }
func balanceAction(ctx *cli.Context) error { func balanceAction(ctx *cli.Context) error {
expiryDetails := ctx.Bool("expiry-details")
client, cancel, err := getClientFromState(ctx) client, cancel, err := getClientFromState(ctx)
if err != nil { if err != nil {
return err return err
@@ -30,26 +43,30 @@ func balanceAction(ctx *cli.Context) error {
chRes := make(chan balanceRes, 2) chRes := make(chan balanceRes, 2)
go func() { go func() {
defer wg.Done() defer wg.Done()
balance, err := getOffchainBalance(ctx, client, offchainAddr) explorer := NewExplorer()
balance, amountByExpiration, err := getOffchainBalance(ctx, explorer, client, offchainAddr, true)
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, err} chRes <- balanceRes{0, 0, nil, err}
return return
} }
chRes <- balanceRes{balance, 0, nil}
chRes <- balanceRes{balance, 0, amountByExpiration, nil}
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
balance, err := getOnchainBalance(onchainAddr) balance, err := getOnchainBalance(onchainAddr)
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, err} chRes <- balanceRes{0, 0, nil, err}
return return
} }
chRes <- balanceRes{0, balance, nil} chRes <- balanceRes{0, balance, nil, nil}
}() }()
wg.Wait() wg.Wait()
details := make([]map[string]interface{}, 0)
offchainBalance, onchainBalance := uint64(0), uint64(0) offchainBalance, onchainBalance := uint64(0), uint64(0)
nextExpiration := int64(0)
count := 0 count := 0
for res := range chRes { for res := range chRes {
if res.err != nil { if res.err != nil {
@@ -61,20 +78,74 @@ func balanceAction(ctx *cli.Context) error {
if res.onchainBalance > 0 { if res.onchainBalance > 0 {
onchainBalance = res.onchainBalance onchainBalance = res.onchainBalance
} }
if res.amountByExpiration != nil {
for timestamp, amount := range res.amountByExpiration {
if nextExpiration == 0 || timestamp < nextExpiration {
nextExpiration = timestamp
}
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
details = append(
details,
map[string]interface{}{
"expiry_time": fancyTime,
"amount": amount,
},
)
}
}
count++ count++
if count == 2 { if count == 2 {
break break
} }
} }
return printJSON(map[string]interface{}{ response := make(map[string]interface{})
"offchain_balance": offchainBalance, response["onchain_balance"] = onchainBalance
"onchain_balance": onchainBalance,
}) offchainBalanceJSON := map[string]interface{}{
"total": offchainBalance,
}
fancyTimeExpiration := ""
if nextExpiration != 0 {
t := time.Unix(nextExpiration, 0)
if t.Before(time.Now().Add(48 * time.Hour)) {
// print the duration instead of the absolute time
until := time.Until(t)
seconds := math.Abs(until.Seconds())
minutes := math.Abs(until.Minutes())
hours := math.Abs(until.Hours())
if hours < 1 {
if minutes < 1 {
fancyTimeExpiration = fmt.Sprintf("%d seconds", int(seconds))
} else {
fancyTimeExpiration = fmt.Sprintf("%d minutes", int(minutes))
}
} else {
fancyTimeExpiration = fmt.Sprintf("%d hours", int(hours))
}
} else {
fancyTimeExpiration = t.Format("2006-01-02 15:04:05")
}
offchainBalanceJSON["next_expiration"] = fancyTimeExpiration
}
if expiryDetails {
offchainBalanceJSON["details"] = details
}
response["offchain_balance"] = offchainBalanceJSON
return printJSON(response)
} }
type balanceRes struct { type balanceRes struct {
offchainBalance uint64 offchainBalance uint64
onchainBalance uint64 onchainBalance uint64
err error amountByExpiration map[int64]uint64
err error
} }

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -16,10 +17,15 @@ type vtxo struct {
txid string txid string
vout uint32 vout uint32
poolTxid string poolTxid string
expireAt *time.Time
} }
func getVtxos( func getVtxos(
ctx *cli.Context, client arkv1.ArkServiceClient, addr string, ctx *cli.Context,
explorer Explorer,
client arkv1.ArkServiceClient,
addr string,
withExpiration bool,
) ([]vtxo, error) { ) ([]vtxo, error) {
response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{ response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{
Address: addr, Address: addr,
@@ -38,6 +44,29 @@ func getVtxos(
}) })
} }
if !withExpiration {
return vtxos, nil
}
redeemBranches, err := getRedeemBranches(ctx, explorer, client, vtxos)
if err != nil {
return nil, err
}
for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpireAt()
if err != nil {
return nil, err
}
for i, vtxo := range vtxos {
if vtxo.txid == vtxoTxid {
vtxos[i].expireAt = expiration
break
}
}
}
return vtxos, nil return vtxos, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"sort"
"syscall" "syscall"
"time" "time"
@@ -150,6 +151,15 @@ func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
notSelected := make([]vtxo, 0) notSelected := make([]vtxo, 0)
selectedAmount := uint64(0) selectedAmount := uint64(0)
// sort vtxos by expiration (older first)
sort.SliceStable(vtxos, func(i, j int) bool {
if vtxos[i].expireAt == nil || vtxos[j].expireAt == nil {
return false
}
return vtxos[i].expireAt.Before(*vtxos[j].expireAt)
})
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
if selectedAmount >= amount { if selectedAmount >= amount {
notSelected = append(notSelected, vtxo) notSelected = append(notSelected, vtxo)
@@ -177,17 +187,30 @@ func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
} }
func getOffchainBalance( func getOffchainBalance(
ctx *cli.Context, client arkv1.ArkServiceClient, addr string, ctx *cli.Context, explorer Explorer, client arkv1.ArkServiceClient, addr string, withExpiration bool,
) (uint64, error) { ) (uint64, map[int64]uint64, error) {
vtxos, err := getVtxos(ctx, client, addr) amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := getVtxos(ctx, explorer, client, addr, withExpiration)
if err != nil { if err != nil {
return 0, err return 0, nil, err
} }
var balance uint64 var balance uint64
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
balance += vtxo.amount balance += vtxo.amount
if withExpiration {
expiration := vtxo.expireAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.amount
}
} }
return balance, nil
return balance, amountByExpiration, nil
} }
type utxo struct { type utxo struct {
@@ -198,11 +221,7 @@ type utxo struct {
} }
func getOnchainUtxos(addr string) ([]utxo, error) { func getOnchainUtxos(addr string) ([]utxo, error) {
_, net, err := getNetwork() _, net := getNetwork()
if err != nil {
return nil, err
}
baseUrl := explorerUrl[net.Name] baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", baseUrl, addr)) resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", baseUrl, addr))
if err != nil { if err != nil {
@@ -230,11 +249,7 @@ func getOnchainBalance(addr string) (uint64, error) {
return 0, err return 0, err
} }
_, net, err := getNetwork() _, net := getNetwork()
if err != nil {
return 0, err
}
balance := uint64(0) balance := uint64(0)
for _, p := range payload { for _, p := range payload {
if p.Asset != net.AssetID { if p.Asset != net.AssetID {
@@ -245,36 +260,43 @@ func getOnchainBalance(addr string) (uint64, error) {
return balance, nil return balance, nil
} }
func getTxHex(txid string) (string, error) { func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
_, net, err := getNetwork() _, net := getNetwork()
if err != nil {
return "", err
}
baseUrl := explorerUrl[net.Name] baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", baseUrl, txid)) resp, err := http.Get(fmt.Sprintf("%s/tx/%s", baseUrl, txid))
if err != nil { if err != nil {
return "", err return false, 0, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return false, 0, err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(body)) return false, 0, fmt.Errorf(string(body))
} }
return string(body), nil var tx struct {
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
if err := json.Unmarshal(body, &tx); err != nil {
return false, 0, err
}
if !tx.Status.Confirmed {
return false, -1, nil
}
return true, tx.Status.Blocktime, nil
} }
func broadcast(txHex string) (string, error) { func broadcast(txHex string) (string, error) {
_, net, err := getNetwork() _, net := getNetwork()
if err != nil {
return "", err
}
body := bytes.NewBuffer([]byte(txHex)) body := bytes.NewBuffer([]byte(txHex))
baseUrl := explorerUrl[net.Name] baseUrl := explorerUrl[net.Name]
@@ -295,20 +317,20 @@ func broadcast(txHex string) (string, error) {
return string(bodyResponse), nil return string(bodyResponse), nil
} }
func getNetwork() (*common.Network, *network.Network, error) { func getNetwork() (*common.Network, *network.Network) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return nil, nil, err return &common.TestNet, &network.Testnet
} }
net, ok := state["network"] net, ok := state["network"]
if !ok { if !ok {
return &common.MainNet, &network.Liquid, nil return &common.MainNet, &network.Liquid
} }
if net == "testnet" { if net == "testnet" {
return &common.TestNet, &network.Testnet, nil return &common.TestNet, &network.Testnet
} }
return &common.MainNet, &network.Liquid, nil return &common.MainNet, &network.Liquid
} }
func getAddress() (offchainAddr, onchainAddr string, err error) { func getAddress() (offchainAddr, onchainAddr string, err error) {
@@ -322,10 +344,7 @@ func getAddress() (offchainAddr, onchainAddr string, err error) {
return return
} }
arkNet, liquidNet, err := getNetwork() arkNet, liquidNet := getNetwork()
if err != nil {
return
}
arkAddr, err := common.EncodeAddress(arkNet.Addr, publicKey, aspPublicKey) arkAddr, err := common.EncodeAddress(arkNet.Addr, publicKey, aspPublicKey)
if err != nil { if err != nil {
@@ -675,3 +694,41 @@ func findSweepClosure(
return return
} }
func getRedeemBranches(
ctx *cli.Context,
explorer Explorer,
client arkv1.ArkServiceClient,
vtxos []vtxo,
) (map[string]RedeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0) // poolTxid -> congestionTree
redeemBranches := make(map[string]RedeemBranch, 0) // vtxo.txid -> redeemBranch
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
round, err := client.GetRound(ctx.Context, &arkv1.GetRoundRequest{
Txid: vtxo.poolTxid,
})
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.poolTxid] = congestionTree
}
redeemBranch, err := newRedeemBranch(ctx, explorer, congestionTrees[vtxo.poolTxid], vtxo)
if err != nil {
return nil, err
}
redeemBranches[vtxo.txid] = redeemBranch
}
return redeemBranches, nil
}

View File

@@ -1,6 +1,9 @@
package main package main
import ( import (
"fmt"
"io"
"net/http"
"strings" "strings"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
@@ -12,12 +15,17 @@ type Explorer interface {
} }
type explorer struct { type explorer struct {
cache map[string]string cache map[string]string
baseUrl string
} }
func NewExplorer() Explorer { func NewExplorer() Explorer {
_, net := getNetwork()
baseUrl := explorerUrl[net.Name]
return &explorer{ return &explorer{
cache: make(map[string]string), cache: make(map[string]string),
baseUrl: baseUrl,
} }
} }
@@ -26,7 +34,7 @@ func (e *explorer) GetTxHex(txid string) (string, error) {
return hex, nil return hex, nil
} }
txHex, err := getTxHex(txid) txHex, err := e.getTxHex(txid)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -55,3 +63,23 @@ func (e *explorer) Broadcast(txHex string) (string, error) {
return txid, nil return txid, nil
} }
func (e *explorer) getTxHex(txid string) (string, error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", e.baseUrl, txid))
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(body))
}
hex := string(body)
e.cache[txid] = hex
return hex, nil
}

View File

@@ -110,10 +110,7 @@ func initWallet(ctx *cli.Context, key, password string) error {
cypher := NewAES128Cypher() cypher := NewAES128Cypher()
arkNetwork, _, err := getNetwork() arkNetwork, _ := getNetwork()
if err != nil {
return err
}
publicKey, err := common.EncodePubKey(arkNetwork.PubKey, privateKey.PubKey()) publicKey, err := common.EncodePubKey(arkNetwork.PubKey, privateKey.PubKey())
if err != nil { if err != nil {

View File

@@ -10,7 +10,6 @@ import (
"time" "time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
@@ -61,7 +60,7 @@ func redeemAction(ctx *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid onchain address: unknown network") return fmt.Errorf("invalid onchain address: unknown network")
} }
_, liquidNet, _ := getNetwork() _, liquidNet := getNetwork()
if net.Name != liquidNet.Name { if net.Name != liquidNet.Name {
return fmt.Errorf("invalid onchain address: must be for %s network", liquidNet.Name) return fmt.Errorf("invalid onchain address: must be for %s network", liquidNet.Name)
} }
@@ -105,7 +104,9 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
} }
defer close() defer close()
vtxos, err := getVtxos(ctx, client, offchainAddr) explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true)
if err != nil { if err != nil {
return err return err
} }
@@ -189,7 +190,8 @@ func unilateralRedeem(ctx *cli.Context, addr string) error {
return err return err
} }
vtxos, err := getVtxos(ctx, client, offchainAddr) explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, false)
if err != nil { if err != nil {
return err return err
} }
@@ -214,46 +216,25 @@ func unilateralRedeem(ctx *cli.Context, addr string) error {
return err return err
} }
congestionTrees := make(map[string]tree.CongestionTree, 0) // transactionsMap avoid duplicates
transactionsMap := make(map[string]struct{}, 0) transactionsMap := make(map[string]struct{}, 0)
transactions := make([]string, 0) transactions := make([]string, 0)
for _, vtxo := range vtxos { redeemBranches, err := getRedeemBranches(ctx, explorer, client, vtxos)
if _, ok := congestionTrees[vtxo.poolTxid]; !ok { if err != nil {
round, err := client.GetRound(ctx.Context, &arkv1.GetRoundRequest{ return err
Txid: vtxo.poolTxid, }
})
if err != nil {
return err
}
treeFromRound := round.GetRound().GetCongestionTree() for _, branch := range redeemBranches {
congestionTree, err := toCongestionTree(treeFromRound) if err := branch.AddVtxoInput(updater); err != nil {
if err != nil { return err
return err
}
congestionTrees[vtxo.poolTxid] = congestionTree
} }
redeemBranch, err := newRedeemBranch(ctx, congestionTrees[vtxo.poolTxid], vtxo) branchTxs, err := branch.RedeemPath()
if err != nil { if err != nil {
return err return err
} }
if err := redeemBranch.UpdatePath(); err != nil {
return err
}
branchTxs, err := redeemBranch.RedeemPath()
if err != nil {
return err
}
if err := redeemBranch.AddVtxoInput(updater); err != nil {
return err
}
for _, txHex := range branchTxs { for _, txHex := range branchTxs {
if _, ok := transactionsMap[txHex]; !ok { if _, ok := transactionsMap[txHex]; !ok {
transactions = append(transactions, txHex) transactions = append(transactions, txHex)
@@ -262,10 +243,7 @@ func unilateralRedeem(ctx *cli.Context, addr string) error {
} }
} }
_, net, err := getNetwork() _, net := getNetwork()
if err != nil {
return err
}
outputs := []psetv2.OutputArgs{ outputs := []psetv2.OutputArgs{
{ {
@@ -307,8 +285,6 @@ func unilateralRedeem(ctx *cli.Context, addr string) error {
return err return err
} }
explorer := NewExplorer()
for i, txHex := range transactions { for i, txHex := range transactions {
for { for {
txid, err := explorer.Broadcast(txHex) txid, err := explorer.Broadcast(txHex)

View File

@@ -102,7 +102,9 @@ func sendAction(ctx *cli.Context) error {
} }
defer close() defer close()
vtxos, err := getVtxos(ctx, client, offchainAddr) explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -81,10 +81,7 @@ func signPset(
return err return err
} }
_, liquidNet, err := getNetwork() _, liquidNet := getNetwork()
if err != nil {
return err
}
prevoutsScripts := make([][]byte, 0) prevoutsScripts := make([][]byte, 0)
prevoutsValues := make([][]byte, 0) prevoutsValues := make([][]byte, 0)

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"time"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -12,12 +13,12 @@ import (
) )
type RedeemBranch interface { type RedeemBranch interface {
// UpdatePath checks for transactions of the branch onchain and updates the branch accordingly // RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
UpdatePath() error
// Redeem will sign the branch of the tree and return the associated signed pset + the vtxo input
RedeemPath() ([]string, error) RedeemPath() ([]string, error)
// AddInput adds the vtxo input created by the branch // AddInput adds the vtxo input created by the branch
AddVtxoInput(updater *psetv2.Updater) error AddVtxoInput(updater *psetv2.Updater) error
// ExpireAt returns the expiration time of the branch
ExpireAt() (*time.Time, error)
} }
type redeemBranch struct { type redeemBranch struct {
@@ -25,10 +26,22 @@ type redeemBranch struct {
branch []*psetv2.Pset branch []*psetv2.Pset
internalKey *secp256k1.PublicKey internalKey *secp256k1.PublicKey
sweepClosure *taproot.TapElementsLeaf sweepClosure *taproot.TapElementsLeaf
lifetime time.Duration
explorer Explorer
} }
func newRedeemBranch(ctx *cli.Context, congestionTree tree.CongestionTree, vtxo vtxo) (RedeemBranch, error) { func newRedeemBranch(
sweepClosure, _, err := findSweepClosure(congestionTree) ctx *cli.Context,
explorer Explorer,
congestionTree tree.CongestionTree,
vtxo vtxo,
) (RedeemBranch, error) {
sweepClosure, seconds, err := findSweepClosure(congestionTree)
if err != nil {
return nil, err
}
lifetime, err := time.ParseDuration(fmt.Sprintf("%ds", seconds))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -58,43 +71,21 @@ func newRedeemBranch(ctx *cli.Context, congestionTree tree.CongestionTree, vtxo
branch: branch, branch: branch,
internalKey: internalKey, internalKey: internalKey,
sweepClosure: sweepClosure, sweepClosure: sweepClosure,
lifetime: lifetime,
explorer: explorer,
}, nil }, nil
} }
// UpdatePath checks for transactions of the branch onchain and updates the branch accordingly
func (r *redeemBranch) UpdatePath() error {
for i := len(r.branch) - 1; i >= 0; i-- {
pset := r.branch[i]
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return err
}
txHash := unsignedTx.TxHash().String()
_, err = getTxHex(txHash)
if err != nil {
continue
}
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
if i == len(r.branch)-1 {
r.branch = []*psetv2.Pset{}
} else {
r.branch = r.branch[i+1:]
}
break
}
return nil
}
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output // RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
func (r *redeemBranch) RedeemPath() ([]string, error) { func (r *redeemBranch) RedeemPath() ([]string, error) {
transactions := make([]string, 0, len(r.branch)) transactions := make([]string, 0, len(r.branch))
for _, pset := range r.branch { offchainPath, err := r.offchainPath()
if err != nil {
return nil, err
}
for _, pset := range offchainPath {
for i, input := range pset.Inputs { for i, input := range pset.Inputs {
if len(input.TapLeafScript) == 0 { if len(input.TapLeafScript) == 0 {
return nil, fmt.Errorf("tap leaf script not found on input #%d", i) return nil, fmt.Errorf("tap leaf script not found on input #%d", i)
@@ -183,3 +174,68 @@ func (r *redeemBranch) AddVtxoInput(updater *psetv2.Updater) error {
return nil return nil
} }
func (r *redeemBranch) ExpireAt() (*time.Time, error) {
lastKnownBlocktime := int64(0)
confirmed, blocktime, _ := getTxBlocktime(r.vtxo.poolTxid)
if confirmed {
lastKnownBlocktime = blocktime
} else {
expirationFromNow := time.Now().Add(time.Minute).Add(r.lifetime)
return &expirationFromNow, nil
}
for _, pset := range r.branch {
utx, _ := pset.UnsignedTx()
txid := utx.TxHash().String()
confirmed, blocktime, err := getTxBlocktime(txid)
if err != nil {
break
}
if confirmed {
lastKnownBlocktime = blocktime
continue
}
break
}
t := time.Unix(lastKnownBlocktime, 0).Add(r.lifetime)
return &t, nil
}
// offchainPath checks for transactions of the branch onchain and returns only the offchain part
func (r *redeemBranch) offchainPath() ([]*psetv2.Pset, error) {
offchainPath := append([]*psetv2.Pset{}, r.branch...)
for i := len(r.branch) - 1; i >= 0; i-- {
pset := r.branch[i]
unsignedTx, err := pset.UnsignedTx()
if err != nil {
fmt.Println("error", err)
return nil, err
}
txHash := unsignedTx.TxHash().String()
_, err = r.explorer.GetTxHex(txHash)
if err != nil {
continue
}
// if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch
if i == len(r.branch)-1 {
offchainPath = []*psetv2.Pset{}
} else {
offchainPath = r.branch[i+1:]
}
break
}
return offchainPath, nil
}

View File

@@ -341,6 +341,31 @@ func (b *txBuilder) createPoolTx(
return nil, err return nil, err
} }
} }
} else if feeAmount-dust > 0 {
newUtxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, feeAmount-dust)
if err != nil {
return nil, err
}
if change > 0 {
if change < dustLimit {
feeAmount += change
} else {
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: b.net.AssetID,
Amount: change,
Script: aspScript,
},
}); err != nil {
return nil, err
}
}
}
if err := addInputs(updater, newUtxos); err != nil {
return nil, err
}
} }
// add fee output // add fee output