Covenant-less ark sdk (#225)

* Add method to compute vtxo taproot script

* Drop debug logs

* Refactor client:
* Remove dep from stubs in interface
* Remove redeem branch, out of scope (moved to ark client)

* Add example for covenant and covenantless sdk

* Simplify explorer - No need for bitcoin and liquid impls

* Refactor wallet:
* wallet struct for common operations (create, lock/unlock, getType, isLocked)
* liquidWallet struct for liquid operations (derive/get addresses, sign tx)
* bitcoinWallet struct for bitcoin operations (derive/get addresses, sign tx)

* Update utils:
* drop methods to parse tree (moved to ark client)
* add methods for encryption, network parsing, key generation
* add methods for covenant/covenantless redeem branches (move to common?)

* Add support for covenantless sdk:
* move interface to dedicated file
* arkCLient struct for common operations (Init, lock/unlock, get config data, receive)
* covenantArkClient struct for covenant operations (onboard, balance, send, redeem)
* covenantlessArkClient struct for covenantless operations (onboard, balance, send, redeem)

* Fix wasm

* Fixes

* Make explorer use utils.Cache

* Renamings

* Lint

* Fix e2e tests

* Fix e2e test
This commit is contained in:
Pietralberto Mazza
2024-08-07 01:37:18 +02:00
committed by GitHub
parent 1c67c56d9d
commit 8de2df3d7f
39 changed files with 5655 additions and 2747 deletions

View File

@@ -4,8 +4,7 @@ import (
"context"
"time"
"github.com/ark-network/ark-sdk/explorer"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
)
const (
@@ -13,48 +12,121 @@ const (
RestClient = "rest"
)
type RoundEvent interface {
isRoundEvent()
}
type ASPClient interface {
GetInfo(ctx context.Context) (*Info, error)
ListVtxos(ctx context.Context, addr string) ([]Vtxo, []Vtxo, error)
GetRound(ctx context.Context, txID string) (*Round, error)
GetRoundByID(ctx context.Context, roundID string) (*Round, error)
Onboard(
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error
RegisterPayment(
ctx context.Context, inputs []VtxoKey,
) (string, error)
ClaimPayment(
ctx context.Context, paymentID string, outputs []Output,
) error
GetEventStream(
ctx context.Context, paymentID string,
) (<-chan RoundEventChannel, error)
Ping(ctx context.Context, paymentID string) (*RoundFinalizationEvent, error)
FinalizePayment(
ctx context.Context, signedForfeitTxs []string,
) error
Close()
}
type Info struct {
Pubkey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
}
type RoundEventChannel struct {
Event *arkv1.GetEventStreamResponse
Event RoundEvent
Err error
}
type VtxoKey struct {
Txid string
VOut uint32
}
type Vtxo struct {
VtxoKey
Amount uint64
Txid string
VOut uint32
RoundTxid string
ExpiresAt *time.Time
}
type ASPClient interface {
GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error)
ListVtxos(ctx context.Context, addr string) (*arkv1.ListVtxosResponse, error)
GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*Vtxo, error)
GetRound(ctx context.Context, txID string) (*arkv1.GetRoundResponse, error)
GetRoundByID(ctx context.Context, roundID string) (*arkv1.GetRoundByIdResponse, error)
GetRedeemBranches(
ctx context.Context, vtxos []*Vtxo, explorerSvc explorer.Explorer,
) (map[string]*RedeemBranch, error)
GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error)
Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error)
RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error)
ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error)
GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
) (<-chan RoundEventChannel, error)
Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error)
FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error)
Close()
type Output struct {
Address string
Amount uint64
}
type RoundStage int
func (s RoundStage) String() string {
switch s {
case RoundStageRegistration:
return "ROUND_STAGE_REGISTRATION"
case RoundStageFinalization:
return "ROUND_STAGE_FINALIZATION"
case RoundStageFinalized:
return "ROUND_STAGE_FINALIZED"
case RoundStageFailed:
return "ROUND_STAGE_FAILED"
default:
return "ROUND_STAGE_UNDEFINED"
}
}
const (
RoundStageUndefined RoundStage = iota
RoundStageRegistration
RoundStageFinalization
RoundStageFinalized
RoundStageFailed
)
type Round struct {
ID string
StartedAt *time.Time
EndedAt *time.Time
Tx string
Tree tree.CongestionTree
ForfeitTxs []string
Connectors []string
Stage RoundStage
}
type RoundFinalizationEvent struct {
ID string
Tx string
ForfeitTxs []string
Tree tree.CongestionTree
Connectors []string
}
func (e RoundFinalizationEvent) isRoundEvent() {}
type RoundFinalizedEvent struct {
ID string
Txid string
}
func (e RoundFinalizedEvent) isRoundEvent() {}
type RoundFailedEvent struct {
ID string
Reason string
}
func (e RoundFailedEvent) isRoundEvent() {}

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"google.golang.org/grpc"
@@ -16,9 +16,10 @@ import (
)
type grpcClient struct {
conn *grpc.ClientConn
svc arkv1.ArkServiceClient
eventsCh chan client.RoundEventChannel
conn *grpc.ClientConn
svc arkv1.ArkServiceClient
eventsCh chan client.RoundEventChannel
treeCache *utils.Cache[tree.CongestionTree]
}
func NewClient(aspUrl string) (client.ASPClient, error) {
@@ -43,8 +44,9 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
svc := arkv1.NewArkServiceClient(conn)
eventsCh := make(chan client.RoundEventChannel)
treeCache := utils.NewCache[tree.CongestionTree]()
return &grpcClient{conn, svc, eventsCh}, nil
return &grpcClient{conn, svc, eventsCh, treeCache}, nil
}
func (c *grpcClient) Close() {
@@ -53,8 +55,9 @@ func (c *grpcClient) Close() {
}
func (a *grpcClient) GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
ctx context.Context, paymentID string,
) (<-chan client.RoundEventChannel, error) {
req := &arkv1.GetEventStreamRequest{}
stream, err := a.svc.GetEventStream(ctx, req)
if err != nil {
return nil, err
@@ -70,187 +73,265 @@ func (a *grpcClient) GetEventStream(
return
}
a.eventsCh <- client.RoundEventChannel{Event: resp}
a.eventsCh <- client.RoundEventChannel{Event: event{resp}.toRoundEvent()}
}
}()
return a.eventsCh, nil
}
func (a *grpcClient) GetInfo(ctx context.Context) (*arkv1.GetInfoResponse, error) {
return a.svc.GetInfo(ctx, &arkv1.GetInfoRequest{})
func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
req := &arkv1.GetInfoRequest{}
resp, err := a.svc.GetInfo(ctx, req)
if err != nil {
return nil, err
}
return &client.Info{
Pubkey: resp.GetPubkey(),
RoundLifetime: resp.GetRoundLifetime(),
UnilateralExitDelay: resp.GetUnilateralExitDelay(),
RoundInterval: resp.GetRoundInterval(),
Network: resp.GetNetwork(),
MinRelayFee: resp.GetMinRelayFee(),
}, nil
}
func (a *grpcClient) ListVtxos(
ctx context.Context,
addr string,
) (*arkv1.ListVtxosResponse, error) {
return a.svc.ListVtxos(ctx, &arkv1.ListVtxosRequest{Address: addr})
ctx context.Context, addr string,
) ([]client.Vtxo, []client.Vtxo, error) {
resp, err := a.svc.ListVtxos(ctx, &arkv1.ListVtxosRequest{Address: addr})
if err != nil {
return nil, nil, err
}
return vtxos(resp.GetSpendableVtxos()).toVtxos(), vtxos(resp.GetSpentVtxos()).toVtxos(), nil
}
func (a *grpcClient) GetRound(
ctx context.Context, txID string,
) (*arkv1.GetRoundResponse, error) {
return a.svc.GetRound(ctx, &arkv1.GetRoundRequest{Txid: txID})
}
func (a *grpcClient) GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*client.Vtxo, error) {
allVtxos, err := a.ListVtxos(ctx, addr)
) (*client.Round, error) {
req := &arkv1.GetRoundRequest{Txid: txID}
resp, err := a.svc.GetRound(ctx, req)
if err != nil {
return nil, err
}
vtxos := make([]*client.Vtxo, 0, len(allVtxos.GetSpendableVtxos()))
for _, v := range allVtxos.GetSpendableVtxos() {
var expireAt *time.Time
if v.ExpireAt > 0 {
t := time.Unix(v.ExpireAt, 0)
expireAt = &t
}
if v.Swept {
continue
}
vtxos = append(vtxos, &client.Vtxo{
Amount: v.Receiver.Amount,
Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout,
RoundTxid: v.PoolTxid,
ExpiresAt: expireAt,
})
round := resp.GetRound()
startedAt := time.Unix(round.GetStart(), 0)
var endedAt *time.Time
if round.GetEnd() > 0 {
t := time.Unix(round.GetEnd(), 0)
endedAt = &t
}
if explorerSvc == nil {
return vtxos, nil
}
redeemBranches, err := a.GetRedeemBranches(ctx, vtxos, explorerSvc)
if err != nil {
return nil, err
}
for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpiresAt()
if err != nil {
return nil, err
}
for i, vtxo := range vtxos {
if vtxo.Txid == vtxoTxid {
vtxos[i].ExpiresAt = expiration
break
}
}
}
return vtxos, nil
}
func (a *grpcClient) GetRedeemBranches(
ctx context.Context, vtxos []*client.Vtxo, explorerSvc explorer.Explorer,
) (map[string]*client.RedeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]*client.RedeemBranch, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.RoundTxid]; !ok {
round, err := a.GetRound(ctx, vtxo.RoundTxid)
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.RoundTxid] = congestionTree
}
redeemBranch, err := client.NewRedeemBranch(
explorerSvc, congestionTrees[vtxo.RoundTxid], vtxo,
)
if err != nil {
return nil, err
}
redeemBranches[vtxo.Txid] = redeemBranch
}
return redeemBranches, nil
}
func (a *grpcClient) GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := a.GetSpendableVtxos(ctx, addr, explorerSvc)
if err != nil {
return 0, nil, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.Amount
if vtxo.ExpiresAt != nil {
expiration := vtxo.ExpiresAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.Amount
}
}
return balance, amountByExpiration, nil
return &client.Round{
ID: round.GetId(),
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: round.GetPoolTx(),
Tree: treeFromProto{round.GetCongestionTree()}.parse(),
ForfeitTxs: round.GetForfeitTxs(),
Connectors: round.GetConnectors(),
Stage: client.RoundStage(int(round.GetStage())),
}, nil
}
func (a *grpcClient) Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error) {
return a.svc.Onboard(ctx, req)
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error {
req := &arkv1.OnboardRequest{
BoardingTx: tx,
UserPubkey: userPubkey,
CongestionTree: treeToProto(congestionTree).parse(),
}
_, err := a.svc.Onboard(ctx, req)
return err
}
func (a *grpcClient) RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error) {
return a.svc.RegisterPayment(ctx, req)
ctx context.Context, inputs []client.VtxoKey,
) (string, error) {
req := &arkv1.RegisterPaymentRequest{
Inputs: ins(inputs).toProto(),
}
resp, err := a.svc.RegisterPayment(ctx, req)
if err != nil {
return "", err
}
return resp.GetId(), nil
}
func (a *grpcClient) ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error) {
return a.svc.ClaimPayment(ctx, req)
ctx context.Context, paymentID string, outputs []client.Output,
) error {
req := &arkv1.ClaimPaymentRequest{
Id: paymentID,
Outputs: outs(outputs).toProto(),
}
_, err := a.svc.ClaimPayment(ctx, req)
return err
}
func (a *grpcClient) Ping(
ctx context.Context, req *arkv1.PingRequest,
) (*arkv1.PingResponse, error) {
return a.svc.Ping(ctx, req)
ctx context.Context, paymentID string,
) (*client.RoundFinalizationEvent, error) {
req := &arkv1.PingRequest{
PaymentId: paymentID,
}
resp, err := a.svc.Ping(ctx, req)
if err != nil {
return nil, err
}
event := resp.GetEvent()
return &client.RoundFinalizationEvent{
ID: event.GetId(),
Tx: event.GetPoolTx(),
ForfeitTxs: event.GetForfeitTxs(),
Tree: treeFromProto{event.GetCongestionTree()}.parse(),
Connectors: event.GetConnectors(),
}, nil
}
func (a *grpcClient) FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error) {
return a.svc.FinalizePayment(ctx, req)
ctx context.Context, signedForfeitTxs []string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
_, err := a.svc.FinalizePayment(ctx, req)
return err
}
func (a *grpcClient) GetRoundByID(
ctx context.Context, roundID string,
) (*arkv1.GetRoundByIdResponse, error) {
return a.svc.GetRoundById(ctx, &arkv1.GetRoundByIdRequest{
Id: roundID,
})
) (*client.Round, error) {
req := &arkv1.GetRoundByIdRequest{Id: roundID}
resp, err := a.svc.GetRoundById(ctx, req)
if err != nil {
return nil, err
}
round := resp.GetRound()
startedAt := time.Unix(round.GetStart(), 0)
var endedAt *time.Time
if round.GetEnd() > 0 {
t := time.Unix(round.GetEnd(), 0)
endedAt = &t
}
tree := treeFromProto{round.GetCongestionTree()}.parse()
return &client.Round{
ID: round.GetId(),
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: round.GetPoolTx(),
Tree: tree,
ForfeitTxs: round.GetForfeitTxs(),
Connectors: round.GetConnectors(),
Stage: client.RoundStage(int(round.GetStage())),
}, nil
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
type out client.Output
for _, level := range treeFromProto.Levels {
func (o out) toProto() *arkv1.Output {
return &arkv1.Output{
Address: o.Address,
Amount: o.Amount,
}
}
type outs []client.Output
func (o outs) toProto() []*arkv1.Output {
list := make([]*arkv1.Output, 0, len(o))
for _, oo := range o {
list = append(list, out(oo).toProto())
}
return list
}
type event struct {
*arkv1.GetEventStreamResponse
}
func (e event) toRoundEvent() client.RoundEvent {
if ee := e.GetRoundFailed(); ee != nil {
return client.RoundFailedEvent{
ID: ee.GetId(),
Reason: ee.GetReason(),
}
}
if ee := e.GetRoundFinalization(); ee != nil {
tree := treeFromProto{ee.GetCongestionTree()}.parse()
return client.RoundFinalizationEvent{
ID: ee.GetId(),
Tx: ee.GetPoolTx(),
ForfeitTxs: ee.GetForfeitTxs(),
Tree: tree,
Connectors: ee.GetConnectors(),
}
}
ee := e.GetRoundFinalized()
return client.RoundFinalizedEvent{
ID: ee.GetId(),
Txid: ee.GetPoolTxid(),
}
}
type vtxo struct {
*arkv1.Vtxo
}
func (v vtxo) toVtxo() client.Vtxo {
var expiresAt *time.Time
if v.GetExpireAt() > 0 {
t := time.Unix(v.GetExpireAt(), 0)
expiresAt = &t
}
return client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(),
},
Amount: v.GetReceiver().GetAmount(),
RoundTxid: v.GetPoolTxid(),
ExpiresAt: expiresAt,
}
}
type vtxos []*arkv1.Vtxo
func (v vtxos) toVtxos() []client.Vtxo {
list := make([]client.Vtxo, 0, len(v))
for _, vv := range v {
list = append(list, vtxo{vv}.toVtxo())
}
return list
}
type input client.VtxoKey
func (i input) toProto() *arkv1.Input {
return &arkv1.Input{
Txid: i.Txid,
Vout: i.VOut,
}
}
type ins []client.VtxoKey
func (i ins) toProto() []*arkv1.Input {
list := make([]*arkv1.Input, 0, len(i))
for _, ii := range i {
list = append(list, input(ii).toProto())
}
return list
}
type treeFromProto struct {
*arkv1.Tree
}
func (t treeFromProto) parse() tree.CongestionTree {
levels := make(tree.CongestionTree, 0, len(t.GetLevels()))
for _, level := range t.GetLevels() {
nodes := make([]tree.Node, 0, len(level.Nodes))
for _, node := range level.Nodes {
@@ -258,7 +339,6 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: false,
})
}
@@ -268,10 +348,39 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
for j, treeLvl := range levels {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
levels[j][i] = tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: true,
}
}
}
}
return levels, nil
return levels
}
type treeToProto tree.CongestionTree
func (t treeToProto) parse() *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(t))
for _, level := range t {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}

