mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-28 15:54:19 +01:00
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:
committed by
GitHub
parent
1c67c56d9d
commit
8de2df3d7f
@@ -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() {}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user