New boarding protocol (#279)

* [domain] add reverse boarding inputs in Payment struct

* [tx-builder] support reverse boarding script

* [wallet] add GetTransaction

* [api-spec][application] add reverse boarding support in covenantless

* [config] add reverse boarding config

* [api-spec] add ReverseBoardingAddress RPC

* [domain][application] support empty forfeits txs in EndFinalization events

* [tx-builder] optional connector output in round tx

* [btc-embedded] fix getTx and taproot finalizer

* whitelist ReverseBoardingAddress RPC

* [test] add reverse boarding integration test

* [client] support reverse boarding

* [sdk] support reverse boarding

* [e2e] add sleep time after faucet

* [test] run using bitcoin-core RPC

* [tx-builder] fix GetSweepInput

* [application][tx-builder] support reverse onboarding in covenant

* [cli] support reverse onboarding in covenant CLI

* [test] rework integration tests

* [sdk] remove onchain wallet, replace by onboarding address

* remove old onboarding protocols

* [sdk] Fix RegisterPayment

* [e2e] add more funds to covenant ASP

* [e2e] add sleeping time

* several fixes

* descriptor boarding

* remove boarding delay from info

* [sdk] implement descriptor boarding

* go mod tidy

* fixes and revert error msgs

* move descriptor pkg to common

* add replace in go.mod

* [sdk] fix unit tests

* rename DescriptorInput --> BoardingInput

* genrest in SDK

* remove boarding input from domain

* remove all "reverse boarding"

* rename "onboarding" ==> "boarding"

* remove outdate payment unit test

* use tmpfs docker volument for compose testing files

* several fixes
This commit is contained in:
Louis Singer
2024-09-04 19:21:26 +02:00
committed by GitHub
parent 8cba9c9d42
commit 4da76ec88b
113 changed files with 5627 additions and 4430 deletions

View File

@@ -16,6 +16,38 @@
"application/json"
],
"paths": {
"/v1/boarding": {
"post": {
"operationId": "ArkService_GetBoardingAddress",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetBoardingAddressResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1GetBoardingAddressRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/events": {
"get": {
"operationId": "ArkService_GetEventStream",
@@ -69,38 +101,6 @@
]
}
},
"/v1/onboard": {
"post": {
"operationId": "ArkService_Onboard",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1OnboardResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1OnboardRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment": {
"post": {
"operationId": "ArkService_CreatePayment",
@@ -475,6 +475,21 @@
}
}
},
"v1BoardingInput": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
},
"descriptor": {
"type": "string"
}
}
},
"v1ClaimPaymentRequest": {
"type": "object",
"properties": {
@@ -555,12 +570,35 @@
"type": "string"
},
"description": "Forfeit txs signed by the user."
},
"signedRoundTx": {
"type": "string",
"description": "If payment has boarding input, the user must sign the associated inputs."
}
}
},
"v1FinalizePaymentResponse": {
"type": "object"
},
"v1GetBoardingAddressRequest": {
"type": "object",
"properties": {
"pubkey": {
"type": "string"
}
}
},
"v1GetBoardingAddressResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"descriptor": {
"type": "string"
}
}
},
"v1GetEventStreamResponse": {
"type": "object",
"properties": {
@@ -605,6 +643,9 @@
"minRelayFee": {
"type": "string",
"format": "int64"
},
"boardingDescriptorTemplate": {
"type": "string"
}
}
},
@@ -627,12 +668,11 @@
"v1Input": {
"type": "object",
"properties": {
"txid": {
"type": "string"
"vtxoInput": {
"$ref": "#/definitions/v1VtxoInput"
},
"vout": {
"type": "integer",
"format": "int64"
"boardingInput": {
"$ref": "#/definitions/v1BoardingInput"
}
}
},
@@ -669,23 +709,6 @@
}
}
},
"v1OnboardRequest": {
"type": "object",
"properties": {
"boardingTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"userPubkey": {
"type": "string"
}
}
},
"v1OnboardResponse": {
"type": "object"
},
"v1Output": {
"type": "object",
"properties": {
@@ -972,6 +995,18 @@
"$ref": "#/definitions/v1PendingPayment"
}
}
},
"v1VtxoInput": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
}
}
}
}
}

View File

@@ -65,12 +65,12 @@ service ArkService {
get: "/v1/info"
};
}
rpc Onboard(OnboardRequest) returns (OnboardResponse) {
rpc GetBoardingAddress(GetBoardingAddressRequest) returns (GetBoardingAddressResponse) {
option (google.api.http) = {
post: "/v1/onboard"
post: "/v1/boarding"
body: "*"
};
}
};
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {
option (google.api.http) = {
post: "/v1/payment"
@@ -100,6 +100,15 @@ message CompletePaymentRequest {
}
message CompletePaymentResponse {}
message GetBoardingAddressRequest {
string pubkey = 1;
}
message GetBoardingAddressResponse {
string address = 1;
string descriptor = 2;
}
message RegisterPaymentRequest {
repeated Input inputs = 1;
optional string ephemeral_pubkey = 2;
@@ -120,6 +129,8 @@ message ClaimPaymentResponse {}
message FinalizePaymentRequest {
// Forfeit txs signed by the user.
repeated string signed_forfeit_txs = 1;
// If payment has boarding input, the user must sign the associated inputs.
optional string signed_round_tx = 2;
}
message FinalizePaymentResponse {}
@@ -177,14 +188,7 @@ message GetInfoResponse {
int64 round_interval = 4;
string network = 5;
int64 min_relay_fee = 6;
}
message OnboardRequest {
string boarding_tx = 1;
Tree congestion_tree = 2;
string user_pubkey = 3;
}
message OnboardResponse {
string boarding_descriptor_template = 7;
}
// EVENT TYPES
@@ -239,11 +243,24 @@ message Round {
RoundStage stage = 8;
}
message Input {
message VtxoInput {
string txid = 1;
uint32 vout = 2;
}
message BoardingInput {
string txid = 1;
uint32 vout = 2;
string descriptor = 3;
}
message Input {
oneof input {
VtxoInput vtxo_input = 1;
BoardingInput boarding_input = 2;
}
}
message Output {
// Either the offchain or onchain address.
string address = 1;

File diff suppressed because it is too large Load Diff

View File

@@ -404,28 +404,28 @@ func local_request_ArkService_GetInfo_0(ctx context.Context, marshaler runtime.M
}
func request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq OnboardRequest
func request_ArkService_GetBoardingAddress_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetBoardingAddressRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Onboard(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.GetBoardingAddress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq OnboardRequest
func local_request_ArkService_GetBoardingAddress_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetBoardingAddressRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Onboard(ctx, &protoReq)
msg, err := server.GetBoardingAddress(ctx, &protoReq)
return msg, metadata, err
}
@@ -745,7 +745,7 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_ArkService_GetBoardingAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -753,12 +753,12 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/GetBoardingAddress", runtime.WithHTTPPathPattern("/v1/boarding"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_ArkService_GetBoardingAddress_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -766,7 +766,7 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
return
}
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_ArkService_GetBoardingAddress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -1103,25 +1103,25 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_ArkService_GetBoardingAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/GetBoardingAddress", runtime.WithHTTPPathPattern("/v1/boarding"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_ArkService_GetBoardingAddress_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_ArkService_GetBoardingAddress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -1195,7 +1195,7 @@ var (
pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, ""))
pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, ""))
pattern_ArkService_GetBoardingAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "boarding"}, ""))
pattern_ArkService_CreatePayment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payment"}, ""))
@@ -1225,7 +1225,7 @@ var (
forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage
forward_ArkService_GetBoardingAddress_0 = runtime.ForwardResponseMessage
forward_ArkService_CreatePayment_0 = runtime.ForwardResponseMessage

View File

@@ -29,7 +29,7 @@ type ArkServiceClient interface {
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error)
GetBoardingAddress(ctx context.Context, in *GetBoardingAddressRequest, opts ...grpc.CallOption) (*GetBoardingAddressResponse, error)
CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error)
CompletePayment(ctx context.Context, in *CompletePaymentRequest, opts ...grpc.CallOption) (*CompletePaymentResponse, error)
}
@@ -164,9 +164,9 @@ func (c *arkServiceClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts
return out, nil
}
func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) {
out := new(OnboardResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Onboard", in, out, opts...)
func (c *arkServiceClient) GetBoardingAddress(ctx context.Context, in *GetBoardingAddressRequest, opts ...grpc.CallOption) (*GetBoardingAddressResponse, error) {
out := new(GetBoardingAddressResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/GetBoardingAddress", in, out, opts...)
if err != nil {
return nil, err
}
@@ -206,7 +206,7 @@ type ArkServiceServer interface {
Ping(context.Context, *PingRequest) (*PingResponse, error)
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
GetBoardingAddress(context.Context, *GetBoardingAddressRequest) (*GetBoardingAddressResponse, error)
CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error)
CompletePayment(context.Context, *CompletePaymentRequest) (*CompletePaymentResponse, error)
}
@@ -248,8 +248,8 @@ func (UnimplementedArkServiceServer) ListVtxos(context.Context, *ListVtxosReques
func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
}
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented")
func (UnimplementedArkServiceServer) GetBoardingAddress(context.Context, *GetBoardingAddressRequest) (*GetBoardingAddressResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBoardingAddress not implemented")
}
func (UnimplementedArkServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePayment not implemented")
@@ -470,20 +470,20 @@ func _ArkService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler)
}
func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OnboardRequest)
func _ArkService_GetBoardingAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBoardingAddressRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ArkServiceServer).Onboard(ctx, in)
return srv.(ArkServiceServer).GetBoardingAddress(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ark.v1.ArkService/Onboard",
FullMethod: "/ark.v1.ArkService/GetBoardingAddress",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ArkServiceServer).Onboard(ctx, req.(*OnboardRequest))
return srv.(ArkServiceServer).GetBoardingAddress(ctx, req.(*GetBoardingAddressRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -572,8 +572,8 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
Handler: _ArkService_GetInfo_Handler,
},
{
MethodName: "Onboard",
Handler: _ArkService_Onboard_Handler,
MethodName: "GetBoardingAddress",
Handler: _ArkService_GetBoardingAddress_Handler,
},
{
MethodName: "CreatePayment",

View File

@@ -9,6 +9,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/flags"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
@@ -21,18 +22,30 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
}
defer cancel()
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
network, err := utils.GetNetwork(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
// No need to check for error here becuase this function is called also by getAddress().
// nolint:all
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
wg := &sync.WaitGroup{}
wg.Add(3)
@@ -54,19 +67,19 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
balance, err := explorer.GetBalance(onchainAddr, toElementsNetwork(network).AssetID)
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err}
return
}
chRes <- balanceRes{0, balance, nil, nil, nil}
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
}()
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
redemptionAddr, unilateralExitDelay,
)
if err != nil {

148
client/covenant/claim.go Normal file
View File

@@ -0,0 +1,148 @@
package covenant
import (
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
func (c *covenantLiquidCLI) Claim(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
explorer := utils.NewExplorer(ctx)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
if err != nil {
return err
}
now := time.Now()
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
for _, utxo := range boardingUtxosFromExplorer {
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
if u.SpendableAt.Before(now) {
continue // cannot claim if onchain spendable
}
boardingUtxos = append(boardingUtxos, u)
}
var pendingBalance uint64
for _, utxo := range boardingUtxos {
pendingBalance += utxo.Amount
}
if pendingBalance == 0 {
return fmt.Errorf("no boarding utxos to claim")
}
receiver := receiver{
To: offchainAddr,
Amount: pendingBalance,
}
if len(ctx.String("password")) == 0 {
if ok := askForConfirmation(
fmt.Sprintf(
"claim %d satoshis from %d boarding utxos",
pendingBalance, len(boardingUtxos),
),
); !ok {
return nil
}
}
return selfTransferAllPendingPayments(
ctx, client, boardingUtxos, receiver, boardingDescriptor,
)
}
func selfTransferAllPendingPayments(
ctx *cli.Context,
client arkv1.ArkServiceClient,
boardingUtxos []utils.Utxo,
myself receiver,
desc string,
) error {
inputs := make([]*arkv1.Input, 0, len(boardingUtxos))
for _, outpoint := range boardingUtxos {
inputs = append(inputs, &arkv1.Input{
Input: &arkv1.Input_BoardingInput{
BoardingInput: &arkv1.BoardingInput{
Txid: outpoint.Txid,
Vout: outpoint.Vout,
Descriptor_: desc,
},
},
})
}
receiversOutput := []*arkv1.Output{
{
Address: myself.To,
Amount: myself.Amount,
},
}
secKey, err := utils.PrivateKeyFromPassword(ctx)
if err != nil {
return err
}
registerResponse, err := client.RegisterPayment(
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
)
if err != nil {
return err
}
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
Id: registerResponse.GetId(),
Outputs: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
})
if err != nil {
return err
}
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(), make([]vtxo, 0),
len(boardingUtxos) > 0, secKey, receiversOutput,
)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"pool_txid": poolTxID,
})
}

View File

@@ -9,16 +9,13 @@ import (
"github.com/ark-network/ark/client/interfaces"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
const dust = 450
@@ -29,19 +26,15 @@ func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) ClaimAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"onchain_address": onchainAddr,
"boarding_address": boardingAddr,
})
}
@@ -131,14 +124,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
explorer := utils.NewExplorer(ctx)
utxos, delayedUtxos, change, err := coinSelectOnchain(
utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, utxos, delayedUtxos, &liquidNet); err != nil {
if err := addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -181,14 +174,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, selected, delayedSelected, &liquidNet); err != nil {
if err := addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -243,20 +236,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
func coinSelectOnchain(
ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
_, onchainAddr, _, err := getAddress(ctx)
) ([]utils.Utxo, uint64, error) {
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
fromExplorer, err := explorer.GetUtxos(onchainAddr)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fromExplorer {
now := time.Now()
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return nil, 0, err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return nil, 0, err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, 0, err
}
for _, utxo := range boardingUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
@@ -267,207 +277,99 @@ func coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
userPubkey, err := utils.GetWalletPublicKey(ctx)
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return nil, nil, 0, err
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return nil, nil, 0, err
}
liquidNet := toElementsNetwork(net)
pay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, 0, err
}
addr, err := pay.TaprootAddress()
if err != nil {
return nil, nil, 0, err
}
fromExplorer, err = explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
delayedUtxos := make([]utils.Utxo, 0)
for _, utxo := range fromExplorer {
for _, utxo := range redemptionUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
func addInputs(
ctx *cli.Context,
updater *psetv2.Updater, utxos, delayedUtxos []utils.Utxo, net *network.Network,
updater *psetv2.Updater,
utxos []utils.Utxo,
) error {
_, onchainAddr, _, err := getAddress(ctx)
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
changeScript, err := address.ToOutputScript(onchainAddr)
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
return err
}
if err := updater.AddInputs([]psetv2.InputArgs{
{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Sequence: sequence,
},
}); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: changeScript,
Nonce: []byte{0x00},
}
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
_, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
inputIndex := len(updater.Pset.Inputs) - 1
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
return err
}
addr, err := pay.TaprootAddress()
if err != nil {
return err
}
script, err := address.ToOutputScript(addr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
if err := addVtxoInput(
updater,
psetv2.InputArgs{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
uint(unilateralExitDelay),
leafProof,
); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, witnessUtxo,
); err != nil {
return err
}
}
}
return nil
@@ -503,32 +405,7 @@ func decodeReceiverAddress(addr string) (
return true, outputScript, nil, nil
}
func addVtxoInput(
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
tapLeafProof *taproot.TapscriptElementsProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Pset.Inputs)
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
return err
}
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
return updater.AddInTapLeafScript(
nextInputIndex,
psetv2.NewTapLeafScript(
*tapLeafProof,
tree.UnspendableKey(),
),
)
}
func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr string, err error) {
func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return
@@ -544,6 +421,21 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
return
}
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return
}
arkNet, err := utils.GetNetwork(ctx)
if err != nil {
return
@@ -556,12 +448,6 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
liquidNet := toElementsNetwork(arkNet)
p2wpkh := payment.FromPublicKey(userPubkey, &liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
@@ -569,18 +455,34 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
return
}
payment, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return
}
redemptionAddr, err = payment.TaprootAddress()
redemptionAddr, err = redemptionPay.TaprootAddress()
if err != nil {
return
}
boardingTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(timeoutBoarding),
)
if err != nil {
return
}
boardingPay, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
if err != nil {
return
}
boardingAddr, err = boardingPay.TaprootAddress()
if err != nil {
return
}
offchainAddr = arkAddr
onchainAddr = liquidAddr
return
}

View File

@@ -51,13 +51,16 @@ func getVtxos(
if v.Swept {
continue
}
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.Txid,
vout: v.Outpoint.Vout,
poolTxid: v.PoolTxid,
expireAt: expireAt,
})
if v.Outpoint.GetVtxoInput() != nil {
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.GetVtxoInput().GetTxid(),
vout: v.Outpoint.GetVtxoInput().GetVout(),
poolTxid: v.PoolTxid,
expireAt: expireAt,
})
}
}
if !computeExpiration {
@@ -193,32 +196,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
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,
}
}
func handleRoundStream(
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
vtxosToSign []vtxo, mustSignRoundTx bool,
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
if err != nil {
@@ -254,8 +235,8 @@ func handleRoundStream(
pingStop()
fmt.Println("round finalization started")
poolTx := e.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(poolTx)
roundTx := e.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil {
return "", err
}
@@ -280,13 +261,13 @@ func handleRoundStream(
if !isOnchainOnly(receivers) {
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
); err != nil {
return "", err
}
}
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
if err := common.ValidateConnectors(roundTx, connectors); err != nil {
return "", err
}
@@ -379,78 +360,100 @@ func handleRoundStream(
fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := utils.NewExplorer(ctx)
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
if len(vtxosToSign) > 0 {
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
connectorsTxids = append(connectorsTxids, txid)
fmt.Print("signing forfeit txs... ")
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 _, forfeit := range forfeits {
pset, err := psetv2.NewPsetFromBase64(forfeit)
if err != nil {
return "", err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
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 := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPset(ctx, pset, explorer, secKey); err != nil {
return "", err
}
signedPset, err := pset.ToBase64()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
fmt.Printf("%d signed\n", len(signedForfeits))
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
}
for _, forfeit := range forfeits {
pset, err := psetv2.NewPsetFromBase64(forfeit)
if mustSignRoundTx {
ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil {
return "", err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
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 := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPset(ctx, pset, explorer, secKey); err != nil {
return "", err
}
signedPset, err := pset.ToBase64()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
if err := signPset(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedRoundTx, err := ptx.ToBase64()
if err != nil {
return "", err
}
fmt.Println("round tx signed")
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
if err != nil {
return "", err
}

View File

@@ -104,6 +104,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
utils.EXPLORER: explorer,
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
})
}

View File

@@ -1,115 +0,0 @@
package covenant
import (
"encoding/hex"
"fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/tree"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
)
const minRelayFee = 30
func (c *covenantLiquidCLI) Onboard(ctx *cli.Context) error {
amount := ctx.Uint64("amount")
if amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return err
}
userPubKey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
roundLifetime, err := utils.GetRoundLifetime(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
congestionTreeLeaf := tree.Receiver{
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
Amount: amount,
}
liquidNet := toElementsNetwork(net)
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
liquidNet.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf},
minRelayFee, roundLifetime, unilateralExitDelay,
)
if err != nil {
return err
}
pay, err := payment.FromScript(sharedOutputScript, &liquidNet, nil)
if err != nil {
return err
}
address, err := pay.TaprootAddress()
if err != nil {
return err
}
onchainReceiver := receiver{
To: address,
Amount: sharedOutputAmount,
}
pset, err := sendOnchain(ctx, []receiver{onchainReceiver})
if err != nil {
return err
}
ptx, _ := psetv2.NewPsetFromBase64(pset)
utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
Txid: txid,
TxIndex: 0,
})
if err != nil {
return err
}
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: pset,
CongestionTree: castCongestionTree(congestionTree),
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
})
if err != nil {
return err
}
fmt.Println("onboard_txid:", txid)
return nil
}

View File

@@ -78,8 +78,12 @@ func collaborativeRedeem(
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -108,6 +112,7 @@ func collaborativeRedeem(
client,
registerResponse.GetId(),
selectedCoins,
false,
secKey,
receivers,
)

View File

@@ -143,8 +143,12 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -170,7 +174,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(),
selectedCoins, secKey, receiversOutput,
selectedCoins, false, secKey, receiversOutput,
)
if err != nil {
return err

View File

@@ -6,14 +6,11 @@ import (
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
@@ -50,13 +47,7 @@ func signPset(
return err
}
sighashType := txscript.SigHashAll
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
return err
}
}
@@ -66,16 +57,6 @@ func signPset(
return err
}
_, onchainAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
onchainWalletScript, err := address.ToOutputScript(onchainAddr)
if err != nil {
return err
}
utx, err := pset.UnsignedTx()
if err != nil {
return err
@@ -99,34 +80,6 @@ func signPset(
liquidNet := toElementsNetwork(net)
for i, input := range pset.Inputs {
if bytes.Equal(input.WitnessUtxo.Script, onchainWalletScript) {
p, err := payment.FromScript(input.WitnessUtxo.Script, &liquidNet, nil)
if err != nil {
return err
}
preimage := utx.HashForWitnessV0(
i,
p.Script,
input.WitnessUtxo.Value,
txscript.SigHashAll,
)
sig := ecdsa.Sign(
prvKey,
preimage[:],
)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
err = signer.SignInput(i, signatureWithSighashType, prvKey.PubKey().SerializeCompressed(), nil, nil)
if err != nil {
fmt.Println("error signing input: ", err)
return err
}
continue
}
if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/flags"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
@@ -21,7 +22,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
}
defer cancel()
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
@@ -29,6 +30,21 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
// nolint:all
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
wg := &sync.WaitGroup{}
wg.Add(3)
@@ -50,19 +66,19 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
balance, err := explorer.GetBalance(onchainAddr.EncodeAddress(), "")
balance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr.EncodeAddress(), int64(timeoutBoarding))
if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err}
return
}
chRes <- balanceRes{0, balance, nil, nil, nil}
chRes <- balanceRes{0, balance, lockedBalance, nil, nil}
}()
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
redemptionAddr.EncodeAddress(), unilateralExitDelay,
)
if err != nil {
@@ -126,6 +142,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
}
response := make(map[string]interface{})
response["onchain_balance"] = map[string]interface{}{
"spendable_amount": onchainBalance,
}

