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" "application/json"
], ],
"paths": { "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": { "/v1/events": {
"get": { "get": {
"operationId": "ArkService_GetEventStream", "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": { "/v1/payment": {
"post": { "post": {
"operationId": "ArkService_CreatePayment", "operationId": "ArkService_CreatePayment",
@@ -475,6 +475,21 @@
} }
} }
}, },
"v1BoardingInput": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
},
"descriptor": {
"type": "string"
}
}
},
"v1ClaimPaymentRequest": { "v1ClaimPaymentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -555,12 +570,35 @@
"type": "string" "type": "string"
}, },
"description": "Forfeit txs signed by the user." "description": "Forfeit txs signed by the user."
},
"signedRoundTx": {
"type": "string",
"description": "If payment has boarding input, the user must sign the associated inputs."
} }
} }
}, },
"v1FinalizePaymentResponse": { "v1FinalizePaymentResponse": {
"type": "object" "type": "object"
}, },
"v1GetBoardingAddressRequest": {
"type": "object",
"properties": {
"pubkey": {
"type": "string"
}
}
},
"v1GetBoardingAddressResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"descriptor": {
"type": "string"
}
}
},
"v1GetEventStreamResponse": { "v1GetEventStreamResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -605,6 +643,9 @@
"minRelayFee": { "minRelayFee": {
"type": "string", "type": "string",
"format": "int64" "format": "int64"
},
"boardingDescriptorTemplate": {
"type": "string"
} }
} }
}, },
@@ -627,12 +668,11 @@
"v1Input": { "v1Input": {
"type": "object", "type": "object",
"properties": { "properties": {
"txid": { "vtxoInput": {
"type": "string" "$ref": "#/definitions/v1VtxoInput"
}, },
"vout": { "boardingInput": {
"type": "integer", "$ref": "#/definitions/v1BoardingInput"
"format": "int64"
} }
} }
}, },
@@ -669,23 +709,6 @@
} }
} }
}, },
"v1OnboardRequest": {
"type": "object",
"properties": {
"boardingTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"userPubkey": {
"type": "string"
}
}
},
"v1OnboardResponse": {
"type": "object"
},
"v1Output": { "v1Output": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -972,6 +995,18 @@
"$ref": "#/definitions/v1PendingPayment" "$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" get: "/v1/info"
}; };
} }
rpc Onboard(OnboardRequest) returns (OnboardResponse) { rpc GetBoardingAddress(GetBoardingAddressRequest) returns (GetBoardingAddressResponse) {
option (google.api.http) = { option (google.api.http) = {
post: "/v1/onboard" post: "/v1/boarding"
body: "*" body: "*"
}; };
} };
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) { rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {
option (google.api.http) = { option (google.api.http) = {
post: "/v1/payment" post: "/v1/payment"
@@ -100,6 +100,15 @@ message CompletePaymentRequest {
} }
message CompletePaymentResponse {} message CompletePaymentResponse {}
message GetBoardingAddressRequest {
string pubkey = 1;
}
message GetBoardingAddressResponse {
string address = 1;
string descriptor = 2;
}
message RegisterPaymentRequest { message RegisterPaymentRequest {
repeated Input inputs = 1; repeated Input inputs = 1;
optional string ephemeral_pubkey = 2; optional string ephemeral_pubkey = 2;
@@ -120,6 +129,8 @@ message ClaimPaymentResponse {}
message FinalizePaymentRequest { message FinalizePaymentRequest {
// Forfeit txs signed by the user. // Forfeit txs signed by the user.
repeated string signed_forfeit_txs = 1; 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 {} message FinalizePaymentResponse {}
@@ -177,14 +188,7 @@ message GetInfoResponse {
int64 round_interval = 4; int64 round_interval = 4;
string network = 5; string network = 5;
int64 min_relay_fee = 6; int64 min_relay_fee = 6;
} string boarding_descriptor_template = 7;
message OnboardRequest {
string boarding_tx = 1;
Tree congestion_tree = 2;
string user_pubkey = 3;
}
message OnboardResponse {
} }
// EVENT TYPES // EVENT TYPES
@@ -239,11 +243,24 @@ message Round {
RoundStage stage = 8; RoundStage stage = 8;
} }
message Input { message VtxoInput {
string txid = 1; string txid = 1;
uint32 vout = 2; 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 { message Output {
// Either the offchain or onchain address. // Either the offchain or onchain address.
string address = 1; 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) { 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 OnboardRequest var protoReq GetBoardingAddressRequest
var metadata runtime.ServerMetadata var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 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 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) { 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 OnboardRequest var protoReq GetBoardingAddressRequest
var metadata runtime.ServerMetadata var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 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 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()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
var stream runtime.ServerTransportStream var stream runtime.ServerTransportStream
@@ -753,12 +753,12 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error var err error
var annotatedContext context.Context 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 { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return 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()) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil { if err != nil {
@@ -766,7 +766,7 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
return 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()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error var err error
var annotatedContext context.Context 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 { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return 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) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil { if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return 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_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"}, "")) 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_GetInfo_0 = runtime.ForwardResponseMessage
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage forward_ArkService_GetBoardingAddress_0 = runtime.ForwardResponseMessage
forward_ArkService_CreatePayment_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) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, 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) CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error)
CompletePayment(ctx context.Context, in *CompletePaymentRequest, opts ...grpc.CallOption) (*CompletePaymentResponse, 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 return out, nil
} }
func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) { func (c *arkServiceClient) GetBoardingAddress(ctx context.Context, in *GetBoardingAddressRequest, opts ...grpc.CallOption) (*GetBoardingAddressResponse, error) {
out := new(OnboardResponse) out := new(GetBoardingAddressResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Onboard", in, out, opts...) err := c.cc.Invoke(ctx, "/ark.v1.ArkService/GetBoardingAddress", in, out, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -206,7 +206,7 @@ type ArkServiceServer interface {
Ping(context.Context, *PingRequest) (*PingResponse, error) Ping(context.Context, *PingRequest) (*PingResponse, error)
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, 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) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error)
CompletePayment(context.Context, *CompletePaymentRequest) (*CompletePaymentResponse, 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) { func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
} }
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) { func (UnimplementedArkServiceServer) GetBoardingAddress(context.Context, *GetBoardingAddressRequest) (*GetBoardingAddressResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetBoardingAddress not implemented")
} }
func (UnimplementedArkServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) { func (UnimplementedArkServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePayment not implemented") 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) return interceptor(ctx, in, info, handler)
} }
func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _ArkService_GetBoardingAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OnboardRequest) in := new(GetBoardingAddressRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(ArkServiceServer).Onboard(ctx, in) return srv.(ArkServiceServer).GetBoardingAddress(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/ark.v1.ArkService/Onboard", FullMethod: "/ark.v1.ArkService/GetBoardingAddress",
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { 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) return interceptor(ctx, in, info, handler)
} }
@@ -572,8 +572,8 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
Handler: _ArkService_GetInfo_Handler, Handler: _ArkService_GetInfo_Handler,
}, },
{ {
MethodName: "Onboard", MethodName: "GetBoardingAddress",
Handler: _ArkService_Onboard_Handler, Handler: _ArkService_GetBoardingAddress_Handler,
}, },
{ {
MethodName: "CreatePayment", MethodName: "CreatePayment",

View File

@@ -9,6 +9,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/flags" "github.com/ark-network/ark/client/flags"
"github.com/ark-network/ark/client/utils" "github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@@ -21,18 +22,30 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
} }
defer cancel() defer cancel()
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx) offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
network, err := utils.GetNetwork(ctx)
if err != nil { if err != nil {
return err return err
} }
// No need to check for error here becuase this function is called also by getAddress(). // No need to check for error here becuase this function is called also by getAddress().
// nolint:all // nolint:all
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx) 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 := &sync.WaitGroup{}
wg.Add(3) wg.Add(3)
@@ -54,19 +67,19 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
go func() { go func() {
defer wg.Done() defer wg.Done()
explorer := utils.NewExplorer(ctx) explorer := utils.NewExplorer(ctx)
balance, err := explorer.GetBalance(onchainAddr, toElementsNetwork(network).AssetID) spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err} chRes <- balanceRes{0, 0, nil, nil, err}
return return
} }
chRes <- balanceRes{0, balance, nil, nil, nil} chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
explorer := utils.NewExplorer(ctx) explorer := utils.NewExplorer(ctx)
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance( spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
redemptionAddr, unilateralExitDelay, redemptionAddr, unilateralExitDelay,
) )
if err != nil { 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/interfaces"
"github.com/ark-network/ark/client/utils" "github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common" "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/common/tree"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
) )
const dust = 450 const dust = 450
@@ -29,19 +26,15 @@ func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented") 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 { func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx) offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil { if err != nil {
return err return err
} }
return utils.PrintJSON(map[string]interface{}{ return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr, "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) explorer := utils.NewExplorer(ctx)
utxos, delayedUtxos, change, err := coinSelectOnchain( utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil, ctx, explorer, targetAmount, nil,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := addInputs(ctx, updater, utxos, delayedUtxos, &liquidNet); err != nil { if err := addInputs(ctx, updater, utxos); err != nil {
return "", err 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] updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
} }
// reselect the difference // reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain( selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...), ctx, explorer, feeAmount-change, utxos,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := addInputs(ctx, updater, selected, delayedSelected, &liquidNet); err != nil { if err := addInputs(ctx, updater, selected); err != nil {
return "", err return "", err
} }
@@ -243,20 +236,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
func coinSelectOnchain( func coinSelectOnchain(
ctx *cli.Context, ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo, explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, []utils.Utxo, uint64, error) { ) ([]utils.Utxo, uint64, error) {
_, onchainAddr, _, err := getAddress(ctx) _, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil { 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 { if err != nil {
return nil, nil, 0, err return nil, 0, err
} }
utxos := make([]utils.Utxo, 0) utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(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 { if selectedAmount >= targetAmount {
break break
} }
@@ -267,207 +277,99 @@ func coinSelectOnchain(
} }
} }
utxos = append(utxos, utxo) utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
selectedAmount += utxo.Amount
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
} }
if selectedAmount >= targetAmount { 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 { 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 { if err != nil {
return nil, nil, 0, err return nil, 0, err
} }
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx) for _, utxo := range redemptionUtxosFromExplorer {
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 {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
break break
} }
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude { for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue continue
} }
} }
delayedUtxos = append(delayedUtxos, utxo) utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
selectedAmount += utxo.Amount
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
} }
if selectedAmount < targetAmount { if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount, "not enough funds to cover amount %d", targetAmount,
) )
} }
return utxos, delayedUtxos, selectedAmount - targetAmount, nil return utxos, selectedAmount - targetAmount, nil
} }
func addInputs( func addInputs(
ctx *cli.Context, ctx *cli.Context,
updater *psetv2.Updater, utxos, delayedUtxos []utils.Utxo, net *network.Network, updater *psetv2.Updater,
utxos []utils.Utxo,
) error { ) error {
_, onchainAddr, _, err := getAddress(ctx) userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil { if err != nil {
return err return err
} }
changeScript, err := address.ToOutputScript(onchainAddr) aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil { if err != nil {
return err return err
} }
for _, utxo := range utxos { for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
return err
}
if err := updater.AddInputs([]psetv2.InputArgs{ if err := updater.AddInputs([]psetv2.InputArgs{
{ {
Txid: utxo.Txid, Txid: utxo.Txid,
TxIndex: utxo.Vout, TxIndex: utxo.Vout,
Sequence: sequence,
}, },
}); err != nil { }); err != nil {
return err return err
} }
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset) _, leafProof, err := computeVtxoTaprootScript(
if err != nil { userPubkey, aspPubkey, utxo.Delay,
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),
) )
if err != nil { if err != nil {
return err return err
} }
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil) inputIndex := len(updater.Pset.Inputs) - 1
if err != nil {
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
return err 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 return nil
@@ -503,32 +405,7 @@ func decodeReceiverAddress(addr string) (
return true, outputScript, nil, nil return true, outputScript, nil, nil
} }
func addVtxoInput( func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
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) {
userPubkey, err := utils.GetWalletPublicKey(ctx) userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil { if err != nil {
return return
@@ -544,6 +421,21 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
return 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) arkNet, err := utils.GetNetwork(ctx)
if err != nil { if err != nil {
return return
@@ -556,12 +448,6 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
liquidNet := toElementsNetwork(arkNet) liquidNet := toElementsNetwork(arkNet)
p2wpkh := payment.FromPublicKey(userPubkey, &liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript( vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay), userPubkey, aspPubkey, uint(unilateralExitDelay),
) )
@@ -569,18 +455,34 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
return return
} }
payment, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil) redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil { if err != nil {
return 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 { if err != nil {
return return
} }
offchainAddr = arkAddr offchainAddr = arkAddr
onchainAddr = liquidAddr
return return
} }