View File

@@ -1,217 +0,0 @@
package client
import (
"fmt"
"time"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
)
type RedeemBranch struct {
vtxo *Vtxo
branch []*psetv2.Pset
internalKey *secp256k1.PublicKey
sweepClosure *taproot.TapElementsLeaf
lifetime time.Duration
explorer explorer.Explorer
}
func NewRedeemBranch(
explorer 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 {
return nil, err
}
nodes, err := congestionTree.Branch(vtxo.Txid)
if err != nil {
return nil, err
}
branch := make([]*psetv2.Pset, 0, len(nodes))
for _, node := range nodes {
pset, err := psetv2.NewPsetFromBase64(node.Tx)
if err != nil {
return nil, err
}
branch = append(branch, pset)
}
xOnlyKey := branch[0].Inputs[0].TapInternalKey
internalKey, err := schnorr.ParsePubKey(xOnlyKey)
if err != nil {
return nil, err
}
return &RedeemBranch{
vtxo: vtxo,
branch: branch,
internalKey: internalKey,
sweepClosure: sweepClosure,
lifetime: lifetime,
explorer: explorer,
}, nil
}
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
func (r *RedeemBranch) RedeemPath() ([]string, error) {
transactions := make([]string, 0, len(r.branch))
offchainPath, err := r.OffchainPath()
if err != nil {
return nil, err
}
for _, pset := range offchainPath {
for i, input := range pset.Inputs {
if len(input.TapLeafScript) == 0 {
return nil, fmt.Errorf("tap leaf script not found on input #%d", i)
}
for _, leaf := range input.TapLeafScript {
closure, err := tree.DecodeClosure(leaf.Script)
if err != nil {
return nil, err
}
switch closure.(type) {
case *tree.UnrollClosure:
controlBlock, err := leaf.ControlBlock.ToBytes()
if err != nil {
return nil, err
}
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return nil, err
}
unsignedTx.Inputs[i].Witness = [][]byte{
leaf.Script,
controlBlock[:],
}
hex, err := unsignedTx.ToHex()
if err != nil {
return nil, err
}
transactions = append(transactions, hex)
}
}
}
}
return transactions, nil
}
func (r *RedeemBranch) ExpiresAt() (*time.Time, error) {
lastKnownBlocktime := int64(0)
confirmed, blocktime, _ := r.explorer.GetTxBlockTime(r.vtxo.RoundTxid)
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 := r.explorer.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 {
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
}
func findSweepClosure(
congestionTree tree.CongestionTree,
) (*taproot.TapElementsLeaf, uint, error) {
root, err := congestionTree.Root()
if err != nil {
return nil, 0, err
}
// find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil {
return nil, 0, err
}
var seconds uint
var sweepClosure *taproot.TapElementsLeaf
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(tapLeaf.Script)
if err != nil {
continue
}
if valid && closure.Seconds > seconds {
seconds = closure.Seconds
sweepClosure = &tapLeaf.TapElementsLeaf
}
}
if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found")
}
return sweepClosure, seconds, nil
}