View File

@@ -2,26 +2,62 @@ package covenantless
import (
"encoding/hex"
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
func (c *clArkBitcoinCLI) Claim(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
myselfOffchain, _, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
vtxos, err := getVtxos(ctx, nil, client, myselfOffchain, false)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
explorer := utils.NewExplorer(ctx)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
if err != nil {
return err
}
now := time.Now()
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
for _, utxo := range boardingUtxosFromExplorer {
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
if u.SpendableAt.Before(now) {
continue // cannot claim if onchain spendable
}
boardingUtxos = append(boardingUtxos, u)
}
vtxos, err := getVtxos(ctx, nil, client, offchainAddr, false)
if err != nil {
return err
}
@@ -34,32 +70,71 @@ func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
pendingVtxos = append(pendingVtxos, vtxo)
}
}
for _, utxo := range boardingUtxos {
pendingBalance += utxo.Amount
}
if pendingBalance == 0 {
return nil
}
receiver := receiver{
To: myselfOffchain,
To: offchainAddr,
Amount: pendingBalance,
}
if len(ctx.String("password")) == 0 {
if ok := askForConfirmation(
fmt.Sprintf(
"claim %d satoshis from %d pending payments and %d boarding utxos",
pendingBalance, len(pendingVtxos), len(boardingUtxos),
),
); !ok {
return nil
}
}
return selfTransferAllPendingPayments(
ctx, client, pendingVtxos, receiver,
ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
)
}
func selfTransferAllPendingPayments(
ctx *cli.Context, client arkv1.ArkServiceClient,
pendingVtxos []vtxo, myself receiver,
ctx *cli.Context,
client arkv1.ArkServiceClient,
pendingVtxos []vtxo,
boardingUtxos []utils.Utxo,
myself receiver,
desc string,
) error {
inputs := make([]*arkv1.Input, 0, len(pendingVtxos))
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
for _, coin := range pendingVtxos {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
if len(boardingUtxos) > 0 {
for _, outpoint := range boardingUtxos {
inputs = append(inputs, &arkv1.Input{
Input: &arkv1.Input_BoardingInput{
BoardingInput: &arkv1.BoardingInput{
Txid: outpoint.Txid,
Vout: outpoint.Vout,
Descriptor_: desc,
},
},
})
}
}
receiversOutput := []*arkv1.Output{
{
Address: myself.To,
@@ -99,8 +174,8 @@ func selfTransferAllPendingPayments(
}
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(),
pendingVtxos, secKey, receiversOutput, ephemeralKey,
ctx, client, registerResponse.GetId(), pendingVtxos,
len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
)
if err != nil {
return err

View File

@@ -9,10 +9,10 @@ import (
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@@ -25,14 +25,14 @@ const dust = 450
type clArkBitcoinCLI struct{}
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"onchain_address": onchainAddr.EncodeAddress(),
"boarding_address": boardingAddr.EncodeAddress(),
})
}
@@ -79,11 +79,6 @@ type receiver struct {
Amount uint64 `json:"amount"`
}
// func (r *receiver) isOnchain() bool {
// _, err := btcutil.DecodeAddress(r.To, nil)
// return err == nil
// }
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
ptx, err := psbt.New(nil, nil, 2, 0, nil)
if err != nil {
@@ -128,14 +123,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
explorer := utils.NewExplorer(ctx)
utxos, delayedUtxos, change, err := coinSelectOnchain(
utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
if err := addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -174,14 +169,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
if err := addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -225,20 +220,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
func coinSelectOnchain(
ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
_, onchainAddr, _, err := getAddress(ctx)
) ([]utils.Utxo, uint64, error) {
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
fromExplorer, err := explorer.GetUtxos(onchainAddr.EncodeAddress())
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fromExplorer {
now := time.Now()
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return nil, 0, err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return nil, 0, err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, 0, err
}
for _, utxo := range boardingUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
@@ -249,102 +261,67 @@ func coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
userPubkey, err := utils.GetWalletPublicKey(ctx)
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return nil, nil, 0, err
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return nil, nil, 0, err
}
liquidNet := toChainParams(net)
p2tr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&liquidNet,
)
if err != nil {
return nil, nil, 0, err
}
addr := p2tr.EncodeAddress()
fromExplorer, err = explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
delayedUtxos := make([]utils.Utxo, 0)
for _, utxo := range fromExplorer {
for _, utxo := range redemptionUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
func addInputs(
ctx *cli.Context,
updater *psbt.Updater,
utxos, delayedUtxos []utils.Utxo,
net *chaincfg.Params,
utxos []utils.Utxo,
) error {
_, onchainAddr, _, err := getAddress(ctx)
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
changeScript, err := txscript.PayToAddrScript(onchainAddr)
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
@@ -355,87 +332,41 @@ func addInputs(
return err
}
sequence, err := utxo.Sequence()
if err != nil {
return err
}
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
},
Sequence: sequence,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: changeScript,
},
len(updater.Upsbt.UnsignedTx.TxIn)-1,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
_, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
script, err := txscript.PayToAddrScript(p2tr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
return err
}
if err := addVtxoInput(
updater,
&wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion,
},
uint(unilateralExitDelay),
leafProof,
); err != nil {
return err
}
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: script,
},
len(updater.Upsbt.Inputs)-1,
); err != nil {
return err
}
}
},
})
}
return nil
@@ -461,40 +392,7 @@ func decodeReceiverAddress(addr string) (
return true, pkscript, nil, nil
}
func addVtxoInput(
updater *psbt.Updater, inputArgs *wire.OutPoint, exitDelay uint,
tapLeafProof *txscript.TapscriptProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Upsbt.Inputs)
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *inputArgs,
Sequence: sequence,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
controlBlock := tapLeafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: tapLeafProof.Script,
LeafVersion: tapLeafProof.LeafVersion,
},
}
return nil
}
func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionAddr btcutil.Address, err error) {
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return
@@ -510,6 +408,21 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
return
}
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return
}
arkNet, err := utils.GetNetwork(ctx)
if err != nil {
return
@@ -522,11 +435,6 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
netParams := toChainParams(arkNet)
p2wpkh, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(userPubkey.SerializeCompressed()), &netParams)
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
@@ -534,7 +442,7 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
return
}
p2tr, err := btcutil.NewAddressTaproot(
redemptionP2TR, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&netParams,
)
@@ -542,8 +450,20 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
return
}
redemptionAddr = p2tr
onchainAddr = p2wpkh
boardingTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(timeoutBoarding),
)
if err != nil {
return
}
boardingP2TR, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(boardingTapKey),
&netParams,
)
redemptionAddr = redemptionP2TR
boardingAddr = boardingP2TR
offchainAddr = arkAddr
return

View File

@@ -53,14 +53,16 @@ func getVtxos(
if v.GetSwept() {
continue
}
vtxos = append(vtxos, vtxo{
amount: v.GetReceiver().GetAmount(),
txid: v.GetOutpoint().GetTxid(),
vout: v.GetOutpoint().GetVout(),
poolTxid: v.GetPoolTxid(),
expireAt: expireAt,
pending: v.GetPending(),
})
if v.Outpoint.GetVtxoInput() != nil {
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.GetVtxoInput().GetTxid(),
vout: v.Outpoint.GetVtxoInput().GetVout(),
poolTxid: v.PoolTxid,
expireAt: expireAt,
pending: v.GetPending(),
})
}
}
if !computeExpiration {
@@ -196,32 +198,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
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,
}
}
func handleRoundStream(
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
vtxosToSign []vtxo, mustSignRoundTx bool,
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
ephemeralKey *secp256k1.PrivateKey,
) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
@@ -398,8 +378,8 @@ func handleRoundStream(
// stop pinging as soon as we receive some forfeit txs
pingStop()
poolTx := e.GetPoolTx()
ptx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
roundTx := e.GetPoolTx()
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil {
return "", err
}
@@ -428,7 +408,7 @@ func handleRoundStream(
if !isOnchainOnly(receivers) {
if err := bitcointree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
congestionTree, roundTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
); err != nil {
return "", err
}
@@ -529,80 +509,103 @@ func handleRoundStream(
fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := utils.NewExplorer(ctx)
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return "", err
}
txid := p.UnsignedTx.TxHash().String()
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
connectorsTxids = append(connectorsTxids, txid)
}
if len(vtxosToSign) > 0 {
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
for _, forfeit := range forfeits {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
if err != nil {
return "", err
fmt.Print("signing forfeit txs... ")
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return "", err
}
txid := p.UnsignedTx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
for _, forfeit := range forfeits {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
if err != nil {
return "", 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
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
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
}
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedPset, err := ptx.B64Encode()
if err != nil {
return "", err
}
signedPset, err := ptx.B64Encode()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(vtxosToSign) > 0 && len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
continue
fmt.Printf("%d signed\n", len(signedForfeits))
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
}
if mustSignRoundTx {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil {
return "", err
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedRoundTx, err := ptx.B64Encode()
if err != nil {
return "", err
}
fmt.Println("round tx signed")
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
}
fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
if err != nil {
return "", err
}

View File

@@ -82,6 +82,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
utils.MIN_RELAY_FEE: strconv.Itoa(int(resp.MinRelayFee)),
utils.EXPLORER: explorer,
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
})
}

View File

@@ -1,205 +0,0 @@
package covenantless
import (
"encoding/hex"
"fmt"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/bitcointree"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
func (c *clArkBitcoinCLI) Onboard(ctx *cli.Context) error {
amount := ctx.Uint64("amount")
if amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return err
}
userPubKey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
roundLifetime, err := utils.GetRoundLifetime(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
minRelayFee, err := utils.GetMinRelayFee(ctx)
if err != nil {
return err
}
congestionTreeLeaf := bitcointree.Receiver{
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
Amount: uint64(amount), // Convert amount to uint64
}
leaves := []bitcointree.Receiver{congestionTreeLeaf}
ephemeralKey, err := secp256k1.GeneratePrivateKey()
if err != nil {
return err
}
cosigners := []*secp256k1.PublicKey{ephemeralKey.PubKey()} // TODO asp as cosigner
sharedOutputScript, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners,
aspPubkey,
leaves,
uint64(minRelayFee),
roundLifetime,
unilateralExitDelay,
)
if err != nil {
return err
}
netParams := toChainParams(net)
address, err := btcutil.NewAddressTaproot(sharedOutputScript[2:], &netParams)
if err != nil {
return err
}
onchainReceiver := receiver{
To: address.EncodeAddress(),
Amount: uint64(sharedOutputAmount),
}
partialTx, err := sendOnchain(ctx, []receiver{onchainReceiver})
if err != nil {
return err
}
ptx, err := psbt.NewFromRawBytes(strings.NewReader(partialTx), true)
if err != nil {
return err
}
txid := ptx.UnsignedTx.TxHash().String()
congestionTree, err := bitcointree.CraftCongestionTree(
&wire.OutPoint{
Hash: ptx.UnsignedTx.TxHash(),
Index: 0,
},
cosigners,
aspPubkey,
leaves,
uint64(minRelayFee),
roundLifetime,
unilateralExitDelay,
)
if err != nil {
return err
}
sweepClosure := bitcointree.CSVSigClosure{
Pubkey: aspPubkey,
Seconds: uint(roundLifetime),
}
sweepTapLeaf, err := sweepClosure.Leaf()
if err != nil {
return err
}
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
root := sweepTapTree.RootNode.TapHash()
signer := bitcointree.NewTreeSignerSession(
ephemeralKey,
congestionTree,
minRelayFee,
root.CloneBytes(),
)
nonces, err := signer.GetNonces() // TODO send nonces to ASP
if err != nil {
return err
}
coordinator, err := bitcointree.NewTreeCoordinatorSession(
congestionTree,
minRelayFee,
root.CloneBytes(),
cosigners,
)
if err != nil {
return err
}
if err := coordinator.AddNonce(ephemeralKey.PubKey(), nonces); err != nil {
return err
}
aggregatedNonces, err := coordinator.AggregateNonces()
if err != nil {
return err
}
if err := signer.SetKeys(cosigners); err != nil {
return err
}
if err := signer.SetAggregatedNonces(aggregatedNonces); err != nil {
return err
}
sigs, err := signer.Sign()
if err != nil {
return err
}
if err := coordinator.AddSig(ephemeralKey.PubKey(), sigs); err != nil {
return err
}
signedTree, err := coordinator.SignTree()
if err != nil {
return err
}
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: partialTx,
CongestionTree: castCongestionTree(signedTree),
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
})
if err != nil {
return err
}
fmt.Println("onboard_txid:", txid)
return nil
}

View File

@@ -72,8 +72,12 @@ func collaborativeRedeem(
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -110,6 +114,7 @@ func collaborativeRedeem(
client,
registerResponse.GetId(),
selectedCoins,
false,
secKey,
receivers,
ephemeralKey,

View File

@@ -14,7 +14,7 @@ import (
)
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
receiver := ctx.String("to")
receiverAddr := ctx.String("to")
amount := ctx.Uint64("amount")
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
@@ -22,15 +22,22 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
}
if receiver == "" {
if receiverAddr == "" {
return fmt.Errorf("receiver address is required")
}
isOnchain, _, _, err := decodeReceiverAddress(receiver)
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
if err != nil {
return err
}
if isOnchain {
return fmt.Errorf("receiver address is onchain")
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"txid": txid,
})
}
offchainAddr, _, _, err := getAddress(ctx)
@@ -41,20 +48,20 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
if err != nil {
return err
}
_, _, aspKey, err := common.DecodeAddress(receiver)
_, _, aspKey, err := common.DecodeAddress(receiverAddr)
if err != nil {
return fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiverAddr)
}
receiversOutput := make([]*arkv1.Output, 0)
sumOfReceivers := uint64(0)
receiversOutput = append(receiversOutput, &arkv1.Output{
Address: receiver,
Address: receiverAddr,
Amount: amount,
})
sumOfReceivers += amount
@@ -88,8 +95,12 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/bitcointree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
@@ -18,7 +17,7 @@ import (
)
func signPsbt(
ctx *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
_ *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
) error {
updater, err := psbt.NewUpdater(ptx)
if err != nil {
@@ -50,27 +49,11 @@ func signPsbt(
return err
}
sighashType := txscript.SigHashAll
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
return err
}
}
_, onchainAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
onchainWalletScript, err := txscript.PayToAddrScript(onchainAddr)
if err != nil {
return err
}
prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs {
@@ -85,40 +68,6 @@ func signPsbt(
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs {
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
return err
}
preimage, err := txscript.CalcWitnessSigHash(
input.WitnessUtxo.PkScript,
txsighashes,
txscript.SigHashAll,
updater.Upsbt.UnsignedTx,
i,
int64(input.WitnessUtxo.Value),
)
if err != nil {
return err
}
sig := ecdsa.Sign(
prvKey,
preimage,
)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
{
PubKey: prvKey.PubKey().SerializeCompressed(),
Signature: signatureWithSighashType,
},
}
continue
}
if len(input.TaprootLeafScript) > 0 {
pubkey := prvKey.PubKey()
for _, leaf := range input.TaprootLeafScript {
@@ -178,7 +127,6 @@ func signPsbt(
}
}
}
}
return nil

View File

@@ -21,10 +21,6 @@ var (
Required: false,
Hidden: true,
}
AmountOnboardFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to onboard in sats",
}
ExpiryDetailsFlag = cli.BoolFlag{
Name: "compute-expiry-details",
Usage: "compute client-side the VTXOs expiry time",

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/client
go 1.22.6
replace github.com/ark-network/ark/common => ../common
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
require (
@@ -19,9 +21,7 @@ require (
require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
)

View File

@@ -1,8 +1,6 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=

View File

@@ -8,7 +8,6 @@ type CLI interface {
Receive(ctx *cli.Context) error
Redeem(ctx *cli.Context) error
Send(ctx *cli.Context) error
ClaimAsync(ctx *cli.Context) error
Claim(ctx *cli.Context) error
SendAsync(ctx *cli.Context) error
Onboard(ctx *cli.Context) error
}

View File

@@ -74,19 +74,6 @@ var (
Flags: []cli.Flag{&flags.PasswordFlag, &flags.PrivateKeyFlag, &flags.NetworkFlag, &flags.UrlFlag, &flags.ExplorerFlag},
}
onboardCommand = cli.Command{
Name: "onboard",
Usage: "Onboard the Ark by lifting your funds",
Action: func(ctx *cli.Context) error {
cli, err := getCLIFromState(ctx)
if err != nil {
return err
}
return cli.Onboard(ctx)
},
Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.PasswordFlag},
}
sendCommand = cli.Command{
Name: "send",
Usage: "Send your onchain or offchain funds to one or many receivers",
@@ -117,7 +104,7 @@ var (
if err != nil {
return err
}
return cli.ClaimAsync(ctx)
return cli.Claim(ctx)
},
Flags: []cli.Flag{&flags.PasswordFlag},
}
@@ -167,7 +154,6 @@ func main() {
&redeemCommand,
&sendCommand,
&claimCommand,
&onboardCommand,
)
app.Flags = []cli.Flag{
flags.DatadirFlag,

View File

@@ -18,7 +18,7 @@ import (
"github.com/vulpemventures/go-elements/transaction"
)
type Utxo struct {
type ExplorerUtxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
@@ -32,9 +32,9 @@ type Utxo struct {
type Explorer interface {
GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]Utxo, error)
GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr, asset string) (uint64, error)
GetRedeemedVtxosBalance(
GetDelayedBalance(
addr string, unilateralExitDelay int64,
) (uint64, map[int64]uint64, error)
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
@@ -130,7 +130,7 @@ func (e *explorer) Broadcast(txStr string) (string, error) {
return txid, nil
}
func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
func (e *explorer) GetUtxos(addr string) ([]ExplorerUtxo, error) {
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
if err != nil {
return nil, err
@@ -149,7 +149,7 @@ func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []Utxo{}
payload := []ExplorerUtxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
@@ -175,7 +175,7 @@ func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
return balance, nil
}
func (e *explorer) GetRedeemedVtxosBalance(
func (e *explorer) GetDelayedBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)

View File

@@ -7,8 +7,10 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
@@ -18,6 +20,7 @@ const (
ASP_PUBKEY = "asp_public_key"
ROUND_LIFETIME = "round_lifetime"
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
BOARDING_TEMPLATE = "boarding_template"
ENCRYPTED_PRVKEY = "encrypted_private_key"
PASSWORD_HASH = "password_hash"
PUBKEY = "public_key"
@@ -109,6 +112,27 @@ func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
return int64(redeemDelay), nil
}
func GetBoardingDescriptor(ctx *cli.Context) (string, error) {
state, err := GetState(ctx)
if err != nil {
return "", err
}
pubkey, err := GetWalletPublicKey(ctx)
if err != nil {
return "", err
}
template := state[BOARDING_TEMPLATE]
if len(template) <= 0 {
return "", fmt.Errorf("missing boarding descriptor template")
}
pubkeyhex := hex.EncodeToString(schnorr.SerializePubKey(pubkey))
return strings.ReplaceAll(template, "USER", pubkeyhex), nil
}
func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
state, err := GetState(ctx)
if err != nil {

36
client/utils/types.go Normal file
View File

@@ -0,0 +1,36 @@
package utils
import (
"time"
"github.com/ark-network/ark/common"
)
type Utxo struct {
Txid string
Vout uint32
Amount uint64
Asset string // optional
Delay uint
SpendableAt time.Time
}
func (u *Utxo) Sequence() (uint32, error) {
return common.BIP68EncodeAsNumber(u.Delay)
}
func NewUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
utxoTime := explorerUtxo.Status.Blocktime
if utxoTime == 0 {
utxoTime = time.Now().Unix()
}
return Utxo{
Txid: explorerUtxo.Txid,
Vout: explorerUtxo.Vout,
Amount: explorerUtxo.Amount,
Asset: explorerUtxo.Asset,
Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
}
}

View File

@@ -0,0 +1,48 @@
package bitcointree
import (
"encoding/hex"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
)
func ComputeOutputScript(desc descriptor.TaprootDescriptor) ([]byte, error) {
leaves := make([]txscript.TapLeaf, 0)
for _, leaf := range desc.ScriptTree {
scriptHex, err := leaf.Script(false)
if err != nil {
return nil, err
}
script, err := hex.DecodeString(scriptHex)
if err != nil {
return nil, err
}
leaves = append(leaves, txscript.NewBaseTapLeaf(script))
}
taprootTree := txscript.AssembleTaprootScriptTree(leaves...)
root := taprootTree.RootNode.TapHash()
internalKey, err := hex.DecodeString(desc.InternalKey.Hex)
if err != nil {
return nil, err
}
internalKeyParsed, err := schnorr.ParsePubKey(internalKey)
if err != nil {
return nil, err
}
taprootKey := txscript.ComputeTaprootOutputKey(internalKeyParsed, root[:])
outputScript, err := taprootOutputScript(taprootKey)
if err != nil {
return nil, err
}
return outputScript, nil
}

45
common/descriptor/ark.go Normal file
View File

@@ -0,0 +1,45 @@
package descriptor
import (
"encoding/hex"
"errors"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
const BoardingDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })"
func ParseBoardingDescriptor(
desc TaprootDescriptor,
) (user *secp256k1.PublicKey, timeout uint, err error) {
for _, leaf := range desc.ScriptTree {
if andLeaf, ok := leaf.(*And); ok {
if first, ok := andLeaf.First.(*Older); ok {
timeout = first.Timeout
}
if second, ok := andLeaf.Second.(*PK); ok {
keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, 0, err
}
user, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, 0, err
}
}
}
}
if user == nil {
return nil, 0, errors.New("boarding descriptor is invalid")
}
if timeout == 0 {
return nil, 0, errors.New("boarding descriptor is invalid")
}
return
}

View File

@@ -0,0 +1,224 @@
package descriptor
import (
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/txscript"
)
var (
ErrInvalidXOnlyKey = errors.New("invalid x only public key")
ErrInvalidPkPolicy = errors.New("invalid public key policy")
ErrInvalidOlderPolicy = errors.New("invalid older policy")
ErrInvalidAndPolicy = errors.New("invalid and() policy")
ErrNotExpectedPolicy = errors.New("not the expected policy")
)
type Expression interface {
Parse(policy string) error
Script(verify bool) (string, error)
String() string
}
type XOnlyKey struct {
Key
}
func (e *XOnlyKey) Parse(policy string) error {
if len(policy) != 64 {
fmt.Println(policy)
return ErrInvalidXOnlyKey
}
e.Hex = policy
return nil
}
func (e *XOnlyKey) Script() string {
return e.Hex
}
// pk(xonlypubkey)
type PK struct {
Key XOnlyKey
}
func (e *PK) String() string {
return fmt.Sprintf("pk(%s)", e.Key.Hex)
}
func (e *PK) Parse(policy string) error {
if !strings.HasPrefix(policy, "pk(") {
return ErrNotExpectedPolicy
}
if len(policy) != 3+64+1 {
return ErrInvalidPkPolicy
}
var key XOnlyKey
if err := key.Parse(policy[3 : 64+3]); err != nil {
return err
}
e.Key = key
return nil
}
func (e *PK) Script(verify bool) (string, error) {
pubkeyBytes, err := hex.DecodeString(e.Key.Hex)
if err != nil {
return "", err
}
checksig := txscript.OP_CHECKSIG
if verify {
checksig = txscript.OP_CHECKSIGVERIFY
}
script, err := txscript.NewScriptBuilder().AddData(
pubkeyBytes,
).AddOp(
byte(checksig),
).Script()
if err != nil {
return "", err
}
return hex.EncodeToString(script), nil
}
type Older struct {
Timeout uint
}
func (e *Older) String() string {
return fmt.Sprintf("older(%d)", e.Timeout)
}
func (e *Older) Parse(policy string) error {
if !strings.HasPrefix(policy, "older(") {
return ErrNotExpectedPolicy
}
index := strings.IndexRune(policy, ')')
if index == -1 {
return ErrInvalidOlderPolicy
}
number := policy[6:index]
if len(number) == 0 {
return ErrInvalidOlderPolicy
}
timeout, err := strconv.Atoi(number)
if err != nil {
return ErrInvalidOlderPolicy
}
e.Timeout = uint(timeout)
return nil
}
func (e *Older) Script(bool) (string, error) {
sequence, err := common.BIP68Encode(e.Timeout)
if err != nil {
return "", err
}
script, err := txscript.NewScriptBuilder().AddData(sequence).AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}).Script()
if err != nil {
return "", err
}
return hex.EncodeToString(script), nil
}
type And struct {
First Expression
Second Expression
}
func (e *And) String() string {
return fmt.Sprintf("and(%s,%s)", e.First.String(), e.Second.String())
}
func (e *And) Parse(policy string) error {
if !strings.HasPrefix(policy, "and(") {
return ErrNotExpectedPolicy
}
index := strings.LastIndexByte(policy, ')')
if index == -1 {
return ErrInvalidAndPolicy
}
childrenPolicy := policy[4:index]
if len(childrenPolicy) == 0 {
return ErrInvalidAndPolicy
}
children := strings.Split(childrenPolicy, ",")
if len(children) != 2 {
fmt.Println(children)
return ErrInvalidAndPolicy
}
first, err := parseExpression(children[0])
if err != nil {
return err
}
second, err := parseExpression(children[1])
if err != nil {
return err
}
e.First = first
e.Second = second
return nil
}
func (e *And) Script(verify bool) (string, error) {
firstScript, err := e.First.Script(true)
if err != nil {
return "", err
}
secondScript, err := e.Second.Script(verify)
if err != nil {
return "", err
}
return firstScript + secondScript, nil
}
func parseExpression(policy string) (Expression, error) {
policy = strings.TrimSpace(policy)
expressions := make([]Expression, 0)
expressions = append(expressions, &PK{})
expressions = append(expressions, &Older{})
expressions = append(expressions, &And{})
for _, e := range expressions {
if err := e.Parse(policy); err != nil {
if err != ErrNotExpectedPolicy {
return nil, err
}
continue
}
return e, nil
}
return nil, fmt.Errorf("unable to parse expression '%s'", policy)
}

