mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 20:24:21 +01:00
[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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
137
client/common.go
137
client/common.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user