mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-22 05:04:20 +01:00
Add reversible policy to pending vtxos (#311)
* [server] descriptor-based vtxo script * [server] fix unit tests * [sdk] descriptor based vtxo * empty config check & version flag support * fix: empty config check & version flag support (#309) * fix * [sdk] several fixes * [sdk][server] several fixes * [common][sdk] add reversible VtxoScript type, use it in async payment * [common] improve parser * [common] fix reversible vtxo parser * [sdk] remove logs * fix forfeit map * remove debug log * [sdk] do not allow reversible vtxo script in case of self-transfer * remove signing pubkey * remove signer public key, craft forfeit txs client side * go work sync * fix linter errors * rename MakeForfeitTxs to BuildForfeitTxs * fix conflicts * fix tests * comment VtxoScript type * revert ROUND_INTERVAL value --------- Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com> Co-authored-by: sekulicd <sekula87@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +38,7 @@ type ASPClient interface {
|
||||
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
|
||||
) error
|
||||
CreatePayment(
|
||||
ctx context.Context, inputs []VtxoKey, outputs []Output,
|
||||
ctx context.Context, inputs []Input, outputs []Output,
|
||||
) (string, []string, error)
|
||||
CompletePayment(
|
||||
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
|
||||
@@ -67,40 +68,19 @@ type RoundEventChannel struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type Input interface {
|
||||
GetTxID() string
|
||||
GetVOut() uint32
|
||||
GetDescriptor() string
|
||||
}
|
||||
|
||||
type VtxoKey struct {
|
||||
type Outpoint struct {
|
||||
Txid string
|
||||
VOut uint32
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetTxID() string {
|
||||
return k.Txid
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetVOut() uint32 {
|
||||
return k.VOut
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetDescriptor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type BoardingInput struct {
|
||||
VtxoKey
|
||||
type Input struct {
|
||||
Outpoint
|
||||
Descriptor string
|
||||
}
|
||||
|
||||
func (k BoardingInput) GetDescriptor() string {
|
||||
return k.Descriptor
|
||||
}
|
||||
|
||||
type Vtxo struct {
|
||||
VtxoKey
|
||||
Outpoint
|
||||
Descriptor string
|
||||
Amount uint64
|
||||
RoundTxid string
|
||||
ExpiresAt *time.Time
|
||||
@@ -111,8 +91,9 @@ type Vtxo struct {
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Address string
|
||||
Amount uint64
|
||||
Address string // onchain output address
|
||||
Descriptor string // offchain vtxo descriptor
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
type RoundStage int
|
||||
@@ -152,11 +133,11 @@ type Round struct {
|
||||
}
|
||||
|
||||
type RoundFinalizationEvent struct {
|
||||
ID string
|
||||
Tx string
|
||||
ForfeitTxs []string
|
||||
Tree tree.CongestionTree
|
||||
Connectors []string
|
||||
ID string
|
||||
Tx string
|
||||
Tree tree.CongestionTree
|
||||
Connectors []string
|
||||
MinRelayFeeRate chainfee.SatPerKVByte
|
||||
}
|
||||
|
||||
func (e RoundFinalizationEvent) isRoundEvent() {}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -202,15 +203,10 @@ func (a *grpcClient) FinalizePayment(
|
||||
}
|
||||
|
||||
func (a *grpcClient) CreatePayment(
|
||||
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
|
||||
ctx context.Context, inputs []client.Input, outputs []client.Output,
|
||||
) (string, []string, error) {
|
||||
insCast := make([]client.Input, 0, len(inputs))
|
||||
for _, in := range inputs {
|
||||
insCast = append(insCast, in)
|
||||
}
|
||||
|
||||
req := &arkv1.CreatePaymentRequest{
|
||||
Inputs: ins(insCast).toProto(),
|
||||
Inputs: ins(inputs).toProto(),
|
||||
Outputs: outs(outputs).toProto(),
|
||||
}
|
||||
resp, err := a.svc.CreatePayment(ctx, req)
|
||||
@@ -323,9 +319,20 @@ func (a *grpcClient) SendTreeSignatures(
|
||||
type out client.Output
|
||||
|
||||
func (o out) toProto() *arkv1.Output {
|
||||
if len(o.Address) > 0 {
|
||||
return &arkv1.Output{
|
||||
Destination: &arkv1.Output_Address{
|
||||
Address: o.Address,
|
||||
},
|
||||
Amount: o.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
return &arkv1.Output{
|
||||
Address: o.Address,
|
||||
Amount: o.Amount,
|
||||
Destination: &arkv1.Output_Descriptor_{
|
||||
Descriptor_: o.Descriptor,
|
||||
},
|
||||
Amount: o.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,11 +369,11 @@ func (e event) toRoundEvent() (client.RoundEvent, error) {
|
||||
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(),
|
||||
ID: ee.GetId(),
|
||||
Tx: ee.GetPoolTx(),
|
||||
Tree: tree,
|
||||
Connectors: ee.GetConnectors(),
|
||||
MinRelayFeeRate: chainfee.SatPerKVByte(ee.MinRelayFeeRate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -430,17 +437,18 @@ func (v vtxo) toVtxo() client.Vtxo {
|
||||
uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs()
|
||||
}
|
||||
return client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.GetOutpoint().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVout(),
|
||||
},
|
||||
Amount: v.GetReceiver().GetAmount(),
|
||||
Amount: v.GetAmount(),
|
||||
RoundTxid: v.GetPoolTxid(),
|
||||
ExpiresAt: expiresAt,
|
||||
Pending: v.GetPending(),
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.GetSpentBy(),
|
||||
Descriptor: v.GetDescriptor_(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,25 +463,12 @@ func (v vtxos) toVtxos() []client.Vtxo {
|
||||
}
|
||||
|
||||
func toProtoInput(i client.Input) *arkv1.Input {
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
return &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
Descriptor_: i.GetDescriptor(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
},
|
||||
Outpoint: &arkv1.Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: i.VOut,
|
||||
},
|
||||
Descriptor_: i.Descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
httptransport "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
type restClient struct {
|
||||
@@ -146,7 +147,7 @@ func (a *restClient) ListVtxos(
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
amount, err := strconv.Atoi(v.Receiver.Amount)
|
||||
amount, err := strconv.Atoi(v.Amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -159,9 +160,9 @@ func (a *restClient) ListVtxos(
|
||||
}
|
||||
|
||||
spendableVtxos = append(spendableVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
@@ -170,6 +171,7 @@ func (a *restClient) ListVtxos(
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.SpentBy,
|
||||
Descriptor: v.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,20 +187,21 @@ func (a *restClient) ListVtxos(
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
amount, err := strconv.Atoi(v.Receiver.Amount)
|
||||
amount, err := strconv.Atoi(v.Amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
spentVtxos = append(spentVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
ExpiresAt: expiresAt,
|
||||
SpentBy: v.SpentBy,
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
ExpiresAt: expiresAt,
|
||||
SpentBy: v.SpentBy,
|
||||
Descriptor: v.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,26 +252,13 @@ func (a *restClient) RegisterPayment(
|
||||
) (string, error) {
|
||||
ins := make([]*models.V1Input, 0, len(inputs))
|
||||
for _, i := range inputs {
|
||||
var input *models.V1Input
|
||||
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
input = &models.V1Input{
|
||||
BoardingInput: &models.V1BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
Descriptor: i.GetDescriptor(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
input = &models.V1Input{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ins = append(ins, input)
|
||||
ins = append(ins, &models.V1Input{
|
||||
Outpoint: &models.V1Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Descriptor,
|
||||
})
|
||||
}
|
||||
body := &models.V1RegisterPaymentRequest{
|
||||
Inputs: ins,
|
||||
@@ -328,12 +318,18 @@ func (a *restClient) Ping(
|
||||
}
|
||||
if e := payload.RoundFinalization; e != nil {
|
||||
tree := treeFromProto{e.CongestionTree}.parse()
|
||||
|
||||
minRelayFeeRate, err := strconv.Atoi(e.MinRelayFeeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RoundFinalizationEvent{
|
||||
ID: e.ID,
|
||||
Tx: e.PoolTx,
|
||||
ForfeitTxs: e.ForfeitTxs,
|
||||
Tree: tree,
|
||||
Connectors: e.Connectors,
|
||||
ID: e.ID,
|
||||
Tx: e.PoolTx,
|
||||
Tree: tree,
|
||||
Connectors: e.Connectors,
|
||||
MinRelayFeeRate: chainfee.SatPerKVByte(minRelayFeeRate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -394,26 +390,24 @@ func (a *restClient) FinalizePayment(
|
||||
}
|
||||
|
||||
func (a *restClient) CreatePayment(
|
||||
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
|
||||
ctx context.Context, inputs []client.Input, outputs []client.Output,
|
||||
) (string, []string, error) {
|
||||
ins := make([]*models.V1Input, 0, len(inputs))
|
||||
for _, i := range inputs {
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
return "", nil, fmt.Errorf("boarding inputs are not allowed in create payment")
|
||||
}
|
||||
|
||||
ins = append(ins, &models.V1Input{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Outpoint: &models.V1Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Descriptor,
|
||||
})
|
||||
}
|
||||
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)),
|
||||
Address: o.Address,
|
||||
Amount: strconv.Itoa(int(o.Amount)),
|
||||
Descriptor: o.Descriptor,
|
||||
})
|
||||
}
|
||||
body := models.V1CreatePaymentRequest{
|
||||
|
||||
@@ -18,22 +18,18 @@ import (
|
||||
// swagger:model v1Input
|
||||
type V1Input struct {
|
||||
|
||||
// boarding input
|
||||
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
|
||||
// descriptor
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
|
||||
// vtxo input
|
||||
VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"`
|
||||
// outpoint
|
||||
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 input
|
||||
func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validateBoardingInput(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateVtxoInput(formats); err != nil {
|
||||
if err := m.validateOutpoint(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
@@ -43,36 +39,17 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.BoardingInput) { // not required
|
||||
func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.Outpoint) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.BoardingInput != nil {
|
||||
if err := m.BoardingInput.Validate(formats); err != nil {
|
||||
if m.Outpoint != nil {
|
||||
if err := m.Outpoint.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("boardingInput")
|
||||
return ve.ValidateName("outpoint")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.VtxoInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.VtxoInput != nil {
|
||||
if err := m.VtxoInput.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("vtxoInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("vtxoInput")
|
||||
return ce.ValidateName("outpoint")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -85,11 +62,7 @@ func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
|
||||
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.contextValidateBoardingInput(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateVtxoInput(ctx, formats); err != nil {
|
||||
if err := m.contextValidateOutpoint(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
@@ -99,40 +72,19 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) contextValidateBoardingInput(ctx context.Context, formats strfmt.Registry) error {
|
||||
func (m *V1Input) contextValidateOutpoint(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.BoardingInput != nil {
|
||||
if m.Outpoint != nil {
|
||||
|
||||
if swag.IsZero(m.BoardingInput) { // not required
|
||||
if swag.IsZero(m.Outpoint) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.BoardingInput.ContextValidate(ctx, formats); err != nil {
|
||||
if err := m.Outpoint.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("boardingInput")
|
||||
return ve.ValidateName("outpoint")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) contextValidateVtxoInput(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.VtxoInput != nil {
|
||||
|
||||
if swag.IsZero(m.VtxoInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.VtxoInput.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("vtxoInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("vtxoInput")
|
||||
return ce.ValidateName("outpoint")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
53
pkg/client-sdk/client/rest/service/models/v1_outpoint.go
Normal file
53
pkg/client-sdk/client/rest/service/models/v1_outpoint.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// V1Outpoint v1 outpoint
|
||||
//
|
||||
// swagger:model v1Outpoint
|
||||
type V1Outpoint struct {
|
||||
|
||||
// txid
|
||||
Txid string `json:"txid,omitempty"`
|
||||
|
||||
// vout
|
||||
Vout int64 `json:"vout,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 outpoint
|
||||
func (m *V1Outpoint) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validates this v1 outpoint based on context it is used
|
||||
func (m *V1Outpoint) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *V1Outpoint) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *V1Outpoint) UnmarshalBinary(b []byte) error {
|
||||
var res V1Outpoint
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
||||
@@ -17,11 +17,14 @@ import (
|
||||
// swagger:model v1Output
|
||||
type V1Output struct {
|
||||
|
||||
// Either the offchain or onchain address.
|
||||
// onchain
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// Amount to send in satoshis.
|
||||
Amount string `json:"amount,omitempty"`
|
||||
|
||||
// offchain
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 output
|
||||
|
||||
@@ -24,12 +24,12 @@ type V1RoundFinalizationEvent struct {
|
||||
// connectors
|
||||
Connectors []string `json:"connectors"`
|
||||
|
||||
// forfeit txs
|
||||
ForfeitTxs []string `json:"forfeitTxs"`
|
||||
|
||||
// id
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// min relay fee rate
|
||||
MinRelayFeeRate string `json:"minRelayFeeRate,omitempty"`
|
||||
|
||||
// pool tx
|
||||
PoolTx string `json:"poolTx,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ import (
|
||||
// swagger:model v1Vtxo
|
||||
type V1Vtxo struct {
|
||||
|
||||
// amount
|
||||
Amount string `json:"amount,omitempty"`
|
||||
|
||||
// descriptor
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
|
||||
// expire at
|
||||
ExpireAt string `json:"expireAt,omitempty"`
|
||||
|
||||
// outpoint
|
||||
Outpoint *V1Input `json:"outpoint,omitempty"`
|
||||
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
|
||||
|
||||
// pending
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
@@ -33,9 +39,6 @@ type V1Vtxo struct {
|
||||
// pool txid
|
||||
PoolTxid string `json:"poolTxid,omitempty"`
|
||||
|
||||
// receiver
|
||||
Receiver *V1Output `json:"receiver,omitempty"`
|
||||
|
||||
// spent
|
||||
Spent bool `json:"spent,omitempty"`
|
||||
|
||||
@@ -58,10 +61,6 @@ func (m *V1Vtxo) Validate(formats strfmt.Registry) error {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateReceiver(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -106,25 +105,6 @@ func (m *V1Vtxo) validatePendingData(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Vtxo) validateReceiver(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.Receiver) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.Receiver != nil {
|
||||
if err := m.Receiver.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("receiver")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("receiver")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validate this v1 vtxo based on the context it is used
|
||||
func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
@@ -137,10 +117,6 @@ func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) e
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateReceiver(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -189,27 +165,6 @@ func (m *V1Vtxo) contextValidatePendingData(ctx context.Context, formats strfmt.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Vtxo) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.Receiver != nil {
|
||||
|
||||
if swag.IsZero(m.Receiver) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.Receiver.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("receiver")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("receiver")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *V1Vtxo) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
|
||||
@@ -226,7 +226,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
return vtxos{}, nil, err
|
||||
}
|
||||
spendable[i] = client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: vtxo.Outpoint.Txid,
|
||||
VOut: vtxo.Outpoint.Vout,
|
||||
},
|
||||
@@ -251,7 +251,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
return vtxos{}, nil, err
|
||||
}
|
||||
spent[i] = client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: vtxo.Outpoint.Txid,
|
||||
VOut: vtxo.Outpoint.Vout,
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -23,9 +22,11 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
type liquidReceiver struct {
|
||||
@@ -435,18 +436,27 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receivers = append(receivers, client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -460,7 +470,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receivers,
|
||||
ctx, paymentID, selectedCoins, nil, "", receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -500,14 +510,22 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
|
||||
return "", fmt.Errorf("no funds to claim")
|
||||
}
|
||||
|
||||
receiver := client.Output{
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
receiver := client.Output{
|
||||
Descriptor: desc,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc)
|
||||
return a.selfTransferAllPendingPayments(
|
||||
ctx,
|
||||
boardingUtxos,
|
||||
receiver,
|
||||
hex.EncodeToString(mypubkey.SerializeCompressed()),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
|
||||
@@ -577,14 +595,18 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]ex
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
@@ -800,9 +822,14 @@ func (a *covenantArkClient) sendOffchain(
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receiversOutput = append(receiversOutput, client.Output{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -828,18 +855,27 @@ func (a *covenantArkClient) sendOffchain(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -859,7 +895,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput,
|
||||
ctx, paymentID, selectedCoins, nil, "", receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -900,16 +936,46 @@ func (a *covenantArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
|
||||
)
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
Owner: userPubkey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: utxo.Delay,
|
||||
}
|
||||
|
||||
forfeitClosure := &tree.MultisigClosure{
|
||||
Pubkey: userPubkey,
|
||||
AspPubkey: aspPubkey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, taprootTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
leafProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controlBlock, err := taproot.ParseControlBlock(leafProof.Script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inputIndex := len(updater.Pset.Inputs) - 1
|
||||
|
||||
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
|
||||
if err := updater.AddInTapLeafScript(
|
||||
inputIndex,
|
||||
psetv2.TapLeafScript{
|
||||
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
|
||||
ControlBlock: *controlBlock,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -921,7 +987,8 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) (string, error) {
|
||||
eventsCh, err := a.client.GetEventStream(ctx, paymentID)
|
||||
@@ -955,7 +1022,7 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -979,30 +1046,104 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
if err = a.validateCongestionTree(event, receivers); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(vtxos) > 0 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
)
|
||||
signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if len(boardingUtxos) > 0 {
|
||||
boardingVtxoScript, err := tree.ParseVtxoScript(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
roundPtx, err := psetv2.NewPsetFromBase64(event.Tx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// add tapscript leaf
|
||||
forfeitClosure := &tree.MultisigClosure{
|
||||
Pubkey: myPubkey,
|
||||
AspPubkey: a.AspPubkey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
_, taprootTree, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
ctrlBlock, err := taproot.ParseControlBlock(forfeitProof.ControlBlock)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
tapscript := psetv2.TapLeafScript{
|
||||
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(forfeitProof.Script),
|
||||
ControlBlock: *ctrlBlock,
|
||||
}
|
||||
|
||||
updater, err := psetv2.NewUpdater(roundPtx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
for i, input := range updater.Pset.Inputs {
|
||||
for _, boardingUtxo := range boardingUtxos {
|
||||
if chainhash.Hash(input.PreviousTxid).String() == boardingUtxo.Txid && boardingUtxo.Vout == input.PreviousTxIndex {
|
||||
if err := updater.AddInTapLeafScript(i, tapscript); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b64, err := updater.Pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return signedForfeits, signedRoundTx, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) validateCongestionTree(
|
||||
@@ -1016,7 +1157,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
||||
|
||||
connectors := event.Connectors
|
||||
|
||||
if !utils.IsLiquidOnchainOnly(receivers) {
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
if err := tree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1029,7 +1170,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
||||
}
|
||||
|
||||
if err := a.validateReceivers(
|
||||
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
|
||||
ptx, receivers, event.Tree,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1043,14 +1184,13 @@ func (a *covenantArkClient) validateReceivers(
|
||||
ptx *psetv2.Pset,
|
||||
receivers []client.Output,
|
||||
congestionTree tree.CongestionTree,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress(
|
||||
isOnChain, onchainScript, err := utils.ParseLiquidAddress(
|
||||
receiver.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
@@ -1059,7 +1199,7 @@ func (a *covenantArkClient) validateReceivers(
|
||||
}
|
||||
} else {
|
||||
if err := a.validateOffChainReceiver(
|
||||
congestionTree, receiver, userPubkey, aspPubkey,
|
||||
congestionTree, receiver,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1095,13 +1235,15 @@ func (a *covenantArkClient) validateOnChainReceiver(
|
||||
func (a *covenantArkClient) validateOffChainReceiver(
|
||||
congestionTree tree.CongestionTree,
|
||||
receiver client.Output,
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
found := false
|
||||
net := utils.ToElementsNetwork(a.Network)
|
||||
outputTapKey, _, _, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
|
||||
)
|
||||
|
||||
receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputTapKey, _, err := receiverVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1136,36 +1278,105 @@ func (a *covenantArkClient) validateOffChainReceiver(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) loopAndSign(
|
||||
func (a *covenantArkClient) createAndSignForfeits(
|
||||
ctx context.Context,
|
||||
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
myPubKey *secp256k1.PublicKey,
|
||||
) ([]string, error) {
|
||||
signedForfeits := make([]string, 0)
|
||||
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
utx, _ := p.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, forfeitTx := range forfeitTxs {
|
||||
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
|
||||
p, err := psetv2.NewPsetFromBase64(connector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
inputTxid := chainhash.Hash(input.PreviousTxid).String()
|
||||
for _, coin := range vtxosToSign {
|
||||
if inputTxid == coin.Txid {
|
||||
signedPset, err := a.signForfeitTx(ctx, forfeitTx, pset, connectorsTxids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
connectorsPsets = append(connectorsPsets, p)
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxoInput := psetv2.InputArgs{
|
||||
Txid: vtxo.Txid,
|
||||
TxIndex: vtxo.VOut,
|
||||
}
|
||||
|
||||
forfeitClosure := &tree.MultisigClosure{
|
||||
Pubkey: myPubKey,
|
||||
AspPubkey: a.AspPubkey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctrlBlock, err := taproot.ParseControlBlock(leafProof.ControlBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapscript := psetv2.TapLeafScript{
|
||||
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
|
||||
ControlBlock: *ctrlBlock,
|
||||
}
|
||||
|
||||
for _, connectorPset := range connectorsPsets {
|
||||
forfeits, err := tree.BuildForfeitTxs(
|
||||
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
updater, err := psetv2.NewUpdater(forfeit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := updater.AddInTapLeafScript(1, tapscript); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b64, err := updater.Pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedForfeit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1173,24 +1384,6 @@ func (a *covenantArkClient) loopAndSign(
|
||||
return signedForfeits, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) signForfeitTx(
|
||||
ctx context.Context, txStr string, tx *psetv2.Pset, connectorsTxids []string,
|
||||
) (string, error) {
|
||||
connectorTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String()
|
||||
connectorFound := false
|
||||
for _, id := range connectorsTxids {
|
||||
if id == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !connectorFound {
|
||||
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
|
||||
return a.wallet.SignTransaction(ctx, a.explorer, txStr)
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) coinSelectOnchain(
|
||||
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
|
||||
) ([]explorer.Utxo, uint64, error) {
|
||||
@@ -1208,14 +1401,17 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -1387,13 +1583,17 @@ func (a *covenantArkClient) getVtxos(
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
ctx context.Context, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
|
||||
ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
|
||||
) (string, error) {
|
||||
inputs := make([]client.Input, 0, len(boardingUtxo))
|
||||
inputs := make([]client.Input, 0, len(boardingUtxos))
|
||||
|
||||
for _, utxo := range boardingUtxo {
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
boardingDescriptor := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
|
||||
)
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
@@ -1413,7 +1613,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
roundTxid, err := a.handleRoundStream(
|
||||
ctx, paymentID, make([]client.Vtxo, 0), len(boardingUtxo) > 0, outputs,
|
||||
ctx, paymentID, make([]client.Vtxo, 0), boardingUtxos, boardingDescriptor, outputs,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1422,6 +1622,21 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
return roundTxid, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
|
||||
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vtxoScript := tree.DefaultVtxoScript{
|
||||
Owner: userPubKey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: uint(a.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
return vtxoScript.ToDescriptor(), nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
|
||||
utxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -425,18 +425,27 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receivers = append(receivers, client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,7 +468,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, nil, "", receivers, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -478,7 +487,7 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||
isOnchain, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -516,9 +525,27 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
|
||||
}
|
||||
|
||||
isSelfTransfer := offchainAddrs[0] == receiver.To()
|
||||
|
||||
var desc string
|
||||
|
||||
// reversible vtxo does not make sense for self transfer
|
||||
// if the receiver is the same as the sender, handle the output like the change
|
||||
if !isSelfTransfer {
|
||||
desc, err = a.offchainAddressToReversibleVtxoDescriptor(offchainAddrs[0], receiver.To())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
desc, err = a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
receiversOutput = append(receiversOutput, client.Output{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -535,17 +562,28 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
}
|
||||
|
||||
if changeAmount > 0 {
|
||||
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddrs[0],
|
||||
Amount: changeAmount,
|
||||
Descriptor: changeDesc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, coin.VtxoKey)
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
redeemTx, unconditionalForfeitTxs, err := a.client.CreatePayment(
|
||||
@@ -556,23 +594,13 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
|
||||
// TODO verify the redeem tx signature
|
||||
|
||||
signedUnconditionalForfeitTxs := make([]string, 0, len(unconditionalForfeitTxs))
|
||||
for _, tx := range unconditionalForfeitTxs {
|
||||
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, tx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
|
||||
}
|
||||
|
||||
signedRedeemTx, err := a.wallet.SignTransaction(ctx, a.explorer, redeemTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.client.CompletePayment(
|
||||
ctx, signedRedeemTx, signedUnconditionalForfeitTxs,
|
||||
ctx, signedRedeemTx, unconditionalForfeitTxs,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -612,13 +640,23 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
receiver := client.Output{
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
|
||||
receiver := client.Output{
|
||||
Descriptor: desc,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
return a.selfTransferAllPendingPayments(
|
||||
ctx,
|
||||
pendingVtxos,
|
||||
boardingUtxos,
|
||||
receiver,
|
||||
hex.EncodeToString(mypubkey.SerializeCompressed()),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
|
||||
@@ -818,9 +856,14 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receiversOutput = append(receiversOutput, client.Output{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -846,18 +889,27 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -882,7 +934,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, nil, "", receiversOutput, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -926,25 +978,38 @@ func (a *covenantlessArkClient) addInputs(
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
vtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Owner: userPubkey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: utxo.Delay,
|
||||
}
|
||||
|
||||
exitClosure := &bitcointree.CSVSigClosure{
|
||||
Pubkey: userPubkey,
|
||||
Seconds: uint(utxo.Delay),
|
||||
}
|
||||
|
||||
exitLeaf, err := exitClosure.Leaf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
_, taprootTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get taproot merkle proof: %s", err)
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
ControlBlock: leafProof.ControlBlock,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -957,7 +1022,8 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
roundEphemeralKey *secp256k1.PrivateKey,
|
||||
) (string, error) {
|
||||
@@ -1033,7 +1099,7 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1134,30 +1200,103 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) ([]string, string, error) {
|
||||
if err := a.validateCongestionTree(event, receivers); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
|
||||
}
|
||||
|
||||
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var forfeits []string
|
||||
|
||||
if len(vtxos) > 0 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
signedForfeits, err := a.createAndSignForfeits(
|
||||
ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeits = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if len(boardingUtxos) > 0 {
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
roundPtx, err := psbt.NewFromRawBytes(strings.NewReader(event.Tx), true)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// add tapscript leaf
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: myPubkey,
|
||||
AspPubkey: a.AspPubkey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
_, taprootTree, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get taproot merkle proof for boarding utxo: %s", err)
|
||||
}
|
||||
|
||||
tapscript := &psbt.TaprootTapLeafScript{
|
||||
ControlBlock: forfeitProof.ControlBlock,
|
||||
Script: forfeitProof.Script,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
}
|
||||
|
||||
for i := range roundPtx.Inputs {
|
||||
previousOutpoint := roundPtx.UnsignedTx.TxIn[i].PreviousOutPoint
|
||||
|
||||
for _, boardingUtxo := range boardingUtxos {
|
||||
if boardingUtxo.Txid == previousOutpoint.Hash.String() && boardingUtxo.Vout == previousOutpoint.Index {
|
||||
roundPtx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapscript}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b64, err := roundPtx.B64Encode()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return forfeits, signedRoundTx, nil
|
||||
}
|
||||
|
||||
return
|
||||
return forfeits, "", nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) validateCongestionTree(
|
||||
@@ -1169,8 +1308,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
||||
return err
|
||||
}
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1183,7 +1321,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
||||
// }
|
||||
|
||||
if err := a.validateReceivers(
|
||||
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
|
||||
ptx, receivers, event.Tree,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1197,15 +1335,14 @@ func (a *covenantlessArkClient) validateReceivers(
|
||||
ptx *psbt.Packet,
|
||||
receivers []client.Output,
|
||||
congestionTree tree.CongestionTree,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress(
|
||||
isOnChain, onchainScript, err := utils.ParseBitcoinAddress(
|
||||
receiver.Address, netParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
@@ -1214,7 +1351,7 @@ func (a *covenantlessArkClient) validateReceivers(
|
||||
}
|
||||
} else {
|
||||
if err := a.validateOffChainReceiver(
|
||||
congestionTree, receiver, userPubkey, aspPubkey,
|
||||
congestionTree, receiver,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1250,12 +1387,15 @@ func (a *covenantlessArkClient) validateOnChainReceiver(
|
||||
func (a *covenantlessArkClient) validateOffChainReceiver(
|
||||
congestionTree tree.CongestionTree,
|
||||
receiver client.Output,
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
found := false
|
||||
outputTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
|
||||
)
|
||||
|
||||
receiverVtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputTapKey, _, err := receiverVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1298,57 +1438,107 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) loopAndSign(
|
||||
func (a *covenantlessArkClient) createAndSignForfeits(
|
||||
ctx context.Context,
|
||||
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
myPubkey *secp256k1.PublicKey,
|
||||
) ([]string, error) {
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
signedForfeits := make([]string, 0)
|
||||
connectorsPsets := make([]*psbt.Packet, 0, len(connectors))
|
||||
|
||||
for _, connector := range connectors {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txid := ptx.UnsignedTx.TxHash().String()
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
|
||||
connectorsPsets = append(connectorsPsets, p)
|
||||
}
|
||||
|
||||
for _, forfeitTx := range forfeitTxs {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, input := range ptx.UnsignedTx.TxIn {
|
||||
inputTxid := input.PreviousOutPoint.Hash.String()
|
||||
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, coin := range vtxosToSign {
|
||||
// check if it contains one of the input to sign
|
||||
if inputTxid == coin.Txid {
|
||||
// verify that the connector is in the connectors list
|
||||
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
|
||||
connectorFound := false
|
||||
for _, txid := range connectorsTxids {
|
||||
if txid == connectorTxid {
|
||||
connectorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return nil, fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, forfeitTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedForfeitTx)
|
||||
vtxoInput := &wire.OutPoint{
|
||||
Hash: *vtxoTxHash,
|
||||
Index: vtxo.VOut,
|
||||
}
|
||||
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: myPubkey,
|
||||
AspPubkey: a.AspPubkey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapscript := psbt.TaprootTapLeafScript{
|
||||
ControlBlock: leafProof.ControlBlock,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
}
|
||||
|
||||
for _, connectorPset := range connectorsPsets {
|
||||
forfeits, err := bitcointree.BuildForfeitTxs(
|
||||
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(forfeits) <= 0 {
|
||||
return nil, fmt.Errorf("no forfeit txs created dust = %d", a.Dust)
|
||||
}
|
||||
|
||||
for _, forfeit := range forfeits {
|
||||
forfeit.Inputs[1].TaprootLeafScript = []*psbt.TaprootTapLeafScript{&tapscript}
|
||||
|
||||
b64, err := forfeit.B64Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedForfeit)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return signedForfeits, nil
|
||||
@@ -1371,14 +1561,18 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -1567,14 +1761,18 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
claimable := make([]explorer.Utxo, 0)
|
||||
@@ -1644,17 +1842,27 @@ func (a *covenantlessArkClient) getVtxos(
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
|
||||
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
|
||||
) (string, error) {
|
||||
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo))
|
||||
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos))
|
||||
|
||||
boardingDescriptor := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
|
||||
)
|
||||
|
||||
for _, coin := range pendingVtxos {
|
||||
inputs = append(inputs, coin.VtxoKey)
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxo {
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
for _, utxo := range boardingUtxos {
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
@@ -1682,7 +1890,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
roundTxid, err := a.handleRoundStream(
|
||||
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
|
||||
ctx, paymentID, pendingVtxos, boardingUtxos, boardingDescriptor, outputs, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1691,6 +1899,42 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
return roundTxid, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) offchainAddressToReversibleVtxoDescriptor(myaddr string, receiveraddr string) (string, error) {
|
||||
_, receiverPubkey, aspPubkey, err := common.DecodeAddress(receiveraddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, userPubKey, _, err := common.DecodeAddress(myaddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vtxoScript := bitcointree.ReversibleVtxoScript{
|
||||
Owner: receiverPubkey,
|
||||
Sender: userPubKey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: uint(a.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
return vtxoScript.ToDescriptor(), nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
|
||||
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vtxoScript := bitcointree.DefaultVtxoScript{
|
||||
Owner: userPubKey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: uint(a.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
return vtxoScript.ToDescriptor(), nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
|
||||
utxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-openapi/swag v0.23.0
|
||||
github.com/go-openapi/validate v0.24.0
|
||||
github.com/lightningnetwork/lnd v0.18.2-beta
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/vulpemventures/go-elements v0.5.4
|
||||
@@ -38,6 +39,7 @@ require (
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/containerd/continuity v0.4.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
@@ -45,6 +47,7 @@ require (
|
||||
github.com/decred/dcrd/lru v1.1.3 // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
@@ -52,16 +55,18 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/loads v0.22.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jessevdk/go-flags v1.6.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jrick/logrotate v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||
github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect
|
||||
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
|
||||
github.com/lightningnetwork/lnd v0.18.2-beta // indirect
|
||||
github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
|
||||
github.com/lightningnetwork/lnd/fn v1.2.1 // indirect
|
||||
github.com/lightningnetwork/lnd/queue v1.1.1 // indirect
|
||||
@@ -74,11 +79,13 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.15 // indirect
|
||||
|
||||
@@ -93,6 +93,7 @@ github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRiz
|
||||
github.com/fergusstrange/embedded-postgres v1.28.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -263,16 +264,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@@ -432,6 +436,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -67,60 +67,34 @@ func CoinSelect(
|
||||
}
|
||||
|
||||
func ParseLiquidAddress(addr string) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
bool, []byte, error,
|
||||
) {
|
||||
outputScript, err := address.ToOutputScript(addr)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, outputScript, nil, nil
|
||||
return true, outputScript, nil
|
||||
}
|
||||
|
||||
func ParseBitcoinAddress(addr string, net chaincfg.Params) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
bool, []byte, error,
|
||||
) {
|
||||
btcAddr, err := btcutil.DecodeAddress(addr, &net)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
onchainScript, err := txscript.PayToAddrScript(btcAddr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
return false, nil, err
|
||||
}
|
||||
return true, onchainScript, nil, nil
|
||||
return true, onchainScript, nil
|
||||
}
|
||||
|
||||
func IsBitcoinOnchainOnly(receivers []client.Output, net chaincfg.Params) bool {
|
||||
func IsOnchainOnly(receivers []client.Output) bool {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, _, _, err := ParseBitcoinAddress(receiver.Address, net)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isOnChain {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func IsLiquidOnchainOnly(receivers []client.Output) bool {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, _, _, err := ParseLiquidAddress(receiver.Address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
isOnChain := len(receiver.Address) > 0
|
||||
|
||||
if !isOnChain {
|
||||
return false
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||
@@ -219,9 +218,13 @@ func (w *bitcoinWallet) getAddress(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(data.Network)
|
||||
|
||||
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
|
||||
)
|
||||
defaultVtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Asp: data.AspPubkey,
|
||||
Owner: w.walletData.Pubkey,
|
||||
ExitDelay: uint(data.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
@@ -239,19 +242,12 @@ func (w *bitcoinWallet) getAddress(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout,
|
||||
)
|
||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -165,7 +165,7 @@ func (s *liquidWallet) SignTransaction(
|
||||
switch c := closure.(type) {
|
||||
case *tree.CSVSigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
|
||||
case *tree.ForfeitClosure:
|
||||
case *tree.MultisigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
|
||||
}
|
||||
|
||||
@@ -242,9 +242,23 @@ func (w *liquidWallet) getAddress(
|
||||
|
||||
liquidNet := utils.ToElementsNetwork(data.Network)
|
||||
|
||||
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
|
||||
)
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
Owner: w.walletData.Pubkey,
|
||||
Asp: data.AspPubkey,
|
||||
ExitDelay: uint(data.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
vtxoP2tr, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
redemptionAddr, err := vtxoP2tr.TaprootAddress()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
@@ -254,19 +268,22 @@ func (w *liquidWallet) getAddress(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
tapKey, _, err := onboardingScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet,
|
||||
)
|
||||
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingAddr, err := p2tr.TaprootAddress()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user