125
common/descriptor/parser.go Normal file
View File

@@ -0,0 +1,125 @@
package descriptor
import (
"encoding/hex"
"fmt"
"strings"
)
// UnspendableKey is the x-only pubkey of the secp256k1 base point G
const UnspendableKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
func ParseTaprootDescriptor(desc string) (*TaprootDescriptor, error) {
desc = strings.ReplaceAll(desc, " ", "")
if !strings.HasPrefix(desc, "tr(") || !strings.HasSuffix(desc, ")") {
return nil, fmt.Errorf("invalid descriptor format")
}
content := desc[3 : len(desc)-1]
parts := strings.SplitN(content, ",", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid descriptor format: missing script tree")
}
internalKey, err := parseKey(parts[0])
if err != nil {
return nil, err
}
scriptTreeStr := parts[1]
if !strings.HasPrefix(scriptTreeStr, "{") || !strings.HasSuffix(scriptTreeStr, "}") {
return nil, fmt.Errorf("invalid script tree format")
}
scriptTreeStr = scriptTreeStr[1 : len(scriptTreeStr)-1]
scriptTree := []Expression{}
if scriptTreeStr != "" {
scriptParts, err := splitScriptTree(scriptTreeStr)
if err != nil {
return nil, err
}
for _, scriptStr := range scriptParts {
leaf, err := parseExpression(scriptStr)
if err != nil {
return nil, err
}
scriptTree = append(scriptTree, leaf)
}
}
return &TaprootDescriptor{
InternalKey: internalKey,
ScriptTree: scriptTree,
}, nil
}
// CompileDescriptor compiles a TaprootDescriptor struct back into a descriptor string
func CompileDescriptor(desc TaprootDescriptor) string {
scriptParts := make([]string, len(desc.ScriptTree))
for i, leaf := range desc.ScriptTree {
scriptParts[i] = leaf.String()
}
scriptTree := strings.Join(scriptParts, ",")
return fmt.Sprintf("tr(%s,{%s})", desc.InternalKey.Hex, scriptTree)
}
func parseKey(keyStr string) (Key, error) {
decoded, err := hex.DecodeString(keyStr)
if err != nil {
return Key{}, fmt.Errorf("invalid key: not a valid hex string: %v", err)
}
switch len(decoded) {
case 32:
// x-only public key, this is correct for Taproot
return Key{Hex: keyStr}, nil
case 33:
// compressed public key, we need to remove the prefix byte
return Key{Hex: keyStr[2:]}, nil
default:
return Key{}, fmt.Errorf("invalid key length: expected 32 or 33 bytes, got %d", len(decoded))
}
}
func splitScriptTree(scriptTreeStr string) ([]string, error) {
var result []string
var current strings.Builder
depth := 0
for _, char := range scriptTreeStr {
switch char {
case '(':
depth++
current.WriteRune(char)
case ')':
depth--
current.WriteRune(char)
if depth == 0 {
result = append(result, current.String())
current.Reset()
}
case ',':
if depth == 0 {
if current.Len() > 0 {
result = append(result, current.String())
current.Reset()
}
} else {
current.WriteRune(char)
}
default:
current.WriteRune(char)
}
}
if current.Len() > 0 {
result = append(result, current.String())
}
if depth != 0 {
return nil, fmt.Errorf("mismatched parentheses in script tree")
}
return result, nil
}

View File

@@ -0,0 +1,350 @@
package descriptor_test
import (
"testing"
"github.com/ark-network/ark/common/descriptor"
"github.com/stretchr/testify/require"
)
func TestParseTaprootDescriptor(t *testing.T) {
tests := []struct {
name string
desc string
expected descriptor.TaprootDescriptor
wantErr bool
}{
{
name: "Basic Taproot",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
ScriptTree: []descriptor.Expression{
&descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
},
},
wantErr: false,
},
{
name: "VTXO",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c),and(pk(59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8),older(144))})",
expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
ScriptTree: []descriptor.Expression{
&descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
&descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8",
},
},
},
Second: &descriptor.Older{
Timeout: 144,
},
},
},
},
wantErr: false,
},
{
name: "Boarding",
desc: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)), and(older(604672), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)) })",
expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"},
ScriptTree: []descriptor.Expression{
&descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
},
},
},
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
},
&descriptor.And{
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
First: &descriptor.Older{
Timeout: 604672,
},
},
},
},
wantErr: false,
},
{
name: "Invalid Key",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798G,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
expected: descriptor.TaprootDescriptor{},
wantErr: true,
},
{
name: "Invalid Descriptor Format",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
expected: descriptor.TaprootDescriptor{},
wantErr: true,
},
{
name: "Invalid Descriptor Format - Missing Script Tree",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
expected: descriptor.TaprootDescriptor{},
wantErr: true,
},
{
name: "Valid Empty Script Tree",
desc: "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{})",
expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"},
ScriptTree: []descriptor.Expression{},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := descriptor.ParseTaprootDescriptor(tt.desc)
if (err != nil) != tt.wantErr {
require.Equal(t, tt.wantErr, err != nil, err)
return
}
require.Equal(t, tt.expected, got)
})
}
}
func TestCompileDescriptor(t *testing.T) {
tests := []struct {
name string
desc descriptor.TaprootDescriptor
expected string
}{
{
name: "Basic Taproot",
desc: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: descriptor.UnspendableKey},
ScriptTree: []descriptor.Expression{
&descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
},
},
expected: "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)})",
},
{
name: "VTXO",
desc: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: descriptor.UnspendableKey},
ScriptTree: []descriptor.Expression{
&descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
&descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8",
},
},
},
Second: &descriptor.Older{
Timeout: 1024,
},
},
},
},
expected: "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c),and(pk(59bffef74a89f39715b9f6b8a83e53a60a458d45542f20e2e2f4f7dbffafc5f8),older(1024))})",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := descriptor.CompileDescriptor(tt.desc)
require.Equal(t, tt.expected, got)
})
}
}
func TestParsePk(t *testing.T) {
tests := []struct {
policy string
expectedScript string
expected descriptor.PK
verify bool
}{
{
policy: "pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)",
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cac",
verify: false,
expected: descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
},
{
policy: "pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c)",
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad",
verify: true,
expected: descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
},
}
for _, test := range tests {
var parsed descriptor.PK
err := parsed.Parse(test.policy)
require.NoError(t, err)
require.Equal(t, test.expected, parsed)
script, err := parsed.Script(test.verify)
require.NoError(t, err)
require.Equal(t, test.expectedScript, script)
}
}
func TestParseOlder(t *testing.T) {
tests := []struct {
policy string
expectedScript string
expected descriptor.Older
}{
{
policy: "older(512)",
expectedScript: "03010040b275",
expected: descriptor.Older{
Timeout: uint(512),
},
},
{
policy: "older(1024)",
expectedScript: "03020040b275",
expected: descriptor.Older{
Timeout: uint(1024),
},
},
}
for _, test := range tests {
var parsed descriptor.Older
err := parsed.Parse(test.policy)
require.NoError(t, err)
require.Equal(t, test.expected, parsed)
script, err := parsed.Script(false)
require.NoError(t, err)
require.Equal(t, test.expectedScript, script)
}
}
func TestParseAnd(t *testing.T) {
tests := []struct {
policy string
expectedScript string
expected descriptor.And
}{
{
policy: "and(pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c), older(512))",
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad03010040b275",
expected: descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
Second: &descriptor.Older{
Timeout: 512,
},
},
},
{
policy: "and(older(512), pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c))",
expectedScript: "03010040b2752081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cac",
expected: descriptor.And{
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
First: &descriptor.Older{
Timeout: 512,
},
},
},
{
policy: "and(pk(81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798))",
expectedScript: "2081e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952cad2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac",
expected: descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "81e0351fc94c3ba05f8d68354ff44711b02223f2b32fb7f3ef3a99a90af7952c",
},
},
},
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
},
},
}
for _, test := range tests {
var parsed descriptor.And
err := parsed.Parse(test.policy)
require.NoError(t, err)
require.Equal(t, test.expected, parsed)
script, err := parsed.Script(false)
require.NoError(t, err)
require.Equal(t, test.expectedScript, script)
}
}

View File

@@ -0,0 +1,10 @@
package descriptor
type Key struct {
Hex string
}
type TaprootDescriptor struct {
InternalKey Key
ScriptTree []Expression
}

View File

@@ -35,7 +35,7 @@ func EncodeAddress(
func DecodeAddress(
addr string,
) (hrp string, userKey *secp256k1.PublicKey, aspKey *secp256k1.PublicKey, err error) {
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) {
prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil {
return

52
common/tree/descriptor.go Normal file
View File

@@ -0,0 +1,52 @@
package tree
import (
"encoding/hex"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/vulpemventures/go-elements/taproot"
)
func ComputeOutputScript(desc descriptor.TaprootDescriptor) ([]byte, error) {
leaves := make([]taproot.TapElementsLeaf, 0)
for _, l := range desc.ScriptTree {
script, err := l.Script(false)
if err != nil {
return nil, err
}
scriptBytes, err := hex.DecodeString(script)
if err != nil {
return nil, err
}
leaves = append(leaves, taproot.NewBaseTapElementsLeaf(scriptBytes))
}
taprootTree := taproot.AssembleTaprootScriptTree(
leaves...,
)
root := taprootTree.RootNode.TapHash()
internalKey, err := hex.DecodeString(desc.InternalKey.Hex)
if err != nil {
return nil, err
}
internalKeyParsed, err := schnorr.ParsePubKey(internalKey)
if err != nil {
return nil, err
}
taprootKey := taproot.ComputeTaprootOutputKey(internalKeyParsed, root[:])
outputScript, err := taprootOutputScript(taprootKey)
if err != nil {
return nil, err
}
return outputScript, nil
}

View File

@@ -3,6 +3,7 @@ package tree
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
@@ -62,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
return closure, nil
}
return nil, fmt.Errorf("invalid closure script")
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
}
func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {

View File

@@ -12,21 +12,18 @@ services:
- ARK_ROUND_LIFETIME=512
- ARK_TX_BUILDER_TYPE=covenantless
- ARK_MIN_RELAY_FEE=200
- ARK_NEUTRINO_PEER=bitcoin:18444
- ARK_ESPLORA_URL=http://chopsticks:3000
- ARK_BITCOIND_RPC_USER=admin1
- ARK_BITCOIND_RPC_PASS=123
- ARK_BITCOIND_RPC_HOST=bitcoin:18443
- ARK_NO_TLS=true
- ARK_NO_MACAROONS=true
- ARK_DATADIR=/app/data
ports:
- "7070:7070"
volumes:
- clarkd:/app/data
- clark:/app/wallet-data
volumes:
clarkd:
external: false
clark:
external: false
- type: tmpfs
target: /app/data
networks:
default:

View File

@@ -12,11 +12,12 @@ services:
- OCEAN_NETWORK=regtest
- OCEAN_UTXO_EXPIRY_DURATION_IN_SECONDS=60
- OCEAN_DB_TYPE=badger
- OCEAN_DATADIR=/app/data
ports:
- "18000:18000"
volumes:
- oceand:/app/data/oceand
- ocean:/app/data/ocean
- type: tmpfs
target: /app/data
arkd:
container_name: arkd
build:
@@ -36,21 +37,12 @@ services:
- ARK_PORT=6060
- ARK_NO_TLS=true
- ARK_NO_MACAROONS=true
- ARK_DATADIR=/app/data
ports:
- "6060:6060"
volumes:
- arkd:/app/data
- ark:/app/wallet-data
volumes:
oceand:
external: false
ocean:
external: false
arkd:
external: false
ark:
external: false
- type: tmpfs
target: /app/data
networks:
default:

View File

@@ -15,7 +15,6 @@ type ArkClient interface {
Unlock(ctx context.Context, password string) error
Lock(ctx context.Context, password string) error
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
Onboard(ctx context.Context, amount uint64) (string, error)
Receive(ctx context.Context) (string, string, error)
SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
SendOffChain(
@@ -26,7 +25,7 @@ type ArkClient interface {
ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool,
) (string, error)
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
ClaimAsync(ctx context.Context) (string, error)
Claim(ctx context.Context) (string, error)
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
}

View File

@@ -92,14 +92,15 @@ func (a *arkClient) InitWithWallet(
}
storeData := store.StoreData{
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.Wallet.GetType(),
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.Wallet.GetType(),
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
}
if err := a.store.AddData(ctx, storeData); err != nil {
return err
@@ -155,14 +156,15 @@ func (a *arkClient) Init(
}
storeData := store.StoreData{
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.WalletType,
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
AspUrl: args.AspUrl,
AspPubkey: aspPubkey,
WalletType: args.WalletType,
ClientType: args.ClientType,
Network: network,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
MinRelayFee: uint64(info.MinRelayFee),
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
}
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
if err != nil {
@@ -201,12 +203,12 @@ func (a *arkClient) IsLocked(ctx context.Context) bool {
}
func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", "", err
}
return offchainAddr, onchainAddr, nil
return offchainAddr, boardingAddr, nil
}
func (a *arkClient) ListVtxos(
@@ -232,14 +234,11 @@ func (a *arkClient) ListVtxos(
func (a *arkClient) ping(
ctx context.Context, paymentID string,
) func() {
_, err := a.client.Ping(ctx, paymentID)
if err != nil {
return nil
}
ticker := time.NewTicker(5 * time.Second)
go func(t *time.Ticker) {
// nolint
a.client.Ping(ctx, paymentID)
for range t.C {
// nolint
a.client.Ping(ctx, paymentID)

View File

@@ -23,11 +23,8 @@ type ASPClient interface {
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, ephemeralPublicKey string,
ctx context.Context, inputs []Input, ephemeralKey string,
) (string, error)
ClaimPayment(
ctx context.Context, paymentID string, outputs []Output,
@@ -37,7 +34,7 @@ type ASPClient interface {
) (<-chan RoundEventChannel, error)
Ping(ctx context.Context, paymentID string) (RoundEvent, error)
FinalizePayment(
ctx context.Context, signedForfeitTxs []string,
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error
CreatePayment(
ctx context.Context, inputs []VtxoKey, outputs []Output,
@@ -45,6 +42,7 @@ type ASPClient interface {
CompletePayment(
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
) error
GetBoardingAddress(ctx context.Context, userPubkey string) (string, error)
SendTreeNonces(
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
) error
@@ -55,12 +53,13 @@ type ASPClient interface {
}
type Info struct {
Pubkey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
Pubkey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
BoardingDescriptorTemplate string
}
type RoundEventChannel struct {
@@ -68,11 +67,38 @@ type RoundEventChannel struct {
Err error
}
type Input interface {
GetTxID() string
GetVOut() uint32
GetDescriptor() string
}
type VtxoKey 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
Descriptor string
}
func (k BoardingInput) GetDescriptor() string {
return k.Descriptor
}
type Vtxo struct {
VtxoKey
Amount uint64

View File

@@ -97,12 +97,13 @@ func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
return nil, err
}
return &client.Info{
Pubkey: resp.GetPubkey(),
RoundLifetime: resp.GetRoundLifetime(),
UnilateralExitDelay: resp.GetUnilateralExitDelay(),
RoundInterval: resp.GetRoundInterval(),
Network: resp.GetNetwork(),
MinRelayFee: resp.GetMinRelayFee(),
Pubkey: resp.GetPubkey(),
RoundLifetime: resp.GetRoundLifetime(),
UnilateralExitDelay: resp.GetUnilateralExitDelay(),
RoundInterval: resp.GetRoundInterval(),
Network: resp.GetNetwork(),
MinRelayFee: resp.GetMinRelayFee(),
BoardingDescriptorTemplate: resp.GetBoardingDescriptorTemplate(),
}, nil
}
@@ -143,20 +144,8 @@ func (a *grpcClient) GetRound(
}, nil
}
func (a *grpcClient) Onboard(
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, inputs []client.VtxoKey, ephemeralPublicKey string,
ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
) (string, error) {
req := &arkv1.RegisterPaymentRequest{
Inputs: ins(inputs).toProto(),
@@ -198,11 +187,16 @@ func (a *grpcClient) Ping(
}
func (a *grpcClient) FinalizePayment(
ctx context.Context, signedForfeitTxs []string,
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
if len(signedRoundTx) > 0 {
req.SignedRoundTx = &signedRoundTx
}
_, err := a.svc.FinalizePayment(ctx, req)
return err
}
@@ -210,8 +204,13 @@ func (a *grpcClient) FinalizePayment(
func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, 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(inputs).toProto(),
Inputs: ins(insCast).toProto(),
Outputs: outs(outputs).toProto(),
}
resp, err := a.svc.CreatePayment(ctx, req)
@@ -260,6 +259,19 @@ func (a *grpcClient) GetRoundByID(
}, nil
}
func (a *grpcClient) GetBoardingAddress(
ctx context.Context, userPubkey string,
) (string, error) {
req := &arkv1.GetBoardingAddressRequest{
Pubkey: userPubkey,
}
resp, err := a.svc.GetBoardingAddress(ctx, req)
if err != nil {
return "", err
}
return resp.GetAddress(), nil
}
func (a *grpcClient) SendTreeNonces(
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
) error {
@@ -418,8 +430,8 @@ func (v vtxo) toVtxo() client.Vtxo {
}
return client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(),
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
},
Amount: v.GetReceiver().GetAmount(),
RoundTxid: v.GetPoolTxid(),
@@ -441,21 +453,35 @@ func (v vtxos) toVtxos() []client.Vtxo {
return list
}
type input client.VtxoKey
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(),
},
},
}
}
func (i input) toProto() *arkv1.Input {
return &arkv1.Input{
Txid: i.Txid,
Vout: i.VOut,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: i.GetTxID(),
Vout: i.GetVOut(),
},
},
}
}
type ins []client.VtxoKey
type ins []client.Input
func (i ins) toProto() []*arkv1.Input {
list := make([]*arkv1.Input, 0, len(i))
for _, ii := range i {
list = append(list, input(ii).toProto())
list = append(list, toProtoInput(ii))
}
return list
}
@@ -496,27 +522,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
return levels
}
type treeToProto tree.CongestionTree
func (t treeToProto) parse() *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(t))
for _, level := range t {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}

View File

@@ -114,12 +114,13 @@ func (a *restClient) GetInfo(
}
return &client.Info{
Pubkey: resp.Payload.Pubkey,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
RoundInterval: int64(roundInterval),
Network: resp.Payload.Network,
MinRelayFee: int64(minRelayFee),
Pubkey: resp.Payload.Pubkey,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
RoundInterval: int64(roundInterval),
Network: resp.Payload.Network,
MinRelayFee: int64(minRelayFee),
BoardingDescriptorTemplate: resp.Payload.BoardingDescriptorTemplate,
}, nil
}
@@ -159,8 +160,8 @@ func (a *restClient) ListVtxos(
spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
@@ -191,8 +192,8 @@ func (a *restClient) ListVtxos(
spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
@@ -243,29 +244,31 @@ func (a *restClient) GetRound(
}, nil
}
func (a *restClient) Onboard(
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
) error {
body := models.V1OnboardRequest{
BoardingTx: tx,
CongestionTree: treeToProto(congestionTree).parse(),
UserPubkey: userPubkey,
}
_, err := a.svc.ArkServiceOnboard(
ark_service.NewArkServiceOnboardParams().WithBody(&body),
)
return err
}
func (a *restClient) RegisterPayment(
ctx context.Context, inputs []client.VtxoKey, ephemeralPublicKey string,
ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
) (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),
})
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)
}
body := &models.V1RegisterPaymentRequest{
Inputs: ins,
@@ -377,13 +380,11 @@ func (a *restClient) Ping(
}
func (a *restClient) FinalizePayment(
ctx context.Context, signedForfeitTxs []string,
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
body := models.V1FinalizePaymentRequest{
SignedForfeitTxs: req.GetSignedForfeitTxs(),
SignedForfeitTxs: signedForfeitTxs,
SignedRoundTx: signedRoundTx,
}
_, err := a.svc.ArkServiceFinalizePayment(
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
@@ -396,9 +397,15 @@ func (a *restClient) CreatePayment(
) (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{
Txid: i.Txid,
Vout: int64(i.VOut),
VtxoInput: &models.V1VtxoInput{
Txid: i.Txid,
Vout: int64(i.VOut),
},
})
}
outs := make([]*models.V1Output, 0, len(outputs))
@@ -477,6 +484,23 @@ func (a *restClient) GetRoundByID(
}, nil
}
func (a *restClient) GetBoardingAddress(
ctx context.Context, pubkey string,
) (string, error) {
body := models.V1GetBoardingAddressRequest{
Pubkey: pubkey,
}
resp, err := a.svc.ArkServiceGetBoardingAddress(
ark_service.NewArkServiceGetBoardingAddressParams().WithBody(&body),
)
if err != nil {
return "",
err
}
return resp.Payload.Address, nil
}
func (a *restClient) SendTreeNonces(
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
) error {
@@ -604,25 +628,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
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,
}
}