View File

@@ -51,13 +51,16 @@ func getVtxos(
if v.Swept { if v.Swept {
continue continue
} }
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount, if v.Outpoint.GetVtxoInput() != nil {
txid: v.Outpoint.Txid, vtxos = append(vtxos, vtxo{
vout: v.Outpoint.Vout, amount: v.Receiver.Amount,
poolTxid: v.PoolTxid, txid: v.Outpoint.GetVtxoInput().GetTxid(),
expireAt: expireAt, vout: v.Outpoint.GetVtxoInput().GetVout(),
}) poolTxid: v.PoolTxid,
expireAt: expireAt,
})
}
} }
if !computeExpiration { if !computeExpiration {
@@ -193,32 +196,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil 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( func handleRoundStream(
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string, 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) { ) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{}) stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
if err != nil { if err != nil {
@@ -254,8 +235,8 @@ func handleRoundStream(
pingStop() pingStop()
fmt.Println("round finalization started") fmt.Println("round finalization started")
poolTx := e.GetPoolTx() roundTx := e.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(poolTx) ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -280,13 +261,13 @@ func handleRoundStream(
if !isOnchainOnly(receivers) { if !isOnchainOnly(receivers) {
// validate the congestion tree // validate the congestion tree
if err := tree.ValidateCongestionTree( if err := tree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime), congestionTree, roundTx, aspPubkey, int64(roundLifetime),
); err != nil { ); err != nil {
return "", err return "", err
} }
} }
if err := common.ValidateConnectors(poolTx, connectors); err != nil { if err := common.ValidateConnectors(roundTx, connectors); err != nil {
return "", err return "", err
} }
@@ -379,78 +360,100 @@ func handleRoundStream(
fmt.Println("congestion tree validated") fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := utils.NewExplorer(ctx) explorer := utils.NewExplorer(ctx)
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
connectorsTxids := make([]string, 0, len(connectors)) if len(vtxosToSign) > 0 {
for _, connector := range connectors { forfeits := e.GetForfeitTxs()
p, _ := psetv2.NewPsetFromBase64(connector) signedForfeits := make([]string, 0)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
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 { if mustSignRoundTx {
pset, err := psetv2.NewPsetFromBase64(forfeit) ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil { if err != nil {
return "", err return "", err
} }
for _, input := range pset.Inputs { if err := signPset(ctx, ptx, explorer, secKey); err != nil {
inputTxid := chainhash.Hash(input.PreviousTxid).String() 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 := 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)
}
}
} }
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... ") fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{ _, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
SignedForfeitTxs: signedForfeits,
})
if err != nil { if err != nil {
return "", err 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.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())), utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
utils.EXPLORER: explorer, 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 { for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{ inputs = append(inputs, &arkv1.Input{
Txid: coin.txid, Input: &arkv1.Input_VtxoInput{
Vout: coin.vout, VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
}) })
} }
@@ -108,6 +112,7 @@ func collaborativeRedeem(
client, client,
registerResponse.GetId(), registerResponse.GetId(),
selectedCoins, selectedCoins,
false,
secKey, secKey,
receivers, receivers,
) )

View File

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

View File

@@ -6,14 +6,11 @@ import (
"github.com/ark-network/ark/client/utils" "github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/tree" "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/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2" "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/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -50,13 +47,7 @@ func signPset(
return err return err
} }
sighashType := txscript.SigHashAll if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
return err return err
} }
} }
@@ -66,16 +57,6 @@ func signPset(
return err 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() utx, err := pset.UnsignedTx()
if err != nil { if err != nil {
return err return err
@@ -99,34 +80,6 @@ func signPset(
liquidNet := toElementsNetwork(net) liquidNet := toElementsNetwork(net)
for i, input := range pset.Inputs { 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 { if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash) genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil { if err != nil {

View File

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

View File

@@ -2,26 +2,62 @@ package covenantless
import ( import (
"encoding/hex" "encoding/hex"
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils" "github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2" "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) client, cancel, err := getClientFromState(ctx)
if err != nil { if err != nil {
return err return err
} }
defer cancel() defer cancel()
myselfOffchain, _, _, err := getAddress(ctx) offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -34,32 +70,71 @@ func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
pendingVtxos = append(pendingVtxos, vtxo) pendingVtxos = append(pendingVtxos, vtxo)
} }
} }
for _, utxo := range boardingUtxos {
pendingBalance += utxo.Amount
}
if pendingBalance == 0 { if pendingBalance == 0 {
return nil return nil
} }
receiver := receiver{ receiver := receiver{
To: myselfOffchain, To: offchainAddr,
Amount: pendingBalance, 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( return selfTransferAllPendingPayments(
ctx, client, pendingVtxos, receiver, ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
) )
} }
func selfTransferAllPendingPayments( func selfTransferAllPendingPayments(
ctx *cli.Context, client arkv1.ArkServiceClient, ctx *cli.Context,
pendingVtxos []vtxo, myself receiver, client arkv1.ArkServiceClient,
pendingVtxos []vtxo,
boardingUtxos []utils.Utxo,
myself receiver,
desc string,
) error { ) error {
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)) inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
for _, coin := range pendingVtxos { for _, coin := range pendingVtxos {
inputs = append(inputs, &arkv1.Input{ inputs = append(inputs, &arkv1.Input{
Txid: coin.txid, Input: &arkv1.Input_VtxoInput{
Vout: coin.vout, 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{ receiversOutput := []*arkv1.Output{
{ {
Address: myself.To, Address: myself.To,
@@ -99,8 +174,8 @@ func selfTransferAllPendingPayments(
} }
poolTxID, err := handleRoundStream( poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(), ctx, client, registerResponse.GetId(), pendingVtxos,
pendingVtxos, secKey, receiversOutput, ephemeralKey, len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
) )
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import (
) )
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error { func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
receiver := ctx.String("to") receiverAddr := ctx.String("to")
amount := ctx.Uint64("amount") amount := ctx.Uint64("amount")
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect") 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) 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") return fmt.Errorf("receiver address is required")
} }
isOnchain, _, _, err := decodeReceiverAddress(receiver) isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
if err != nil { if err != nil {
return err return err
} }
if isOnchain { 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) offchainAddr, _, _, err := getAddress(ctx)
@@ -41,20 +48,20 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
_, _, aspKey, err := common.DecodeAddress(receiver) _, _, aspKey, err := common.DecodeAddress(receiverAddr)
if err != nil { if err != nil {
return fmt.Errorf("invalid receiver address: %s", err) return fmt.Errorf("invalid receiver address: %s", err)
} }
if !bytes.Equal( if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(), 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) receiversOutput := make([]*arkv1.Output, 0)
sumOfReceivers := uint64(0) sumOfReceivers := uint64(0)
receiversOutput = append(receiversOutput, &arkv1.Output{ receiversOutput = append(receiversOutput, &arkv1.Output{
Address: receiver, Address: receiverAddr,
Amount: amount, Amount: amount,
}) })
sumOfReceivers += amount sumOfReceivers += amount
@@ -88,8 +95,12 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{ inputs = append(inputs, &arkv1.Input{
Txid: coin.txid, Input: &arkv1.Input_VtxoInput{
Vout: coin.vout, 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/client/utils"
"github.com/ark-network/ark/common/bitcointree" "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/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -18,7 +17,7 @@ import (
) )
func signPsbt( 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 { ) error {
updater, err := psbt.NewUpdater(ptx) updater, err := psbt.NewUpdater(ptx)
if err != nil { if err != nil {
@@ -50,27 +49,11 @@ func signPsbt(
return err return err
} }
sighashType := txscript.SigHashAll if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
return err 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) prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs { for i, input := range updater.Upsbt.Inputs {
@@ -85,40 +68,6 @@ func signPsbt(
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher) txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs { 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 { if len(input.TaprootLeafScript) > 0 {
pubkey := prvKey.PubKey() pubkey := prvKey.PubKey()
for _, leaf := range input.TaprootLeafScript { for _, leaf := range input.TaprootLeafScript {
@@ -178,7 +127,6 @@ func signPsbt(
} }
} }
} }
} }
return nil return nil

View File

@@ -21,10 +21,6 @@ var (
Required: false, Required: false,
Hidden: true, Hidden: true,
} }
AmountOnboardFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to onboard in sats",
}
ExpiryDetailsFlag = cli.BoolFlag{ ExpiryDetailsFlag = cli.BoolFlag{
Name: "compute-expiry-details", Name: "compute-expiry-details",
Usage: "compute client-side the VTXOs expiry time", 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 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 replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
require ( require (
@@ -19,9 +21,7 @@ require (
require ( require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 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/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 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/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 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/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.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.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= 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 Receive(ctx *cli.Context) error
Redeem(ctx *cli.Context) error Redeem(ctx *cli.Context) error
Send(ctx *cli.Context) error Send(ctx *cli.Context) error
ClaimAsync(ctx *cli.Context) error Claim(ctx *cli.Context) error
SendAsync(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}, 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{ sendCommand = cli.Command{
Name: "send", Name: "send",
Usage: "Send your onchain or offchain funds to one or many receivers", Usage: "Send your onchain or offchain funds to one or many receivers",
@@ -117,7 +104,7 @@ var (
if err != nil { if err != nil {
return err return err
} }
return cli.ClaimAsync(ctx) return cli.Claim(ctx)
}, },
Flags: []cli.Flag{&flags.PasswordFlag}, Flags: []cli.Flag{&flags.PasswordFlag},
} }
@@ -167,7 +154,6 @@ func main() {
&redeemCommand, &redeemCommand,
&sendCommand, &sendCommand,
&claimCommand, &claimCommand,
&onboardCommand,
) )
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
flags.DatadirFlag, flags.DatadirFlag,

View File

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

View File

@@ -7,8 +7,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@@ -18,6 +20,7 @@ const (
ASP_PUBKEY = "asp_public_key" ASP_PUBKEY = "asp_public_key"
ROUND_LIFETIME = "round_lifetime" ROUND_LIFETIME = "round_lifetime"
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay" UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
BOARDING_TEMPLATE = "boarding_template"
ENCRYPTED_PRVKEY = "encrypted_private_key" ENCRYPTED_PRVKEY = "encrypted_private_key"
PASSWORD_HASH = "password_hash" PASSWORD_HASH = "password_hash"
PUBKEY = "public_key" PUBKEY = "public_key"
@@ -109,6 +112,27 @@ func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
return int64(redeemDelay), nil 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) { func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
state, err := GetState(ctx) state, err := GetState(ctx)
if err != nil { 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( func DecodeAddress(
addr string, 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) prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil { if err != nil {
return 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 ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
@@ -62,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
return closure, nil 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) { func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {

View File

@@ -12,21 +12,18 @@ services:
- ARK_ROUND_LIFETIME=512 - ARK_ROUND_LIFETIME=512
- ARK_TX_BUILDER_TYPE=covenantless - ARK_TX_BUILDER_TYPE=covenantless
- ARK_MIN_RELAY_FEE=200 - ARK_MIN_RELAY_FEE=200
- ARK_NEUTRINO_PEER=bitcoin:18444
- ARK_ESPLORA_URL=http://chopsticks:3000 - 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_TLS=true
- ARK_NO_MACAROONS=true - ARK_NO_MACAROONS=true
- ARK_DATADIR=/app/data
ports: ports:
- "7070:7070" - "7070:7070"
volumes: volumes:
- clarkd:/app/data - type: tmpfs
- clark:/app/wallet-data target: /app/data
volumes:
clarkd:
external: false
clark:
external: false
networks: networks:
default: default:

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,12 +97,13 @@ func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
return nil, err return nil, err
} }
return &client.Info{ return &client.Info{
Pubkey: resp.GetPubkey(), Pubkey: resp.GetPubkey(),
RoundLifetime: resp.GetRoundLifetime(), RoundLifetime: resp.GetRoundLifetime(),
UnilateralExitDelay: resp.GetUnilateralExitDelay(), UnilateralExitDelay: resp.GetUnilateralExitDelay(),
RoundInterval: resp.GetRoundInterval(), RoundInterval: resp.GetRoundInterval(),
Network: resp.GetNetwork(), Network: resp.GetNetwork(),
MinRelayFee: resp.GetMinRelayFee(), MinRelayFee: resp.GetMinRelayFee(),
BoardingDescriptorTemplate: resp.GetBoardingDescriptorTemplate(),
}, nil }, nil
} }
@@ -143,20 +144,8 @@ func (a *grpcClient) GetRound(
}, nil }, 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( func (a *grpcClient) RegisterPayment(
ctx context.Context, inputs []client.VtxoKey, ephemeralPublicKey string, ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
) (string, error) { ) (string, error) {
req := &arkv1.RegisterPaymentRequest{ req := &arkv1.RegisterPaymentRequest{
Inputs: ins(inputs).toProto(), Inputs: ins(inputs).toProto(),
@@ -198,11 +187,16 @@ func (a *grpcClient) Ping(
} }
func (a *grpcClient) FinalizePayment( func (a *grpcClient) FinalizePayment(
ctx context.Context, signedForfeitTxs []string, ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error { ) error {
req := &arkv1.FinalizePaymentRequest{ req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs, SignedForfeitTxs: signedForfeitTxs,
} }
if len(signedRoundTx) > 0 {
req.SignedRoundTx = &signedRoundTx
}
_, err := a.svc.FinalizePayment(ctx, req) _, err := a.svc.FinalizePayment(ctx, req)
return err return err
} }
@@ -210,8 +204,13 @@ func (a *grpcClient) FinalizePayment(
func (a *grpcClient) CreatePayment( func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output, ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
) (string, []string, error) { ) (string, []string, error) {
insCast := make([]client.Input, 0, len(inputs))
for _, in := range inputs {
insCast = append(insCast, in)
}
req := &arkv1.CreatePaymentRequest{ req := &arkv1.CreatePaymentRequest{
Inputs: ins(inputs).toProto(), Inputs: ins(insCast).toProto(),
Outputs: outs(outputs).toProto(), Outputs: outs(outputs).toProto(),
} }
resp, err := a.svc.CreatePayment(ctx, req) resp, err := a.svc.CreatePayment(ctx, req)
@@ -260,6 +259,19 @@ func (a *grpcClient) GetRoundByID(
}, nil }, 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( func (a *grpcClient) SendTreeNonces(
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces, ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
) error { ) error {
@@ -418,8 +430,8 @@ func (v vtxo) toVtxo() client.Vtxo {
} }
return client.Vtxo{ return client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetTxid(), Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
VOut: v.GetOutpoint().GetVout(), VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
}, },
Amount: v.GetReceiver().GetAmount(), Amount: v.GetReceiver().GetAmount(),
RoundTxid: v.GetPoolTxid(), RoundTxid: v.GetPoolTxid(),
@@ -441,21 +453,35 @@ func (v vtxos) toVtxos() []client.Vtxo {
return list 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{ return &arkv1.Input{
Txid: i.Txid, Input: &arkv1.Input_VtxoInput{
Vout: i.VOut, VtxoInput: &arkv1.VtxoInput{
Txid: i.GetTxID(),
Vout: i.GetVOut(),
},
},
} }
} }
type ins []client.VtxoKey type ins []client.Input
func (i ins) toProto() []*arkv1.Input { func (i ins) toProto() []*arkv1.Input {
list := make([]*arkv1.Input, 0, len(i)) list := make([]*arkv1.Input, 0, len(i))
for _, ii := range i { for _, ii := range i {
list = append(list, input(ii).toProto()) list = append(list, toProtoInput(ii))
} }
return list return list
} }
@@ -496,27 +522,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
return levels 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{ return &client.Info{
Pubkey: resp.Payload.Pubkey, Pubkey: resp.Payload.Pubkey,
RoundLifetime: int64(roundLifetime), RoundLifetime: int64(roundLifetime),
UnilateralExitDelay: int64(unilateralExitDelay), UnilateralExitDelay: int64(unilateralExitDelay),
RoundInterval: int64(roundInterval), RoundInterval: int64(roundInterval),
Network: resp.Payload.Network, Network: resp.Payload.Network,
MinRelayFee: int64(minRelayFee), MinRelayFee: int64(minRelayFee),
BoardingDescriptorTemplate: resp.Payload.BoardingDescriptorTemplate,
}, nil }, nil
} }
@@ -159,8 +160,8 @@ func (a *restClient) ListVtxos(
spendableVtxos = append(spendableVtxos, client.Vtxo{ spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid, Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.Vout), VOut: uint32(v.Outpoint.VtxoInput.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.PoolTxid, RoundTxid: v.PoolTxid,
@@ -191,8 +192,8 @@ func (a *restClient) ListVtxos(
spentVtxos = append(spentVtxos, client.Vtxo{ spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{ VtxoKey: client.VtxoKey{
Txid: v.Outpoint.Txid, Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.Vout), VOut: uint32(v.Outpoint.VtxoInput.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.PoolTxid, RoundTxid: v.PoolTxid,
@@ -243,29 +244,31 @@ func (a *restClient) GetRound(
}, nil }, 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( func (a *restClient) RegisterPayment(
ctx context.Context, inputs []client.VtxoKey, ephemeralPublicKey string, ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
) (string, error) { ) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs)) ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs { for _, i := range inputs {
ins = append(ins, &models.V1Input{ var input *models.V1Input
Txid: i.Txid,
Vout: int64(i.VOut), 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{ body := &models.V1RegisterPaymentRequest{
Inputs: ins, Inputs: ins,
@@ -377,13 +380,11 @@ func (a *restClient) Ping(
} }
func (a *restClient) FinalizePayment( func (a *restClient) FinalizePayment(
ctx context.Context, signedForfeitTxs []string, ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error { ) error {
req := &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeitTxs,
}
body := models.V1FinalizePaymentRequest{ body := models.V1FinalizePaymentRequest{
SignedForfeitTxs: req.GetSignedForfeitTxs(), SignedForfeitTxs: signedForfeitTxs,
SignedRoundTx: signedRoundTx,
} }
_, err := a.svc.ArkServiceFinalizePayment( _, err := a.svc.ArkServiceFinalizePayment(
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body), ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
@@ -396,9 +397,15 @@ func (a *restClient) CreatePayment(
) (string, []string, error) { ) (string, []string, error) {
ins := make([]*models.V1Input, 0, len(inputs)) ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range 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{ ins = append(ins, &models.V1Input{
Txid: i.Txid, VtxoInput: &models.V1VtxoInput{
Vout: int64(i.VOut), Txid: i.Txid,
Vout: int64(i.VOut),
},
}) })
} }
outs := make([]*models.V1Output, 0, len(outputs)) outs := make([]*models.V1Output, 0, len(outputs))
@@ -477,6 +484,23 @@ func (a *restClient) GetRoundByID(
}, nil }, 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( func (a *restClient) SendTreeNonces(
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces, ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
) error { ) error {
@@ -604,25 +628,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
return 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) ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentParams, opts ...ClientOption) (*ArkServiceFinalizePaymentOK, error)
ArkServiceGetBoardingAddress(params *ArkServiceGetBoardingAddressParams, opts ...ClientOption) (*ArkServiceGetBoardingAddressOK, error)
ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error) ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error)
ArkServiceGetInfo(params *ArkServiceGetInfoParams, opts ...ClientOption) (*ArkServiceGetInfoOK, error) ArkServiceGetInfo(params *ArkServiceGetInfoParams, opts ...ClientOption) (*ArkServiceGetInfoOK, error)
@@ -72,8 +74,6 @@ type ClientService interface {
ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...ClientOption) (*ArkServiceListVtxosOK, error) ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...ClientOption) (*ArkServiceListVtxosOK, error)
ArkServiceOnboard(params *ArkServiceOnboardParams, opts ...ClientOption) (*ArkServiceOnboardOK, error)
ArkServicePing(params *ArkServicePingParams, opts ...ClientOption) (*ArkServicePingOK, error) ArkServicePing(params *ArkServicePingParams, opts ...ClientOption) (*ArkServicePingOK, error)
ArkServiceRegisterPayment(params *ArkServiceRegisterPaymentParams, opts ...ClientOption) (*ArkServiceRegisterPaymentOK, 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()) 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 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()) 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 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. // Forfeit txs signed by the user.
SignedForfeitTxs []string `json:"signedForfeitTxs"` 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 // 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 // swagger:model v1GetInfoResponse
type V1GetInfoResponse struct { type V1GetInfoResponse struct {
// boarding descriptor template
BoardingDescriptorTemplate string `json:"boardingDescriptorTemplate,omitempty"`
// min relay fee // min relay fee
MinRelayFee string `json:"minRelayFee,omitempty"` MinRelayFee string `json:"minRelayFee,omitempty"`

View File

@@ -8,6 +8,7 @@ package models
import ( import (
"context" "context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag" "github.com/go-openapi/swag"
) )
@@ -17,20 +18,126 @@ import (
// swagger:model v1Input // swagger:model v1Input
type V1Input struct { type V1Input struct {
// txid // boarding input
Txid string `json:"txid,omitempty"` BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
// vout // vtxo input
Vout int64 `json:"vout,omitempty"` VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"`
} }
// Validate validates this v1 input // Validate validates this v1 input
func (m *V1Input) Validate(formats strfmt.Registry) error { 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 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 { 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 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" "time"
"github.com/ark-network/ark/common" "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/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -23,12 +24,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address" "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/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
) )
type liquidReceiver struct { type liquidReceiver struct {
@@ -139,92 +135,24 @@ func LoadCovenantClientWithWallet(
}, nil }, 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( func (a *covenantArkClient) Balance(
ctx context.Context, computeVtxoExpiration bool, ctx context.Context, computeVtxoExpiration bool,
) (*Balance, error) { ) (*Balance, error) {
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
const nbWorkers = 3
wg := &sync.WaitGroup{} 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 { for i := range offchainAddrs {
offchainAddr := offchainAddrs[i] offchainAddr := offchainAddrs[i]
onchainAddr := onchainAddrs[i] boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i] redeemAddr := redeemAddrs[i]
go func(addr string) { go func(addr string) {
defer wg.Done() defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance( balance, amountByExpiration, err := a.getOffchainBalance(
@@ -241,17 +169,7 @@ func (a *covenantArkClient) Balance(
} }
}(offchainAddr) }(offchainAddr)
go func(addr string) { getDelayedBalance := 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) {
defer wg.Done() defer wg.Done()
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance( spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
@@ -267,7 +185,10 @@ func (a *covenantArkClient) Balance(
onchainLockedBalance: lockedBalance, onchainLockedBalance: lockedBalance,
err: err, err: err,
} }
}(redeemAddr) }
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
} }
wg.Wait() wg.Wait()
@@ -317,7 +238,7 @@ func (a *covenantArkClient) Balance(
} }
count++ count++
if count == 3 { if count == nbWorkers {
break 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 { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.VtxoKey{
@@ -538,7 +459,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
} }
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receivers, ctx, paymentID, selectedCoins, false, receivers,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -554,8 +475,85 @@ func (a *covenantArkClient) SendAsync(
return "", fmt.Errorf("not implemented") return "", fmt.Errorf("not implemented")
} }
func (a *covenantArkClient) ClaimAsync(ctx context.Context) (string, error) { func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
return "", fmt.Errorf("not implemented") 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( 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, ctx, targetAmount, nil,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, net); err != nil { if err := a.addInputs(ctx, updater, utxos); err != nil {
return "", err return "", err
} }
@@ -649,14 +647,14 @@ func (a *covenantArkClient) sendOnchain(
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1] updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
} }
// reselect the difference // reselect the difference
selected, delayedSelected, newChange, err := a.coinSelectOnchain( selected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, append(utxos, delayedUtxos...), ctx, feeAmount-change, utxos,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := a.addInputs(ctx, updater, selected, delayedSelected, net); err != nil { if err := a.addInputs(ctx, updater, selected); err != nil {
return "", err return "", err
} }
@@ -787,7 +785,7 @@ func (a *covenantArkClient) sendOffchain(
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
inputs := make([]client.VtxoKey, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid, Txid: coin.Txid,
@@ -811,7 +809,7 @@ func (a *covenantArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID) log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receiversOutput, ctx, paymentID, selectedCoins, false, receiversOutput,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -821,126 +819,60 @@ func (a *covenantArkClient) sendOffchain(
} }
func (a *covenantArkClient) addInputs( 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 { ) 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 { if err != nil {
return err return err
} }
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr) _, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
changeScript, err := address.ToOutputScript(onchainAddr)
if err != nil { if err != nil {
return err return err
} }
for _, utxo := range utxos { for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
return err
}
if err := updater.AddInputs([]psetv2.InputArgs{ if err := updater.AddInputs([]psetv2.InputArgs{
{ {
Txid: utxo.Txid, Txid: utxo.Txid,
TxIndex: utxo.Vout, TxIndex: utxo.Vout,
Sequence: sequence,
}, },
}); err != nil { }); err != nil {
return err return err
} }
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset) _, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
if err != nil { userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
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,
) )
if err != nil { if err != nil {
return err return err
} }
for _, utxo := range delayedUtxos { inputIndex := len(updater.Pset.Inputs) - 1
if err := a.addVtxoInput(
updater,
psetv2.InputArgs{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
uint(a.UnilateralExitDelay),
leafProof,
); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset) if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
if err != nil { return err
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 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( func (a *covenantArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, vtxosToSign []client.Vtxo, receivers []client.Output, paymentID string,
vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
receivers []client.Output,
) (string, error) { ) (string, error) {
eventsCh, err := a.client.GetEventStream(ctx, paymentID) eventsCh, err := a.client.GetEventStream(ctx, paymentID)
if err != nil { if err != nil {
@@ -972,20 +904,20 @@ func (a *covenantArkClient) handleRoundStream(
pingStop() pingStop()
log.Info("a round finalization started") log.Info("a round finalization started")
signedForfeitTxs, err := a.handleRoundFinalization( signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers, ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
) )
if err != nil { if err != nil {
return "", err 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") log.Info("no forfeit txs to sign, waiting for the next round")
continue continue
} }
log.Info("finalizing payment... ") 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 return "", err
} }
@@ -998,15 +930,29 @@ func (a *covenantArkClient) handleRoundStream(
func (a *covenantArkClient) handleRoundFinalization( func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent, ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, receivers []client.Output, vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
) ([]string, error) { ) (signedForfeits []string, signedRoundTx string, err error) {
if err := a.validateCongestionTree(event, receivers); err != nil { if err = a.validateCongestionTree(event, receivers); err != nil {
return nil, fmt.Errorf("failed to verify congestion tree: %s", err) return
} }
return a.loopAndSign( if len(vtxos) > 0 {
ctx, event.ForfeitTxs, vtxos, event.Connectors, 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( func (a *covenantArkClient) validateCongestionTree(
@@ -1197,23 +1143,49 @@ func (a *covenantArkClient) signForfeitTx(
func (a *covenantArkClient) coinSelectOnchain( func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) { ) ([]explorer.Utxo, uint64, error) {
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { 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) fetchedUtxos := make([]explorer.Utxo, 0)
for _, onchainAddr := range onchainAddrs { for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(onchainAddr) utxos, err := a.explorer.GetUtxos(addr)
if err != nil { 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) selectedAmount := uint64(0)
for _, utxo := range fetchedUtxos { for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
@@ -1226,61 +1198,51 @@ func (a *covenantArkClient) coinSelectOnchain(
} }
} }
utxos = append(utxos, utxo) selected = append(selected, utxo)
selectedAmount += utxo.Amount selectedAmount += utxo.Amount
} }
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil return selected, selectedAmount - targetAmount, nil
} }
fetchedUtxos = make([]explorer.Utxo, 0) fetchedUtxos = make([]explorer.Utxo, 0)
for _, offchainAddr := range offchainAddrs { for _, addr := range redemptionAddrs {
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr) utxos, err := a.explorer.GetUtxos(addr)
_, _, _, addr, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, 0, err
} }
utxos, err = a.explorer.GetUtxos(addr) for _, utxo := range utxos {
if err != nil { u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
return nil, nil, 0, err if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u)
}
} }
fetchedUtxos = append(fetchedUtxos, utxos...)
} }
delayedUtxos := make([]explorer.Utxo, 0)
for _, utxo := range fetchedUtxos { for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
break 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 { for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue continue
} }
} }
delayedUtxos = append(delayedUtxos, utxo) selected = append(selected, utxo)
selectedAmount += utxo.Amount selectedAmount += utxo.Amount
} }
if selectedAmount < targetAmount { if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount, "not enough funds to cover amount %d", targetAmount,
) )
} }
return utxos, delayedUtxos, selectedAmount - targetAmount, nil return selected, selectedAmount - targetAmount, nil
} }
func (a *covenantArkClient) getRedeemBranches( func (a *covenantArkClient) getRedeemBranches(
@@ -1373,3 +1335,39 @@ func (a *covenantArkClient) getVtxos(
return vtxos, nil 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"
"github.com/ark-network/ark/common/bitcointree" "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/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "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/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@@ -139,176 +139,24 @@ func LoadCovenantlessClientWithWallet(
}, nil }, 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( func (a *covenantlessArkClient) Balance(
ctx context.Context, computeVtxoExpiration bool, ctx context.Context, computeVtxoExpiration bool,
) (*Balance, error) { ) (*Balance, error) {
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
const nbWorkers = 3
wg := &sync.WaitGroup{} 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 { for i := range offchainAddrs {
offchainAddr := offchainAddrs[i] offchainAddr := offchainAddrs[i]
onchainAddr := onchainAddrs[i] boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i] redeemAddr := redeemAddrs[i]
go func(addr string) { go func(addr string) {
defer wg.Done() defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance( balance, amountByExpiration, err := a.getOffchainBalance(
@@ -325,17 +173,7 @@ func (a *covenantlessArkClient) Balance(
} }
}(offchainAddr) }(offchainAddr)
go func(addr string) { getDelayedBalance := 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) {
defer wg.Done() defer wg.Done()
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance( spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
@@ -351,7 +189,10 @@ func (a *covenantlessArkClient) Balance(
onchainLockedBalance: lockedBalance, onchainLockedBalance: lockedBalance,
err: err, err: err,
} }
}(redeemAddr) }
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
} }
wg.Wait() wg.Wait()
@@ -401,7 +242,7 @@ func (a *covenantlessArkClient) Balance(
} }
count++ count++
if count == 3 { if count == nbWorkers {
break 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 { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.VtxoKey{
@@ -631,7 +472,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
} }
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receivers, roundEphemeralKey, ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -751,9 +592,7 @@ func (a *covenantlessArkClient) SendAsync(
return signedRedeemTx, nil return signedRedeemTx, nil
} }
func (a *covenantlessArkClient) ClaimAsync( func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
ctx context.Context,
) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false) myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil { if err != nil {
return "", err return "", err
@@ -764,10 +603,23 @@ func (a *covenantlessArkClient) ClaimAsync(
return "", err 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 var pendingBalance uint64
for _, vtxo := range pendingVtxos { for _, vtxo := range pendingVtxos {
pendingBalance += vtxo.Amount pendingBalance += vtxo.Amount
} }
for _, vtxo := range boardingUtxos {
pendingBalance += vtxo.Amount
}
if pendingBalance == 0 { if pendingBalance == 0 {
return "", nil return "", nil
} }
@@ -776,7 +628,9 @@ func (a *covenantlessArkClient) ClaimAsync(
Address: myselfOffchain, Address: myselfOffchain,
Amount: pendingBalance, 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( func (a *covenantlessArkClient) sendOnchain(
@@ -822,14 +676,14 @@ func (a *covenantlessArkClient) sendOnchain(
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{}) updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
} }
utxos, delayedUtxos, change, err := a.coinSelectOnchain( utxos, change, err := a.coinSelectOnchain(
ctx, targetAmount, nil, ctx, targetAmount, nil,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil { if err := a.addInputs(ctx, updater, utxos); err != nil {
return "", err return "", err
} }
@@ -869,14 +723,14 @@ func (a *covenantlessArkClient) sendOnchain(
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1] updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
} }
// reselect the difference // reselect the difference
selected, delayedSelected, newChange, err := a.coinSelectOnchain( selected, newChange, err := a.coinSelectOnchain(
ctx, feeAmount-change, append(utxos, delayedUtxos...), ctx, feeAmount-change, utxos,
) )
if err != nil { if err != nil {
return "", err return "", err
} }
if err := a.addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil { if err := a.addInputs(ctx, updater, selected); err != nil {
return "", err return "", err
} }
@@ -995,7 +849,7 @@ func (a *covenantlessArkClient) sendOffchain(
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
inputs := make([]client.VtxoKey, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid, Txid: coin.Txid,
@@ -1024,7 +878,7 @@ func (a *covenantlessArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID) log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, receiversOutput, roundEphemeralKey, ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -1034,16 +888,17 @@ func (a *covenantlessArkClient) sendOffchain(
} }
func (a *covenantlessArkClient) addInputs( func (a *covenantlessArkClient) addInputs(
ctx context.Context, updater *psbt.Updater, ctx context.Context,
utxos, delayedUtxos []explorer.Utxo, net *chaincfg.Params, updater *psbt.Updater,
utxos []explorer.Utxo,
) error { ) 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 { if err != nil {
return err return err
} }
addr, _ := btcutil.DecodeAddress(onchainAddr, net)
changeScript, err := txscript.PayToAddrScript(addr) _, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
if err != nil { if err != nil {
return err return err
} }
@@ -1054,107 +909,41 @@ func (a *covenantlessArkClient) addInputs(
return err return err
} }
sequence, err := utxo.Sequence()
if err != nil {
return err
}
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{ updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{ PreviousOutPoint: wire.OutPoint{
Hash: *previousHash, Hash: *previousHash,
Index: utxo.Vout, Index: utxo.Vout,
}, },
Sequence: sequence,
}) })
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{}) _, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
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),
) )
if err != nil { if err != nil {
return err return err
} }
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net) controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil { if err != nil {
return err return err
} }
script, err := txscript.PayToAddrScript(p2tr) updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
if err != nil { TaprootLeafScript: []*psbt.TaprootTapLeafScript{
return err {
} ControlBlock: controlBlockBytes,
Script: leafProof.Script,
for _, utxo := range delayedUtxos { LeafVersion: leafProof.LeafVersion,
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
return err
}
if err := a.addVtxoInput(
updater,
&wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
}, },
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 return nil
@@ -1164,6 +953,7 @@ func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.Vtxo, vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
receivers []client.Output, receivers []client.Output,
roundEphemeralKey *secp256k1.PrivateKey, roundEphemeralKey *secp256k1.PrivateKey,
) (string, error) { ) (string, error) {
@@ -1218,20 +1008,20 @@ func (a *covenantlessArkClient) handleRoundStream(
pingStop() pingStop()
log.Info("a round finalization started") log.Info("a round finalization started")
signedForfeitTxs, err := a.handleRoundFinalization( signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers, ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
) )
if err != nil { if err != nil {
return "", err 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") log.Info("no forfeit txs to sign, waiting for the next round")
continue continue
} }
log.Info("finalizing payment... ") 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 return "", err
} }
@@ -1311,15 +1101,29 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
func (a *covenantlessArkClient) handleRoundFinalization( func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent, ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, receivers []client.Output, vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
) ([]string, error) { ) (signedForfeits []string, signedRoundTx string, err error) {
if err := a.validateCongestionTree(event, receivers); err != nil { 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( if len(vtxos) > 0 {
ctx, event.ForfeitTxs, vtxos, event.Connectors, 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( func (a *covenantlessArkClient) validateCongestionTree(
@@ -1516,24 +1320,49 @@ func (a *covenantlessArkClient) loopAndSign(
func (a *covenantlessArkClient) coinSelectOnchain( func (a *covenantlessArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) { ) ([]explorer.Utxo, uint64, error) {
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx) offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil { 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) fetchedUtxos := make([]explorer.Utxo, 0)
for _, onchainAddr := range onchainAddrs { for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(onchainAddr) utxos, err := a.explorer.GetUtxos(addr)
if err != nil { 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) selectedAmount := uint64(0)
for _, utxo := range fetchedUtxos { for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
@@ -1546,70 +1375,51 @@ func (a *covenantlessArkClient) coinSelectOnchain(
} }
} }
utxos = append(utxos, utxo) selected = append(selected, utxo)
selectedAmount += utxo.Amount selectedAmount += utxo.Amount
} }
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil return selected, selectedAmount - targetAmount, nil
} }
fetchedUtxos = make([]explorer.Utxo, 0) fetchedUtxos = make([]explorer.Utxo, 0)
for _, offchainAddr := range offchainAddrs { for _, addr := range redemptionAddrs {
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr) utxos, err := a.explorer.GetUtxos(addr)
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, 0, err
}
p2tr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey), &net,
)
if err != nil {
return nil, nil, 0, err
} }
addr := p2tr.EncodeAddress() for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
utxos, err = a.explorer.GetUtxos(addr) if u.SpendableAt.Before(now) {
if err != nil { fetchedUtxos = append(fetchedUtxos, u)
return nil, nil, 0, err }
} }
fetchedUtxos = append(fetchedUtxos, utxos...)
} }
delayedUtxos := make([]explorer.Utxo, 0)
for _, utxo := range fetchedUtxos { for _, utxo := range fetchedUtxos {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
break 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 { for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout { if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue continue
} }
} }
delayedUtxos = append(delayedUtxos, utxo) selected = append(selected, utxo)
selectedAmount += utxo.Amount selectedAmount += utxo.Amount
} }
if selectedAmount < targetAmount { if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf( return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount, "not enough funds to cover amount %d", targetAmount,
) )
} }
return utxos, delayedUtxos, selectedAmount - targetAmount, nil return selected, selectedAmount - targetAmount, nil
} }
func (a *covenantlessArkClient) getRedeemBranches( func (a *covenantlessArkClient) getRedeemBranches(
@@ -1673,6 +1483,53 @@ func (a *covenantlessArkClient) getOffchainBalance(
return balance, amountByExpiration, nil 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( func (a *covenantlessArkClient) getVtxos(
ctx context.Context, addr string, computeVtxoExpiration bool, ctx context.Context, addr string, computeVtxoExpiration bool,
) ([]client.Vtxo, []client.Vtxo, error) { ) ([]client.Vtxo, []client.Vtxo, error) {
@@ -1718,14 +1575,25 @@ func (a *covenantlessArkClient) getVtxos(
} }
func (a *covenantlessArkClient) selfTransferAllPendingPayments( 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) { ) (string, error) {
inputs := make([]client.VtxoKey, 0, len(pendingVtxos)) inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo))
for _, coin := range pendingVtxos { for _, coin := range pendingVtxos {
inputs = append(inputs, coin.VtxoKey) 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} outputs := []client.Output{myself}
roundEphemeralKey, err := secp256k1.GeneratePrivateKey() roundEphemeralKey, err := secp256k1.GeneratePrivateKey()
@@ -1747,7 +1615,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
} }
roundTxid, err := a.handleRoundStream( roundTxid, err := a.handleRoundStream(
ctx, paymentID, pendingVtxos, outputs, roundEphemeralKey, ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -38,31 +38,27 @@ func main() {
defer aliceArkClient.Lock(ctx, password) defer aliceArkClient.Lock(ctx, password)
log.Info("alice is acquiring onchain funds...") log.Info("alice is acquiring onchain funds...")
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx) _, aliceBoardingAddr, err := aliceArkClient.Receive(ctx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if _, err := runCommand("nigiri", "faucet", "--liquid", aliceOnchainAddr); err != nil { if _, err := runCommand("nigiri", "faucet", "--liquid", aliceBoardingAddr); err != nil {
log.Fatal(err) log.Fatal(err)
} }
time.Sleep(5 * time.Second) 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) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if err := generateBlock(); err != nil { log.Infof("onboarding completed in round tx: %s", txid)
log.Fatal(err)
}
time.Sleep(5 * time.Second)
log.Infof("alice onboarded with tx: %s", txid)
aliceBalance, err := aliceArkClient.Balance(ctx, false) aliceBalance, err := aliceArkClient.Balance(ctx, false)
if err != nil { if err != nil {

View File

@@ -38,31 +38,19 @@ func main() {
defer aliceArkClient.Lock(ctx, password) defer aliceArkClient.Lock(ctx, password)
log.Info("alice is acquiring onchain funds...") log.Info("alice is acquiring onchain funds...")
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx) _, boardingAddress, err := aliceArkClient.Receive(ctx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if _, err := runCommand("nigiri", "faucet", aliceOnchainAddr); err != nil { if _, err := runCommand("nigiri", "faucet", boardingAddress); err != nil {
log.Fatal(err) log.Fatal(err)
} }
time.Sleep(5 * time.Second) 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) 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) aliceBalance, err := aliceArkClient.Balance(ctx, false)
if err != nil { if err != nil {
@@ -72,6 +60,14 @@ func main() {
log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount) log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount)
log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total) 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("") fmt.Println("")
log.Info("bob is setting up his ark wallet...") log.Info("bob is setting up his ark wallet...")
bobArkClient, err := setupArkClient() bobArkClient, err := setupArkClient()
@@ -137,7 +133,7 @@ func main() {
fmt.Println("") fmt.Println("")
log.Info("bob is claiming the incoming payment...") log.Info("bob is claiming the incoming payment...")
roundTxid, err := bobArkClient.ClaimAsync(ctx) roundTxid, err := bobArkClient.Claim(ctx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -25,6 +25,35 @@ const (
) )
type Utxo struct { 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"` Txid string `json:"txid"`
Vout uint32 `json:"vout"` Vout uint32 `json:"vout"`
Amount uint64 `json:"value"` Amount uint64 `json:"value"`
@@ -35,10 +64,14 @@ type Utxo struct {
} `json:"status"` } `json:"status"`
} }
func (e ExplorerUtxo) ToUtxo(delay uint) Utxo {
return newUtxo(e, delay)
}
type Explorer interface { type Explorer interface {
GetTxHex(txid string) (string, error) GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error) Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]Utxo, error) GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr string) (uint64, error) GetBalance(addr string) (uint64, error)
GetRedeemedVtxosBalance( GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64, addr string, unilateralExitDelay int64,
@@ -143,7 +176,7 @@ func (e *explorerSvc) Broadcast(txStr string) (string, error) {
return txid, nil 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)) resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -157,7 +190,7 @@ func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body)) return nil, fmt.Errorf(string(body))
} }
payload := []Utxo{} payload := []ExplorerUtxo{}
if err := json.Unmarshal(body, &payload); err != nil { if err := json.Unmarshal(body, &payload); err != nil {
return nil, err return nil, err
} }

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/pkg/client-sdk
go 1.22.6 go 1.22.6
replace github.com/ark-network/ark/common => ../../common
require ( require (
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87 github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87
github.com/ark-network/ark/common 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/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 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/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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 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= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,12 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "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/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "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/store"
"github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/ark-network/ark/pkg/client-sdk/wallet"
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" 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/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
@@ -41,42 +41,42 @@ func NewBitcoinWallet(
func (w *bitcoinWallet) GetAddresses( func (w *bitcoinWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]string, []string, []string, error) { ) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []string{offchainAddr} offchainAddrs := []string{offchainAddr}
onchainAddrs := []string{onchainAddr} boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr} redemptionAddrs := []string{redemptionAddr}
return offchainAddrs, onchainAddrs, redemptionAddrs, nil return offchainAddrs, boardingAddrs, redemptionAddrs, nil
} }
func (w *bitcoinWallet) NewAddress( func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (string, string, error) { ) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return offchainAddr, onchainAddr, nil return offchainAddr, boardingAddr, nil
} }
func (w *bitcoinWallet) NewAddresses( func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int, ctx context.Context, _ bool, num int,
) ([]string, []string, error) { ) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]string, 0, num) offchainAddrs := make([]string, 0, num)
onchainAddrs := make([]string, 0, num) boardingAddrs := make([]string, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr) offchainAddrs = append(offchainAddrs, offchainAddr)
onchainAddrs = append(onchainAddrs, onchainAddr) boardingAddrs = append(boardingAddrs, boardingAddr)
} }
return offchainAddrs, onchainAddrs, nil return offchainAddrs, boardingAddrs, nil
} }
func (s *bitcoinWallet) SignTransaction( func (s *bitcoinWallet) SignTransaction(
@@ -92,11 +92,6 @@ func (s *bitcoinWallet) SignTransaction(
return "", err return "", err
} }
data, err := s.configStore.GetData(ctx)
if err != nil {
return "", err
}
for i, input := range updater.Upsbt.UnsignedTx.TxIn { for i, input := range updater.Upsbt.UnsignedTx.TxIn {
if updater.Upsbt.Inputs[i].WitnessUtxo != nil { if updater.Upsbt.Inputs[i].WitnessUtxo != nil {
continue continue
@@ -122,28 +117,11 @@ func (s *bitcoinWallet) SignTransaction(
return "", err return "", err
} }
sighashType := txscript.SigHashAll if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
return "", err 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) prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs { for i, input := range updater.Upsbt.Inputs {
@@ -158,36 +136,6 @@ func (s *bitcoinWallet) SignTransaction(
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher) txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs { 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 { if len(input.TaprootLeafScript) > 0 {
pubkey := s.walletData.Pubkey pubkey := s.walletData.Pubkey
for _, leaf := range input.TaprootLeafScript { for _, leaf := range input.TaprootLeafScript {
@@ -269,11 +217,6 @@ func (w *bitcoinWallet) getAddress(
netParams := utils.ToBitcoinNetwork(data.Network) 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( vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
) )
@@ -289,5 +232,35 @@ func (w *bitcoinWallet) getAddress(
return "", "", "", err 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 ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"fmt" "fmt"
"strings"
"github.com/ark-network/ark/common" "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/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "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/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store"
"github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/ark-network/ark/pkg/client-sdk/wallet"
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" 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/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -40,42 +41,42 @@ func NewLiquidWallet(
func (w *liquidWallet) GetAddresses( func (w *liquidWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]string, []string, []string, error) { ) ([]string, []string, []string, error) {
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []string{offchainAddr} offchainAddrs := []string{offchainAddr}
onchainAddrs := []string{onchainAddr} boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr} redemptionAddrs := []string{redemptionAddr}
return offchainAddrs, onchainAddrs, redemptionAddrs, nil return offchainAddrs, boardingAddrs, redemptionAddrs, nil
} }
func (w *liquidWallet) NewAddress( func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (string, string, error) { ) (string, string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return offchainAddr, onchainAddr, nil return offchainAddr, boardingAddr, nil
} }
func (w *liquidWallet) NewAddresses( func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int, ctx context.Context, _ bool, num int,
) ([]string, []string, error) { ) ([]string, []string, error) {
offchainAddr, onchainAddr, _, err := w.getAddress(ctx) offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]string, 0, num) offchainAddrs := make([]string, 0, num)
onchainAddrs := make([]string, 0, num) boardingAddrs := make([]string, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr) offchainAddrs = append(offchainAddrs, offchainAddr)
onchainAddrs = append(onchainAddrs, onchainAddr) boardingAddrs = append(boardingAddrs, boardingAddr)
} }
return offchainAddrs, onchainAddrs, nil return offchainAddrs, boardingAddrs, nil
} }
func (s *liquidWallet) SignTransaction( func (s *liquidWallet) SignTransaction(
@@ -114,13 +115,7 @@ func (s *liquidWallet) SignTransaction(
return "", err return "", err
} }
sighashType := txscript.SigHashAll if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
return "", err return "", err
} }
} }
@@ -135,8 +130,6 @@ func (s *liquidWallet) SignTransaction(
return "", err return "", err
} }
liquidNet := utils.ToElementsNetwork(storeData.Network) liquidNet := utils.ToElementsNetwork(storeData.Network)
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
onchainWalletScript := p2wpkh.WitnessScript
utx, err := pset.UnsignedTx() utx, err := pset.UnsignedTx()
if err != nil { if err != nil {
@@ -156,33 +149,6 @@ func (s *liquidWallet) SignTransaction(
serializedPubKey := s.walletData.Pubkey.SerializeCompressed() serializedPubKey := s.walletData.Pubkey.SerializeCompressed()
for i, input := range pset.Inputs { 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 { if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash) genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil { if err != nil {
@@ -276,12 +242,6 @@ func (w *liquidWallet) getAddress(
liquidNet := utils.ToElementsNetwork(data.Network) 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( _, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet, w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
) )
@@ -289,5 +249,27 @@ func (w *liquidWallet) getAddress(
return "", "", "", err 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 IsLocked() bool
GetAddresses( GetAddresses(
ctx context.Context, ctx context.Context,
) (offchainAddresses, onchainAddresses, redemptionAddresses []string, err error) ) (offchainAddresses, boardingAddresses, redemptionAddresses []string, err error)
NewAddress( NewAddress(
ctx context.Context, change bool, ctx context.Context, change bool,
) (offchainAddr, onchainAddr string, err error) ) (offchainAddr, onchainAddr string, err error)
@@ -29,5 +29,5 @@ type WalletService interface {
) (offchainAddresses, onchainAddresses []string, err error) ) (offchainAddresses, onchainAddresses []string, err error)
SignTransaction( SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string, 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() key, _ := btcec.NewPrivateKey()
password := "password" password := "password"
testStoreData := store.StoreData{ testStoreData := store.StoreData{
AspUrl: "localhost:7070", AspUrl: "localhost:7070",
AspPubkey: key.PubKey(), AspPubkey: key.PubKey(),
WalletType: wallet.SingleKeyWallet, WalletType: wallet.SingleKeyWallet,
ClientType: client.GrpcClient, ClientType: client.GrpcClient,
Network: common.LiquidRegTest, Network: common.LiquidRegTest,
RoundLifetime: 512, RoundLifetime: 512,
UnilateralExitDelay: 512, UnilateralExitDelay: 512,
MinRelayFee: 300, MinRelayFee: 300,
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
} }
tests := []struct { tests := []struct {
name string name string

View File

@@ -27,7 +27,6 @@ func init() {
js.Global().Set("lock", LockWrapper()) js.Global().Set("lock", LockWrapper())
js.Global().Set("locked", IsLockedWrapper()) js.Global().Set("locked", IsLockedWrapper())
js.Global().Set("balance", BalanceWrapper()) js.Global().Set("balance", BalanceWrapper())
js.Global().Set("onboard", OnboardWrapper())
js.Global().Set("receive", ReceiveWrapper()) js.Global().Set("receive", ReceiveWrapper())
js.Global().Set("sendOnChain", SendOnChainWrapper()) js.Global().Set("sendOnChain", SendOnChainWrapper())
js.Global().Set("sendOffChain", SendOffChainWrapper()) 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 { func ReceiveWrapper() js.Func {
return JSPromise(func(args []js.Value) (interface{}, error) { return JSPromise(func(args []js.Value) (interface{}, error) {
if arkSdkClient == nil { if arkSdkClient == nil {
return nil, errors.New("ARK SDK client is not initialized") 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 { if err != nil {
return nil, err return nil, err
} }
result := map[string]interface{}{ result := map[string]interface{}{
"offchainAddr": offchainAddr, "offchainAddr": offchainAddr,
"onchainAddr": onchainAddr, "boardingAddr": boardingAddr,
} }
return js.ValueOf(result), nil return js.ValueOf(result), nil
}) })

View File

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

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/server
go 1.22.6 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 replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
require ( 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/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 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/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 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/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= 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 MinRelayFee uint64
RoundLifetime int64 RoundLifetime int64
UnilateralExitDelay int64 UnilateralExitDelay int64
BoardingExitDelay int64
EsploraURL string EsploraURL string
NeutrinoPeer 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 { if c.RoundLifetime%minAllowedSequence != 0 {
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
log.Infof( 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 { if err := c.repoManager(); err != nil {
return err return err
} }
@@ -275,11 +290,11 @@ func (c *Config) txBuilderService() error {
switch c.TxBuilderType { switch c.TxBuilderType {
case "covenant": case "covenant":
svc = txbuilder.NewTxBuilder( svc = txbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
) )
case "covenantless": case "covenantless":
svc = cltxbuilder.NewTxBuilder( svc = cltxbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
) )
default: default:
err = fmt.Errorf("unknown tx builder type") err = fmt.Errorf("unknown tx builder type")
@@ -323,7 +338,7 @@ func (c *Config) schedulerService() error {
func (c *Config) appService() error { func (c *Config) appService() error {
if common.IsLiquid(c.Network) { if common.IsLiquid(c.Network) {
svc, err := application.NewCovenantService( 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, c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
) )
if err != nil { if err != nil {
@@ -335,7 +350,7 @@ func (c *Config) appService() error {
} }
svc, err := application.NewCovenantlessService( 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, c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
) )
if err != nil { if err != nil {

View File

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

View File

@@ -10,14 +10,18 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "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/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "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/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
) )
var ( var (
@@ -30,6 +34,7 @@ type covenantService struct {
roundLifetime int64 roundLifetime int64
roundInterval int64 roundInterval int64
unilateralExitDelay int64 unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64 minRelayFee uint64
wallet ports.WalletService wallet ports.WalletService
@@ -41,23 +46,22 @@ type covenantService struct {
paymentRequests *paymentsMap paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
lastEvent domain.RoundEvent currentRoundLock sync.Mutex
currentRound *domain.Round currentRound *domain.Round
lastEvent domain.RoundEvent
} }
func NewCovenantService( func NewCovenantService(
network common.Network, network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64, roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager, walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner, builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService, scheduler ports.SchedulerService,
) (Service, error) { ) (Service, error) {
eventsCh := make(chan domain.RoundEvent) eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding) paymentRequests := newPaymentsMap()
paymentRequests := newPaymentsMap(nil)
forfeitTxs := newForfeitTxsMap(builder) forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background()) pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -69,9 +73,9 @@ func NewCovenantService(
svc := &covenantService{ svc := &covenantService{
network, pubkey, network, pubkey,
roundLifetime, roundInterval, unilateralExitDelay, minRelayFee, roundLifetime, roundInterval, unilateralExitDelay, boardingExitDelay, minRelayFee,
walletSvc, repoManager, builder, scanner, sweeper, walletSvc, repoManager, builder, scanner, sweeper,
paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, nil, paymentRequests, forfeitTxs, eventsCh, sync.Mutex{}, nil, nil,
} }
repoManager.RegisterEventsHandler( repoManager.RegisterEventsHandler(
func(round *domain.Round) { func(round *domain.Round) {
@@ -88,7 +92,6 @@ func NewCovenantService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err) return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
} }
go svc.listenToScannerNotifications() go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil return svc, nil
} }
@@ -116,30 +119,164 @@ func (s *covenantService) Stop() {
s.repoManager.Close() s.repoManager.Close()
log.Debug("closed connection to db") log.Debug("closed connection to db")
close(s.eventsCh) close(s.eventsCh)
close(s.onboardingCh)
} }
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) { func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs) addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil { if err != nil {
return "", err return "", err
} }
for _, v := range vtxos { return addr, nil
if v.Spent { }
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
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) payment, err := domain.NewPayment(vtxos)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment); err != nil { if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err return "", err
} }
return payment.Id, nil 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 { func (s *covenantService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials // Check credentials
payment, ok := s.paymentRequests.view(creds) 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) 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) { func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed()) pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk) return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -209,50 +359,17 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee), 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 }, 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 { func (s *covenantService) RegisterCosignerPubkey(ctx context.Context, paymentId string, _ string) error {
// if the user sends an ephemeral pubkey, something is going wrong client-side // if the user sends an ephemeral pubkey, something is going wrong client-side
// we should delete the associated payment // we should delete the associated payment
@@ -329,7 +446,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
num = paymentsThreshold num = paymentsThreshold
} }
payments, _ := s.paymentRequests.pop(num) payments, boardingInputs, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil { if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err)) round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments") log.WithError(err).Warn("failed to register payments")
@@ -343,7 +460,7 @@ func (s *covenantService) startFinalization() {
return 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 { if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx") 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) log.Debugf("pool tx created for round %s", round.Id)
// TODO BTC make the senders sign the tree needForfeits := false
for _, pay := range payments {
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee) if len(pay.Inputs) > 0 {
if err != nil { needForfeits = true
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) break
log.WithError(err).Warn("failed to create connectors and forfeit txs") }
return
} }
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( if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx, connectorAddress, connectors, tree, unsignedPoolTx,
@@ -401,71 +528,63 @@ func (s *covenantService) finalizeRound() {
} }
log.Debugf("signing round transaction %s\n", round.Id) 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 { if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err)) changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx") log.WithError(err).Warn("failed to sign round tx")
return return
} }
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx) txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil { if err != nil {
log.Debugf("failed to broadcast round tx: %s", signedRoundTx)
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err)) changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx") log.WithError(err).Warn("failed to broadcast pool tx")
return return
} }
changes, _ = round.EndFinalization(forfeitTxs, txid) changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid) changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
} log.WithError(err).Warn("failed to finalize round")
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")
return return
} }
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
} }
func (s *covenantService) listenToScannerNotifications() { func (s *covenantService) listenToScannerNotifications() {
@@ -867,23 +986,6 @@ func (s *covenantService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) 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( func findForfeitTxLiquid(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string, forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) { ) (string, error) {

View File

@@ -11,12 +11,15 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "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/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "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/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -27,6 +30,7 @@ type covenantlessService struct {
roundLifetime int64 roundLifetime int64
roundInterval int64 roundInterval int64
unilateralExitDelay int64 unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64 minRelayFee uint64
wallet ports.WalletService wallet ports.WalletService
@@ -38,11 +42,11 @@ type covenantlessService struct {
paymentRequests *paymentsMap paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
// cached data for the current round // cached data for the current round
lastEvent domain.RoundEvent lastEvent domain.RoundEvent
currentRoundLock sync.Mutex
currentRound *domain.Round currentRound *domain.Round
treeSigningSessions map[string]*musigSigningSession treeSigningSessions map[string]*musigSigningSession
asyncPaymentsCache map[domain.VtxoKey]struct { asyncPaymentsCache map[domain.VtxoKey]struct {
@@ -53,14 +57,13 @@ type covenantlessService struct {
func NewCovenantlessService( func NewCovenantlessService(
network common.Network, network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64, roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager, walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner, builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService, scheduler ports.SchedulerService,
) (Service, error) { ) (Service, error) {
eventsCh := make(chan domain.RoundEvent) eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding) paymentRequests := newPaymentsMap()
paymentRequests := newPaymentsMap(nil)
forfeitTxs := newForfeitTxsMap(builder) forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background()) pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -89,9 +92,10 @@ func NewCovenantlessService(
paymentRequests: paymentRequests, paymentRequests: paymentRequests,
forfeitTxs: forfeitTxs, forfeitTxs: forfeitTxs,
eventsCh: eventsCh, eventsCh: eventsCh,
onboardingCh: onboardingCh, currentRoundLock: sync.Mutex{},
asyncPaymentsCache: asyncPaymentsCache, asyncPaymentsCache: asyncPaymentsCache,
treeSigningSessions: make(map[string]*musigSigningSession), treeSigningSessions: make(map[string]*musigSigningSession),
boardingExitDelay: boardingExitDelay,
} }
repoManager.RegisterEventsHandler( repoManager.RegisterEventsHandler(
@@ -109,7 +113,6 @@ func NewCovenantlessService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err) return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
} }
go svc.listenToScannerNotifications() go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil return svc, nil
} }
@@ -137,7 +140,6 @@ func (s *covenantlessService) Stop() {
s.repoManager.Close() s.repoManager.Close()
log.Debug("closed connection to db") log.Debug("closed connection to db")
close(s.eventsCh) close(s.eventsCh)
close(s.onboardingCh)
} }
func (s *covenantlessService) CompleteAsyncPayment( func (s *covenantlessService) CompleteAsyncPayment(
@@ -242,27 +244,147 @@ func (s *covenantlessService) CreateAsyncPayment(
return res.RedeemTx, res.UnconditionalForfeitTxs, nil return res.RedeemTx, res.UnconditionalForfeitTxs, nil
} }
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) { func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs) vtxosInputs := make([]domain.VtxoKey, 0)
if err != nil { boardingInputs := make([]Input, 0)
return "", err
} for _, input := range inputs {
for _, v := range vtxos { if input.IsVtxo() {
if v.Spent { vtxosInputs = append(vtxosInputs, input.VtxoKey())
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut) 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) payment, err := domain.NewPayment(vtxos)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment); err != nil { if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err return "", err
} }
return payment.Id, nil 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 { func (s *covenantlessService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials // Check credentials
payment, ok := s.paymentRequests.view(creds) payment, ok := s.paymentRequests.view(creds)
@@ -293,6 +415,19 @@ func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string
return s.forfeitTxs.sign(forfeitTxs) 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) { func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed()) pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk) return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -324,50 +459,26 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee), 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 }, nil
} }
// TODO clArk changes the onboard flow (2 rounds ?) func (s *covenantlessService) GetBoardingAddress(
func (s *covenantlessService) Onboard( ctx context.Context, userPubkey *secp256k1.PublicKey,
ctx context.Context, boardingTx string, ) (string, error) {
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey, addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
) error {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(boardingTx), true)
if err != nil { 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( return addr, nil
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
} }
func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error { func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error {
@@ -504,7 +615,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
num = paymentsThreshold num = paymentsThreshold
} }
payments, cosigners := s.paymentRequests.pop(num) payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) { if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments") err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err)) round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -534,7 +645,7 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey()) 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 { if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx") log.WithError(err).Warn("failed to create pool tx")
@@ -683,14 +794,25 @@ func (s *covenantlessService) startFinalization() {
tree = signedTree tree = signedTree
} }
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee) needForfeits := false
if err != nil { for _, pay := range payments {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) if len(pay.Inputs) > 0 {
log.WithError(err).Warn("failed to create connectors and forfeit txs") needForfeits = true
return 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( if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx, connectorAddress, connectors, tree, unsignedPoolTx,
@@ -763,73 +885,61 @@ func (s *covenantlessService) finalizeRound() {
} }
log.Debugf("signing round transaction %s\n", round.Id) 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 { if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err)) changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx") log.WithError(err).Warn("failed to sign round tx")
return return
} }
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx) txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil { if err != nil {
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err)) changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx") log.WithError(err).Warn("failed to broadcast pool tx")
return return
} }
changes, _ = round.EndFinalization(forfeitTxs, txid) changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid) changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
} log.WithError(err).Warn("failed to finalize round")
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")
return return
} }
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
} }
func (s *covenantlessService) listenToScannerNotifications() { func (s *covenantlessService) listenToScannerNotifications() {
@@ -1232,24 +1342,6 @@ func (s *covenantlessService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) 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( func findForfeitTxBitcoin(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string, forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) { ) (string, error) {

View File

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

View File

@@ -10,14 +10,16 @@ import (
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type timedPayment struct { type timedPayment struct {
domain.Payment domain.Payment
timestamp time.Time boardingInputs []ports.BoardingInput
pingTimestamp time.Time timestamp time.Time
pingTimestamp time.Time
} }
type paymentsMap struct { type paymentsMap struct {
@@ -26,11 +28,8 @@ type paymentsMap struct {
ephemeralKeys map[string]*secp256k1.PublicKey ephemeralKeys map[string]*secp256k1.PublicKey
} }
func newPaymentsMap(payments []domain.Payment) *paymentsMap { func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment) paymentsById := make(map[string]*timedPayment)
for _, p := range payments {
paymentsById[p.Id] = &timedPayment{p, time.Now(), time.Time{}}
}
lock := &sync.RWMutex{} lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)} return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
} }
@@ -60,7 +59,7 @@ func (m *paymentsMap) delete(id string) error {
return nil return nil
} }
func (m *paymentsMap) push(payment domain.Payment) error { func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@@ -68,7 +67,7 @@ func (m *paymentsMap) push(payment domain.Payment) error {
return fmt.Errorf("duplicated inputs") 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 return nil
} }
@@ -84,7 +83,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil 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() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@@ -109,8 +108,10 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
} }
payments := make([]domain.Payment, 0, num) payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num) cosigners := make([]*secp256k1.PublicKey, 0, num)
for _, p := range paymentsByTime[:num] { for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment) payments = append(payments, p.Payment)
if pubkey, ok := m.ephemeralKeys[p.Payment.Id]; ok { if pubkey, ok := m.ephemeralKeys[p.Payment.Id]; ok {
cosigners = append(cosigners, pubkey) cosigners = append(cosigners, pubkey)
@@ -118,7 +119,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
} }
delete(m.payments, p.Id) delete(m.payments, p.Id)
} }
return payments, cosigners return payments, boardingInputs, cosigners
} }
func (m *paymentsMap) update(payment domain.Payment) error { func (m *paymentsMap) update(payment domain.Payment) error {
@@ -312,3 +313,26 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
} }
return vtxos 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 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) { func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
if p.Receivers == nil { if p.Receivers == nil {
p.Receivers = make([]Receiver, 0) p.Receivers = make([]Receiver, 0)
@@ -70,18 +62,13 @@ func (p Payment) validate(ignoreOuts bool) error {
if len(p.Id) <= 0 { if len(p.Id) <= 0 {
return fmt.Errorf("missing id") return fmt.Errorf("missing id")
} }
if len(p.Inputs) <= 0 {
return fmt.Errorf("missing inputs")
}
if ignoreOuts { if ignoreOuts {
return nil return nil
} }
if len(p.Receivers) <= 0 { if len(p.Receivers) <= 0 {
return fmt.Errorf("missing outputs") 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 { for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 { if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
return fmt.Errorf("missing receiver destination") return fmt.Errorf("missing receiver destination")
@@ -89,10 +76,6 @@ func (p Payment) validate(ignoreOuts bool) error {
if r.Amount < dustAmount { if r.Amount < dustAmount {
return fmt.Errorf("receiver amount must be greater than dust") 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 return nil
} }

View File

@@ -22,7 +22,7 @@ var inputs = []domain.Vtxo{
func TestPayment(t *testing.T) { func TestPayment(t *testing.T) {
t.Run("new_payment", func(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) payment, err := domain.NewPayment(inputs)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, payment) require.NotNil(t, payment)
@@ -30,24 +30,6 @@ func TestPayment(t *testing.T) {
require.Exactly(t, inputs, payment.Inputs) require.Exactly(t, inputs, payment.Inputs)
require.Empty(t, payment.Receivers) 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) { 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", 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) 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 { func NewRoundFromEvents(events []RoundEvent) *Round {
r := &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) { func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent, error) {
if len(forfeitTxs) <= 0 { 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 { if len(txid) <= 0 {
return nil, fmt.Errorf("missing pool txid") return nil, fmt.Errorf("missing pool txid")
@@ -216,6 +187,10 @@ func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent,
if r.Stage.Ended { if r.Stage.Ended {
return nil, fmt.Errorf("round already finalized") return nil, fmt.Errorf("round already finalized")
} }
if forfeitTxs == nil {
forfeitTxs = make([]string, 0)
}
event := RoundFinalized{ event := RoundFinalized{
Id: r.Id, Id: r.Id,
Txid: txid, Txid: txid,

View File

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

View File

@@ -16,9 +16,16 @@ type SweepInput interface {
GetInternalKey() *secp256k1.PublicKey GetInternalKey() *secp256k1.PublicKey
} }
type BoardingInput interface {
GetAmount() uint64
GetIndex() uint32
GetHash() chainhash.Hash
GetBoardingPubkey() *secp256k1.PublicKey
}
type TxBuilder interface { type TxBuilder interface {
BuildPoolTx( 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, cosigners ...*secp256k1.PublicKey,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) ) (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) 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, vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64,
) (*domain.AsyncPaymentTxs, error) ) (*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) MainAccountBalance(ctx context.Context) (uint64, uint64, error)
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
GetTransaction(ctx context.Context, txid string) (string, error)
Close() Close()
} }

View File

@@ -95,7 +95,7 @@ func deserializeEvent(buf []byte) (domain.RoundEvent, error) {
} }
{ {
var event = domain.RoundFinalizationStarted{} 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 return event, nil
} }
} }

View File

@@ -11,12 +11,15 @@ import (
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
) )
const ( const (
@@ -25,10 +28,11 @@ const (
) )
type txBuilder struct { type txBuilder struct {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
roundLifetime int64 // in seconds roundLifetime int64 // in seconds
exitDelay int64 // in seconds exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds
} }
func NewTxBuilder( func NewTxBuilder(
@@ -36,8 +40,18 @@ func NewTxBuilder(
net common.Network, net common.Network,
roundLifetime int64, roundLifetime int64,
exitDelay int64, exitDelay int64,
boardingExitDelay int64,
) ports.TxBuilder { ) 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) { func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
@@ -112,7 +126,11 @@ func (b *txBuilder) BuildForfeitTxs(
} }
func (b *txBuilder) BuildPoolTx( 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 _ ...*secp256k1.PublicKey, // cosigners are not used in the covenant
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) { ) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
// The creation of the tree and the pool tx are tightly coupled: // The creation of the tree and the pool tx are tightly coupled:
@@ -146,7 +164,7 @@ func (b *txBuilder) BuildPoolTx(
} }
ptx, err := b.createPoolTx( ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds, sharedOutputAmount, sharedOutputScript, payments, boardingInputs, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
) )
if err != nil { if err != nil {
return return
@@ -197,9 +215,19 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
expirationTime := parentblocktime + lifetime expirationTime := parentblocktime + lifetime
amount := uint64(0) txhex, err := b.wallet.GetTransaction(context.Background(), txid)
for _, out := range pset.Outputs { if err != nil {
amount += out.Value 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{ sweepInput = &sweepLiquidInput{
@@ -208,7 +236,7 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
TxIndex: index, TxIndex: index,
}, },
sweepLeaf: sweepLeaf, sweepLeaf: sweepLeaf,
amount: amount, amount: inputValue,
} }
return expirationTime, sweepInput, nil return expirationTime, sweepInput, nil
@@ -362,8 +390,11 @@ func (b *txBuilder) getLeafScriptAndTree(
} }
func (b *txBuilder) createPoolTx( func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputScript []byte, sharedOutputAmount uint64,
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64, sharedOutputScript []byte,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round, sweptRounds []domain.Round,
) (*psetv2.Pset, error) { ) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork()) aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
@@ -396,11 +427,13 @@ func (b *txBuilder) createPoolTx(
}) })
} }
outputs = append(outputs, psetv2.OutputArgs{ if connectorsAmount > 0 {
Asset: b.onchainNetwork().AssetID, outputs = append(outputs, psetv2.OutputArgs{
Amount: connectorsAmount, Asset: b.onchainNetwork().AssetID,
Script: connectorScript, Amount: connectorsAmount,
}) Script: connectorScript,
})
}
for _, receiver := range receivers { for _, receiver := range receivers {
targetAmount += receiver.Amount targetAmount += receiver.Amount
@@ -417,6 +450,9 @@ func (b *txBuilder) createPoolTx(
}) })
} }
for _, in := range boardingInputs {
targetAmount -= in.GetAmount()
}
ctx := context.Background() ctx := context.Background()
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount) utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
if err != nil { if err != nil {
@@ -447,6 +483,48 @@ func (b *txBuilder) createPoolTx(
return nil, err 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 { if err := addInputs(updater, utxos); err != nil {
return nil, err return nil, err
} }
@@ -471,14 +549,23 @@ func (b *txBuilder) createPoolTx(
if feeAmount == change { if feeAmount == change {
// fees = change, remove change output // fees = change, remove change output
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
feeAmount += change
} else if feeAmount < change { } else if feeAmount < change {
// change covers the fees, reduce change amount // 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 { } else {
// change is not enough to cover fees, re-select utxos // change is not enough to cover fees, re-select utxos
if change > 0 { if change > 0 {
// remove change output if present // remove change output if present
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
} }
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change) newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
if err != nil { if err != nil {
@@ -486,14 +573,18 @@ func (b *txBuilder) createPoolTx(
} }
if change > 0 { if change > 0 {
if err := updater.AddOutputs([]psetv2.OutputArgs{ if change < dustLimit {
{ feeAmount += change
Asset: b.onchainNetwork().AssetID, } else {
Amount: change, if err := updater.AddOutputs([]psetv2.OutputArgs{
Script: aspScript, {
}, Asset: b.onchainNetwork().AssetID,
}); err != nil { Amount: change,
return nil, err Script: aspScript,
},
}); err != nil {
return nil, err
}
} }
} }
@@ -541,6 +632,77 @@ func (b *txBuilder) createPoolTx(
return ptx, nil 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( func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, connectorAddress string, minRelayFee uint64, poolTx string, payments []domain.Payment, connectorAddress string, minRelayFee uint64,
) ([]*psetv2.Pset, error) { ) ([]*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) { func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
for _, leaf := range input.TapLeafScript { for _, leaf := range input.TapLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}

View File

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

View File

@@ -229,6 +229,16 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
return res, res2, args.Error(2) 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 { type mockedInput struct {
mock.Mock mock.Mock
} }

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