View File

@@ -5,15 +5,17 @@ import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/ark-network/ark-sdk/client"
"github.com/ark-network/ark-sdk/client/rest/service/arkservice"
"github.com/ark-network/ark-sdk/client/rest/service/arkservice/ark_service"
"github.com/ark-network/ark-sdk/client/rest/service/models"
"github.com/ark-network/ark-sdk/explorer"
"github.com/ark-network/ark-sdk/internal/utils"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcutil/psbt"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/vulpemventures/go-elements/psetv2"
@@ -23,6 +25,7 @@ type restClient struct {
svc ark_service.ClientService
eventsCh chan client.RoundEventChannel
requestTimeout time.Duration
treeCache *utils.Cache[tree.CongestionTree]
}
func NewClient(aspUrl string) (client.ASPClient, error) {
@@ -35,14 +38,15 @@ func NewClient(aspUrl string) (client.ASPClient, error) {
}
eventsCh := make(chan client.RoundEventChannel)
reqTimeout := 15 * time.Second
treeCache := utils.NewCache[tree.CongestionTree]()
return &restClient{svc, eventsCh, reqTimeout}, nil
return &restClient{svc, eventsCh, reqTimeout, treeCache}, nil
}
func (c *restClient) Close() {}
func (a *restClient) GetEventStream(
ctx context.Context, paymentID string, req *arkv1.GetEventStreamRequest,
ctx context.Context, paymentID string,
) (<-chan client.RoundEventChannel, error) {
go func(payID string) {
defer close(a.eventsCh)
@@ -57,9 +61,7 @@ func (a *restClient) GetEventStream(
}
return
default:
resp, err := a.Ping(ctx, &arkv1.PingRequest{
PaymentId: payID,
})
event, err := a.Ping(ctx, payID)
if err != nil {
a.eventsCh <- client.RoundEventChannel{
Err: err,
@@ -67,39 +69,13 @@ func (a *restClient) GetEventStream(
return
}
if resp.GetEvent() != nil {
levels := make([]*arkv1.TreeLevel, 0, len(resp.GetEvent().GetCongestionTree().GetLevels()))
for _, l := range resp.GetEvent().GetCongestionTree().GetLevels() {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
}
if event != nil {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFinalization{
RoundFinalization: &arkv1.RoundFinalizationEvent{
Id: resp.GetEvent().GetId(),
PoolTx: resp.GetEvent().GetPoolTx(),
ForfeitTxs: resp.GetEvent().GetForfeitTxs(),
CongestionTree: &arkv1.Tree{
Levels: levels,
},
Connectors: resp.GetEvent().GetConnectors(),
},
},
},
Event: *event,
}
for {
roundID := resp.GetEvent().GetId()
roundID := event.ID
round, err := a.GetRoundByID(ctx, roundID)
if err != nil {
a.eventsCh <- client.RoundEventChannel{
@@ -108,29 +84,20 @@ func (a *restClient) GetEventStream(
return
}
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FINALIZED {
ptx, _ := psetv2.NewPsetFromBase64(round.GetRound().GetPoolTx())
utx, _ := ptx.UnsignedTx()
if round.Stage == client.RoundStageFinalized {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFinalized{
RoundFinalized: &arkv1.RoundFinalizedEvent{
PoolTxid: utx.TxHash().String(),
},
},
Event: client.RoundFinalizedEvent{
ID: roundID,
Txid: getTxid(round.Tx),
},
}
return
}
if round.GetRound().GetStage() == arkv1.RoundStage_ROUND_STAGE_FAILED {
if round.Stage == client.RoundStageFailed {
a.eventsCh <- client.RoundEventChannel{
Event: &arkv1.GetEventStreamResponse{
Event: &arkv1.GetEventStreamResponse_RoundFailed{
RoundFailed: &arkv1.RoundFailed{
Id: round.GetRound().GetId(),
},
},
Event: client.RoundFailedEvent{
ID: roundID,
},
}
return
@@ -150,7 +117,7 @@ func (a *restClient) GetEventStream(
func (a *restClient) GetInfo(
ctx context.Context,
) (*arkv1.GetInfoResponse, error) {
) (*client.Info, error) {
resp, err := a.svc.ArkServiceGetInfo(ark_service.NewArkServiceGetInfoParams())
if err != nil {
return nil, err
@@ -176,7 +143,7 @@ func (a *restClient) GetInfo(
return nil, err
}
return &arkv1.GetInfoResponse{
return &client.Info{
Pubkey: resp.Payload.Pubkey,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
@@ -188,51 +155,76 @@ func (a *restClient) GetInfo(
func (a *restClient) ListVtxos(
ctx context.Context, addr string,
) (*arkv1.ListVtxosResponse, error) {
) ([]client.Vtxo, []client.Vtxo, error) {
resp, err := a.svc.ArkServiceListVtxos(
ark_service.NewArkServiceListVtxosParams().WithAddress(addr),
)
if err != nil {
return nil, err
return nil, nil, err
}
vtxos := make([]*arkv1.Vtxo, 0, len(resp.Payload.SpendableVtxos))
spendableVtxos := make([]client.Vtxo, 0, len(resp.Payload.SpendableVtxos))
for _, v := range resp.Payload.SpendableVtxos {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, err
var expiresAt *time.Time
if v.ExpireAt != "" && v.ExpireAt != "0" {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, nil, err
}
t := time.Unix(int64(expAt), 0)
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
if err != nil {
return nil, err
return nil, nil, err
}
vtxos = append(vtxos, &arkv1.Vtxo{
Outpoint: &arkv1.Input{
spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
Vout: uint32(v.Outpoint.Vout),
VOut: uint32(v.Outpoint.Vout),
},
Receiver: &arkv1.Output{
Address: v.Receiver.Address,
Amount: uint64(amount),
},
Spent: v.Spent,
PoolTxid: v.PoolTxid,
SpentBy: v.SpentBy,
ExpireAt: int64(expAt),
Swept: v.Swept,
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
})
}
return &arkv1.ListVtxosResponse{
SpendableVtxos: vtxos,
}, nil
spentVtxos := make([]client.Vtxo, 0, len(resp.Payload.SpentVtxos))
for _, v := range resp.Payload.SpentVtxos {
var expiresAt *time.Time
if v.ExpireAt != "" && v.ExpireAt != "0" {
expAt, err := strconv.Atoi(v.ExpireAt)
if err != nil {
return nil, nil, err
}
t := time.Unix(int64(expAt), 0)
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
if err != nil {
return nil, nil, err
}
spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
})
}
return spendableVtxos, spentVtxos, nil
}
func (a *restClient) GetRound(
ctx context.Context, txID string,
) (*arkv1.GetRoundResponse, error) {
) (*client.Round, error) {
resp, err := a.svc.ArkServiceGetRound(
ark_service.NewArkServiceGetRoundParams().WithTxid(txID),
)
@@ -250,304 +242,125 @@ func (a *restClient) GetRound(
return nil, err
}
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
for _, l := range resp.Payload.Round.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
startedAt := time.Unix(int64(start), 0)
var endedAt *time.Time
if end > 0 {
t := time.Unix(int64(end), 0)
endedAt = &t
}
return &arkv1.GetRoundResponse{
Round: &arkv1.Round{
Id: resp.Payload.Round.ID,
Start: int64(start),
End: int64(end),
PoolTx: resp.Payload.Round.PoolTx,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
},
return &client.Round{
ID: resp.Payload.Round.ID,
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: resp.Payload.Round.PoolTx,
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: toRoundStage(*resp.Payload.Round.Stage),
}, nil
}
func (a *restClient) GetSpendableVtxos(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) ([]*client.Vtxo, error) {
allVtxos, err := a.ListVtxos(ctx, addr)
if err != nil {
return nil, err
}
vtxos := make([]*client.Vtxo, 0, len(allVtxos.GetSpendableVtxos()))
for _, v := range allVtxos.GetSpendableVtxos() {
var expireAt *time.Time
if v.ExpireAt > 0 {
t := time.Unix(v.ExpireAt, 0)
expireAt = &t
}
if v.Swept {
continue
}
vtxos = append(vtxos, &client.Vtxo{
Amount: v.Receiver.Amount,
Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout,
RoundTxid: v.PoolTxid,
ExpiresAt: expireAt,
})
}
if explorerSvc == nil {
return vtxos, nil
}
redeemBranches, err := a.GetRedeemBranches(ctx, vtxos, explorerSvc)
if err != nil {
return nil, err
}
for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpiresAt()
if err != nil {
return nil, err
}
for i, vtxo := range vtxos {
if vtxo.Txid == vtxoTxid {
vtxos[i].ExpiresAt = expiration
break
}
}
}
return vtxos, nil
}
func (a *restClient) GetRedeemBranches(
ctx context.Context, vtxos []*client.Vtxo, explorerSvc explorer.Explorer,
) (map[string]*client.RedeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]*client.RedeemBranch, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.RoundTxid]; !ok {
round, err := a.GetRound(ctx, vtxo.RoundTxid)
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.RoundTxid] = congestionTree
}
redeemBranch, err := client.NewRedeemBranch(
explorerSvc, congestionTrees[vtxo.RoundTxid], vtxo,
)
if err != nil {
return nil, err
}
redeemBranches[vtxo.Txid] = redeemBranch
}
return redeemBranches, nil
}
func (a *restClient) GetOffchainBalance(
ctx context.Context, addr string, explorerSvc explorer.Explorer,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := a.GetSpendableVtxos(ctx, addr, explorerSvc)
if err != nil {
return 0, nil, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.Amount
if vtxo.ExpiresAt != nil {
expiration := vtxo.ExpiresAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.Amount
}
}
return balance, amountByExpiration, nil
}
func (a *restClient) Onboard(
ctx context.Context, req *arkv1.OnboardRequest,
) (*arkv1.OnboardResponse, error) {
levels := make([]*models.V1TreeLevel, 0, len(req.GetCongestionTree().GetLevels()))
for _, l := range req.GetCongestionTree().GetLevels() {
nodes := make([]*models.V1Node, 0, len(l.GetNodes()))
for _, n := range l.GetNodes() {
nodes = append(nodes, &models.V1Node{
Txid: n.GetTxid(),
Tx: n.GetTx(),
ParentTxid: n.GetParentTxid(),
})
}
levels = append(levels, &models.V1TreeLevel{
Nodes: nodes,
})
}
congestionTree := models.V1Tree{
Levels: levels,
}
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error {
body := models.V1OnboardRequest{
BoardingTx: req.GetBoardingTx(),
CongestionTree: &congestionTree,
UserPubkey: req.GetUserPubkey(),
BoardingTx: tx,
CongestionTree: treeToProto(congestionTree).parse(),
UserPubkey: userPubkey,
}
_, err := a.svc.ArkServiceOnboard(
ark_service.NewArkServiceOnboardParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.OnboardResponse{}, nil
return err
}
func (a *restClient) RegisterPayment(
ctx context.Context, req *arkv1.RegisterPaymentRequest,
) (*arkv1.RegisterPaymentResponse, error) {
inputs := make([]*models.V1Input, 0, len(req.GetInputs()))
for _, i := range req.GetInputs() {
inputs = append(inputs, &models.V1Input{
Txid: i.GetTxid(),
Vout: int64(i.GetVout()),
ctx context.Context, inputs []client.VtxoKey,
) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs {
ins = append(ins, &models.V1Input{
Txid: i.Txid,
Vout: int64(i.VOut),
})
}
body := models.V1RegisterPaymentRequest{
Inputs: inputs,
Inputs: ins,
}
resp, err := a.svc.ArkServiceRegisterPayment(
ark_service.NewArkServiceRegisterPaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
return "", err
}
return &arkv1.RegisterPaymentResponse{
Id: resp.Payload.ID,
}, nil
return resp.Payload.ID, nil
}
func (a *restClient) ClaimPayment(
ctx context.Context, req *arkv1.ClaimPaymentRequest,
) (*arkv1.ClaimPaymentResponse, error) {
outputs := make([]*models.V1Output, 0, len(req.GetOutputs()))
for _, o := range req.GetOutputs() {
outputs = append(outputs, &models.V1Output{
Address: o.GetAddress(),
Amount: strconv.Itoa(int(o.GetAmount())),
ctx context.Context, paymentID string, outputs []client.Output,
) error {
outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs {
outs = append(outs, &models.V1Output{
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
})
}
body := models.V1ClaimPaymentRequest{
ID: req.GetId(),
Outputs: outputs,
ID: paymentID,
Outputs: outs,
}
_, err := a.svc.ArkServiceClaimPayment(
ark_service.NewArkServiceClaimPaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.ClaimPaymentResponse{}, nil
return err
}
func (a *restClient) Ping(
ctx context.Context, req *arkv1.PingRequest,
) (*arkv1.PingResponse, error) {
ctx context.Context, paymentID string,
) (*client.RoundFinalizationEvent, error) {
r := ark_service.NewArkServicePingParams()
r.SetPaymentID(req.GetPaymentId())
r.SetPaymentID(paymentID)
resp, err := a.svc.ArkServicePing(r)
if err != nil {
return nil, err
}
var event *arkv1.RoundFinalizationEvent
if resp.Payload.Event != nil &&
resp.Payload.Event.ID != "" &&
len(resp.Payload.Event.ForfeitTxs) > 0 &&
len(resp.Payload.Event.CongestionTree.Levels) > 0 &&
len(resp.Payload.Event.Connectors) > 0 &&
resp.Payload.Event.PoolTx != "" {
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Event.CongestionTree.Levels))
for _, l := range resp.Payload.Event.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
}
event = &arkv1.RoundFinalizationEvent{
Id: resp.Payload.Event.ID,
PoolTx: resp.Payload.Event.PoolTx,
var event *client.RoundFinalizationEvent
if resp.Payload.Event != nil {
event = &client.RoundFinalizationEvent{
ID: resp.Payload.Event.ID,
Tx: resp.Payload.Event.PoolTx,
ForfeitTxs: resp.Payload.Event.ForfeitTxs,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
Tree: treeFromProto{resp.Payload.Event.CongestionTree}.parse(),
Connectors: resp.Payload.Event.Connectors,
}
}
return &arkv1.PingResponse{
ForfeitTxs: resp.Payload.ForfeitTxs,
Event: event,
}, nil
return event, nil
}
func (a *restClient) FinalizePayment(
ctx context.Context, req *arkv1.FinalizePaymentRequest,
) (*arkv1.FinalizePaymentResponse, error) {
ctx context.Context, signedForfeitTxs []string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
body := models.V1FinalizePaymentRequest{
SignedForfeitTxs: req.GetSignedForfeitTxs(),
}
_, err := a.svc.ArkServiceFinalizePayment(
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
)
if err != nil {
return nil, err
}
return &arkv1.FinalizePaymentResponse{}, nil
return err
}
func (a *restClient) GetRoundByID(
ctx context.Context, roundID string,
) (*arkv1.GetRoundByIdResponse, error) {
) (*client.Round, error) {
resp, err := a.svc.ArkServiceGetRoundByID(
ark_service.NewArkServiceGetRoundByIDParams().WithID(roundID),
)
@@ -565,36 +378,22 @@ func (a *restClient) GetRoundByID(
return nil, err
}
levels := make([]*arkv1.TreeLevel, 0, len(resp.Payload.Round.CongestionTree.Levels))
for _, l := range resp.Payload.Round.CongestionTree.Levels {
nodes := make([]*arkv1.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
nodes = append(nodes, &arkv1.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &arkv1.TreeLevel{
Nodes: nodes,
})
startedAt := time.Unix(int64(start), 0)
var endedAt *time.Time
if end > 0 {
t := time.Unix(int64(end), 0)
endedAt = &t
}
stage := stageStrToInt(*resp.Payload.Round.Stage)
return &arkv1.GetRoundByIdResponse{
Round: &arkv1.Round{
Id: resp.Payload.Round.ID,
Start: int64(start),
End: int64(end),
PoolTx: resp.Payload.Round.PoolTx,
CongestionTree: &arkv1.Tree{
Levels: levels,
},
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: arkv1.RoundStage(stage),
},
return &client.Round{
ID: resp.Payload.Round.ID,
StartedAt: &startedAt,
EndedAt: endedAt,
Tx: resp.Payload.Round.PoolTx,
Tree: treeFromProto{resp.Payload.Round.CongestionTree}.parse(),
ForfeitTxs: resp.Payload.Round.ForfeitTxs,
Connectors: resp.Payload.Round.Connectors,
Stage: toRoundStage(*resp.Payload.Round.Stage),
}, nil
}
@@ -625,48 +424,83 @@ func newRestClient(
return svc.ArkService, nil
}
func stageStrToInt(stage models.V1RoundStage) int {
func toRoundStage(stage models.V1RoundStage) client.RoundStage {
switch stage {
case models.V1RoundStageROUNDSTAGEUNSPECIFIED:
return 0
case models.V1RoundStageROUNDSTAGEREGISTRATION:
return 1
return client.RoundStageRegistration
case models.V1RoundStageROUNDSTAGEFINALIZATION:
return 2
return client.RoundStageFinalization
case models.V1RoundStageROUNDSTAGEFINALIZED:
return 3
return client.RoundStageFinalized
case models.V1RoundStageROUNDSTAGEFAILED:
return 4
return client.RoundStageFailed
default:
return client.RoundStageUndefined
}
return -1
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
type treeFromProto struct {
*models.V1Tree
}
for _, level := range treeFromProto.Levels {
nodes := make([]tree.Node, 0, len(level.Nodes))
for _, node := range level.Nodes {
nodes = append(nodes, tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: false,
func (t treeFromProto) parse() tree.CongestionTree {
congestionTree := make(tree.CongestionTree, 0, len(t.Levels))
for _, l := range t.Levels {
level := make([]tree.Node, 0, len(l.Nodes))
for _, n := range l.Nodes {
level = append(level, tree.Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, nodes)
congestionTree = append(congestionTree, level)
}
for j, treeLvl := range levels {
for j, treeLvl := range congestionTree {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
if len(congestionTree.Children(node.Txid)) == 0 {
congestionTree[j][i] = tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: true,
}
}
}
}
return levels, nil
return congestionTree
}
type treeToProto tree.CongestionTree
func (t treeToProto) parse() *models.V1Tree {
levels := make([]*models.V1TreeLevel, 0, len(t))
for _, level := range t {
nodes := make([]*models.V1Node, 0, len(level))
for _, n := range level {
nodes = append(nodes, &models.V1Node{
Txid: n.Txid,
Tx: n.Tx,
ParentTxid: n.ParentTxid,
})
}
levels = append(levels, &models.V1TreeLevel{
Nodes: nodes,
})
}
return &models.V1Tree{
Levels: levels,
}
}
func getTxid(tx string) string {
if ptx, _ := psetv2.NewPsetFromBase64(tx); ptx != nil {
utx, _ := ptx.UnsignedTx()
return utx.TxHash().String()
}
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
return ptx.UnsignedTx.TxID()
}