View File

@@ -62,6 +62,8 @@ type ClientService interface {
ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentParams, opts ...ClientOption) (*ArkServiceFinalizePaymentOK, error)
ArkServiceGetBoardingAddress(params *ArkServiceGetBoardingAddressParams, opts ...ClientOption) (*ArkServiceGetBoardingAddressOK, error)
ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error)
ArkServiceGetInfo(params *ArkServiceGetInfoParams, opts ...ClientOption) (*ArkServiceGetInfoOK, error)
@@ -72,8 +74,6 @@ type ClientService interface {
ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...ClientOption) (*ArkServiceListVtxosOK, error)
ArkServiceOnboard(params *ArkServiceOnboardParams, opts ...ClientOption) (*ArkServiceOnboardOK, error)
ArkServicePing(params *ArkServicePingParams, opts ...ClientOption) (*ArkServicePingOK, error)
ArkServiceRegisterPayment(params *ArkServiceRegisterPaymentParams, opts ...ClientOption) (*ArkServiceRegisterPaymentOK, error)
@@ -233,6 +233,43 @@ func (a *Client) ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentPara
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/*
ArkServiceGetBoardingAddress ark service get boarding address API
*/
func (a *Client) ArkServiceGetBoardingAddress(params *ArkServiceGetBoardingAddressParams, opts ...ClientOption) (*ArkServiceGetBoardingAddressOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewArkServiceGetBoardingAddressParams()
}
op := &runtime.ClientOperation{
ID: "ArkService_GetBoardingAddress",
Method: "POST",
PathPattern: "/v1/boarding",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &ArkServiceGetBoardingAddressReader{formats: a.formats},
Context: params.Context,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
success, ok := result.(*ArkServiceGetBoardingAddressOK)
if ok {
return success, nil
}
// unexpected success response
unexpectedSuccess := result.(*ArkServiceGetBoardingAddressDefault)
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/*
ArkServiceGetEventStream ark service get event stream API
*/
@@ -418,43 +455,6 @@ func (a *Client) ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/*
ArkServiceOnboard ark service onboard API
*/
func (a *Client) ArkServiceOnboard(params *ArkServiceOnboardParams, opts ...ClientOption) (*ArkServiceOnboardOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewArkServiceOnboardParams()
}
op := &runtime.ClientOperation{
ID: "ArkService_Onboard",
Method: "POST",
PathPattern: "/v1/onboard",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &ArkServiceOnboardReader{formats: a.formats},
Context: params.Context,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
success, ok := result.(*ArkServiceOnboardOK)
if ok {
return success, nil
}
// unexpected success response
unexpectedSuccess := result.(*ArkServiceOnboardDefault)
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
}
/*
ArkServicePing ark service ping API
*/

View File

@@ -0,0 +1,150 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// NewArkServiceGetBoardingAddressParams creates a new ArkServiceGetBoardingAddressParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewArkServiceGetBoardingAddressParams() *ArkServiceGetBoardingAddressParams {
return &ArkServiceGetBoardingAddressParams{
timeout: cr.DefaultTimeout,
}
}
// NewArkServiceGetBoardingAddressParamsWithTimeout creates a new ArkServiceGetBoardingAddressParams object
// with the ability to set a timeout on a request.
func NewArkServiceGetBoardingAddressParamsWithTimeout(timeout time.Duration) *ArkServiceGetBoardingAddressParams {
return &ArkServiceGetBoardingAddressParams{
timeout: timeout,
}
}
// NewArkServiceGetBoardingAddressParamsWithContext creates a new ArkServiceGetBoardingAddressParams object
// with the ability to set a context for a request.
func NewArkServiceGetBoardingAddressParamsWithContext(ctx context.Context) *ArkServiceGetBoardingAddressParams {
return &ArkServiceGetBoardingAddressParams{
Context: ctx,
}
}
// NewArkServiceGetBoardingAddressParamsWithHTTPClient creates a new ArkServiceGetBoardingAddressParams object
// with the ability to set a custom HTTPClient for a request.
func NewArkServiceGetBoardingAddressParamsWithHTTPClient(client *http.Client) *ArkServiceGetBoardingAddressParams {
return &ArkServiceGetBoardingAddressParams{
HTTPClient: client,
}
}
/*
ArkServiceGetBoardingAddressParams contains all the parameters to send to the API endpoint
for the ark service get boarding address operation.
Typically these are written to a http.Request.
*/
type ArkServiceGetBoardingAddressParams struct {
// Body.
Body *models.V1GetBoardingAddressRequest
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the ark service get boarding address params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceGetBoardingAddressParams) WithDefaults() *ArkServiceGetBoardingAddressParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the ark service get boarding address params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceGetBoardingAddressParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) WithTimeout(timeout time.Duration) *ArkServiceGetBoardingAddressParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) WithContext(ctx context.Context) *ArkServiceGetBoardingAddressParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) WithHTTPClient(client *http.Client) *ArkServiceGetBoardingAddressParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) WithBody(body *models.V1GetBoardingAddressRequest) *ArkServiceGetBoardingAddressParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the ark service get boarding address params
func (o *ArkServiceGetBoardingAddressParams) SetBody(body *models.V1GetBoardingAddressRequest) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *ArkServiceGetBoardingAddressParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Body != nil {
if err := r.SetBodyParam(o.Body); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,187 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// ArkServiceGetBoardingAddressReader is a Reader for the ArkServiceGetBoardingAddress structure.
type ArkServiceGetBoardingAddressReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *ArkServiceGetBoardingAddressReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewArkServiceGetBoardingAddressOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
default:
result := NewArkServiceGetBoardingAddressDefault(response.Code())
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
if response.Code()/100 == 2 {
return result, nil
}
return nil, result
}
}
// NewArkServiceGetBoardingAddressOK creates a ArkServiceGetBoardingAddressOK with default headers values
func NewArkServiceGetBoardingAddressOK() *ArkServiceGetBoardingAddressOK {
return &ArkServiceGetBoardingAddressOK{}
}
/*
ArkServiceGetBoardingAddressOK describes a response with status code 200, with default header values.
A successful response.
*/
type ArkServiceGetBoardingAddressOK struct {
Payload *models.V1GetBoardingAddressResponse
}
// IsSuccess returns true when this ark service get boarding address o k response has a 2xx status code
func (o *ArkServiceGetBoardingAddressOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ark service get boarding address o k response has a 3xx status code
func (o *ArkServiceGetBoardingAddressOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ark service get boarding address o k response has a 4xx status code
func (o *ArkServiceGetBoardingAddressOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ark service get boarding address o k response has a 5xx status code
func (o *ArkServiceGetBoardingAddressOK) IsServerError() bool {
return false
}
// IsCode returns true when this ark service get boarding address o k response a status code equal to that given
func (o *ArkServiceGetBoardingAddressOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ark service get boarding address o k response
func (o *ArkServiceGetBoardingAddressOK) Code() int {
return 200
}
func (o *ArkServiceGetBoardingAddressOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/boarding][%d] arkServiceGetBoardingAddressOK %s", 200, payload)
}
func (o *ArkServiceGetBoardingAddressOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/boarding][%d] arkServiceGetBoardingAddressOK %s", 200, payload)
}
func (o *ArkServiceGetBoardingAddressOK) GetPayload() *models.V1GetBoardingAddressResponse {
return o.Payload
}
func (o *ArkServiceGetBoardingAddressOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.V1GetBoardingAddressResponse)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewArkServiceGetBoardingAddressDefault creates a ArkServiceGetBoardingAddressDefault with default headers values
func NewArkServiceGetBoardingAddressDefault(code int) *ArkServiceGetBoardingAddressDefault {
return &ArkServiceGetBoardingAddressDefault{
_statusCode: code,
}
}
/*
ArkServiceGetBoardingAddressDefault describes a response with status code -1, with default header values.
An unexpected error response.
*/
type ArkServiceGetBoardingAddressDefault struct {
_statusCode int
Payload *models.RPCStatus
}
// IsSuccess returns true when this ark service get boarding address default response has a 2xx status code
func (o *ArkServiceGetBoardingAddressDefault) IsSuccess() bool {
return o._statusCode/100 == 2
}
// IsRedirect returns true when this ark service get boarding address default response has a 3xx status code
func (o *ArkServiceGetBoardingAddressDefault) IsRedirect() bool {
return o._statusCode/100 == 3
}
// IsClientError returns true when this ark service get boarding address default response has a 4xx status code
func (o *ArkServiceGetBoardingAddressDefault) IsClientError() bool {
return o._statusCode/100 == 4
}
// IsServerError returns true when this ark service get boarding address default response has a 5xx status code
func (o *ArkServiceGetBoardingAddressDefault) IsServerError() bool {
return o._statusCode/100 == 5
}
// IsCode returns true when this ark service get boarding address default response a status code equal to that given
func (o *ArkServiceGetBoardingAddressDefault) IsCode(code int) bool {
return o._statusCode == code
}
// Code gets the status code for the ark service get boarding address default response
func (o *ArkServiceGetBoardingAddressDefault) Code() int {
return o._statusCode
}
func (o *ArkServiceGetBoardingAddressDefault) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/boarding][%d] ArkService_GetBoardingAddress default %s", o._statusCode, payload)
}
func (o *ArkServiceGetBoardingAddressDefault) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/boarding][%d] ArkService_GetBoardingAddress default %s", o._statusCode, payload)
}
func (o *ArkServiceGetBoardingAddressDefault) GetPayload() *models.RPCStatus {
return o.Payload
}
func (o *ArkServiceGetBoardingAddressDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.RPCStatus)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}

View File

@@ -1,150 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// NewArkServiceOnboardParams creates a new ArkServiceOnboardParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewArkServiceOnboardParams() *ArkServiceOnboardParams {
return &ArkServiceOnboardParams{
timeout: cr.DefaultTimeout,
}
}
// NewArkServiceOnboardParamsWithTimeout creates a new ArkServiceOnboardParams object
// with the ability to set a timeout on a request.
func NewArkServiceOnboardParamsWithTimeout(timeout time.Duration) *ArkServiceOnboardParams {
return &ArkServiceOnboardParams{
timeout: timeout,
}
}
// NewArkServiceOnboardParamsWithContext creates a new ArkServiceOnboardParams object
// with the ability to set a context for a request.
func NewArkServiceOnboardParamsWithContext(ctx context.Context) *ArkServiceOnboardParams {
return &ArkServiceOnboardParams{
Context: ctx,
}
}
// NewArkServiceOnboardParamsWithHTTPClient creates a new ArkServiceOnboardParams object
// with the ability to set a custom HTTPClient for a request.
func NewArkServiceOnboardParamsWithHTTPClient(client *http.Client) *ArkServiceOnboardParams {
return &ArkServiceOnboardParams{
HTTPClient: client,
}
}
/*
ArkServiceOnboardParams contains all the parameters to send to the API endpoint
for the ark service onboard operation.
Typically these are written to a http.Request.
*/
type ArkServiceOnboardParams struct {
// Body.
Body *models.V1OnboardRequest
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the ark service onboard params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceOnboardParams) WithDefaults() *ArkServiceOnboardParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the ark service onboard params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceOnboardParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the ark service onboard params
func (o *ArkServiceOnboardParams) WithTimeout(timeout time.Duration) *ArkServiceOnboardParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the ark service onboard params
func (o *ArkServiceOnboardParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the ark service onboard params
func (o *ArkServiceOnboardParams) WithContext(ctx context.Context) *ArkServiceOnboardParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the ark service onboard params
func (o *ArkServiceOnboardParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the ark service onboard params
func (o *ArkServiceOnboardParams) WithHTTPClient(client *http.Client) *ArkServiceOnboardParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the ark service onboard params
func (o *ArkServiceOnboardParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the ark service onboard params
func (o *ArkServiceOnboardParams) WithBody(body *models.V1OnboardRequest) *ArkServiceOnboardParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the ark service onboard params
func (o *ArkServiceOnboardParams) SetBody(body *models.V1OnboardRequest) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *ArkServiceOnboardParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Body != nil {
if err := r.SetBodyParam(o.Body); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,185 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// ArkServiceOnboardReader is a Reader for the ArkServiceOnboard structure.
type ArkServiceOnboardReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *ArkServiceOnboardReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewArkServiceOnboardOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
default:
result := NewArkServiceOnboardDefault(response.Code())
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
if response.Code()/100 == 2 {
return result, nil
}
return nil, result
}
}
// NewArkServiceOnboardOK creates a ArkServiceOnboardOK with default headers values
func NewArkServiceOnboardOK() *ArkServiceOnboardOK {
return &ArkServiceOnboardOK{}
}
/*
ArkServiceOnboardOK describes a response with status code 200, with default header values.
A successful response.
*/
type ArkServiceOnboardOK struct {
Payload models.V1OnboardResponse
}
// IsSuccess returns true when this ark service onboard o k response has a 2xx status code
func (o *ArkServiceOnboardOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ark service onboard o k response has a 3xx status code
func (o *ArkServiceOnboardOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ark service onboard o k response has a 4xx status code
func (o *ArkServiceOnboardOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ark service onboard o k response has a 5xx status code
func (o *ArkServiceOnboardOK) IsServerError() bool {
return false
}
// IsCode returns true when this ark service onboard o k response a status code equal to that given
func (o *ArkServiceOnboardOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ark service onboard o k response
func (o *ArkServiceOnboardOK) Code() int {
return 200
}
func (o *ArkServiceOnboardOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard][%d] arkServiceOnboardOK %s", 200, payload)
}
func (o *ArkServiceOnboardOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard][%d] arkServiceOnboardOK %s", 200, payload)
}
func (o *ArkServiceOnboardOK) GetPayload() models.V1OnboardResponse {
return o.Payload
}
func (o *ArkServiceOnboardOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// response payload
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewArkServiceOnboardDefault creates a ArkServiceOnboardDefault with default headers values
func NewArkServiceOnboardDefault(code int) *ArkServiceOnboardDefault {
return &ArkServiceOnboardDefault{
_statusCode: code,
}
}
/*
ArkServiceOnboardDefault describes a response with status code -1, with default header values.
An unexpected error response.
*/
type ArkServiceOnboardDefault struct {
_statusCode int
Payload *models.RPCStatus
}
// IsSuccess returns true when this ark service onboard default response has a 2xx status code
func (o *ArkServiceOnboardDefault) IsSuccess() bool {
return o._statusCode/100 == 2
}
// IsRedirect returns true when this ark service onboard default response has a 3xx status code
func (o *ArkServiceOnboardDefault) IsRedirect() bool {
return o._statusCode/100 == 3
}
// IsClientError returns true when this ark service onboard default response has a 4xx status code
func (o *ArkServiceOnboardDefault) IsClientError() bool {
return o._statusCode/100 == 4
}
// IsServerError returns true when this ark service onboard default response has a 5xx status code
func (o *ArkServiceOnboardDefault) IsServerError() bool {
return o._statusCode/100 == 5
}
// IsCode returns true when this ark service onboard default response a status code equal to that given
func (o *ArkServiceOnboardDefault) IsCode(code int) bool {
return o._statusCode == code
}
// Code gets the status code for the ark service onboard default response
func (o *ArkServiceOnboardDefault) Code() int {
return o._statusCode
}
func (o *ArkServiceOnboardDefault) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard][%d] ArkService_Onboard default %s", o._statusCode, payload)
}
func (o *ArkServiceOnboardDefault) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard][%d] ArkService_Onboard default %s", o._statusCode, payload)
}
func (o *ArkServiceOnboardDefault) GetPayload() *models.RPCStatus {
return o.Payload
}
func (o *ArkServiceOnboardDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.RPCStatus)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}

View File

@@ -1,150 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// NewArkServiceTrustedOnboardingParams creates a new ArkServiceTrustedOnboardingParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewArkServiceTrustedOnboardingParams() *ArkServiceTrustedOnboardingParams {
return &ArkServiceTrustedOnboardingParams{
timeout: cr.DefaultTimeout,
}
}
// NewArkServiceTrustedOnboardingParamsWithTimeout creates a new ArkServiceTrustedOnboardingParams object
// with the ability to set a timeout on a request.
func NewArkServiceTrustedOnboardingParamsWithTimeout(timeout time.Duration) *ArkServiceTrustedOnboardingParams {
return &ArkServiceTrustedOnboardingParams{
timeout: timeout,
}
}
// NewArkServiceTrustedOnboardingParamsWithContext creates a new ArkServiceTrustedOnboardingParams object
// with the ability to set a context for a request.
func NewArkServiceTrustedOnboardingParamsWithContext(ctx context.Context) *ArkServiceTrustedOnboardingParams {
return &ArkServiceTrustedOnboardingParams{
Context: ctx,
}
}
// NewArkServiceTrustedOnboardingParamsWithHTTPClient creates a new ArkServiceTrustedOnboardingParams object
// with the ability to set a custom HTTPClient for a request.
func NewArkServiceTrustedOnboardingParamsWithHTTPClient(client *http.Client) *ArkServiceTrustedOnboardingParams {
return &ArkServiceTrustedOnboardingParams{
HTTPClient: client,
}
}
/*
ArkServiceTrustedOnboardingParams contains all the parameters to send to the API endpoint
for the ark service trusted onboarding operation.
Typically these are written to a http.Request.
*/
type ArkServiceTrustedOnboardingParams struct {
// Body.
Body *models.V1TrustedOnboardingRequest
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the ark service trusted onboarding params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceTrustedOnboardingParams) WithDefaults() *ArkServiceTrustedOnboardingParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the ark service trusted onboarding params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *ArkServiceTrustedOnboardingParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) WithTimeout(timeout time.Duration) *ArkServiceTrustedOnboardingParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) WithContext(ctx context.Context) *ArkServiceTrustedOnboardingParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) WithHTTPClient(client *http.Client) *ArkServiceTrustedOnboardingParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) WithBody(body *models.V1TrustedOnboardingRequest) *ArkServiceTrustedOnboardingParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the ark service trusted onboarding params
func (o *ArkServiceTrustedOnboardingParams) SetBody(body *models.V1TrustedOnboardingRequest) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *ArkServiceTrustedOnboardingParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Body != nil {
if err := r.SetBodyParam(o.Body); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -1,187 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
package ark_service
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/ark-network/ark/pkg/client-sdk/client/rest/service/models"
)
// ArkServiceTrustedOnboardingReader is a Reader for the ArkServiceTrustedOnboarding structure.
type ArkServiceTrustedOnboardingReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *ArkServiceTrustedOnboardingReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewArkServiceTrustedOnboardingOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
default:
result := NewArkServiceTrustedOnboardingDefault(response.Code())
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
if response.Code()/100 == 2 {
return result, nil
}
return nil, result
}
}
// NewArkServiceTrustedOnboardingOK creates a ArkServiceTrustedOnboardingOK with default headers values
func NewArkServiceTrustedOnboardingOK() *ArkServiceTrustedOnboardingOK {
return &ArkServiceTrustedOnboardingOK{}
}
/*
ArkServiceTrustedOnboardingOK describes a response with status code 200, with default header values.
A successful response.
*/
type ArkServiceTrustedOnboardingOK struct {
Payload *models.V1TrustedOnboardingResponse
}
// IsSuccess returns true when this ark service trusted onboarding o k response has a 2xx status code
func (o *ArkServiceTrustedOnboardingOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ark service trusted onboarding o k response has a 3xx status code
func (o *ArkServiceTrustedOnboardingOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ark service trusted onboarding o k response has a 4xx status code
func (o *ArkServiceTrustedOnboardingOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ark service trusted onboarding o k response has a 5xx status code
func (o *ArkServiceTrustedOnboardingOK) IsServerError() bool {
return false
}
// IsCode returns true when this ark service trusted onboarding o k response a status code equal to that given
func (o *ArkServiceTrustedOnboardingOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ark service trusted onboarding o k response
func (o *ArkServiceTrustedOnboardingOK) Code() int {
return 200
}
func (o *ArkServiceTrustedOnboardingOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard/address][%d] arkServiceTrustedOnboardingOK %s", 200, payload)
}
func (o *ArkServiceTrustedOnboardingOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard/address][%d] arkServiceTrustedOnboardingOK %s", 200, payload)
}
func (o *ArkServiceTrustedOnboardingOK) GetPayload() *models.V1TrustedOnboardingResponse {
return o.Payload
}
func (o *ArkServiceTrustedOnboardingOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.V1TrustedOnboardingResponse)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}
// NewArkServiceTrustedOnboardingDefault creates a ArkServiceTrustedOnboardingDefault with default headers values
func NewArkServiceTrustedOnboardingDefault(code int) *ArkServiceTrustedOnboardingDefault {
return &ArkServiceTrustedOnboardingDefault{
_statusCode: code,
}
}
/*
ArkServiceTrustedOnboardingDefault describes a response with status code -1, with default header values.
An unexpected error response.
*/
type ArkServiceTrustedOnboardingDefault struct {
_statusCode int
Payload *models.RPCStatus
}
// IsSuccess returns true when this ark service trusted onboarding default response has a 2xx status code
func (o *ArkServiceTrustedOnboardingDefault) IsSuccess() bool {
return o._statusCode/100 == 2
}
// IsRedirect returns true when this ark service trusted onboarding default response has a 3xx status code
func (o *ArkServiceTrustedOnboardingDefault) IsRedirect() bool {
return o._statusCode/100 == 3
}
// IsClientError returns true when this ark service trusted onboarding default response has a 4xx status code
func (o *ArkServiceTrustedOnboardingDefault) IsClientError() bool {
return o._statusCode/100 == 4
}
// IsServerError returns true when this ark service trusted onboarding default response has a 5xx status code
func (o *ArkServiceTrustedOnboardingDefault) IsServerError() bool {
return o._statusCode/100 == 5
}
// IsCode returns true when this ark service trusted onboarding default response a status code equal to that given
func (o *ArkServiceTrustedOnboardingDefault) IsCode(code int) bool {
return o._statusCode == code
}
// Code gets the status code for the ark service trusted onboarding default response
func (o *ArkServiceTrustedOnboardingDefault) Code() int {
return o._statusCode
}
func (o *ArkServiceTrustedOnboardingDefault) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard/address][%d] ArkService_TrustedOnboarding default %s", o._statusCode, payload)
}
func (o *ArkServiceTrustedOnboardingDefault) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[POST /v1/onboard/address][%d] ArkService_TrustedOnboarding default %s", o._statusCode, payload)
}
func (o *ArkServiceTrustedOnboardingDefault) GetPayload() *models.RPCStatus {
return o.Payload
}
func (o *ArkServiceTrustedOnboardingDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(models.RPCStatus)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
return err
}
return nil
}

View File

@@ -0,0 +1,56 @@
// 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"
)
// V1BoardingInput v1 boarding input
//
// swagger:model v1BoardingInput
type V1BoardingInput struct {
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// txid
Txid string `json:"txid,omitempty"`
// vout
Vout int64 `json:"vout,omitempty"`
}
// Validate validates this v1 boarding input
func (m *V1BoardingInput) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 boarding input based on context it is used
func (m *V1BoardingInput) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1BoardingInput) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1BoardingInput) UnmarshalBinary(b []byte) error {
var res V1BoardingInput
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -19,6 +19,9 @@ type V1FinalizePaymentRequest struct {
// Forfeit txs signed by the user.
SignedForfeitTxs []string `json:"signedForfeitTxs"`
// If payment has reverse boarding, the user must sign the associated inputs.
SignedRoundTx string `json:"signedRoundTx,omitempty"`
}
// Validate validates this v1 finalize payment request

View File

@@ -0,0 +1,50 @@
// 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"
)
// V1GetBoardingAddressRequest v1 get boarding address request
//
// swagger:model v1GetBoardingAddressRequest
type V1GetBoardingAddressRequest struct {
// pubkey
Pubkey string `json:"pubkey,omitempty"`
}
// Validate validates this v1 get boarding address request
func (m *V1GetBoardingAddressRequest) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 get boarding address request based on context it is used
func (m *V1GetBoardingAddressRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1GetBoardingAddressRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1GetBoardingAddressRequest) UnmarshalBinary(b []byte) error {
var res V1GetBoardingAddressRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View 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"
)
// V1GetBoardingAddressResponse v1 get boarding address response
//
// swagger:model v1GetBoardingAddressResponse
type V1GetBoardingAddressResponse struct {
// address
Address string `json:"address,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
}
// Validate validates this v1 get boarding address response
func (m *V1GetBoardingAddressResponse) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 get boarding address response based on context it is used
func (m *V1GetBoardingAddressResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1GetBoardingAddressResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1GetBoardingAddressResponse) UnmarshalBinary(b []byte) error {
var res V1GetBoardingAddressResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -17,6 +17,9 @@ import (
// swagger:model v1GetInfoResponse
type V1GetInfoResponse struct {
// boarding descriptor template
BoardingDescriptorTemplate string `json:"boardingDescriptorTemplate,omitempty"`
// min relay fee
MinRelayFee string `json:"minRelayFee,omitempty"`

View File

@@ -8,6 +8,7 @@ package models
import (
"context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@@ -17,20 +18,126 @@ import (
// swagger:model v1Input
type V1Input struct {
// txid
Txid string `json:"txid,omitempty"`
// boarding input
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
// vout
Vout int64 `json:"vout,omitempty"`
// vtxo input
VtxoInput *V1VtxoInput `json:"vtxoInput,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 {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContextValidate validates this v1 input based on context it is used
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error {
if swag.IsZero(m.BoardingInput) { // not required
return nil
}
if m.BoardingInput != nil {
if err := m.BoardingInput.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput")
} 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 err
}
}
return nil
}
// ContextValidate validate this v1 input based on the context it is used
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 {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1Input) contextValidateBoardingInput(ctx context.Context, formats strfmt.Registry) error {
if m.BoardingInput != nil {
if swag.IsZero(m.BoardingInput) { // not required
return nil
}
if err := m.BoardingInput.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput")
} 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 err
}
}
return nil
}

View File

@@ -1,115 +0,0 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1OnboardRequest v1 onboard request
//
// swagger:model v1OnboardRequest
type V1OnboardRequest struct {
// boarding tx
BoardingTx string `json:"boardingTx,omitempty"`
// congestion tree
CongestionTree *V1Tree `json:"congestionTree,omitempty"`
// user pubkey
UserPubkey string `json:"userPubkey,omitempty"`
}
// Validate validates this v1 onboard request
func (m *V1OnboardRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCongestionTree(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1OnboardRequest) validateCongestionTree(formats strfmt.Registry) error {
if swag.IsZero(m.CongestionTree) { // not required
return nil
}
if m.CongestionTree != nil {
if err := m.CongestionTree.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("congestionTree")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("congestionTree")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 onboard request based on the context it is used
func (m *V1OnboardRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateCongestionTree(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1OnboardRequest) contextValidateCongestionTree(ctx context.Context, formats strfmt.Registry) error {
if m.CongestionTree != nil {
if swag.IsZero(m.CongestionTree) { // not required
return nil
}
if err := m.CongestionTree.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("congestionTree")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("congestionTree")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *V1OnboardRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1OnboardRequest) UnmarshalBinary(b []byte) error {
var res V1OnboardRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -1,11 +0,0 @@
// 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
// V1OnboardResponse v1 onboard response
//
// swagger:model v1OnboardResponse
type V1OnboardResponse interface{}

View File

@@ -1,50 +0,0 @@
// 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"
)
// V1TrustedOnboardingRequest v1 trusted onboarding request
//
// swagger:model v1TrustedOnboardingRequest
type V1TrustedOnboardingRequest struct {
// user pubkey
UserPubkey string `json:"userPubkey,omitempty"`
}
// Validate validates this v1 trusted onboarding request
func (m *V1TrustedOnboardingRequest) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 trusted onboarding request based on context it is used
func (m *V1TrustedOnboardingRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1TrustedOnboardingRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1TrustedOnboardingRequest) UnmarshalBinary(b []byte) error {
var res V1TrustedOnboardingRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -1,50 +0,0 @@
// 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"
)
// V1TrustedOnboardingResponse v1 trusted onboarding response
//
// swagger:model v1TrustedOnboardingResponse
type V1TrustedOnboardingResponse struct {
// address
Address string `json:"address,omitempty"`
}
// Validate validates this v1 trusted onboarding response
func (m *V1TrustedOnboardingResponse) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 trusted onboarding response based on context it is used
func (m *V1TrustedOnboardingResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1TrustedOnboardingResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1TrustedOnboardingResponse) UnmarshalBinary(b []byte) error {
var res V1TrustedOnboardingResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View 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"
)
// V1VtxoInput v1 vtxo input
//
// swagger:model v1VtxoInput
type V1VtxoInput struct {
// txid
Txid string `json:"txid,omitempty"`
// vout
Vout int64 `json:"vout,omitempty"`
}
// Validate validates this v1 vtxo input
func (m *V1VtxoInput) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 vtxo input based on context it is used
func (m *V1VtxoInput) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1VtxoInput) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1VtxoInput) UnmarshalBinary(b []byte) error {
var res V1VtxoInput
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -11,6 +11,7 @@ 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,12 +24,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
type liquidReceiver struct {
@@ -139,92 +135,24 @@ func LoadCovenantClientWithWallet(
}, nil
}
func (a *covenantArkClient) Onboard(
ctx context.Context, amount uint64,
) (string, error) {
if amount <= 0 {
return "", fmt.Errorf("invalid amount to onboard %d", amount)
}
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
net := utils.ToElementsNetwork(a.Network)
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
userPubkeyStr := hex.EncodeToString(userPubkey.SerializeCompressed())
congestionTreeLeaf := tree.Receiver{
Pubkey: userPubkeyStr,
Amount: amount,
}
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
net.AssetID,
aspPubkey,
[]tree.Receiver{congestionTreeLeaf},
a.MinRelayFee,
a.RoundLifetime,
a.UnilateralExitDelay,
)
if err != nil {
return "", err
}
pay, err := payment.FromScript(sharedOutputScript, &net, nil)
if err != nil {
return "", err
}
addr, err := pay.TaprootAddress()
if err != nil {
return "", err
}
onchainReceiver := NewLiquidReceiver(addr, sharedOutputAmount)
pset, err := a.sendOnchain(ctx, []Receiver{onchainReceiver})
if err != nil {
return "", err
}
ptx, _ := psetv2.NewPsetFromBase64(pset)
utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
Txid: txid,
TxIndex: 0,
})
if err != nil {
return "", err
}
if err := a.client.Onboard(
ctx, pset, userPubkeyStr, congestionTree,
); err != nil {
return "", err
}
return txid, nil
}
func (a *covenantArkClient) Balance(
ctx context.Context, computeVtxoExpiration bool,
) (*Balance, error) {
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
const nbWorkers = 3
wg := &sync.WaitGroup{}
wg.Add(3 * len(offchainAddrs))
wg.Add(nbWorkers * len(offchainAddrs))
chRes := make(chan balanceRes, 3)
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
for i := range offchainAddrs {
offchainAddr := offchainAddrs[i]
onchainAddr := onchainAddrs[i]
boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i]
go func(addr string) {
defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance(
@@ -241,17 +169,7 @@ func (a *covenantArkClient) Balance(
}
}(offchainAddr)
go func(addr string) {
defer wg.Done()
balance, err := a.explorer.GetBalance(addr)
if err != nil {
chRes <- balanceRes{err: err}
return
}
chRes <- balanceRes{onchainSpendableBalance: balance}
}(onchainAddr)
go func(addr string) {
getDelayedBalance := func(addr string) {
defer wg.Done()
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
@@ -267,7 +185,10 @@ func (a *covenantArkClient) Balance(
onchainLockedBalance: lockedBalance,
err: err,
}
}(redeemAddr)
}
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
}
wg.Wait()
@@ -317,7 +238,7 @@ func (a *covenantArkClient) Balance(
}
count++
if count == 3 {
if count == nbWorkers {
break
}
}
@@ -519,7 +440,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
})
}
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
@@ -538,7 +459,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
}
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receivers,
ctx, paymentID, selectedCoins, false, receivers,
)
if err != nil {
return "", err
@@ -554,8 +475,85 @@ func (a *covenantArkClient) SendAsync(
return "", fmt.Errorf("not implemented")
}
func (a *covenantArkClient) ClaimAsync(ctx context.Context) (string, error) {
return "", fmt.Errorf("not implemented")
func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
if err != nil {
return "", err
}
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {
return "", err
}
var pendingBalance uint64
for _, vtxo := range boardingUtxos {
pendingBalance += vtxo.Amount
}
if pendingBalance == 0 {
return "", fmt.Errorf("no funds to claim")
}
receiver := client.Output{
Address: myselfOffchain,
Amount: pendingBalance,
}
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc)
}
func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
claimable := make([]explorer.Utxo, 0)
now := time.Now()
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
if err != nil {
return nil, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, err
}
for _, addr := range boardingAddrs {
boardingUtxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, err
}
for _, utxo := range boardingUtxos {
u := utxo.ToUtxo(boardingTimeout)
if u.SpendableAt.Before(now) {
continue
}
claimable = append(claimable, u)
}
}
return claimable, nil
}
func (a *covenantArkClient) sendOnchain(
@@ -599,14 +597,14 @@ func (a *covenantArkClient) sendOnchain(
}
}
utxos, delayedUtxos, change, err := a.coinSelectOnchain(
utxos, change, err := a.coinSelectOnchain(
ctx, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, net); err != nil {
if err := a.addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -649,14 +647,14 @@ func (a *covenantArkClient) sendOnchain(
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := a.addInputs(ctx, updater, selected, delayedSelected, net); err != nil {
if err := a.addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -787,7 +785,7 @@ func (a *covenantArkClient) sendOffchain(
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, client.VtxoKey{
Txid: coin.Txid,
@@ -811,7 +809,7 @@ func (a *covenantArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receiversOutput,
ctx, paymentID, selectedCoins, false, receiversOutput,
)
if err != nil {
return "", err
@@ -821,126 +819,60 @@ func (a *covenantArkClient) sendOffchain(
}
func (a *covenantArkClient) addInputs(
ctx context.Context, updater *psetv2.Updater, utxos, delayedUtxos []explorer.Utxo, net network.Network,
ctx context.Context,
updater *psetv2.Updater,
utxos []explorer.Utxo,
) error {
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
// TODO works only with single-key wallet
offchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return err
}
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
changeScript, err := address.ToOutputScript(onchainAddr)
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
if err != nil {
return err
}
for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
return err
}
if err := updater.AddInputs([]psetv2.InputArgs{
{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Sequence: sequence,
},
}); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: changeScript,
Nonce: []byte{0x00},
}
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
_, leafProof, script, _, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
if err := a.addVtxoInput(
updater,
psetv2.InputArgs{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
uint(a.UnilateralExitDelay),
leafProof,
); err != nil {
return err
}
inputIndex := len(updater.Pset.Inputs) - 1
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, witnessUtxo,
); err != nil {
return err
}
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
return err
}
}
return nil
}
func (a *covenantArkClient) addVtxoInput(
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
tapLeafProof *taproot.TapscriptElementsProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Pset.Inputs)
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
return err
}
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
return updater.AddInTapLeafScript(
nextInputIndex,
psetv2.NewTapLeafScript(
*tapLeafProof,
tree.UnspendableKey(),
),
)
}
func (a *covenantArkClient) handleRoundStream(
ctx context.Context,
paymentID string, vtxosToSign []client.Vtxo, receivers []client.Output,
paymentID string,
vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
receivers []client.Output,
) (string, error) {
eventsCh, err := a.client.GetEventStream(ctx, paymentID)
if err != nil {
@@ -972,20 +904,20 @@ func (a *covenantArkClient) handleRoundStream(
pingStop()
log.Info("a round finalization started")
signedForfeitTxs, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers,
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
)
if err != nil {
return "", err
}
if len(signedForfeitTxs) <= 0 {
if len(signedForfeitTxs) <= 0 && len(vtxosToSign) > 0 {
log.Info("no forfeit txs to sign, waiting for the next round")
continue
}
log.Info("finalizing payment... ")
if err := a.client.FinalizePayment(ctx, signedForfeitTxs); err != nil {
if err := a.client.FinalizePayment(ctx, signedForfeitTxs, signedRoundTx); err != nil {
return "", err
}
@@ -998,15 +930,29 @@ func (a *covenantArkClient) handleRoundStream(
func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, receivers []client.Output,
) ([]string, error) {
if err := a.validateCongestionTree(event, receivers); err != nil {
return nil, fmt.Errorf("failed to verify congestion tree: %s", err)
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) {
if err = a.validateCongestionTree(event, receivers); err != nil {
return
}
return a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
if err != nil {
return
}
}
if mustSignRoundTx {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
if err != nil {
return
}
}
return
}
func (a *covenantArkClient) validateCongestionTree(
@@ -1197,23 +1143,49 @@ func (a *covenantArkClient) signForfeitTx(
func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) {
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx)
) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
net := utils.ToElementsNetwork(a.Network)
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
if err != nil {
return nil, 0, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, 0, err
}
now := time.Now()
fetchedUtxos := make([]explorer.Utxo, 0)
for _, onchainAddr := range onchainAddrs {
utxos, err := a.explorer.GetUtxos(onchainAddr)
for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout)
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
}
fetchedUtxos = append(fetchedUtxos, utxos...)
}
utxos := make([]explorer.Utxo, 0)
selected := make([]explorer.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount {
@@ -1226,61 +1198,51 @@ func (a *covenantArkClient) coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selected = append(selected, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return selected, selectedAmount - targetAmount, nil
}
fetchedUtxos = make([]explorer.Utxo, 0)
for _, offchainAddr := range offchainAddrs {
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
_, _, _, addr, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
)
for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
utxos, err = a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
}
fetchedUtxos = append(fetchedUtxos, utxos...)
}
delayedUtxos := make([]explorer.Utxo, 0)
for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(a.UnilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selected = append(selected, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return selected, selectedAmount - targetAmount, nil
}
func (a *covenantArkClient) getRedeemBranches(
@@ -1373,3 +1335,39 @@ func (a *covenantArkClient) getVtxos(
return vtxos, nil
}
func (a *covenantArkClient) selfTransferAllPendingPayments(
ctx context.Context, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) {
inputs := make([]client.Input, 0, len(boardingUtxo))
for _, utxo := range boardingUtxo {
inputs = append(inputs, client.BoardingInput{
VtxoKey: client.VtxoKey{
Txid: utxo.Txid,
VOut: utxo.Vout,
},
Descriptor: boardingDescriptor,
})
}
outputs := []client.Output{myself}
paymentID, err := a.client.RegisterPayment(ctx, inputs, "") // ephemeralPublicKey is not required for covenant
if err != nil {
return "", err
}
if err := a.client.ClaimPayment(ctx, paymentID, outputs); err != nil {
return "", err
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, make([]client.Vtxo, 0), len(boardingUtxo) > 0, outputs,
)
if err != nil {
return "", err
}
return roundTxid, nil
}

View File

@@ -12,6 +12,7 @@ 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"
@@ -22,7 +23,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@@ -139,176 +139,24 @@ func LoadCovenantlessClientWithWallet(
}, nil
}
func (a *covenantlessArkClient) Onboard(
ctx context.Context, amount uint64,
) (string, error) {
if amount <= 0 {
return "", fmt.Errorf("invalid amount to onboard %d", amount)
}
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
userPubkeyStr := hex.EncodeToString(userPubkey.SerializeCompressed())
congestionTreeLeaf := bitcointree.Receiver{
Pubkey: userPubkeyStr,
Amount: amount,
}
leaves := []bitcointree.Receiver{congestionTreeLeaf}
ephemeralKey, err := secp256k1.GeneratePrivateKey()
if err != nil {
return "", err
}
cosigners := []*secp256k1.PublicKey{ephemeralKey.PubKey()} // TODO asp as cosigner
sharedOutputScript, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners,
aspPubkey,
leaves,
a.MinRelayFee,
a.RoundLifetime,
a.UnilateralExitDelay,
)
if err != nil {
return "", err
}
netParams := utils.ToBitcoinNetwork(a.Network)
address, err := btcutil.NewAddressTaproot(sharedOutputScript[2:], &netParams)
if err != nil {
return "", err
}
onchainReceiver := NewBitcoinReceiver(
address.EncodeAddress(), uint64(sharedOutputAmount),
)
partialTx, err := a.sendOnchain(ctx, []Receiver{onchainReceiver})
if err != nil {
return "", err
}
ptx, err := psbt.NewFromRawBytes(strings.NewReader(partialTx), true)
if err != nil {
return "", err
}
txid := ptx.UnsignedTx.TxHash().String()
congestionTree, err := bitcointree.CraftCongestionTree(
&wire.OutPoint{
Hash: ptx.UnsignedTx.TxHash(),
Index: 0,
},
cosigners,
aspPubkey,
leaves,
a.MinRelayFee,
a.RoundLifetime,
a.UnilateralExitDelay,
)
if err != nil {
return "", err
}
sweepClosure := bitcointree.CSVSigClosure{
Pubkey: aspPubkey,
Seconds: uint(a.RoundLifetime),
}
sweepTapLeaf, err := sweepClosure.Leaf()
if err != nil {
return "", err
}
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
root := sweepTapTree.RootNode.TapHash()
signer := bitcointree.NewTreeSignerSession(
ephemeralKey,
congestionTree,
int64(a.MinRelayFee),
root.CloneBytes(),
)
nonces, err := signer.GetNonces() // TODO send nonces to ASP
if err != nil {
return "", err
}
coordinator, err := bitcointree.NewTreeCoordinatorSession(
congestionTree,
int64(a.MinRelayFee),
root.CloneBytes(),
cosigners,
)
if err != nil {
return "", err
}
if err := coordinator.AddNonce(ephemeralKey.PubKey(), nonces); err != nil {
return "", err
}
aggregatedNonces, err := coordinator.AggregateNonces()
if err != nil {
return "", err
}
if err := signer.SetKeys(cosigners); err != nil {
return "", err
}
if err := signer.SetAggregatedNonces(aggregatedNonces); err != nil {
return "", err
}
sigs, err := signer.Sign()
if err != nil {
return "", err
}
if err := coordinator.AddSig(ephemeralKey.PubKey(), sigs); err != nil {
return "", err
}
signedTree, err := coordinator.SignTree()
if err != nil {
return "", err
}
if err := a.client.Onboard(
ctx, partialTx, userPubkeyStr, signedTree,
); err != nil {
return "", err
}
return txid, nil
}
func (a *covenantlessArkClient) Balance(
ctx context.Context, computeVtxoExpiration bool,
) (*Balance, error) {
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
const nbWorkers = 3
wg := &sync.WaitGroup{}
wg.Add(3 * len(offchainAddrs))
wg.Add(nbWorkers * len(offchainAddrs))
chRes := make(chan balanceRes, 3)
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
for i := range offchainAddrs {
offchainAddr := offchainAddrs[i]
onchainAddr := onchainAddrs[i]
boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i]
go func(addr string) {
defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance(
@@ -325,17 +173,7 @@ func (a *covenantlessArkClient) Balance(
}
}(offchainAddr)
go func(addr string) {
defer wg.Done()
balance, err := a.explorer.GetBalance(addr)
if err != nil {
chRes <- balanceRes{err: err}
return
}
chRes <- balanceRes{onchainSpendableBalance: balance}
}(onchainAddr)
go func(addr string) {
getDelayedBalance := func(addr string) {
defer wg.Done()
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
@@ -351,7 +189,10 @@ func (a *covenantlessArkClient) Balance(
onchainLockedBalance: lockedBalance,
err: err,
}
}(redeemAddr)
}
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
}
wg.Wait()
@@ -401,7 +242,7 @@ func (a *covenantlessArkClient) Balance(
}
count++
if count == 3 {
if count == nbWorkers {
break
}
}
@@ -603,7 +444,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
})
}
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
@@ -631,7 +472,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
}
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receivers, roundEphemeralKey,
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
)
if err != nil {
return "", err
@@ -751,9 +592,7 @@ func (a *covenantlessArkClient) SendAsync(
return signedRedeemTx, nil
}
func (a *covenantlessArkClient) ClaimAsync(
ctx context.Context,
) (string, error) {
func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
@@ -764,10 +603,23 @@ func (a *covenantlessArkClient) ClaimAsync(
return "", err
}
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
if err != nil {
return "", err
}
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {
return "", err
}
var pendingBalance uint64
for _, vtxo := range pendingVtxos {
pendingBalance += vtxo.Amount
}
for _, vtxo := range boardingUtxos {
pendingBalance += vtxo.Amount
}
if pendingBalance == 0 {
return "", nil
}
@@ -776,7 +628,9 @@ func (a *covenantlessArkClient) ClaimAsync(
Address: myselfOffchain,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, receiver)
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
}
func (a *covenantlessArkClient) sendOnchain(
@@ -822,14 +676,14 @@ func (a *covenantlessArkClient) sendOnchain(
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
}
utxos, delayedUtxos, change, err := a.coinSelectOnchain(
utxos, change, err := a.coinSelectOnchain(
ctx, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
if err := a.addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -869,14 +723,14 @@ func (a *covenantlessArkClient) sendOnchain(
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := a.addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
if err := a.addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -995,7 +849,7 @@ func (a *covenantlessArkClient) sendOffchain(
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, client.VtxoKey{
Txid: coin.Txid,
@@ -1024,7 +878,7 @@ func (a *covenantlessArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receiversOutput, roundEphemeralKey,
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
)
if err != nil {
return "", err
@@ -1034,16 +888,17 @@ func (a *covenantlessArkClient) sendOffchain(
}
func (a *covenantlessArkClient) addInputs(
ctx context.Context, updater *psbt.Updater,
utxos, delayedUtxos []explorer.Utxo, net *chaincfg.Params,
ctx context.Context,
updater *psbt.Updater,
utxos []explorer.Utxo,
) error {
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
// TODO works only with single-key wallet
offchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return err
}
addr, _ := btcutil.DecodeAddress(onchainAddr, net)
changeScript, err := txscript.PayToAddrScript(addr)
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
if err != nil {
return err
}
@@ -1054,107 +909,41 @@ func (a *covenantlessArkClient) addInputs(
return err
}
sequence, err := utxo.Sequence()
if err != nil {
return err
}
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
},
Sequence: sequence,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: changeScript,
},
len(updater.Upsbt.UnsignedTx.TxIn)-1,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
vtxoTapKey, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
script, err := txscript.PayToAddrScript(p2tr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
return err
}
if err := a.addVtxoInput(
updater,
&wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion,
},
uint(a.UnilateralExitDelay),
leafProof,
); err != nil {
return err
}
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: script,
},
len(updater.Upsbt.Inputs)-1,
); err != nil {
return err
}
}
}
return nil
}
func (a *covenantlessArkClient) addVtxoInput(
updater *psbt.Updater, inputArgs *wire.OutPoint, exitDelay uint,
tapLeafProof *txscript.TapscriptProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Upsbt.Inputs)
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *inputArgs,
Sequence: sequence,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
controlBlock := tapLeafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: tapLeafProof.Script,
LeafVersion: tapLeafProof.LeafVersion,
},
},
})
}
return nil
@@ -1164,6 +953,7 @@ func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context,
paymentID string,
vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
receivers []client.Output,
roundEphemeralKey *secp256k1.PrivateKey,
) (string, error) {
@@ -1218,20 +1008,20 @@ func (a *covenantlessArkClient) handleRoundStream(
pingStop()
log.Info("a round finalization started")
signedForfeitTxs, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers,
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
)
if err != nil {
return "", err
}
if len(signedForfeitTxs) <= 0 {
if len(signedForfeitTxs) <= 0 && len(vtxosToSign) > 0 {
log.Info("no forfeit txs to sign, waiting for the next round")
continue
}
log.Info("finalizing payment... ")
if err := a.client.FinalizePayment(ctx, signedForfeitTxs); err != nil {
if err := a.client.FinalizePayment(ctx, signedForfeitTxs, signedRoundTx); err != nil {
return "", err
}
@@ -1311,15 +1101,29 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, receivers []client.Output,
) ([]string, error) {
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) {
if err := a.validateCongestionTree(event, receivers); err != nil {
return nil, fmt.Errorf("failed to verify congestion tree: %s", err)
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
}
return a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
if err != nil {
return
}
}
if mustSignRoundTx {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
if err != nil {
return
}
}
return
}
func (a *covenantlessArkClient) validateCongestionTree(
@@ -1516,24 +1320,49 @@ func (a *covenantlessArkClient) loopAndSign(
func (a *covenantlessArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) {
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx)
) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
net := utils.ToBitcoinNetwork(a.Network)
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
if err != nil {
return nil, 0, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, 0, err
}
now := time.Now()
fetchedUtxos := make([]explorer.Utxo, 0)
for _, onchainAddr := range onchainAddrs {
utxos, err := a.explorer.GetUtxos(onchainAddr)
for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout)
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
}
fetchedUtxos = append(fetchedUtxos, utxos...)
}
utxos := make([]explorer.Utxo, 0)
selected := make([]explorer.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount {
@@ -1546,70 +1375,51 @@ func (a *covenantlessArkClient) coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selected = append(selected, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return selected, selectedAmount - targetAmount, nil
}
fetchedUtxos = make([]explorer.Utxo, 0)
for _, offchainAddr := range offchainAddrs {
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
)
for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
p2tr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey), &net,
)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
addr := p2tr.EncodeAddress()
utxos, err = a.explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
}
fetchedUtxos = append(fetchedUtxos, utxos...)
}
delayedUtxos := make([]explorer.Utxo, 0)
for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(a.UnilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selected = append(selected, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return selected, selectedAmount - targetAmount, nil
}
func (a *covenantlessArkClient) getRedeemBranches(
@@ -1673,6 +1483,53 @@ func (a *covenantlessArkClient) getOffchainBalance(
return balance, amountByExpiration, nil
}
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
if err != nil {
return nil, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, err
}
claimable := make([]explorer.Utxo, 0)
now := time.Now()
for _, addr := range boardingAddrs {
boardingUtxos, err := a.explorer.GetUtxos(addr)
if err != nil {
return nil, err
}
for _, utxo := range boardingUtxos {
u := utxo.ToUtxo(boardingTimeout)
if u.SpendableAt.Before(now) {
continue
}
claimable = append(claimable, u)
}
}
return claimable, nil
}
func (a *covenantlessArkClient) getVtxos(
ctx context.Context, addr string, computeVtxoExpiration bool,
) ([]client.Vtxo, []client.Vtxo, error) {
@@ -1718,14 +1575,25 @@ func (a *covenantlessArkClient) getVtxos(
}
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, myself client.Output,
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) {
inputs := make([]client.VtxoKey, 0, len(pendingVtxos))
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo))
for _, coin := range pendingVtxos {
inputs = append(inputs, coin.VtxoKey)
}
for _, utxo := range boardingUtxo {
fmt.Println(utxo)
fmt.Println(boardingDescriptor)
inputs = append(inputs, client.BoardingInput{
VtxoKey: client.VtxoKey{
Txid: utxo.Txid,
VOut: utxo.Vout,
},
Descriptor: boardingDescriptor,
})
}
outputs := []client.Output{myself}
roundEphemeralKey, err := secp256k1.GeneratePrivateKey()
@@ -1747,7 +1615,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, pendingVtxos, outputs, roundEphemeralKey,
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
)
if err != nil {
return "", err

View File

@@ -38,31 +38,27 @@ func main() {
defer aliceArkClient.Lock(ctx, password)
log.Info("alice is acquiring onchain funds...")
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx)
_, aliceBoardingAddr, err := aliceArkClient.Receive(ctx)
if err != nil {
log.Fatal(err)
}
if _, err := runCommand("nigiri", "faucet", "--liquid", aliceOnchainAddr); err != nil {
if _, err := runCommand("nigiri", "faucet", "--liquid", aliceBoardingAddr); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
onboardAmount := uint64(20000)
onboardAmount := uint64(1_0000_0000) // 1 BTC
log.Infof("alice is onboarding with %d sats offchain...", onboardAmount)
txid, err := aliceArkClient.Onboard(ctx, onboardAmount)
log.Infof("alice claiming onboarding funds...")
txid, err := aliceArkClient.Claim(ctx)
if err != nil {
log.Fatal(err)
}
if err := generateBlock(); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
log.Infof("alice onboarded with tx: %s", txid)
log.Infof("onboarding completed in round tx: %s", txid)
aliceBalance, err := aliceArkClient.Balance(ctx, false)
if err != nil {

View File

@@ -38,31 +38,19 @@ func main() {
defer aliceArkClient.Lock(ctx, password)
log.Info("alice is acquiring onchain funds...")
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx)
_, boardingAddress, err := aliceArkClient.Receive(ctx)
if err != nil {
log.Fatal(err)
}
if _, err := runCommand("nigiri", "faucet", aliceOnchainAddr); err != nil {
if _, err := runCommand("nigiri", "faucet", boardingAddress); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
onboardAmount := uint64(20000)
onboardAmount := uint64(1_0000_0000) // 1 BTC
log.Infof("alice is onboarding with %d sats offchain...", onboardAmount)
txid, err := aliceArkClient.Onboard(ctx, onboardAmount)
if err != nil {
log.Fatal(err)
}
if err := generateBlock(); err != nil {
log.Fatal(err)
}
time.Sleep(5 * time.Second)
log.Infof("alice onboarded with tx: %s", txid)
aliceBalance, err := aliceArkClient.Balance(ctx, false)
if err != nil {
@@ -72,6 +60,14 @@ func main() {
log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount)
log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total)
log.Infof("alice claiming onboarding funds...")
txid, err := aliceArkClient.Claim(ctx)
if err != nil {
log.Fatal(err)
}
log.Infof("alice claimed onboarding funds in round %s", txid)
fmt.Println("")
log.Info("bob is setting up his ark wallet...")
bobArkClient, err := setupArkClient()
@@ -137,7 +133,7 @@ func main() {
fmt.Println("")
log.Info("bob is claiming the incoming payment...")
roundTxid, err := bobArkClient.ClaimAsync(ctx)
roundTxid, err := bobArkClient.Claim(ctx)
if err != nil {
log.Fatal(err)
}

View File

@@ -25,6 +25,35 @@ const (
)
type Utxo struct {
Txid string
Vout uint32
Amount uint64
Asset string // liquid only
Delay uint
SpendableAt time.Time
}
func (u *Utxo) Sequence() (uint32, error) {
return common.BIP68EncodeAsNumber(u.Delay)
}
func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
utxoTime := explorerUtxo.Status.Blocktime
if utxoTime == 0 {
utxoTime = time.Now().Unix()
}
return Utxo{
Txid: explorerUtxo.Txid,
Vout: explorerUtxo.Vout,
Amount: explorerUtxo.Amount,
Asset: explorerUtxo.Asset,
Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
}
}
type ExplorerUtxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
@@ -35,10 +64,14 @@ type Utxo struct {
} `json:"status"`
}
func (e ExplorerUtxo) ToUtxo(delay uint) Utxo {
return newUtxo(e, delay)
}
type Explorer interface {
GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]Utxo, error)
GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr string) (uint64, error)
GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
@@ -143,7 +176,7 @@ func (e *explorerSvc) Broadcast(txStr string) (string, error) {
return txid, nil
}
func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
if err != nil {
return nil, err
@@ -157,7 +190,7 @@ func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []Utxo{}
payload := []ExplorerUtxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/pkg/client-sdk
go 1.22.6
replace github.com/ark-network/ark/common => ../../common
require (
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87

View File

@@ -1,8 +1,6 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87 h1:VBY4KqHqxE4q6NnmvEZTLvLZoNA0Q6NhMhjBs1hzy9Y=
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87/go.mod h1:m5H86Dx+k8cQjLXeYL1MV+h3x/XnhJCXJP/PL3KgZqY=
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87 h1:TIv00zlpxLKmY2LjFAIMF8RxNtn9rFqQsv73Lwoj2ds=
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87/go.mod h1:aYAGDfoeBLofnZt9n85wusFyCkrS7hvwdo5TynBlkuY=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=

View File

@@ -21,14 +21,15 @@ const (
)
type storeData struct {
AspUrl string `json:"asp_url"`
AspPubkey string `json:"asp_pubkey"`
WalletType string `json:"wallet_type"`
ClientType string `json:"client_type"`
Network string `json:"network"`
RoundLifetime string `json:"round_lifetime"`
UnilateralExitDelay string `json:"unilateral_exit_delay"`
MinRelayFee string `json:"min_relay_fee"`
AspUrl string `json:"asp_url"`
AspPubkey string `json:"asp_pubkey"`
WalletType string `json:"wallet_type"`
ClientType string `json:"client_type"`
Network string `json:"network"`
RoundLifetime string `json:"round_lifetime"`
UnilateralExitDelay string `json:"unilateral_exit_delay"`
MinRelayFee string `json:"min_relay_fee"`
BoardingDescriptorTemplate string `json:"boarding_descriptor_template"`
}
func (d storeData) isEmpty() bool {
@@ -43,27 +44,29 @@ func (d storeData) decode() store.StoreData {
buf, _ := hex.DecodeString(d.AspPubkey)
aspPubkey, _ := secp256k1.ParsePubKey(buf)
return store.StoreData{
AspUrl: d.AspUrl,
AspPubkey: aspPubkey,
WalletType: d.WalletType,
ClientType: d.ClientType,
Network: network,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
MinRelayFee: uint64(minRelayFee),
AspUrl: d.AspUrl,
AspPubkey: aspPubkey,
WalletType: d.WalletType,
ClientType: d.ClientType,
Network: network,
RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay),
MinRelayFee: uint64(minRelayFee),
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
}
}
func (d storeData) asMap() map[string]string {
return map[string]string{
"asp_url": d.AspUrl,
"asp_pubkey": d.AspPubkey,
"wallet_type": d.WalletType,
"client_type": d.ClientType,
"network": d.Network,
"round_lifetime": d.RoundLifetime,
"unilateral_exit_delay": d.UnilateralExitDelay,
"min_relay_fee": d.MinRelayFee,
"asp_url": d.AspUrl,
"asp_pubkey": d.AspPubkey,
"wallet_type": d.WalletType,
"client_type": d.ClientType,
"network": d.Network,
"round_lifetime": d.RoundLifetime,
"unilateral_exit_delay": d.UnilateralExitDelay,
"min_relay_fee": d.MinRelayFee,
"boarding_descriptor_template": d.BoardingDescriptorTemplate,
}
}
@@ -100,14 +103,15 @@ func (s *Store) GetDatadir() string {
func (s *Store) AddData(ctx context.Context, data store.StoreData) error {
sd := &storeData{
AspUrl: data.AspUrl,
AspPubkey: hex.EncodeToString(data.AspPubkey.SerializeCompressed()),
WalletType: data.WalletType,
ClientType: data.ClientType,
Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
MinRelayFee: fmt.Sprintf("%d", data.MinRelayFee),
AspUrl: data.AspUrl,
AspPubkey: hex.EncodeToString(data.AspPubkey.SerializeCompressed()),
WalletType: data.WalletType,
ClientType: data.ClientType,
Network: data.Network.Name,
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
MinRelayFee: fmt.Sprintf("%d", data.MinRelayFee),
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
}
if err := s.write(sd); err != nil {

View File

@@ -13,14 +13,15 @@ const (
)
type StoreData struct {
AspUrl string
AspPubkey *secp256k1.PublicKey
WalletType string
ClientType string
Network common.Network
RoundLifetime int64
UnilateralExitDelay int64
MinRelayFee uint64
AspUrl string
AspPubkey *secp256k1.PublicKey
WalletType string
ClientType string
Network common.Network
RoundLifetime int64
UnilateralExitDelay int64
MinRelayFee uint64
BoardingDescriptorTemplate string
}
type ConfigStore interface {

View File

@@ -18,14 +18,15 @@ func TestStore(t *testing.T) {
key, _ := btcec.NewPrivateKey()
ctx := context.Background()
testStoreData := store.StoreData{
AspUrl: "localhost:7070",
AspPubkey: key.PubKey(),
WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient,
Network: common.LiquidRegTest,
RoundLifetime: 512,
UnilateralExitDelay: 512,
MinRelayFee: 300,
AspUrl: "localhost:7070",
AspPubkey: key.PubKey(),
WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient,
Network: common.LiquidRegTest,
RoundLifetime: 512,
UnilateralExitDelay: 512,
MinRelayFee: 300,
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
}
tests := []struct {

View File

@@ -9,12 +9,12 @@ 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"
"github.com/ark-network/ark/pkg/client-sdk/wallet"
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
@@ -41,42 +41,42 @@ func NewBitcoinWallet(
func (w *bitcoinWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []string{offchainAddr}
onchainAddrs := []string{onchainAddr}
boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr}
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
}
func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, onchainAddr, nil
return offchainAddr, boardingAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]string, 0, num)
onchainAddrs := make([]string, 0, num)
boardingAddrs := make([]string, 0, num)
for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr)
onchainAddrs = append(onchainAddrs, onchainAddr)
boardingAddrs = append(boardingAddrs, boardingAddr)
}
return offchainAddrs, onchainAddrs, nil
return offchainAddrs, boardingAddrs, nil
}
func (s *bitcoinWallet) SignTransaction(
@@ -92,11 +92,6 @@ func (s *bitcoinWallet) SignTransaction(
return "", err
}
data, err := s.configStore.GetData(ctx)
if err != nil {
return "", err
}
for i, input := range updater.Upsbt.UnsignedTx.TxIn {
if updater.Upsbt.Inputs[i].WitnessUtxo != nil {
continue
@@ -122,28 +117,11 @@ func (s *bitcoinWallet) SignTransaction(
return "", err
}
sighashType := txscript.SigHashAll
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
return "", err
}
}
_, onchainAddr, _, err := s.getAddress(ctx)
if err != nil {
return "", err
}
net := utils.ToBitcoinNetwork(data.Network)
addr, _ := btcutil.DecodeAddress(onchainAddr, &net)
onchainWalletScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return "", err
}
prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs {
@@ -158,36 +136,6 @@ func (s *bitcoinWallet) SignTransaction(
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs {
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
return "", err
}
preimage, err := txscript.CalcWitnessSigHash(
input.WitnessUtxo.PkScript,
txsighashes,
txscript.SigHashAll,
updater.Upsbt.UnsignedTx,
i,
int64(input.WitnessUtxo.Value),
)
if err != nil {
return "", err
}
sig := ecdsa.Sign(s.privateKey, preimage)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
{
PubKey: s.walletData.Pubkey.SerializeCompressed(),
Signature: signatureWithSighashType,
},
}
continue
}
if len(input.TaprootLeafScript) > 0 {
pubkey := s.walletData.Pubkey
for _, leaf := range input.TaprootLeafScript {
@@ -269,11 +217,6 @@ func (w *bitcoinWallet) getAddress(
netParams := utils.ToBitcoinNetwork(data.Network)
onchainAddr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(w.walletData.Pubkey.SerializeCompressed()), &netParams)
if err != nil {
return "", "", "", err
}
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
)
@@ -289,5 +232,35 @@ func (w *bitcoinWallet) getAddress(
return "", "", "", err
}
return offchainAddr, onchainAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
descriptorStr := strings.ReplaceAll(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(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,
)
if err != nil {
return "", "", "", err
}
boardingAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(boardingTapKey),
&netParams,
)
if err != nil {
return "", "", "", err
}
return offchainAddr, boardingAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
}

View File

@@ -3,20 +3,21 @@ package singlekeywallet
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"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"
"github.com/ark-network/ark/pkg/client-sdk/store"
"github.com/ark-network/ark/pkg/client-sdk/wallet"
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"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"
)
@@ -40,42 +41,42 @@ func NewLiquidWallet(
func (w *liquidWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []string{offchainAddr}
onchainAddrs := []string{onchainAddr}
boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr}
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
}
func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, onchainAddr, nil
return offchainAddr, boardingAddr, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]string, 0, num)
onchainAddrs := make([]string, 0, num)
boardingAddrs := make([]string, 0, num)
for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr)
onchainAddrs = append(onchainAddrs, onchainAddr)
boardingAddrs = append(boardingAddrs, boardingAddr)
}
return offchainAddrs, onchainAddrs, nil
return offchainAddrs, boardingAddrs, nil
}
func (s *liquidWallet) SignTransaction(
@@ -114,13 +115,7 @@ func (s *liquidWallet) SignTransaction(
return "", err
}
sighashType := txscript.SigHashAll
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
return "", err
}
}
@@ -135,8 +130,6 @@ func (s *liquidWallet) SignTransaction(
return "", err
}
liquidNet := utils.ToElementsNetwork(storeData.Network)
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
onchainWalletScript := p2wpkh.WitnessScript
utx, err := pset.UnsignedTx()
if err != nil {
@@ -156,33 +149,6 @@ func (s *liquidWallet) SignTransaction(
serializedPubKey := s.walletData.Pubkey.SerializeCompressed()
for i, input := range pset.Inputs {
prevout := input.GetUtxo()
if bytes.Equal(prevout.Script, onchainWalletScript) {
p, err := payment.FromScript(prevout.Script, &liquidNet, nil)
if err != nil {
return "", err
}
preimage := utx.HashForWitnessV0(
i, p.Script, prevout.Value, txscript.SigHashAll,
)
sig := ecdsa.Sign(s.privateKey, preimage[:])
signatureWithSighashType := append(
sig.Serialize(), byte(txscript.SigHashAll),
)
err = signer.SignInput(
i, signatureWithSighashType, serializedPubKey, nil, nil,
)
if err != nil {
return "", err
}
continue
}
if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil {
@@ -276,12 +242,6 @@ func (w *liquidWallet) getAddress(
liquidNet := utils.ToElementsNetwork(data.Network)
p2wpkh := payment.FromPublicKey(w.walletData.Pubkey, &liquidNet, nil)
onchainAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
return "", "", "", err
}
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
)
@@ -289,5 +249,27 @@ func (w *liquidWallet) getAddress(
return "", "", "", err
}
return offchainAddr, onchainAddr, redemptionAddr, nil
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
descriptorStr := strings.ReplaceAll(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
if err != nil {
return "", "", "", err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return "", "", "", err
}
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet,
)
if err != nil {
return "", "", "", err
}
return offchainAddr, boardingAddr, redemptionAddr, nil
}

View File

@@ -20,7 +20,7 @@ type WalletService interface {
IsLocked() bool
GetAddresses(
ctx context.Context,
) (offchainAddresses, onchainAddresses, redemptionAddresses []string, err error)
) (offchainAddresses, boardingAddresses, redemptionAddresses []string, err error)
NewAddress(
ctx context.Context, change bool,
) (offchainAddr, onchainAddr string, err error)
@@ -29,5 +29,5 @@ type WalletService interface {
) (offchainAddresses, onchainAddresses []string, err error)
SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (singedTx string, err error)
) (signedTx string, err error)
}

View File

@@ -21,14 +21,15 @@ func TestWallet(t *testing.T) {
key, _ := btcec.NewPrivateKey()
password := "password"
testStoreData := store.StoreData{
AspUrl: "localhost:7070",
AspPubkey: key.PubKey(),
WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient,
Network: common.LiquidRegTest,
RoundLifetime: 512,
UnilateralExitDelay: 512,
MinRelayFee: 300,
AspUrl: "localhost:7070",
AspPubkey: key.PubKey(),
WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient,
Network: common.LiquidRegTest,
RoundLifetime: 512,
UnilateralExitDelay: 512,
MinRelayFee: 300,
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
}
tests := []struct {
name string

View File

@@ -27,7 +27,6 @@ func init() {
js.Global().Set("lock", LockWrapper())
js.Global().Set("locked", IsLockedWrapper())
js.Global().Set("balance", BalanceWrapper())
js.Global().Set("onboard", OnboardWrapper())
js.Global().Set("receive", ReceiveWrapper())
js.Global().Set("sendOnChain", SendOnChainWrapper())
js.Global().Set("sendOffChain", SendOffChainWrapper())

View File

@@ -140,33 +140,18 @@ func BalanceWrapper() js.Func {
})
}
func OnboardWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.New("invalid number of args")
}
amount := uint64(args[0].Int())
txID, err := arkSdkClient.Onboard(context.Background(), amount)
if err != nil {
return nil, err
}
return js.ValueOf(txID), nil
})
}
func ReceiveWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) {
if arkSdkClient == nil {
return nil, errors.New("ARK SDK client is not initialized")
}
offchainAddr, onchainAddr, err := arkSdkClient.Receive(context.Background())
offchainAddr, boardingAddr, err := arkSdkClient.Receive(context.Background())
if err != nil {
return nil, err
}
result := map[string]interface{}{
"offchainAddr": offchainAddr,
"onchainAddr": onchainAddr,
"boardingAddr": boardingAddr,
}
return js.ValueOf(result), nil
})

View File

@@ -79,6 +79,7 @@ func mainAction(_ *cli.Context) error {
BitcoindRpcUser: cfg.BitcoindRpcUser,
BitcoindRpcPass: cfg.BitcoindRpcPass,
BitcoindRpcHost: cfg.BitcoindRpcHost,
BoardingExitDelay: cfg.BoardingExitDelay,
}
svc, err := grpcservice.NewService(svcConfig, appConfig)
if err != nil {

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/server
go 1.22.6
replace github.com/ark-network/ark/common => ../common
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
require (

View File

@@ -24,8 +24,6 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812233307-18e343b31899 h1:pUtYz5kx/hvm8EiF4aj1BNdLFKMGnV3g4s1lQST9jFg=
github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812233307-18e343b31899/go.mod h1:8DXpdLoJXeIPh3JPd6AoANVTf7Rw63QLL8l+OJkNBlU=
github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812233307-18e343b31899 h1:Vlc9pbGToiqeBbAx3q4Wovg/DK6DJXAE2k5YAQiqza4=

View File

@@ -63,6 +63,7 @@ type Config struct {
MinRelayFee uint64
RoundLifetime int64
UnilateralExitDelay int64
BoardingExitDelay int64
EsploraURL string
NeutrinoPeer string
@@ -126,6 +127,12 @@ func (c *Config) Validate() error {
)
}
if c.BoardingExitDelay < minAllowedSequence {
return fmt.Errorf(
"invalid boarding exit delay, must at least %d", minAllowedSequence,
)
}
if c.RoundLifetime%minAllowedSequence != 0 {
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
log.Infof(
@@ -142,6 +149,14 @@ func (c *Config) Validate() error {
)
}
if c.BoardingExitDelay%minAllowedSequence != 0 {
c.BoardingExitDelay -= c.BoardingExitDelay % minAllowedSequence
log.Infof(
"boarding exit delay must be a multiple of %d, rounded to %d",
minAllowedSequence, c.BoardingExitDelay,
)
}
if err := c.repoManager(); err != nil {
return err
}
@@ -275,11 +290,11 @@ func (c *Config) txBuilderService() error {
switch c.TxBuilderType {
case "covenant":
svc = txbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
)
case "covenantless":
svc = cltxbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
)
default:
err = fmt.Errorf("unknown tx builder type")
@@ -323,7 +338,7 @@ func (c *Config) schedulerService() error {
func (c *Config) appService() error {
if common.IsLiquid(c.Network) {
svc, err := application.NewCovenantService(
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
)
if err != nil {
@@ -335,7 +350,7 @@ func (c *Config) appService() error {
}
svc, err := application.NewCovenantlessService(
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
)
if err != nil {

View File

@@ -29,6 +29,7 @@ type Config struct {
MinRelayFee uint64
RoundLifetime int64
UnilateralExitDelay int64
BoardingExitDelay int64
EsploraURL string
NeutrinoPeer string
BitcoindRpcUser string
@@ -54,6 +55,7 @@ var (
MinRelayFee = "MIN_RELAY_FEE"
RoundLifetime = "ROUND_LIFETIME"
UnilateralExitDelay = "UNILATERAL_EXIT_DELAY"
BoardingExitDelay = "BOARDING_EXIT_DELAY"
EsploraURL = "ESPLORA_URL"
NeutrinoPeer = "NEUTRINO_PEER"
BitcoindRpcUser = "BITCOIND_RPC_USER"
@@ -79,6 +81,7 @@ var (
defaultMinRelayFee = 30 // 0.1 sat/vbyte on Liquid
defaultRoundLifetime = 604672
defaultUnilateralExitDelay = 1024
defaultBoardingExitDelay = 604672
defaultNoMacaroons = false
defaultNoTLS = false
)
@@ -104,6 +107,7 @@ func LoadConfig() (*Config, error) {
viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay)
viper.SetDefault(BlockchainScannerType, defaultBlockchainScannerType)
viper.SetDefault(NoMacaroons, defaultNoMacaroons)
viper.SetDefault(BoardingExitDelay, defaultBoardingExitDelay)
net, err := getNetwork()
if err != nil {
@@ -132,6 +136,7 @@ func LoadConfig() (*Config, error) {
MinRelayFee: viper.GetUint64(MinRelayFee),
RoundLifetime: viper.GetInt64(RoundLifetime),
UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay),
BoardingExitDelay: viper.GetInt64(BoardingExitDelay),
EsploraURL: viper.GetString(EsploraURL),
NeutrinoPeer: viper.GetString(NeutrinoPeer),
BitcoindRpcUser: viper.GetString(BitcoindRpcUser),

View File

@@ -10,14 +10,18 @@ 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/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
var (
@@ -30,6 +34,7 @@ type covenantService struct {
roundLifetime int64
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64
wallet ports.WalletService
@@ -41,23 +46,22 @@ type covenantService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
eventsCh chan domain.RoundEvent
lastEvent domain.RoundEvent
currentRound *domain.Round
currentRoundLock sync.Mutex
currentRound *domain.Round
lastEvent domain.RoundEvent
}
func NewCovenantService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding)
paymentRequests := newPaymentsMap(nil)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -69,9 +73,9 @@ func NewCovenantService(
svc := &covenantService{
network, pubkey,
roundLifetime, roundInterval, unilateralExitDelay, minRelayFee,
roundLifetime, roundInterval, unilateralExitDelay, boardingExitDelay, minRelayFee,
walletSvc, repoManager, builder, scanner, sweeper,
paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, nil,
paymentRequests, forfeitTxs, eventsCh, sync.Mutex{}, nil, nil,
}
repoManager.RegisterEventsHandler(
func(round *domain.Round) {
@@ -88,7 +92,6 @@ func NewCovenantService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
}
go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil
}
@@ -116,30 +119,164 @@ func (s *covenantService) Stop() {
s.repoManager.Close()
log.Debug("closed connection to db")
close(s.eventsCh)
close(s.onboardingCh)
}
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
return addr, nil
}
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
vtxosInputs := make([]domain.VtxoKey, 0)
boardingInputs := make([]Input, 0)
for _, in := range inputs {
if in.IsVtxo() {
vtxosInputs = append(vtxosInputs, in.VtxoKey())
continue
}
boardingInputs = append(boardingInputs, in)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix()
for _, in := range boardingInputs {
if _, ok := boardingTxs[in.Txid]; !ok {
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
}
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
}
boardingTxs[in.Txid] = txhex
}
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment); err != nil {
if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err
}
return payment.Id, nil
}
func (s *covenantService) newBoardingInput(
txhex string, vout uint32, desc descriptor.TaprootDescriptor,
) (ports.BoardingInput, error) {
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return nil, fmt.Errorf("failed to parse tx: %s", err)
}
if len(tx.Outputs) <= int(vout) {
return nil, fmt.Errorf("output not found")
}
out := tx.Outputs[vout]
script := out.Script
if len(out.RangeProof) > 0 || len(out.SurjectionProof) > 0 {
return nil, fmt.Errorf("output is confidential")
}
scriptFromDescriptor, err := tree.ComputeOutputScript(desc)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
if timeout != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to compute boarding script: %s", err)
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("output script mismatch expected script")
}
value, err := elementsutil.ValueFromBytes(out.Value)
if err != nil {
return nil, fmt.Errorf("failed to parse value: %s", err)
}
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: value,
}, nil
}
func (s *covenantService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials
payment, ok := s.paymentRequests.view(creds)
@@ -178,6 +315,19 @@ func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) er
return s.forfeitTxs.sign(forfeitTxs)
}
func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
s.currentRoundLock.Lock()
defer s.currentRoundLock.Unlock()
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
if err != nil {
return err
}
s.currentRound.UnsignedTx = combined
return nil
}
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -209,50 +359,17 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
RoundInterval: s.roundInterval,
Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee),
BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate,
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER",
s.boardingExitDelay,
"USER",
),
}, nil
}
func (s *covenantService) Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error {
ptx, err := psetv2.NewPsetFromBase64(boardingTx)
if err != nil {
return fmt.Errorf("failed to parse boarding tx: %s", err)
}
if err := tree.ValidateCongestionTree(
congestionTree, boardingTx, s.pubkey, s.roundLifetime,
); err != nil {
return err
}
extracted, err := psetv2.Extract(ptx)
if err != nil {
return fmt.Errorf("failed to extract boarding tx: %s", err)
}
boardingTxHex, err := extracted.ToHex()
if err != nil {
return fmt.Errorf("failed to convert boarding tx to hex: %s", err)
}
txid, err := s.wallet.BroadcastTransaction(ctx, boardingTxHex)
if err != nil {
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
}
log.Debugf("broadcasted boarding tx %s", txid)
s.onboardingCh <- onboarding{
tx: boardingTx,
congestionTree: congestionTree,
userPubkey: userPubkey,
}
return nil
}
func (s *covenantService) RegisterCosignerPubkey(ctx context.Context, paymentId string, _ string) error {
// if the user sends an ephemeral pubkey, something is going wrong client-side
// we should delete the associated payment
@@ -329,7 +446,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, _ := s.paymentRequests.pop(num)
payments, boardingInputs, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments")
@@ -343,7 +460,7 @@ func (s *covenantService) startFinalization() {
return
}
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds)
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -351,16 +468,26 @@ func (s *covenantService) startFinalization() {
}
log.Debugf("pool tx created for round %s", round.Id)
// TODO BTC make the senders sign the tree
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
log.Debugf("forfeit transactions created for round %s", round.Id)
var forfeitTxs, connectors []string
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
}
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx,
@@ -401,71 +528,63 @@ func (s *covenantService) finalizeRound() {
}
log.Debugf("signing round transaction %s\n", round.Id)
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
boardingInputs := make([]int, 0)
roundTx, err := psetv2.NewPsetFromBase64(round.UnsignedTx)
if err != nil {
log.Debugf("failed to parse round tx: %s", round.UnsignedTx)
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
log.WithError(err).Warn("failed to parse round tx")
return
}
for i, in := range roundTx.Inputs {
if len(in.TapLeafScript) > 0 {
if len(in.TapScriptSig) == 0 {
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
changes = round.Fail(err)
log.WithError(err).Warn("missing boarding sig")
return
}
boardingInputs = append(boardingInputs, i)
}
}
signedRoundTx := round.UnsignedTx
if len(boardingInputs) > 0 {
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
}
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil {
log.Debugf("failed to broadcast round tx: %s", signedRoundTx)
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx")
return
}
changes, _ = round.EndFinalization(forfeitTxs, txid)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantService) listenToOnboarding() {
for onboarding := range s.onboardingCh {
go s.handleOnboarding(onboarding)
}
}
func (s *covenantService) handleOnboarding(onboarding onboarding) {
ctx := context.Background()
ptx, _ := psetv2.NewPsetFromBase64(onboarding.tx)
utx, _ := psetv2.Extract(ptx)
txid := utx.TxHash().String()
// wait for the tx to be confirmed with a timeout
timeout := time.NewTimer(5 * time.Minute)
defer timeout.Stop()
isConfirmed := false
for !isConfirmed {
select {
case <-timeout.C:
log.WithError(fmt.Errorf("operation timed out")).Warnf("failed to get confirmation for boarding tx %s", txid)
return
default:
var err error
isConfirmed, _, err = s.wallet.IsTransactionConfirmed(ctx, txid)
if err != nil {
log.WithError(err).Warn("failed to check tx confirmation")
}
if err != nil || !isConfirmed {
time.Sleep(5 * time.Second)
}
}
}
pubkey := hex.EncodeToString(onboarding.userPubkey.SerializeCompressed())
payments := getPaymentsFromOnboardingLiquid(onboarding.congestionTree, pubkey)
round := domain.NewFinalizedRound(
dustAmount, pubkey, txid, onboarding.tx, onboarding.congestionTree, payments,
)
if err := s.saveEvents(ctx, round.Id, round.Events()); err != nil {
log.WithError(err).Warn("failed to store new round events")
changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return
}
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantService) listenToScannerNotifications() {
@@ -867,23 +986,6 @@ func (s *covenantService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
}
func getPaymentsFromOnboardingLiquid(
congestionTree tree.CongestionTree, userKey string,
) []domain.Payment {
leaves := congestionTree.Leaves()
receivers := make([]domain.Receiver, 0, len(leaves))
for _, node := range leaves {
ptx, _ := psetv2.NewPsetFromBase64(node.Tx)
receiver := domain.Receiver{
Pubkey: userKey,
Amount: ptx.Outputs[0].Value,
}
receivers = append(receivers, receiver)
}
payment := domain.NewPaymentUnsafe(nil, receivers)
return []domain.Payment{*payment}
}
func findForfeitTxLiquid(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) {

View File

@@ -11,12 +11,15 @@ 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/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
)
@@ -27,6 +30,7 @@ type covenantlessService struct {
roundLifetime int64
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64
wallet ports.WalletService
@@ -38,11 +42,11 @@ type covenantlessService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
eventsCh chan domain.RoundEvent
// cached data for the current round
lastEvent domain.RoundEvent
currentRoundLock sync.Mutex
currentRound *domain.Round
treeSigningSessions map[string]*musigSigningSession
asyncPaymentsCache map[domain.VtxoKey]struct {
@@ -53,14 +57,13 @@ type covenantlessService struct {
func NewCovenantlessService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding)
paymentRequests := newPaymentsMap(nil)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -89,9 +92,10 @@ func NewCovenantlessService(
paymentRequests: paymentRequests,
forfeitTxs: forfeitTxs,
eventsCh: eventsCh,
onboardingCh: onboardingCh,
currentRoundLock: sync.Mutex{},
asyncPaymentsCache: asyncPaymentsCache,
treeSigningSessions: make(map[string]*musigSigningSession),
boardingExitDelay: boardingExitDelay,
}
repoManager.RegisterEventsHandler(
@@ -109,7 +113,6 @@ func NewCovenantlessService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
}
go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil
}
@@ -137,7 +140,6 @@ func (s *covenantlessService) Stop() {
s.repoManager.Close()
log.Debug("closed connection to db")
close(s.eventsCh)
close(s.onboardingCh)
}
func (s *covenantlessService) CompleteAsyncPayment(
@@ -242,27 +244,147 @@ func (s *covenantlessService) CreateAsyncPayment(
return res.RedeemTx, res.UnconditionalForfeitTxs, nil
}
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
vtxosInputs := make([]domain.VtxoKey, 0)
boardingInputs := make([]Input, 0)
for _, input := range inputs {
if input.IsVtxo() {
vtxosInputs = append(vtxosInputs, input.VtxoKey())
continue
}
boardingInputs = append(boardingInputs, input)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix()
for _, in := range boardingInputs {
if _, ok := boardingTxs[in.Txid]; !ok {
// check if the tx exists and is confirmed
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
}
// if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
}
boardingTxs[in.Txid] = txhex
}
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment); err != nil {
if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err
}
return payment.Id, nil
}
func (s *covenantlessService) newBoardingInput(txhex string, vout uint32, desc descriptor.TaprootDescriptor) (ports.BoardingInput, error) {
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return nil, fmt.Errorf("failed to deserialize tx: %s", err)
}
if len(tx.TxOut) <= int(vout) {
return nil, fmt.Errorf("output not found")
}
out := tx.TxOut[vout]
script := out.PkScript
scriptFromDescriptor, err := bitcointree.ComputeOutputScript(desc)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
if timeout != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to get boarding script: %s", err)
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("invalid boarding input output script")
}
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: uint64(out.Value),
}, nil
}
func (s *covenantlessService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials
payment, ok := s.paymentRequests.view(creds)
@@ -293,6 +415,19 @@ func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string
return s.forfeitTxs.sign(forfeitTxs)
}
func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
s.currentRoundLock.Lock()
defer s.currentRoundLock.Unlock()
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
if err != nil {
return fmt.Errorf("failed to verify and combine partial tx: %s", err)
}
s.currentRound.UnsignedTx = combined
return nil
}
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -324,50 +459,26 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
RoundInterval: s.roundInterval,
Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee),
BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate,
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER",
s.boardingExitDelay,
"USER",
),
}, nil
}
// TODO clArk changes the onboard flow (2 rounds ?)
func (s *covenantlessService) Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(boardingTx), true)
func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey,
) (string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil {
return fmt.Errorf("failed to parse boarding tx: %s", err)
return "", fmt.Errorf("failed to compute boarding script: %s", err)
}
if err := bitcointree.ValidateCongestionTree(
congestionTree, boardingTx, s.pubkey, s.roundLifetime, int64(s.minRelayFee),
); err != nil {
return err
}
extracted, err := psbt.Extract(ptx)
if err != nil {
return fmt.Errorf("failed to extract boarding tx: %s", err)
}
var serialized bytes.Buffer
if err := extracted.Serialize(&serialized); err != nil {
return fmt.Errorf("failed to serialize boarding tx: %s", err)
}
txid, err := s.wallet.BroadcastTransaction(ctx, hex.EncodeToString(serialized.Bytes()))
if err != nil {
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
}
log.Debugf("broadcasted boarding tx %s", txid)
s.onboardingCh <- onboarding{
tx: boardingTx,
congestionTree: congestionTree,
userPubkey: userPubkey,
}
return nil
return addr, nil
}
func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error {
@@ -504,7 +615,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, cosigners := s.paymentRequests.pop(num)
payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -534,7 +645,7 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey())
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds, cosigners...)
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds, cosigners...)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -683,14 +794,25 @@ func (s *covenantlessService) startFinalization() {
tree = signedTree
}
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
log.Debugf("forfeit transactions created for round %s", round.Id)
var forfeitTxs, connectors []string
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
}
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx,
@@ -763,73 +885,61 @@ func (s *covenantlessService) finalizeRound() {
}
log.Debugf("signing round transaction %s\n", round.Id)
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
boardingInputs := make([]int, 0)
roundTx, err := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
log.WithError(err).Warn("failed to parse round tx")
return
}
for i, in := range roundTx.Inputs {
if len(in.TaprootLeafScript) > 0 {
if len(in.TaprootScriptSpendSig) == 0 {
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
changes = round.Fail(err)
log.WithError(err).Warn("missing boarding sig")
return
}
boardingInputs = append(boardingInputs, i)
}
}
signedRoundTx := round.UnsignedTx
if len(boardingInputs) > 0 {
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
}
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx")
return
}
changes, _ = round.EndFinalization(forfeitTxs, txid)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantlessService) listenToOnboarding() {
for onboarding := range s.onboardingCh {
go s.handleOnboarding(onboarding)
}
}
func (s *covenantlessService) handleOnboarding(onboarding onboarding) {
ctx := context.Background()
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(onboarding.tx), true)
txid := ptx.UnsignedTx.TxHash().String()
// wait for the tx to be confirmed with a timeout
timeout := time.NewTimer(15 * time.Minute)
defer timeout.Stop()
isConfirmed := false
for !isConfirmed {
select {
case <-timeout.C:
log.WithError(fmt.Errorf("operation timed out")).Warnf("failed to get confirmation for boarding tx %s", txid)
return
default:
var err error
isConfirmed, _, err = s.wallet.IsTransactionConfirmed(ctx, txid)
if err != nil {
log.WithError(err).Warn("failed to check tx confirmation")
}
if err != nil || !isConfirmed {
log.Debugf("waiting for boarding tx %s to be confirmed", txid)
time.Sleep(5 * time.Second)
}
}
}
log.Debugf("boarding tx %s confirmed", txid)
pubkey := hex.EncodeToString(onboarding.userPubkey.SerializeCompressed())
payments := getPaymentsFromOnboardingBitcoin(onboarding.congestionTree, pubkey)
round := domain.NewFinalizedRound(
dustAmount, pubkey, txid, onboarding.tx, onboarding.congestionTree, payments,
)
if err := s.saveEvents(ctx, round.Id, round.Events()); err != nil {
log.WithError(err).Warn("failed to store new round events")
changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return
}
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantlessService) listenToScannerNotifications() {
@@ -1232,24 +1342,6 @@ func (s *covenantlessService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
}
func getPaymentsFromOnboardingBitcoin(
congestionTree tree.CongestionTree, userKey string,
) []domain.Payment {
leaves := congestionTree.Leaves()
receivers := make([]domain.Receiver, 0, len(leaves))
for _, node := range leaves {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
receiver := domain.Receiver{
Pubkey: userKey,
Amount: uint64(ptx.UnsignedTx.TxOut[0].Value),
}
receivers = append(receivers, receiver)
}
payment := domain.NewPaymentUnsafe(nil, receivers)
return []domain.Payment{*payment}
}
func findForfeitTxBitcoin(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) {

View File

@@ -2,8 +2,9 @@ package application
import (
"context"
"fmt"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
@@ -16,9 +17,10 @@ var (
type Service interface {
Start() error
Stop()
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
SpendVtxos(ctx context.Context, inputs []Input) (string, error)
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
SignVtxos(ctx context.Context, forfeitTxs []string) error
SignRoundTx(ctx context.Context, roundTx string) error
GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error)
GetRoundById(ctx context.Context, id string) (*domain.Round, error)
GetCurrentRound(ctx context.Context) (*domain.Round, error)
@@ -30,10 +32,6 @@ type Service interface {
ctx context.Context, pubkey *secp256k1.PublicKey,
) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
GetInfo(ctx context.Context) (*ServiceInfo, error)
Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error
// Async payments
CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
@@ -41,6 +39,7 @@ type Service interface {
CompleteAsyncPayment(
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
) error
GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error)
// Tree signing methods
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
RegisterCosignerNonces(
@@ -54,12 +53,13 @@ type Service interface {
}
type ServiceInfo struct {
PubKey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
PubKey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
BoardingDescriptorTemplate string
}
type WalletStatus struct {
@@ -68,10 +68,28 @@ type WalletStatus struct {
IsSynced bool
}
type onboarding struct {
tx string
congestionTree tree.CongestionTree
userPubkey *secp256k1.PublicKey
type Input struct {
Txid string
Index uint32
Descriptor string
}
func (i Input) IsVtxo() bool {
return len(i.Descriptor) <= 0
}
func (i Input) VtxoKey() domain.VtxoKey {
return domain.VtxoKey{
Txid: i.Txid,
VOut: i.Index,
}
}
func (i Input) GetDescriptor() (*descriptor.TaprootDescriptor, error) {
if i.IsVtxo() {
return nil, fmt.Errorf("input is not a boarding input")
}
return descriptor.ParseTaprootDescriptor(i.Descriptor)
}
type txOutpoint struct {

View File

@@ -10,14 +10,16 @@ import (
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/sirupsen/logrus"
)
type timedPayment struct {
domain.Payment
timestamp time.Time
pingTimestamp time.Time
boardingInputs []ports.BoardingInput
timestamp time.Time
pingTimestamp time.Time
}
type paymentsMap struct {
@@ -26,11 +28,8 @@ type paymentsMap struct {
ephemeralKeys map[string]*secp256k1.PublicKey
}
func newPaymentsMap(payments []domain.Payment) *paymentsMap {
func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment)
for _, p := range payments {
paymentsById[p.Id] = &timedPayment{p, time.Now(), time.Time{}}
}
lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
}
@@ -60,7 +59,7 @@ func (m *paymentsMap) delete(id string) error {
return nil
}
func (m *paymentsMap) push(payment domain.Payment) error {
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
m.lock.Lock()
defer m.lock.Unlock()
@@ -68,7 +67,7 @@ func (m *paymentsMap) push(payment domain.Payment) error {
return fmt.Errorf("duplicated inputs")
}
m.payments[payment.Id] = &timedPayment{payment, time.Now(), time.Time{}}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
return nil
}
@@ -84,7 +83,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil
}
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey) {
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) {
m.lock.Lock()
defer m.lock.Unlock()
@@ -109,8 +108,10 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
}
payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num)
for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment)
if pubkey, ok := m.ephemeralKeys[p.Payment.Id]; ok {
cosigners = append(cosigners, pubkey)
@@ -118,7 +119,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
}
delete(m.payments, p.Id)
}
return payments, cosigners
return payments, boardingInputs, cosigners
}
func (m *paymentsMap) update(payment domain.Payment) error {
@@ -312,3 +313,26 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
}
return vtxos
}
type boardingInput struct {
txId chainhash.Hash
vout uint32
boardingPubKey *secp256k1.PublicKey
amount uint64
}
func (b boardingInput) GetHash() chainhash.Hash {
return b.txId
}
func (b boardingInput) GetIndex() uint32 {
return b.vout
}
func (b boardingInput) GetAmount() uint64 {
return b.amount
}
func (b boardingInput) GetBoardingPubkey() *secp256k1.PublicKey {
return b.boardingPubKey
}

View File

@@ -28,14 +28,6 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
return p, nil
}
func NewPaymentUnsafe(inputs []Vtxo, receivers []Receiver) *Payment {
return &Payment{
Id: uuid.New().String(),
Inputs: inputs,
Receivers: receivers,
}
}
func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
if p.Receivers == nil {
p.Receivers = make([]Receiver, 0)
@@ -70,18 +62,13 @@ func (p Payment) validate(ignoreOuts bool) error {
if len(p.Id) <= 0 {
return fmt.Errorf("missing id")
}
if len(p.Inputs) <= 0 {
return fmt.Errorf("missing inputs")
}
if ignoreOuts {
return nil
}
if len(p.Receivers) <= 0 {
return fmt.Errorf("missing outputs")
}
// Check that input and output and output amounts match.
inAmount := p.TotalInputAmount()
outAmount := uint64(0)
for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
return fmt.Errorf("missing receiver destination")
@@ -89,10 +76,6 @@ func (p Payment) validate(ignoreOuts bool) error {
if r.Amount < dustAmount {
return fmt.Errorf("receiver amount must be greater than dust")
}
outAmount += r.Amount
}
if inAmount != outAmount {
return fmt.Errorf("input and output amounts mismatch")
}
return nil
}

View File

@@ -22,7 +22,7 @@ var inputs = []domain.Vtxo{
func TestPayment(t *testing.T) {
t.Run("new_payment", func(t *testing.T) {
t.Run("vaild", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
payment, err := domain.NewPayment(inputs)
require.NoError(t, err)
require.NotNil(t, payment)
@@ -30,24 +30,6 @@ func TestPayment(t *testing.T) {
require.Exactly(t, inputs, payment.Inputs)
require.Empty(t, payment.Receivers)
})
t.Run("invaild", func(t *testing.T) {
fixtures := []struct {
inputs []domain.Vtxo
expectedErr string
}{
{
inputs: nil,
expectedErr: "missing inputs",
},
}
for _, f := range fixtures {
payment, err := domain.NewPayment(f.inputs)
require.EqualError(t, err, f.expectedErr)
require.Nil(t, payment)
}
})
})
t.Run("add_receivers", func(t *testing.T) {
@@ -87,15 +69,6 @@ func TestPayment(t *testing.T) {
},
expectedErr: "receiver amount must be greater than dust",
},
{
receivers: []domain.Receiver{
{
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
Amount: 600,
},
},
expectedErr: "input and output amounts mismatch",
},
}
payment, err := domain.NewPayment(inputs)

View File

@@ -60,39 +60,6 @@ func NewRound(dustAmount uint64) *Round {
}
}
func NewFinalizedRound(
dustAmount uint64, userKey, poolTxid, poolTx string,
congestionTree tree.CongestionTree, payments []Payment,
) *Round {
r := NewRound(dustAmount)
events := []RoundEvent{
RoundStarted{
Id: r.Id,
Timestamp: time.Now().Unix(),
},
PaymentsRegistered{
Id: r.Id,
Payments: payments,
},
RoundFinalizationStarted{
Id: r.Id,
CongestionTree: congestionTree,
PoolTx: poolTx,
},
RoundFinalized{
Id: r.Id,
Txid: poolTxid,
Timestamp: time.Now().Unix(),
},
}
for _, event := range events {
r.raise(event)
}
return r
}
func NewRoundFromEvents(events []RoundEvent) *Round {
r := &Round{}
@@ -205,7 +172,11 @@ func (r *Round) StartFinalization(connectorAddress string, connectors []string,
func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent, error) {
if len(forfeitTxs) <= 0 {
return nil, fmt.Errorf("missing list of signed forfeit txs")
for _, p := range r.Payments {
if len(p.Inputs) > 0 {
return nil, fmt.Errorf("missing list of signed forfeit txs")
}
}
}
if len(txid) <= 0 {
return nil, fmt.Errorf("missing pool txid")
@@ -216,6 +187,10 @@ func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent,
if r.Stage.Ended {
return nil, fmt.Errorf("round already finalized")
}
if forfeitTxs == nil {
forfeitTxs = make([]string, 0)
}
event := RoundFinalized{
Id: r.Id,
Txid: txid,

View File

@@ -449,6 +449,7 @@ func testEndFinalization(t *testing.T) {
Stage: domain.Stage{
Code: domain.FinalizationStage,
},
Payments: paymentsById,
},
forfeitTxs: nil,
txid: txid,

View File

@@ -16,9 +16,16 @@ type SweepInput interface {
GetInternalKey() *secp256k1.PublicKey
}
type BoardingInput interface {
GetAmount() uint64
GetIndex() uint32
GetHash() chainhash.Hash
GetBoardingPubkey() *secp256k1.PublicKey
}
type TxBuilder interface {
BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, minRelayFee uint64, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
@@ -33,4 +40,6 @@ type TxBuilder interface {
vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64,
) (*domain.AsyncPaymentTxs, error)
GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (addr string, script []byte, err error)
VerifyAndCombinePartialTx(dest string, src string) (string, error)
}

View File

@@ -33,6 +33,7 @@ type WalletService interface {
MainAccountBalance(ctx context.Context) (uint64, uint64, error)
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
GetTransaction(ctx context.Context, txid string) (string, error)
Close()
}

View File

@@ -95,7 +95,7 @@ func deserializeEvent(buf []byte) (domain.RoundEvent, error) {
}
{
var event = domain.RoundFinalizationStarted{}
if err := json.Unmarshal(buf, &event); err == nil && len(event.Connectors) > 0 {
if err := json.Unmarshal(buf, &event); err == nil && len(event.PoolTx) > 0 {
return event, nil
}
}

View File

@@ -11,12 +11,15 @@ import (
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
const (
@@ -25,10 +28,11 @@ const (
)
type txBuilder struct {
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds
}
func NewTxBuilder(
@@ -36,8 +40,18 @@ func NewTxBuilder(
net common.Network,
roundLifetime int64,
exitDelay int64,
boardingExitDelay int64,
) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, exitDelay}
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay}
}
func (b *txBuilder) GetBoardingScript(owner, asp *secp256k1.PublicKey) (string, []byte, error) {
addr, script, _, err := b.getBoardingTaproot(owner, asp)
if err != nil {
return "", nil, err
}
return addr, script, nil
}
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
@@ -112,7 +126,11 @@ func (b *txBuilder) BuildForfeitTxs(
}
func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
aspPubkey *secp256k1.PublicKey,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
minRelayFee uint64,
sweptRounds []domain.Round,
_ ...*secp256k1.PublicKey, // cosigners are not used in the covenant
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
// The creation of the tree and the pool tx are tightly coupled:
@@ -146,7 +164,7 @@ func (b *txBuilder) BuildPoolTx(
}
ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
sharedOutputAmount, sharedOutputScript, payments, boardingInputs, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
)
if err != nil {
return
@@ -197,9 +215,19 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
expirationTime := parentblocktime + lifetime
amount := uint64(0)
for _, out := range pset.Outputs {
amount += out.Value
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
if err != nil {
return -1, nil, err
}
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return -1, nil, err
}
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
if err != nil {
return -1, nil, err
}
sweepInput = &sweepLiquidInput{
@@ -208,7 +236,7 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
TxIndex: index,
},
sweepLeaf: sweepLeaf,
amount: amount,
amount: inputValue,
}
return expirationTime, sweepInput, nil
@@ -362,8 +390,11 @@ func (b *txBuilder) getLeafScriptAndTree(
}
func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputScript []byte,
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sharedOutputAmount uint64,
sharedOutputScript []byte,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round,
) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
@@ -396,11 +427,13 @@ func (b *txBuilder) createPoolTx(
})
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: connectorsAmount,
Script: connectorScript,
})
if connectorsAmount > 0 {
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: connectorsAmount,
Script: connectorScript,
})
}
for _, receiver := range receivers {
targetAmount += receiver.Amount
@@ -417,6 +450,9 @@ func (b *txBuilder) createPoolTx(
})
}
for _, in := range boardingInputs {
targetAmount -= in.GetAmount()
}
ctx := context.Background()
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
if err != nil {
@@ -447,6 +483,48 @@ func (b *txBuilder) createPoolTx(
return nil, err
}
for _, in := range boardingInputs {
if err := updater.AddInputs(
[]psetv2.InputArgs{
{
Txid: in.GetHash().String(),
TxIndex: in.GetIndex(),
},
},
); err != nil {
return nil, err
}
index := len(ptx.Inputs) - 1
assetBytes, err := elementsutil.AssetHashToBytes(b.onchainNetwork().AssetID)
if err != nil {
return nil, fmt.Errorf("failed to convert asset to bytes: %s", err)
}
valueBytes, err := elementsutil.ValueToBytes(in.GetAmount())
if err != nil {
return nil, fmt.Errorf("failed to convert value to bytes: %s", err)
}
_, script, tapLeafProof, err := b.getBoardingTaproot(in.GetBoardingPubkey(), aspPubKey)
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(index, transaction.NewTxOutput(assetBytes, valueBytes, script)); err != nil {
return nil, err
}
if err := updater.AddInTapLeafScript(index, psetv2.NewTapLeafScript(*tapLeafProof, tree.UnspendableKey())); err != nil {
return nil, err
}
if err := updater.AddInSighashType(index, txscript.SigHashDefault); err != nil {
return nil, err
}
}
if err := addInputs(updater, utxos); err != nil {
return nil, err
}
@@ -471,14 +549,23 @@ func (b *txBuilder) createPoolTx(
if feeAmount == change {
// fees = change, remove change output
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
feeAmount += change
} else if feeAmount < change {
// change covers the fees, reduce change amount
ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount
if change-feeAmount < dustLimit {
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
feeAmount += change
} else {
ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount
}
} else {
// change is not enough to cover fees, re-select utxos
if change > 0 {
// remove change output if present
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
}
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
if err != nil {
@@ -486,14 +573,18 @@ func (b *txBuilder) createPoolTx(
}
if change > 0 {
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: b.onchainNetwork().AssetID,
Amount: change,
Script: aspScript,
},
}); err != nil {
return nil, err
if change < dustLimit {
feeAmount += change
} else {
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: b.onchainNetwork().AssetID,
Amount: change,
Script: aspScript,
},
}); err != nil {
return nil, err
}
}
}
@@ -541,6 +632,77 @@ func (b *txBuilder) createPoolTx(
return ptx, nil
}
// This method aims to verify and add partial signature from boarding input
func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, error) {
roundPset, err := psetv2.NewPsetFromBase64(dest)
if err != nil {
return "", err
}
sourcePset, err := psetv2.NewPsetFromBase64(src)
if err != nil {
return "", err
}
roundUtx, err := roundPset.UnsignedTx()
if err != nil {
return "", err
}
sourceUtx, err := sourcePset.UnsignedTx()
if err != nil {
return "", err
}
if roundUtx.TxHash().String() != sourceUtx.TxHash().String() {
return "", fmt.Errorf("txid mismatch")
}
roundSigner, err := psetv2.NewSigner(roundPset)
if err != nil {
return "", err
}
for i, input := range sourcePset.Inputs {
if len(input.TapScriptSig) == 0 || len(input.TapLeafScript) == 0 {
continue
}
partialSig := input.TapScriptSig[0]
leafHash, err := chainhash.NewHash(partialSig.LeafHash)
if err != nil {
return "", err
}
preimage, err := b.getTaprootPreimage(src, i, leafHash)
if err != nil {
return "", err
}
sig, err := schnorr.ParseSignature(partialSig.Signature)
if err != nil {
return "", err
}
pubkey, err := schnorr.ParsePubKey(partialSig.PubKey)
if err != nil {
return "", err
}
if !sig.Verify(preimage, pubkey) {
return "", fmt.Errorf("invalid signature")
}
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil {
return "", err
}
}
return roundSigner.Pset.ToBase64()
}
func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, connectorAddress string, minRelayFee uint64,
) ([]*psetv2.Pset, error) {
@@ -733,6 +895,52 @@ func (b *txBuilder) onchainNetwork() *network.Network {
}
}
func (b *txBuilder) getBoardingTaproot(owner, asp *secp256k1.PublicKey) (string, []byte, *taproot.TapscriptElementsProof, error) {
multisigClosure := tree.ForfeitClosure{
Pubkey: owner,
AspPubkey: asp,
}
csvClosure := tree.CSVSigClosure{
Pubkey: owner,
Seconds: uint(b.boardingExitDelay),
}
multisigLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
csvLeaf, err := csvClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
tapTree := taproot.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
root := tapTree.RootNode.TapHash()
tapKey := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), root[:])
p2tr, err := payment.FromTweakedKey(tapKey, b.onchainNetwork(), nil)
if err != nil {
return "", nil, nil, err
}
addr, err := p2tr.TaprootAddress()
if err != nil {
return "", nil, nil, err
}
tapLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
leafProofIndex := tapTree.LeafProofIndex[tapLeaf.TapHash()]
leafProof := tapTree.LeafMerkleProofs[leafProofIndex]
return addr, p2tr.Script, &leafProof, nil
}
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
for _, leaf := range input.TapLeafScript {
closure := &tree.CSVSigClosure{}

View File

@@ -25,6 +25,7 @@ const (
minRelayFee = uint64(30)
roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
boardingExitDelay = int64(512)
)
var (
@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, roundLifetime, unilateralExitDelay,
wallet, common.Liquid, roundLifetime, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parsePoolTxFixtures()
@@ -60,7 +61,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{},
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
)
require.NoError(t, err)
require.NotEmpty(t, poolTx)
@@ -81,7 +82,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{},
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, poolTx)
@@ -94,7 +95,7 @@ func TestBuildPoolTx(t *testing.T) {
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, 1209344, unilateralExitDelay,
wallet, common.Liquid, 1209344, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parseForfeitTxsFixtures()

View File

@@ -229,6 +229,16 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
return res, res2, args.Error(2)
}
func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string, error) {
args := m.Called(ctx, txid)
var res string
if a := args.Get(0); a != nil {
res = a.(string)
}
return res, args.Error(1)
}
type mockedInput struct {
mock.Mock
}

Some files were not shown because too many files have changed in this diff Show More