mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
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:
@@ -16,6 +16,38 @@
|
||||
"application/json"
|
||||
],
|
||||
"paths": {
|
||||
"/v1/boarding": {
|
||||
"post": {
|
||||
"operationId": "ArkService_GetBoardingAddress",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1GetBoardingAddressResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1GetBoardingAddressRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"ArkService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/events": {
|
||||
"get": {
|
||||
"operationId": "ArkService_GetEventStream",
|
||||
@@ -69,38 +101,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/onboard": {
|
||||
"post": {
|
||||
"operationId": "ArkService_Onboard",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1OnboardResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1OnboardRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"ArkService"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/payment": {
|
||||
"post": {
|
||||
"operationId": "ArkService_CreatePayment",
|
||||
@@ -475,6 +475,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1BoardingInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1ClaimPaymentRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -555,12 +570,35 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Forfeit txs signed by the user."
|
||||
},
|
||||
"signedRoundTx": {
|
||||
"type": "string",
|
||||
"description": "If payment has boarding input, the user must sign the associated inputs."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1FinalizePaymentResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1GetBoardingAddressRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pubkey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1GetBoardingAddressResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1GetEventStreamResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -605,6 +643,9 @@
|
||||
"minRelayFee": {
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
"boardingDescriptorTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -627,12 +668,11 @@
|
||||
"v1Input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
"vtxoInput": {
|
||||
"$ref": "#/definitions/v1VtxoInput"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"boardingInput": {
|
||||
"$ref": "#/definitions/v1BoardingInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -669,23 +709,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1OnboardRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"boardingTx": {
|
||||
"type": "string"
|
||||
},
|
||||
"congestionTree": {
|
||||
"$ref": "#/definitions/v1Tree"
|
||||
},
|
||||
"userPubkey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1OnboardResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1Output": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -972,6 +995,18 @@
|
||||
"$ref": "#/definitions/v1PendingPayment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1VtxoInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ service ArkService {
|
||||
get: "/v1/info"
|
||||
};
|
||||
}
|
||||
rpc Onboard(OnboardRequest) returns (OnboardResponse) {
|
||||
rpc GetBoardingAddress(GetBoardingAddressRequest) returns (GetBoardingAddressResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/onboard"
|
||||
post: "/v1/boarding"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
};
|
||||
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/payment"
|
||||
@@ -100,6 +100,15 @@ message CompletePaymentRequest {
|
||||
}
|
||||
message CompletePaymentResponse {}
|
||||
|
||||
message GetBoardingAddressRequest {
|
||||
string pubkey = 1;
|
||||
}
|
||||
|
||||
message GetBoardingAddressResponse {
|
||||
string address = 1;
|
||||
string descriptor = 2;
|
||||
}
|
||||
|
||||
message RegisterPaymentRequest {
|
||||
repeated Input inputs = 1;
|
||||
optional string ephemeral_pubkey = 2;
|
||||
@@ -120,6 +129,8 @@ message ClaimPaymentResponse {}
|
||||
message FinalizePaymentRequest {
|
||||
// Forfeit txs signed by the user.
|
||||
repeated string signed_forfeit_txs = 1;
|
||||
// If payment has boarding input, the user must sign the associated inputs.
|
||||
optional string signed_round_tx = 2;
|
||||
}
|
||||
message FinalizePaymentResponse {}
|
||||
|
||||
@@ -177,14 +188,7 @@ message GetInfoResponse {
|
||||
int64 round_interval = 4;
|
||||
string network = 5;
|
||||
int64 min_relay_fee = 6;
|
||||
}
|
||||
|
||||
message OnboardRequest {
|
||||
string boarding_tx = 1;
|
||||
Tree congestion_tree = 2;
|
||||
string user_pubkey = 3;
|
||||
}
|
||||
message OnboardResponse {
|
||||
string boarding_descriptor_template = 7;
|
||||
}
|
||||
|
||||
// EVENT TYPES
|
||||
@@ -239,11 +243,24 @@ message Round {
|
||||
RoundStage stage = 8;
|
||||
}
|
||||
|
||||
message Input {
|
||||
message VtxoInput {
|
||||
string txid = 1;
|
||||
uint32 vout = 2;
|
||||
}
|
||||
|
||||
message BoardingInput {
|
||||
string txid = 1;
|
||||
uint32 vout = 2;
|
||||
string descriptor = 3;
|
||||
}
|
||||
|
||||
message Input {
|
||||
oneof input {
|
||||
VtxoInput vtxo_input = 1;
|
||||
BoardingInput boarding_input = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Output {
|
||||
// Either the offchain or onchain address.
|
||||
string address = 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -404,28 +404,28 @@ func local_request_ArkService_GetInfo_0(ctx context.Context, marshaler runtime.M
|
||||
|
||||
}
|
||||
|
||||
func request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq OnboardRequest
|
||||
func request_ArkService_GetBoardingAddress_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetBoardingAddressRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.Onboard(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
msg, err := client.GetBoardingAddress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq OnboardRequest
|
||||
func local_request_ArkService_GetBoardingAddress_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetBoardingAddressRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.Onboard(ctx, &protoReq)
|
||||
msg, err := server.GetBoardingAddress(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
@@ -745,7 +745,7 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle("POST", pattern_ArkService_GetBoardingAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
@@ -753,12 +753,12 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/GetBoardingAddress", runtime.WithHTTPPathPattern("/v1/boarding"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
resp, md, err := local_request_ArkService_GetBoardingAddress_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
@@ -766,7 +766,7 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
forward_ArkService_GetBoardingAddress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
@@ -1103,25 +1103,25 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
mux.Handle("POST", pattern_ArkService_GetBoardingAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/GetBoardingAddress", runtime.WithHTTPPathPattern("/v1/boarding"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
resp, md, err := request_ArkService_GetBoardingAddress_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
forward_ArkService_GetBoardingAddress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
@@ -1195,7 +1195,7 @@ var (
|
||||
|
||||
pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, ""))
|
||||
|
||||
pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, ""))
|
||||
pattern_ArkService_GetBoardingAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "boarding"}, ""))
|
||||
|
||||
pattern_ArkService_CreatePayment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "payment"}, ""))
|
||||
|
||||
@@ -1225,7 +1225,7 @@ var (
|
||||
|
||||
forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage
|
||||
forward_ArkService_GetBoardingAddress_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ArkService_CreatePayment_0 = runtime.ForwardResponseMessage
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ type ArkServiceClient interface {
|
||||
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
|
||||
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
|
||||
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
|
||||
Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error)
|
||||
GetBoardingAddress(ctx context.Context, in *GetBoardingAddressRequest, opts ...grpc.CallOption) (*GetBoardingAddressResponse, error)
|
||||
CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error)
|
||||
CompletePayment(ctx context.Context, in *CompletePaymentRequest, opts ...grpc.CallOption) (*CompletePaymentResponse, error)
|
||||
}
|
||||
@@ -164,9 +164,9 @@ func (c *arkServiceClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) {
|
||||
out := new(OnboardResponse)
|
||||
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Onboard", in, out, opts...)
|
||||
func (c *arkServiceClient) GetBoardingAddress(ctx context.Context, in *GetBoardingAddressRequest, opts ...grpc.CallOption) (*GetBoardingAddressResponse, error) {
|
||||
out := new(GetBoardingAddressResponse)
|
||||
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/GetBoardingAddress", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,7 +206,7 @@ type ArkServiceServer interface {
|
||||
Ping(context.Context, *PingRequest) (*PingResponse, error)
|
||||
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
|
||||
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
|
||||
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
|
||||
GetBoardingAddress(context.Context, *GetBoardingAddressRequest) (*GetBoardingAddressResponse, error)
|
||||
CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error)
|
||||
CompletePayment(context.Context, *CompletePaymentRequest) (*CompletePaymentResponse, error)
|
||||
}
|
||||
@@ -248,8 +248,8 @@ func (UnimplementedArkServiceServer) ListVtxos(context.Context, *ListVtxosReques
|
||||
func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented")
|
||||
func (UnimplementedArkServiceServer) GetBoardingAddress(context.Context, *GetBoardingAddressRequest) (*GetBoardingAddressResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBoardingAddress not implemented")
|
||||
}
|
||||
func (UnimplementedArkServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreatePayment not implemented")
|
||||
@@ -470,20 +470,20 @@ func _ArkService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(OnboardRequest)
|
||||
func _ArkService_GetBoardingAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetBoardingAddressRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ArkServiceServer).Onboard(ctx, in)
|
||||
return srv.(ArkServiceServer).GetBoardingAddress(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/ark.v1.ArkService/Onboard",
|
||||
FullMethod: "/ark.v1.ArkService/GetBoardingAddress",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ArkServiceServer).Onboard(ctx, req.(*OnboardRequest))
|
||||
return srv.(ArkServiceServer).GetBoardingAddress(ctx, req.(*GetBoardingAddressRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
@@ -572,8 +572,8 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _ArkService_GetInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Onboard",
|
||||
Handler: _ArkService_Onboard_Handler,
|
||||
MethodName: "GetBoardingAddress",
|
||||
Handler: _ArkService_GetBoardingAddress_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreatePayment",
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -21,18 +22,30 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
network, err := utils.GetNetwork(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to check for error here becuase this function is called also by getAddress().
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
@@ -54,19 +67,19 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, err := explorer.GetBalance(onchainAddr, toElementsNetwork(network).AssetID)
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, balance, nil, nil, nil}
|
||||
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr, unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
148
client/covenant/claim.go
Normal file
148
client/covenant/claim.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -9,16 +9,13 @@ import (
|
||||
"github.com/ark-network/ark/client/interfaces"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
const dust = 450
|
||||
@@ -29,19 +26,15 @@ func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, onchainAddr, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"onchain_address": onchainAddr,
|
||||
"boarding_address": boardingAddr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,14 +124,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, delayedUtxos, change, err := coinSelectOnchain(
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos, delayedUtxos, &liquidNet); err != nil {
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -181,14 +174,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected, delayedSelected, &liquidNet); err != nil {
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -243,20 +236,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err := explorer.GetUtxos(onchainAddr)
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fromExplorer {
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
@@ -267,207 +277,99 @@ func coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
pay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
addr, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err = explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
delayedUtxos := make([]utils.Utxo, 0)
|
||||
for _, utxo := range fromExplorer {
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(unilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.Before(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psetv2.Updater, utxos, delayedUtxos []utils.Utxo, net *network.Network,
|
||||
updater *psetv2.Updater,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changeScript, err := address.ToOutputScript(onchainAddr)
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{
|
||||
{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
Sequence: sequence,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(utxo.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
witnessUtxo := transaction.TxOutput{
|
||||
Asset: assetID,
|
||||
Value: value,
|
||||
Script: changeScript,
|
||||
Nonce: []byte{0x00},
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
len(updater.Pset.Inputs)-1, &witnessUtxo,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputIndex := len(updater.Pset.Inputs) - 1
|
||||
|
||||
addr, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := address.ToOutputScript(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
if err := addVtxoInput(
|
||||
updater,
|
||||
psetv2.InputArgs{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
},
|
||||
uint(unilateralExitDelay),
|
||||
leafProof,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(utxo.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
len(updater.Pset.Inputs)-1, witnessUtxo,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -503,32 +405,7 @@ func decodeReceiverAddress(addr string) (
|
||||
return true, outputScript, nil, nil
|
||||
}
|
||||
|
||||
func addVtxoInput(
|
||||
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
|
||||
tapLeafProof *taproot.TapscriptElementsProof,
|
||||
) error {
|
||||
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextInputIndex := len(updater.Pset.Inputs)
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
|
||||
|
||||
return updater.AddInTapLeafScript(
|
||||
nextInputIndex,
|
||||
psetv2.NewTapLeafScript(
|
||||
*tapLeafProof,
|
||||
tree.UnspendableKey(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr string, err error) {
|
||||
func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -544,6 +421,21 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -556,12 +448,6 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
|
||||
liquidNet := toElementsNetwork(arkNet)
|
||||
|
||||
p2wpkh := payment.FromPublicKey(userPubkey, &liquidNet, nil)
|
||||
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
@@ -569,18 +455,34 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
|
||||
return
|
||||
}
|
||||
|
||||
payment, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
redemptionAddr, err = payment.TaprootAddress()
|
||||
redemptionAddr, err = redemptionPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingPay, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingAddr, err = boardingPay.TaprootAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offchainAddr = arkAddr
|
||||
onchainAddr = liquidAddr
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,14 +51,17 @@ func getVtxos(
|
||||
if v.Swept {
|
||||
continue
|
||||
}
|
||||
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.Txid,
|
||||
vout: v.Outpoint.Vout,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
return vtxos, nil
|
||||
@@ -193,32 +196,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
|
||||
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
|
||||
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
|
||||
for _, level := range congestionTree {
|
||||
levelProto := &arkv1.TreeLevel{
|
||||
Nodes: make([]*arkv1.Node, 0, len(level)),
|
||||
}
|
||||
|
||||
for _, node := range level {
|
||||
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, levelProto)
|
||||
}
|
||||
return &arkv1.Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
if err != nil {
|
||||
@@ -254,8 +235,8 @@ func handleRoundStream(
|
||||
pingStop()
|
||||
fmt.Println("round finalization started")
|
||||
|
||||
poolTx := e.GetPoolTx()
|
||||
ptx, err := psetv2.NewPsetFromBase64(poolTx)
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -280,13 +261,13 @@ func handleRoundStream(
|
||||
if !isOnchainOnly(receivers) {
|
||||
// validate the congestion tree
|
||||
if err := tree.ValidateCongestionTree(
|
||||
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
|
||||
if err := common.ValidateConnectors(roundTx, connectors); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -379,13 +360,15 @@ func handleRoundStream(
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
@@ -447,10 +430,30 @@ func handleRoundStream(
|
||||
}
|
||||
|
||||
fmt.Printf("%d signed\n", len(signedForfeits))
|
||||
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
ptx, err := psetv2.NewPsetFromBase64(roundTx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signPset(ctx, ptx, explorer, secKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedRoundTx, err := ptx.ToBase64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("round tx signed")
|
||||
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
fmt.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeits,
|
||||
})
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -78,8 +78,12 @@ func collaborativeRedeem(
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,6 +112,7 @@ func collaborativeRedeem(
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
)
|
||||
|
||||
@@ -143,8 +143,12 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,7 +174,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(),
|
||||
selectedCoins, secKey, receiversOutput,
|
||||
selectedCoins, false, secKey, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,14 +6,11 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -50,13 +47,7 @@ func signPset(
|
||||
return err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.Script[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(i, sighashType); err != nil {
|
||||
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -66,16 +57,6 @@ func signPset(
|
||||
return err
|
||||
}
|
||||
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainWalletScript, err := address.ToOutputScript(onchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,34 +80,6 @@ func signPset(
|
||||
liquidNet := toElementsNetwork(net)
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if bytes.Equal(input.WitnessUtxo.Script, onchainWalletScript) {
|
||||
p, err := payment.FromScript(input.WitnessUtxo.Script, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preimage := utx.HashForWitnessV0(
|
||||
i,
|
||||
p.Script,
|
||||
input.WitnessUtxo.Value,
|
||||
txscript.SigHashAll,
|
||||
)
|
||||
|
||||
sig := ecdsa.Sign(
|
||||
prvKey,
|
||||
preimage[:],
|
||||
)
|
||||
|
||||
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||
|
||||
err = signer.SignInput(i, signatureWithSighashType, prvKey.PubKey().SerializeCompressed(), nil, nil)
|
||||
if err != nil {
|
||||
fmt.Println("error signing input: ", err)
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TapLeafScript) > 0 {
|
||||
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/flags"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -21,7 +22,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -29,6 +30,21 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
// nolint:all
|
||||
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
@@ -50,19 +66,19 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
balance, err := explorer.GetBalance(onchainAddr.EncodeAddress(), "")
|
||||
balance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr.EncodeAddress(), int64(timeoutBoarding))
|
||||
if err != nil {
|
||||
chRes <- balanceRes{0, 0, nil, nil, err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{0, balance, nil, nil, nil}
|
||||
chRes <- balanceRes{0, balance, lockedBalance, nil, nil}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
|
||||
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
|
||||
redemptionAddr.EncodeAddress(), unilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -126,6 +142,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
response := make(map[string]interface{})
|
||||
|
||||
response["onchain_balance"] = map[string]interface{}{
|
||||
"spendable_amount": onchainBalance,
|
||||
}
|
||||
|
||||
@@ -2,26 +2,62 @@ package covenantless
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
func (c *clArkBitcoinCLI) Claim(ctx *cli.Context) error {
|
||||
client, cancel, err := getClientFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
myselfOffchain, _, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxos, err := getVtxos(ctx, nil, client, myselfOffchain, false)
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue // cannot claim if onchain spendable
|
||||
}
|
||||
|
||||
boardingUtxos = append(boardingUtxos, u)
|
||||
}
|
||||
|
||||
vtxos, err := getVtxos(ctx, nil, client, offchainAddr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -34,32 +70,71 @@ func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
|
||||
pendingVtxos = append(pendingVtxos, vtxo)
|
||||
}
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
pendingBalance += utxo.Amount
|
||||
}
|
||||
|
||||
if pendingBalance == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
receiver := receiver{
|
||||
To: myselfOffchain,
|
||||
To: offchainAddr,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
if len(ctx.String("password")) == 0 {
|
||||
if ok := askForConfirmation(
|
||||
fmt.Sprintf(
|
||||
"claim %d satoshis from %d pending payments and %d boarding utxos",
|
||||
pendingBalance, len(pendingVtxos), len(boardingUtxos),
|
||||
),
|
||||
); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return selfTransferAllPendingPayments(
|
||||
ctx, client, pendingVtxos, receiver,
|
||||
ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
|
||||
)
|
||||
}
|
||||
|
||||
func selfTransferAllPendingPayments(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient,
|
||||
pendingVtxos []vtxo, myself receiver,
|
||||
ctx *cli.Context,
|
||||
client arkv1.ArkServiceClient,
|
||||
pendingVtxos []vtxo,
|
||||
boardingUtxos []utils.Utxo,
|
||||
myself receiver,
|
||||
desc string,
|
||||
) error {
|
||||
inputs := make([]*arkv1.Input, 0, len(pendingVtxos))
|
||||
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
|
||||
|
||||
for _, coin := range pendingVtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(boardingUtxos) > 0 {
|
||||
for _, outpoint := range boardingUtxos {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: outpoint.Txid,
|
||||
Vout: outpoint.Vout,
|
||||
Descriptor_: desc,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
receiversOutput := []*arkv1.Output{
|
||||
{
|
||||
Address: myself.To,
|
||||
@@ -99,8 +174,8 @@ func selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
poolTxID, err := handleRoundStream(
|
||||
ctx, client, registerResponse.GetId(),
|
||||
pendingVtxos, secKey, receiversOutput, ephemeralKey,
|
||||
ctx, client, registerResponse.GetId(), pendingVtxos,
|
||||
len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@@ -25,14 +25,14 @@ const dust = 450
|
||||
type clArkBitcoinCLI struct{}
|
||||
|
||||
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
|
||||
offchainAddr, onchainAddr, _, err := getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"offchain_address": offchainAddr,
|
||||
"onchain_address": onchainAddr.EncodeAddress(),
|
||||
"boarding_address": boardingAddr.EncodeAddress(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,11 +79,6 @@ type receiver struct {
|
||||
Amount uint64 `json:"amount"`
|
||||
}
|
||||
|
||||
// func (r *receiver) isOnchain() bool {
|
||||
// _, err := btcutil.DecodeAddress(r.To, nil)
|
||||
// return err == nil
|
||||
// }
|
||||
|
||||
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
ptx, err := psbt.New(nil, nil, 2, 0, nil)
|
||||
if err != nil {
|
||||
@@ -128,14 +123,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
utxos, delayedUtxos, change, err := coinSelectOnchain(
|
||||
utxos, change, err := coinSelectOnchain(
|
||||
ctx, explorer, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
|
||||
if err := addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -174,14 +169,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := coinSelectOnchain(
|
||||
ctx, explorer, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
|
||||
if err := addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -225,20 +220,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
|
||||
func coinSelectOnchain(
|
||||
ctx *cli.Context,
|
||||
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
|
||||
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
) ([]utils.Utxo, uint64, error) {
|
||||
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
fromExplorer, err := explorer.GetUtxos(onchainAddr.EncodeAddress())
|
||||
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]utils.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fromExplorer {
|
||||
now := time.Now()
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
@@ -249,102 +261,67 @@ func coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
net, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
liquidNet := toChainParams(net)
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey),
|
||||
&liquidNet,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
addr := p2tr.EncodeAddress()
|
||||
|
||||
fromExplorer, err = explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
delayedUtxos := make([]utils.Utxo, 0)
|
||||
for _, utxo := range fromExplorer {
|
||||
for _, utxo := range redemptionUtxosFromExplorer {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(unilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
|
||||
|
||||
if utxo.SpendableAt.After(now) {
|
||||
utxos = append(utxos, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return utxos, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func addInputs(
|
||||
ctx *cli.Context,
|
||||
updater *psbt.Updater,
|
||||
utxos, delayedUtxos []utils.Utxo,
|
||||
net *chaincfg.Params,
|
||||
utxos []utils.Utxo,
|
||||
) error {
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(onchainAddr)
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,87 +332,41 @@ func addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
},
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
&wire.TxOut{
|
||||
Value: int64(utxo.Amount),
|
||||
PkScript: changeScript,
|
||||
},
|
||||
len(updater.Upsbt.UnsignedTx.TxIn)-1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aspPubkey, err := utils.GetAspPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
_, leafProof, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := txscript.PayToAddrScript(p2tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := addVtxoInput(
|
||||
updater,
|
||||
&wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
},
|
||||
uint(unilateralExitDelay),
|
||||
leafProof,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
&wire.TxOut{
|
||||
Value: int64(utxo.Amount),
|
||||
PkScript: script,
|
||||
},
|
||||
len(updater.Upsbt.Inputs)-1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -461,40 +392,7 @@ func decodeReceiverAddress(addr string) (
|
||||
return true, pkscript, nil, nil
|
||||
}
|
||||
|
||||
func addVtxoInput(
|
||||
updater *psbt.Updater, inputArgs *wire.OutPoint, exitDelay uint,
|
||||
tapLeafProof *txscript.TapscriptProof,
|
||||
) error {
|
||||
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextInputIndex := len(updater.Upsbt.Inputs)
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *inputArgs,
|
||||
Sequence: sequence,
|
||||
})
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
|
||||
|
||||
controlBlock := tapLeafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: tapLeafProof.Script,
|
||||
LeafVersion: tapLeafProof.LeafVersion,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionAddr btcutil.Address, err error) {
|
||||
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
|
||||
userPubkey, err := utils.GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -510,6 +408,21 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arkNet, err := utils.GetNetwork(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -522,11 +435,6 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
|
||||
netParams := toChainParams(arkNet)
|
||||
|
||||
p2wpkh, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(userPubkey.SerializeCompressed()), &netParams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(unilateralExitDelay),
|
||||
)
|
||||
@@ -534,7 +442,7 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(
|
||||
redemptionP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey),
|
||||
&netParams,
|
||||
)
|
||||
@@ -542,8 +450,20 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
|
||||
return
|
||||
}
|
||||
|
||||
redemptionAddr = p2tr
|
||||
onchainAddr = p2wpkh
|
||||
boardingTapKey, _, err := computeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(timeoutBoarding),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
boardingP2TR, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(boardingTapKey),
|
||||
&netParams,
|
||||
)
|
||||
|
||||
redemptionAddr = redemptionP2TR
|
||||
boardingAddr = boardingP2TR
|
||||
offchainAddr = arkAddr
|
||||
|
||||
return
|
||||
|
||||
@@ -53,15 +53,17 @@ func getVtxos(
|
||||
if v.GetSwept() {
|
||||
continue
|
||||
}
|
||||
if v.Outpoint.GetVtxoInput() != nil {
|
||||
vtxos = append(vtxos, vtxo{
|
||||
amount: v.GetReceiver().GetAmount(),
|
||||
txid: v.GetOutpoint().GetTxid(),
|
||||
vout: v.GetOutpoint().GetVout(),
|
||||
poolTxid: v.GetPoolTxid(),
|
||||
amount: v.Receiver.Amount,
|
||||
txid: v.Outpoint.GetVtxoInput().GetTxid(),
|
||||
vout: v.Outpoint.GetVtxoInput().GetVout(),
|
||||
poolTxid: v.PoolTxid,
|
||||
expireAt: expireAt,
|
||||
pending: v.GetPending(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !computeExpiration {
|
||||
return vtxos, nil
|
||||
@@ -196,32 +198,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
|
||||
return levels, nil
|
||||
}
|
||||
|
||||
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
|
||||
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
|
||||
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
|
||||
for _, level := range congestionTree {
|
||||
levelProto := &arkv1.TreeLevel{
|
||||
Nodes: make([]*arkv1.Node, 0, len(level)),
|
||||
}
|
||||
|
||||
for _, node := range level {
|
||||
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, levelProto)
|
||||
}
|
||||
return &arkv1.Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoundStream(
|
||||
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
|
||||
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
vtxosToSign []vtxo, mustSignRoundTx bool,
|
||||
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
|
||||
ephemeralKey *secp256k1.PrivateKey,
|
||||
) (poolTxID string, err error) {
|
||||
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
|
||||
@@ -398,8 +378,8 @@ func handleRoundStream(
|
||||
// stop pinging as soon as we receive some forfeit txs
|
||||
pingStop()
|
||||
|
||||
poolTx := e.GetPoolTx()
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||
roundTx := e.GetPoolTx()
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -428,7 +408,7 @@ func handleRoundStream(
|
||||
|
||||
if !isOnchainOnly(receivers) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
congestionTree, poolTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
|
||||
congestionTree, roundTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -529,13 +509,16 @@ func handleRoundStream(
|
||||
|
||||
fmt.Println("congestion tree validated")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
|
||||
|
||||
if len(vtxosToSign) > 0 {
|
||||
forfeits := e.GetForfeitTxs()
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
fmt.Print("signing forfeit txs... ")
|
||||
|
||||
explorer := utils.NewExplorer(ctx)
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
|
||||
@@ -589,7 +572,7 @@ func handleRoundStream(
|
||||
}
|
||||
|
||||
// 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")
|
||||
pingStop = nil
|
||||
for pingStop == nil {
|
||||
@@ -599,10 +582,30 @@ func handleRoundStream(
|
||||
}
|
||||
|
||||
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.Print("finalizing payment... ")
|
||||
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeits,
|
||||
})
|
||||
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
|
||||
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
|
||||
utils.MIN_RELAY_FEE: strconv.Itoa(int(resp.MinRelayFee)),
|
||||
utils.EXPLORER: explorer,
|
||||
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -72,8 +72,12 @@ func collaborativeRedeem(
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +114,7 @@ func collaborativeRedeem(
|
||||
client,
|
||||
registerResponse.GetId(),
|
||||
selectedCoins,
|
||||
false,
|
||||
secKey,
|
||||
receivers,
|
||||
ephemeralKey,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
receiver := ctx.String("to")
|
||||
receiverAddr := ctx.String("to")
|
||||
amount := ctx.Uint64("amount")
|
||||
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
|
||||
|
||||
@@ -22,15 +22,22 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
|
||||
}
|
||||
|
||||
if receiver == "" {
|
||||
if receiverAddr == "" {
|
||||
return fmt.Errorf("receiver address is required")
|
||||
}
|
||||
isOnchain, _, _, err := decodeReceiverAddress(receiver)
|
||||
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOnchain {
|
||||
return fmt.Errorf("receiver address is onchain")
|
||||
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.PrintJSON(map[string]interface{}{
|
||||
"txid": txid,
|
||||
})
|
||||
}
|
||||
|
||||
offchainAddr, _, _, err := getAddress(ctx)
|
||||
@@ -41,20 +48,20 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, aspKey, err := common.DecodeAddress(receiver)
|
||||
_, _, aspKey, err := common.DecodeAddress(receiverAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid receiver address: %s", err)
|
||||
}
|
||||
if !bytes.Equal(
|
||||
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
|
||||
) {
|
||||
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
|
||||
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiverAddr)
|
||||
}
|
||||
|
||||
receiversOutput := make([]*arkv1.Output, 0)
|
||||
sumOfReceivers := uint64(0)
|
||||
receiversOutput = append(receiversOutput, &arkv1.Output{
|
||||
Address: receiver,
|
||||
Address: receiverAddr,
|
||||
Amount: amount,
|
||||
})
|
||||
sumOfReceivers += amount
|
||||
@@ -88,8 +95,12 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: coin.txid,
|
||||
Vout: coin.vout,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/client/utils"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func signPsbt(
|
||||
ctx *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
_ *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
|
||||
) error {
|
||||
updater, err := psbt.NewUpdater(ptx)
|
||||
if err != nil {
|
||||
@@ -50,27 +49,11 @@ func signPsbt(
|
||||
return err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.PkScript[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(sighashType, i); err != nil {
|
||||
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, onchainAddr, _, err := getAddress(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
onchainWalletScript, err := txscript.PayToAddrScript(onchainAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prevouts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
for i, input := range updater.Upsbt.Inputs {
|
||||
@@ -85,40 +68,6 @@ func signPsbt(
|
||||
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
|
||||
|
||||
for i, input := range ptx.Inputs {
|
||||
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
|
||||
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preimage, err := txscript.CalcWitnessSigHash(
|
||||
input.WitnessUtxo.PkScript,
|
||||
txsighashes,
|
||||
txscript.SigHashAll,
|
||||
updater.Upsbt.UnsignedTx,
|
||||
i,
|
||||
int64(input.WitnessUtxo.Value),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := ecdsa.Sign(
|
||||
prvKey,
|
||||
preimage,
|
||||
)
|
||||
|
||||
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||
|
||||
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
|
||||
{
|
||||
PubKey: prvKey.PubKey().SerializeCompressed(),
|
||||
Signature: signatureWithSighashType,
|
||||
},
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TaprootLeafScript) > 0 {
|
||||
pubkey := prvKey.PubKey()
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
@@ -178,7 +127,6 @@ func signPsbt(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,10 +21,6 @@ var (
|
||||
Required: false,
|
||||
Hidden: true,
|
||||
}
|
||||
AmountOnboardFlag = cli.Uint64Flag{
|
||||
Name: "amount",
|
||||
Usage: "amount to onboard in sats",
|
||||
}
|
||||
ExpiryDetailsFlag = cli.BoolFlag{
|
||||
Name: "compute-expiry-details",
|
||||
Usage: "compute client-side the VTXOs expiry time",
|
||||
|
||||
@@ -2,6 +2,8 @@ module github.com/ark-network/ark/client
|
||||
|
||||
go 1.22.6
|
||||
|
||||
replace github.com/ark-network/ark/common => ../common
|
||||
|
||||
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
|
||||
require (
|
||||
@@ -19,9 +21,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
|
||||
@@ -8,7 +8,6 @@ type CLI interface {
|
||||
Receive(ctx *cli.Context) error
|
||||
Redeem(ctx *cli.Context) error
|
||||
Send(ctx *cli.Context) error
|
||||
ClaimAsync(ctx *cli.Context) error
|
||||
Claim(ctx *cli.Context) error
|
||||
SendAsync(ctx *cli.Context) error
|
||||
Onboard(ctx *cli.Context) error
|
||||
}
|
||||
|
||||
@@ -74,19 +74,6 @@ var (
|
||||
Flags: []cli.Flag{&flags.PasswordFlag, &flags.PrivateKeyFlag, &flags.NetworkFlag, &flags.UrlFlag, &flags.ExplorerFlag},
|
||||
}
|
||||
|
||||
onboardCommand = cli.Command{
|
||||
Name: "onboard",
|
||||
Usage: "Onboard the Ark by lifting your funds",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
cli, err := getCLIFromState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Onboard(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.PasswordFlag},
|
||||
}
|
||||
|
||||
sendCommand = cli.Command{
|
||||
Name: "send",
|
||||
Usage: "Send your onchain or offchain funds to one or many receivers",
|
||||
@@ -117,7 +104,7 @@ var (
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.ClaimAsync(ctx)
|
||||
return cli.Claim(ctx)
|
||||
},
|
||||
Flags: []cli.Flag{&flags.PasswordFlag},
|
||||
}
|
||||
@@ -167,7 +154,6 @@ func main() {
|
||||
&redeemCommand,
|
||||
&sendCommand,
|
||||
&claimCommand,
|
||||
&onboardCommand,
|
||||
)
|
||||
app.Flags = []cli.Flag{
|
||||
flags.DatadirFlag,
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
type Utxo struct {
|
||||
type ExplorerUtxo struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout uint32 `json:"vout"`
|
||||
Amount uint64 `json:"value"`
|
||||
@@ -32,9 +32,9 @@ type Utxo struct {
|
||||
type Explorer interface {
|
||||
GetTxHex(txid string) (string, error)
|
||||
Broadcast(txHex string) (string, error)
|
||||
GetUtxos(addr string) ([]Utxo, error)
|
||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||
GetBalance(addr, asset string) (uint64, error)
|
||||
GetRedeemedVtxosBalance(
|
||||
GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (uint64, map[int64]uint64, error)
|
||||
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
|
||||
@@ -130,7 +130,7 @@ func (e *explorer) Broadcast(txStr string) (string, error) {
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
|
||||
func (e *explorer) GetUtxos(addr string) ([]ExplorerUtxo, error) {
|
||||
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -149,7 +149,7 @@ func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(string(body))
|
||||
}
|
||||
payload := []Utxo{}
|
||||
payload := []ExplorerUtxo{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func (e *explorer) GetRedeemedVtxosBalance(
|
||||
func (e *explorer) GetDelayedBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
|
||||
utxos, err := e.GetUtxos(addr)
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -18,6 +20,7 @@ const (
|
||||
ASP_PUBKEY = "asp_public_key"
|
||||
ROUND_LIFETIME = "round_lifetime"
|
||||
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
|
||||
BOARDING_TEMPLATE = "boarding_template"
|
||||
ENCRYPTED_PRVKEY = "encrypted_private_key"
|
||||
PASSWORD_HASH = "password_hash"
|
||||
PUBKEY = "public_key"
|
||||
@@ -109,6 +112,27 @@ func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
|
||||
return int64(redeemDelay), nil
|
||||
}
|
||||
|
||||
func GetBoardingDescriptor(ctx *cli.Context) (string, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkey, err := GetWalletPublicKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
template := state[BOARDING_TEMPLATE]
|
||||
if len(template) <= 0 {
|
||||
return "", fmt.Errorf("missing boarding descriptor template")
|
||||
}
|
||||
|
||||
pubkeyhex := hex.EncodeToString(schnorr.SerializePubKey(pubkey))
|
||||
|
||||
return strings.ReplaceAll(template, "USER", pubkeyhex), nil
|
||||
}
|
||||
|
||||
func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
|
||||
state, err := GetState(ctx)
|
||||
if err != nil {
|
||||
|
||||
36
client/utils/types.go
Normal file
36
client/utils/types.go
Normal 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),
|
||||
}
|
||||
}
|
||||
48
common/bitcointree/descriptor.go
Normal file
48
common/bitcointree/descriptor.go
Normal 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
45
common/descriptor/ark.go
Normal 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
|
||||
}
|
||||
224
common/descriptor/expression.go
Normal file
224
common/descriptor/expression.go
Normal 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
125
common/descriptor/parser.go
Normal 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
|
||||
}
|
||||
350
common/descriptor/parser_test.go
Normal file
350
common/descriptor/parser_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
10
common/descriptor/types.go
Normal file
10
common/descriptor/types.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package descriptor
|
||||
|
||||
type Key struct {
|
||||
Hex string
|
||||
}
|
||||
|
||||
type TaprootDescriptor struct {
|
||||
InternalKey Key
|
||||
ScriptTree []Expression
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func EncodeAddress(
|
||||
|
||||
func DecodeAddress(
|
||||
addr string,
|
||||
) (hrp string, userKey *secp256k1.PublicKey, aspKey *secp256k1.PublicKey, err error) {
|
||||
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) {
|
||||
prefix, buf, err := bech32.DecodeNoLimit(addr)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
52
common/tree/descriptor.go
Normal file
52
common/tree/descriptor.go
Normal 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
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package tree
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
@@ -62,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
|
||||
return closure, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid closure script")
|
||||
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
|
||||
}
|
||||
|
||||
func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
|
||||
@@ -12,21 +12,18 @@ services:
|
||||
- ARK_ROUND_LIFETIME=512
|
||||
- ARK_TX_BUILDER_TYPE=covenantless
|
||||
- ARK_MIN_RELAY_FEE=200
|
||||
- ARK_NEUTRINO_PEER=bitcoin:18444
|
||||
- ARK_ESPLORA_URL=http://chopsticks:3000
|
||||
- ARK_BITCOIND_RPC_USER=admin1
|
||||
- ARK_BITCOIND_RPC_PASS=123
|
||||
- ARK_BITCOIND_RPC_HOST=bitcoin:18443
|
||||
- ARK_NO_TLS=true
|
||||
- ARK_NO_MACAROONS=true
|
||||
- ARK_DATADIR=/app/data
|
||||
ports:
|
||||
- "7070:7070"
|
||||
volumes:
|
||||
- clarkd:/app/data
|
||||
- clark:/app/wallet-data
|
||||
|
||||
volumes:
|
||||
clarkd:
|
||||
external: false
|
||||
clark:
|
||||
external: false
|
||||
- type: tmpfs
|
||||
target: /app/data
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -12,11 +12,12 @@ services:
|
||||
- OCEAN_NETWORK=regtest
|
||||
- OCEAN_UTXO_EXPIRY_DURATION_IN_SECONDS=60
|
||||
- OCEAN_DB_TYPE=badger
|
||||
- OCEAN_DATADIR=/app/data
|
||||
ports:
|
||||
- "18000:18000"
|
||||
volumes:
|
||||
- oceand:/app/data/oceand
|
||||
- ocean:/app/data/ocean
|
||||
- type: tmpfs
|
||||
target: /app/data
|
||||
arkd:
|
||||
container_name: arkd
|
||||
build:
|
||||
@@ -36,21 +37,12 @@ services:
|
||||
- ARK_PORT=6060
|
||||
- ARK_NO_TLS=true
|
||||
- ARK_NO_MACAROONS=true
|
||||
- ARK_DATADIR=/app/data
|
||||
ports:
|
||||
- "6060:6060"
|
||||
volumes:
|
||||
- arkd:/app/data
|
||||
- ark:/app/wallet-data
|
||||
|
||||
volumes:
|
||||
oceand:
|
||||
external: false
|
||||
ocean:
|
||||
external: false
|
||||
arkd:
|
||||
external: false
|
||||
ark:
|
||||
external: false
|
||||
- type: tmpfs
|
||||
target: /app/data
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -15,7 +15,6 @@ type ArkClient interface {
|
||||
Unlock(ctx context.Context, password string) error
|
||||
Lock(ctx context.Context, password string) error
|
||||
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
|
||||
Onboard(ctx context.Context, amount uint64) (string, error)
|
||||
Receive(ctx context.Context) (string, string, error)
|
||||
SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
|
||||
SendOffChain(
|
||||
@@ -26,7 +25,7 @@ type ArkClient interface {
|
||||
ctx context.Context, addr string, amount uint64, withExpiryCoinselect bool,
|
||||
) (string, error)
|
||||
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
|
||||
ClaimAsync(ctx context.Context) (string, error)
|
||||
Claim(ctx context.Context) (string, error)
|
||||
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func (a *arkClient) InitWithWallet(
|
||||
RoundLifetime: info.RoundLifetime,
|
||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
||||
MinRelayFee: uint64(info.MinRelayFee),
|
||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||
}
|
||||
if err := a.store.AddData(ctx, storeData); err != nil {
|
||||
return err
|
||||
@@ -163,6 +164,7 @@ func (a *arkClient) Init(
|
||||
RoundLifetime: info.RoundLifetime,
|
||||
UnilateralExitDelay: info.UnilateralExitDelay,
|
||||
MinRelayFee: uint64(info.MinRelayFee),
|
||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||
}
|
||||
walletSvc, err := getWallet(a.store, &storeData, supportedWallets)
|
||||
if err != nil {
|
||||
@@ -201,12 +203,12 @@ func (a *arkClient) IsLocked(ctx context.Context) bool {
|
||||
}
|
||||
|
||||
func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
|
||||
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
|
||||
offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return offchainAddr, onchainAddr, nil
|
||||
return offchainAddr, boardingAddr, nil
|
||||
}
|
||||
|
||||
func (a *arkClient) ListVtxos(
|
||||
@@ -232,14 +234,11 @@ func (a *arkClient) ListVtxos(
|
||||
func (a *arkClient) ping(
|
||||
ctx context.Context, paymentID string,
|
||||
) func() {
|
||||
_, err := a.client.Ping(ctx, paymentID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
go func(t *time.Ticker) {
|
||||
// nolint
|
||||
a.client.Ping(ctx, paymentID)
|
||||
for range t.C {
|
||||
// nolint
|
||||
a.client.Ping(ctx, paymentID)
|
||||
|
||||
@@ -23,11 +23,8 @@ type ASPClient interface {
|
||||
ListVtxos(ctx context.Context, addr string) ([]Vtxo, []Vtxo, error)
|
||||
GetRound(ctx context.Context, txID string) (*Round, error)
|
||||
GetRoundByID(ctx context.Context, roundID string) (*Round, error)
|
||||
Onboard(
|
||||
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
|
||||
) error
|
||||
RegisterPayment(
|
||||
ctx context.Context, inputs []VtxoKey, ephemeralPublicKey string,
|
||||
ctx context.Context, inputs []Input, ephemeralKey string,
|
||||
) (string, error)
|
||||
ClaimPayment(
|
||||
ctx context.Context, paymentID string, outputs []Output,
|
||||
@@ -37,7 +34,7 @@ type ASPClient interface {
|
||||
) (<-chan RoundEventChannel, error)
|
||||
Ping(ctx context.Context, paymentID string) (RoundEvent, error)
|
||||
FinalizePayment(
|
||||
ctx context.Context, signedForfeitTxs []string,
|
||||
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
|
||||
) error
|
||||
CreatePayment(
|
||||
ctx context.Context, inputs []VtxoKey, outputs []Output,
|
||||
@@ -45,6 +42,7 @@ type ASPClient interface {
|
||||
CompletePayment(
|
||||
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
|
||||
) error
|
||||
GetBoardingAddress(ctx context.Context, userPubkey string) (string, error)
|
||||
SendTreeNonces(
|
||||
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
|
||||
) error
|
||||
@@ -61,6 +59,7 @@ type Info struct {
|
||||
RoundInterval int64
|
||||
Network string
|
||||
MinRelayFee int64
|
||||
BoardingDescriptorTemplate string
|
||||
}
|
||||
|
||||
type RoundEventChannel struct {
|
||||
@@ -68,11 +67,38 @@ type RoundEventChannel struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type Input interface {
|
||||
GetTxID() string
|
||||
GetVOut() uint32
|
||||
GetDescriptor() string
|
||||
}
|
||||
|
||||
type VtxoKey struct {
|
||||
Txid string
|
||||
VOut uint32
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetTxID() string {
|
||||
return k.Txid
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetVOut() uint32 {
|
||||
return k.VOut
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetDescriptor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type BoardingInput struct {
|
||||
VtxoKey
|
||||
Descriptor string
|
||||
}
|
||||
|
||||
func (k BoardingInput) GetDescriptor() string {
|
||||
return k.Descriptor
|
||||
}
|
||||
|
||||
type Vtxo struct {
|
||||
VtxoKey
|
||||
Amount uint64
|
||||
|
||||
@@ -103,6 +103,7 @@ func (a *grpcClient) GetInfo(ctx context.Context) (*client.Info, error) {
|
||||
RoundInterval: resp.GetRoundInterval(),
|
||||
Network: resp.GetNetwork(),
|
||||
MinRelayFee: resp.GetMinRelayFee(),
|
||||
BoardingDescriptorTemplate: resp.GetBoardingDescriptorTemplate(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -143,20 +144,8 @@ func (a *grpcClient) GetRound(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *grpcClient) Onboard(
|
||||
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
|
||||
) error {
|
||||
req := &arkv1.OnboardRequest{
|
||||
BoardingTx: tx,
|
||||
UserPubkey: userPubkey,
|
||||
CongestionTree: treeToProto(congestionTree).parse(),
|
||||
}
|
||||
_, err := a.svc.Onboard(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *grpcClient) RegisterPayment(
|
||||
ctx context.Context, inputs []client.VtxoKey, ephemeralPublicKey string,
|
||||
ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
|
||||
) (string, error) {
|
||||
req := &arkv1.RegisterPaymentRequest{
|
||||
Inputs: ins(inputs).toProto(),
|
||||
@@ -198,11 +187,16 @@ func (a *grpcClient) Ping(
|
||||
}
|
||||
|
||||
func (a *grpcClient) FinalizePayment(
|
||||
ctx context.Context, signedForfeitTxs []string,
|
||||
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
|
||||
) error {
|
||||
req := &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeitTxs,
|
||||
}
|
||||
|
||||
if len(signedRoundTx) > 0 {
|
||||
req.SignedRoundTx = &signedRoundTx
|
||||
}
|
||||
|
||||
_, err := a.svc.FinalizePayment(ctx, req)
|
||||
return err
|
||||
}
|
||||
@@ -210,8 +204,13 @@ func (a *grpcClient) FinalizePayment(
|
||||
func (a *grpcClient) CreatePayment(
|
||||
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
|
||||
) (string, []string, error) {
|
||||
insCast := make([]client.Input, 0, len(inputs))
|
||||
for _, in := range inputs {
|
||||
insCast = append(insCast, in)
|
||||
}
|
||||
|
||||
req := &arkv1.CreatePaymentRequest{
|
||||
Inputs: ins(inputs).toProto(),
|
||||
Inputs: ins(insCast).toProto(),
|
||||
Outputs: outs(outputs).toProto(),
|
||||
}
|
||||
resp, err := a.svc.CreatePayment(ctx, req)
|
||||
@@ -260,6 +259,19 @@ func (a *grpcClient) GetRoundByID(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *grpcClient) GetBoardingAddress(
|
||||
ctx context.Context, userPubkey string,
|
||||
) (string, error) {
|
||||
req := &arkv1.GetBoardingAddressRequest{
|
||||
Pubkey: userPubkey,
|
||||
}
|
||||
resp, err := a.svc.GetBoardingAddress(ctx, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetAddress(), nil
|
||||
}
|
||||
|
||||
func (a *grpcClient) SendTreeNonces(
|
||||
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
|
||||
) error {
|
||||
@@ -418,8 +430,8 @@ func (v vtxo) toVtxo() client.Vtxo {
|
||||
}
|
||||
return client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.GetOutpoint().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVout(),
|
||||
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
|
||||
},
|
||||
Amount: v.GetReceiver().GetAmount(),
|
||||
RoundTxid: v.GetPoolTxid(),
|
||||
@@ -441,21 +453,35 @@ func (v vtxos) toVtxos() []client.Vtxo {
|
||||
return list
|
||||
}
|
||||
|
||||
type input client.VtxoKey
|
||||
|
||||
func (i input) toProto() *arkv1.Input {
|
||||
func toProtoInput(i client.Input) *arkv1.Input {
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
return &arkv1.Input{
|
||||
Txid: i.Txid,
|
||||
Vout: i.VOut,
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
Descriptor_: i.GetDescriptor(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ins []client.VtxoKey
|
||||
type ins []client.Input
|
||||
|
||||
func (i ins) toProto() []*arkv1.Input {
|
||||
list := make([]*arkv1.Input, 0, len(i))
|
||||
for _, ii := range i {
|
||||
list = append(list, input(ii).toProto())
|
||||
list = append(list, toProtoInput(ii))
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -496,27 +522,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
|
||||
|
||||
return levels
|
||||
}
|
||||
|
||||
type treeToProto tree.CongestionTree
|
||||
|
||||
func (t treeToProto) parse() *arkv1.Tree {
|
||||
levels := make([]*arkv1.TreeLevel, 0, len(t))
|
||||
for _, level := range t {
|
||||
levelProto := &arkv1.TreeLevel{
|
||||
Nodes: make([]*arkv1.Node, 0, len(level)),
|
||||
}
|
||||
|
||||
for _, node := range level {
|
||||
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
|
||||
Txid: node.Txid,
|
||||
Tx: node.Tx,
|
||||
ParentTxid: node.ParentTxid,
|
||||
})
|
||||
}
|
||||
|
||||
levels = append(levels, levelProto)
|
||||
}
|
||||
return &arkv1.Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ func (a *restClient) GetInfo(
|
||||
RoundInterval: int64(roundInterval),
|
||||
Network: resp.Payload.Network,
|
||||
MinRelayFee: int64(minRelayFee),
|
||||
BoardingDescriptorTemplate: resp.Payload.BoardingDescriptorTemplate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -159,8 +160,8 @@ func (a *restClient) ListVtxos(
|
||||
|
||||
spendableVtxos = append(spendableVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
@@ -191,8 +192,8 @@ func (a *restClient) ListVtxos(
|
||||
|
||||
spentVtxos = append(spentVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
@@ -243,29 +244,31 @@ func (a *restClient) GetRound(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *restClient) Onboard(
|
||||
ctx context.Context, tx, userPubkey string, congestionTree tree.CongestionTree,
|
||||
) error {
|
||||
body := models.V1OnboardRequest{
|
||||
BoardingTx: tx,
|
||||
CongestionTree: treeToProto(congestionTree).parse(),
|
||||
UserPubkey: userPubkey,
|
||||
}
|
||||
_, err := a.svc.ArkServiceOnboard(
|
||||
ark_service.NewArkServiceOnboardParams().WithBody(&body),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *restClient) RegisterPayment(
|
||||
ctx context.Context, inputs []client.VtxoKey, ephemeralPublicKey string,
|
||||
ctx context.Context, inputs []client.Input, ephemeralPublicKey string,
|
||||
) (string, error) {
|
||||
ins := make([]*models.V1Input, 0, len(inputs))
|
||||
for _, i := range inputs {
|
||||
ins = append(ins, &models.V1Input{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
})
|
||||
var input *models.V1Input
|
||||
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
input = &models.V1Input{
|
||||
BoardingInput: &models.V1BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
Descriptor: i.GetDescriptor(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
input = &models.V1Input{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ins = append(ins, input)
|
||||
}
|
||||
body := &models.V1RegisterPaymentRequest{
|
||||
Inputs: ins,
|
||||
@@ -377,13 +380,11 @@ func (a *restClient) Ping(
|
||||
}
|
||||
|
||||
func (a *restClient) FinalizePayment(
|
||||
ctx context.Context, signedForfeitTxs []string,
|
||||
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
|
||||
) error {
|
||||
req := &arkv1.FinalizePaymentRequest{
|
||||
SignedForfeitTxs: signedForfeitTxs,
|
||||
}
|
||||
body := models.V1FinalizePaymentRequest{
|
||||
SignedForfeitTxs: req.GetSignedForfeitTxs(),
|
||||
SignedForfeitTxs: signedForfeitTxs,
|
||||
SignedRoundTx: signedRoundTx,
|
||||
}
|
||||
_, err := a.svc.ArkServiceFinalizePayment(
|
||||
ark_service.NewArkServiceFinalizePaymentParams().WithBody(&body),
|
||||
@@ -396,9 +397,15 @@ func (a *restClient) CreatePayment(
|
||||
) (string, []string, error) {
|
||||
ins := make([]*models.V1Input, 0, len(inputs))
|
||||
for _, i := range inputs {
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
return "", nil, fmt.Errorf("boarding inputs are not allowed in create payment")
|
||||
}
|
||||
|
||||
ins = append(ins, &models.V1Input{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
})
|
||||
}
|
||||
outs := make([]*models.V1Output, 0, len(outputs))
|
||||
@@ -477,6 +484,23 @@ func (a *restClient) GetRoundByID(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *restClient) GetBoardingAddress(
|
||||
ctx context.Context, pubkey string,
|
||||
) (string, error) {
|
||||
body := models.V1GetBoardingAddressRequest{
|
||||
Pubkey: pubkey,
|
||||
}
|
||||
|
||||
resp, err := a.svc.ArkServiceGetBoardingAddress(
|
||||
ark_service.NewArkServiceGetBoardingAddressParams().WithBody(&body),
|
||||
)
|
||||
if err != nil {
|
||||
return "",
|
||||
err
|
||||
}
|
||||
return resp.Payload.Address, nil
|
||||
}
|
||||
|
||||
func (a *restClient) SendTreeNonces(
|
||||
ctx context.Context, roundID, cosignerPubkey string, nonces bitcointree.TreeNonces,
|
||||
) error {
|
||||
@@ -604,25 +628,3 @@ func (t treeFromProto) parse() tree.CongestionTree {
|
||||
|
||||
return congestionTree
|
||||
}
|
||||
|
||||
type treeToProto tree.CongestionTree
|
||||
|
||||
func (t treeToProto) parse() *models.V1Tree {
|
||||
levels := make([]*models.V1TreeLevel, 0, len(t))
|
||||
for _, level := range t {
|
||||
nodes := make([]*models.V1Node, 0, len(level))
|
||||
for _, n := range level {
|
||||
nodes = append(nodes, &models.V1Node{
|
||||
Txid: n.Txid,
|
||||
Tx: n.Tx,
|
||||
ParentTxid: n.ParentTxid,
|
||||
})
|
||||
}
|
||||
levels = append(levels, &models.V1TreeLevel{
|
||||
Nodes: nodes,
|
||||
})
|
||||
}
|
||||
return &models.V1Tree{
|
||||
Levels: levels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ type ClientService interface {
|
||||
|
||||
ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentParams, opts ...ClientOption) (*ArkServiceFinalizePaymentOK, error)
|
||||
|
||||
ArkServiceGetBoardingAddress(params *ArkServiceGetBoardingAddressParams, opts ...ClientOption) (*ArkServiceGetBoardingAddressOK, error)
|
||||
|
||||
ArkServiceGetEventStream(params *ArkServiceGetEventStreamParams, opts ...ClientOption) (*ArkServiceGetEventStreamOK, error)
|
||||
|
||||
ArkServiceGetInfo(params *ArkServiceGetInfoParams, opts ...ClientOption) (*ArkServiceGetInfoOK, error)
|
||||
@@ -72,8 +74,6 @@ type ClientService interface {
|
||||
|
||||
ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...ClientOption) (*ArkServiceListVtxosOK, error)
|
||||
|
||||
ArkServiceOnboard(params *ArkServiceOnboardParams, opts ...ClientOption) (*ArkServiceOnboardOK, error)
|
||||
|
||||
ArkServicePing(params *ArkServicePingParams, opts ...ClientOption) (*ArkServicePingOK, error)
|
||||
|
||||
ArkServiceRegisterPayment(params *ArkServiceRegisterPaymentParams, opts ...ClientOption) (*ArkServiceRegisterPaymentOK, error)
|
||||
@@ -233,6 +233,43 @@ func (a *Client) ArkServiceFinalizePayment(params *ArkServiceFinalizePaymentPara
|
||||
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
|
||||
}
|
||||
|
||||
/*
|
||||
ArkServiceGetBoardingAddress ark service get boarding address API
|
||||
*/
|
||||
func (a *Client) ArkServiceGetBoardingAddress(params *ArkServiceGetBoardingAddressParams, opts ...ClientOption) (*ArkServiceGetBoardingAddressOK, error) {
|
||||
// TODO: Validate the params before sending
|
||||
if params == nil {
|
||||
params = NewArkServiceGetBoardingAddressParams()
|
||||
}
|
||||
op := &runtime.ClientOperation{
|
||||
ID: "ArkService_GetBoardingAddress",
|
||||
Method: "POST",
|
||||
PathPattern: "/v1/boarding",
|
||||
ProducesMediaTypes: []string{"application/json"},
|
||||
ConsumesMediaTypes: []string{"application/json"},
|
||||
Schemes: []string{"http"},
|
||||
Params: params,
|
||||
Reader: &ArkServiceGetBoardingAddressReader{formats: a.formats},
|
||||
Context: params.Context,
|
||||
Client: params.HTTPClient,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(op)
|
||||
}
|
||||
|
||||
result, err := a.transport.Submit(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
success, ok := result.(*ArkServiceGetBoardingAddressOK)
|
||||
if ok {
|
||||
return success, nil
|
||||
}
|
||||
// unexpected success response
|
||||
unexpectedSuccess := result.(*ArkServiceGetBoardingAddressDefault)
|
||||
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
|
||||
}
|
||||
|
||||
/*
|
||||
ArkServiceGetEventStream ark service get event stream API
|
||||
*/
|
||||
@@ -418,43 +455,6 @@ func (a *Client) ArkServiceListVtxos(params *ArkServiceListVtxosParams, opts ...
|
||||
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
|
||||
}
|
||||
|
||||
/*
|
||||
ArkServiceOnboard ark service onboard API
|
||||
*/
|
||||
func (a *Client) ArkServiceOnboard(params *ArkServiceOnboardParams, opts ...ClientOption) (*ArkServiceOnboardOK, error) {
|
||||
// TODO: Validate the params before sending
|
||||
if params == nil {
|
||||
params = NewArkServiceOnboardParams()
|
||||
}
|
||||
op := &runtime.ClientOperation{
|
||||
ID: "ArkService_Onboard",
|
||||
Method: "POST",
|
||||
PathPattern: "/v1/onboard",
|
||||
ProducesMediaTypes: []string{"application/json"},
|
||||
ConsumesMediaTypes: []string{"application/json"},
|
||||
Schemes: []string{"http"},
|
||||
Params: params,
|
||||
Reader: &ArkServiceOnboardReader{formats: a.formats},
|
||||
Context: params.Context,
|
||||
Client: params.HTTPClient,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(op)
|
||||
}
|
||||
|
||||
result, err := a.transport.Submit(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
success, ok := result.(*ArkServiceOnboardOK)
|
||||
if ok {
|
||||
return success, nil
|
||||
}
|
||||
// unexpected success response
|
||||
unexpectedSuccess := result.(*ArkServiceOnboardDefault)
|
||||
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
|
||||
}
|
||||
|
||||
/*
|
||||
ArkServicePing ark service ping API
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -19,6 +19,9 @@ type V1FinalizePaymentRequest struct {
|
||||
|
||||
// Forfeit txs signed by the user.
|
||||
SignedForfeitTxs []string `json:"signedForfeitTxs"`
|
||||
|
||||
// If payment has reverse boarding, the user must sign the associated inputs.
|
||||
SignedRoundTx string `json:"signedRoundTx,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 finalize payment request
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
// swagger:model v1GetInfoResponse
|
||||
type V1GetInfoResponse struct {
|
||||
|
||||
// boarding descriptor template
|
||||
BoardingDescriptorTemplate string `json:"boardingDescriptorTemplate,omitempty"`
|
||||
|
||||
// min relay fee
|
||||
MinRelayFee string `json:"minRelayFee,omitempty"`
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
@@ -17,20 +18,126 @@ import (
|
||||
// swagger:model v1Input
|
||||
type V1Input struct {
|
||||
|
||||
// txid
|
||||
Txid string `json:"txid,omitempty"`
|
||||
// boarding input
|
||||
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
|
||||
|
||||
// vout
|
||||
Vout int64 `json:"vout,omitempty"`
|
||||
// vtxo input
|
||||
VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 input
|
||||
func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validateBoardingInput(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateVtxoInput(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validates this v1 input based on context it is used
|
||||
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.BoardingInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.BoardingInput != nil {
|
||||
if err := m.BoardingInput.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("boardingInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.VtxoInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.VtxoInput != nil {
|
||||
if err := m.VtxoInput.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("vtxoInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("vtxoInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validate this v1 input based on the context it is used
|
||||
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.contextValidateBoardingInput(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateVtxoInput(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) contextValidateBoardingInput(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.BoardingInput != nil {
|
||||
|
||||
if swag.IsZero(m.BoardingInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.BoardingInput.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("boardingInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) contextValidateVtxoInput(ctx context.Context, formats strfmt.Registry) error {
|
||||
|
||||
if m.VtxoInput != nil {
|
||||
|
||||
if swag.IsZero(m.VtxoInput) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.VtxoInput.ContextValidate(ctx, formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("vtxoInput")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("vtxoInput")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
53
pkg/client-sdk/client/rest/service/models/v1_vtxo_input.go
Normal file
53
pkg/client-sdk/client/rest/service/models/v1_vtxo_input.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -23,12 +24,7 @@ import (
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
type liquidReceiver struct {
|
||||
@@ -139,92 +135,24 @@ func LoadCovenantClientWithWallet(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) Onboard(
|
||||
ctx context.Context, amount uint64,
|
||||
) (string, error) {
|
||||
if amount <= 0 {
|
||||
return "", fmt.Errorf("invalid amount to onboard %d", amount)
|
||||
}
|
||||
|
||||
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
net := utils.ToElementsNetwork(a.Network)
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
userPubkeyStr := hex.EncodeToString(userPubkey.SerializeCompressed())
|
||||
congestionTreeLeaf := tree.Receiver{
|
||||
Pubkey: userPubkeyStr,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
|
||||
net.AssetID,
|
||||
aspPubkey,
|
||||
[]tree.Receiver{congestionTreeLeaf},
|
||||
a.MinRelayFee,
|
||||
a.RoundLifetime,
|
||||
a.UnilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pay, err := payment.FromScript(sharedOutputScript, &net, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
addr, err := pay.TaprootAddress()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
onchainReceiver := NewLiquidReceiver(addr, sharedOutputAmount)
|
||||
|
||||
pset, err := a.sendOnchain(ctx, []Receiver{onchainReceiver})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ptx, _ := psetv2.NewPsetFromBase64(pset)
|
||||
utx, _ := ptx.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
|
||||
Txid: txid,
|
||||
TxIndex: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.client.Onboard(
|
||||
ctx, pset, userPubkeyStr, congestionTree,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) Balance(
|
||||
ctx context.Context, computeVtxoExpiration bool,
|
||||
) (*Balance, error) {
|
||||
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const nbWorkers = 3
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3 * len(offchainAddrs))
|
||||
wg.Add(nbWorkers * len(offchainAddrs))
|
||||
|
||||
chRes := make(chan balanceRes, 3)
|
||||
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
|
||||
for i := range offchainAddrs {
|
||||
offchainAddr := offchainAddrs[i]
|
||||
onchainAddr := onchainAddrs[i]
|
||||
boardingAddr := boardingAddrs[i]
|
||||
redeemAddr := redeemAddrs[i]
|
||||
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
balance, amountByExpiration, err := a.getOffchainBalance(
|
||||
@@ -241,17 +169,7 @@ func (a *covenantArkClient) Balance(
|
||||
}
|
||||
}(offchainAddr)
|
||||
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
balance, err := a.explorer.GetBalance(addr)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{err: err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{onchainSpendableBalance: balance}
|
||||
}(onchainAddr)
|
||||
|
||||
go func(addr string) {
|
||||
getDelayedBalance := func(addr string) {
|
||||
defer wg.Done()
|
||||
|
||||
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
|
||||
@@ -267,7 +185,10 @@ func (a *covenantArkClient) Balance(
|
||||
onchainLockedBalance: lockedBalance,
|
||||
err: err,
|
||||
}
|
||||
}(redeemAddr)
|
||||
}
|
||||
|
||||
go getDelayedBalance(boardingAddr)
|
||||
go getDelayedBalance(redeemAddr)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -317,7 +238,7 @@ func (a *covenantArkClient) Balance(
|
||||
}
|
||||
|
||||
count++
|
||||
if count == 3 {
|
||||
if count == nbWorkers {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -519,7 +440,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
@@ -538,7 +459,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, receivers,
|
||||
ctx, paymentID, selectedCoins, false, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -554,8 +475,85 @@ func (a *covenantArkClient) SendAsync(
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) ClaimAsync(ctx context.Context) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
|
||||
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var pendingBalance uint64
|
||||
for _, vtxo := range boardingUtxos {
|
||||
pendingBalance += vtxo.Amount
|
||||
}
|
||||
if pendingBalance == 0 {
|
||||
return "", fmt.Errorf("no funds to claim")
|
||||
}
|
||||
|
||||
receiver := client.Output{
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
|
||||
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc)
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
|
||||
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimable := make([]explorer.Utxo, 0)
|
||||
now := time.Now()
|
||||
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingUtxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
u := utxo.ToUtxo(boardingTimeout)
|
||||
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
claimable = append(claimable, u)
|
||||
}
|
||||
}
|
||||
|
||||
return claimable, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) sendOnchain(
|
||||
@@ -599,14 +597,14 @@ func (a *covenantArkClient) sendOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxos, delayedUtxos, change, err := a.coinSelectOnchain(
|
||||
utxos, change, err := a.coinSelectOnchain(
|
||||
ctx, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, net); err != nil {
|
||||
if err := a.addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -649,14 +647,14 @@ func (a *covenantArkClient) sendOnchain(
|
||||
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := a.coinSelectOnchain(
|
||||
ctx, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := a.coinSelectOnchain(
|
||||
ctx, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.addInputs(ctx, updater, selected, delayedSelected, net); err != nil {
|
||||
if err := a.addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -787,7 +785,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
@@ -811,7 +809,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, receiversOutput,
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -821,126 +819,60 @@ func (a *covenantArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) addInputs(
|
||||
ctx context.Context, updater *psetv2.Updater, utxos, delayedUtxos []explorer.Utxo, net network.Network,
|
||||
ctx context.Context,
|
||||
updater *psetv2.Updater,
|
||||
utxos []explorer.Utxo,
|
||||
) error {
|
||||
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
|
||||
// TODO works only with single-key wallet
|
||||
offchain, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
|
||||
changeScript, err := address.ToOutputScript(onchainAddr)
|
||||
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{
|
||||
{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
Sequence: sequence,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueToBytes(utxo.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
witnessUtxo := transaction.TxOutput{
|
||||
Asset: assetID,
|
||||
Value: value,
|
||||
Script: changeScript,
|
||||
Nonce: []byte{0x00},
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
len(updater.Pset.Inputs)-1, &witnessUtxo,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
_, leafProof, script, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
|
||||
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
if err := a.addVtxoInput(
|
||||
updater,
|
||||
psetv2.InputArgs{
|
||||
Txid: utxo.Txid,
|
||||
TxIndex: utxo.Vout,
|
||||
},
|
||||
uint(a.UnilateralExitDelay),
|
||||
leafProof,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
inputIndex := len(updater.Pset.Inputs) - 1
|
||||
|
||||
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
|
||||
if err != nil {
|
||||
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); 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
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) addVtxoInput(
|
||||
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
|
||||
tapLeafProof *taproot.TapscriptElementsProof,
|
||||
) error {
|
||||
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextInputIndex := len(updater.Pset.Inputs)
|
||||
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
|
||||
|
||||
return updater.AddInTapLeafScript(
|
||||
nextInputIndex,
|
||||
psetv2.NewTapLeafScript(
|
||||
*tapLeafProof,
|
||||
tree.UnspendableKey(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string, vtxosToSign []client.Vtxo, receivers []client.Output,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
receivers []client.Output,
|
||||
) (string, error) {
|
||||
eventsCh, err := a.client.GetEventStream(ctx, paymentID)
|
||||
if err != nil {
|
||||
@@ -972,20 +904,20 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
pingStop()
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers,
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(signedForfeitTxs) <= 0 {
|
||||
if len(signedForfeitTxs) <= 0 && len(vtxosToSign) > 0 {
|
||||
log.Info("no forfeit txs to sign, waiting for the next round")
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("finalizing payment... ")
|
||||
if err := a.client.FinalizePayment(ctx, signedForfeitTxs); err != nil {
|
||||
if err := a.client.FinalizePayment(ctx, signedForfeitTxs, signedRoundTx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -998,15 +930,29 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
|
||||
func (a *covenantArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, receivers []client.Output,
|
||||
) ([]string, error) {
|
||||
if err := a.validateCongestionTree(event, receivers); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify congestion tree: %s", err)
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
if err = a.validateCongestionTree(event, receivers); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return a.loopAndSign(
|
||||
if len(vtxos) > 0 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) validateCongestionTree(
|
||||
@@ -1197,23 +1143,49 @@ func (a *covenantArkClient) signForfeitTx(
|
||||
|
||||
func (a *covenantArkClient) coinSelectOnchain(
|
||||
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
|
||||
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) {
|
||||
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx)
|
||||
) ([]explorer.Utxo, uint64, error) {
|
||||
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
net := utils.ToElementsNetwork(a.Network)
|
||||
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
fetchedUtxos := make([]explorer.Utxo, 0)
|
||||
for _, onchainAddr := range onchainAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(onchainAddr)
|
||||
for _, addr := range boardingAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
fetchedUtxos = append(fetchedUtxos, utxos...)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]explorer.Utxo, 0)
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(boardingTimeout)
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected := make([]explorer.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fetchedUtxos {
|
||||
if selectedAmount >= targetAmount {
|
||||
@@ -1226,61 +1198,51 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxos = append(utxos, utxo)
|
||||
selected = append(selected, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return selected, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
fetchedUtxos = make([]explorer.Utxo, 0)
|
||||
for _, offchainAddr := range offchainAddrs {
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
_, _, _, addr, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
|
||||
)
|
||||
for _, addr := range redemptionAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos, err = a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
}
|
||||
fetchedUtxos = append(fetchedUtxos, utxos...)
|
||||
}
|
||||
|
||||
delayedUtxos := make([]explorer.Utxo, 0)
|
||||
for _, utxo := range fetchedUtxos {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(a.UnilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
selected = append(selected, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return selected, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) getRedeemBranches(
|
||||
@@ -1373,3 +1335,39 @@ func (a *covenantArkClient) getVtxos(
|
||||
|
||||
return vtxos, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
ctx context.Context, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
|
||||
) (string, error) {
|
||||
inputs := make([]client.Input, 0, len(boardingUtxo))
|
||||
|
||||
for _, utxo := range boardingUtxo {
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
Descriptor: boardingDescriptor,
|
||||
})
|
||||
}
|
||||
|
||||
outputs := []client.Output{myself}
|
||||
|
||||
paymentID, err := a.client.RegisterPayment(ctx, inputs, "") // ephemeralPublicKey is not required for covenant
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.client.ClaimPayment(ctx, paymentID, outputs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
roundTxid, err := a.handleRoundStream(
|
||||
ctx, paymentID, make([]client.Vtxo, 0), len(boardingUtxo) > 0, outputs,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return roundTxid, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@@ -139,176 +139,24 @@ func LoadCovenantlessClientWithWallet(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) Onboard(
|
||||
ctx context.Context, amount uint64,
|
||||
) (string, error) {
|
||||
if amount <= 0 {
|
||||
return "", fmt.Errorf("invalid amount to onboard %d", amount)
|
||||
}
|
||||
|
||||
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
userPubkeyStr := hex.EncodeToString(userPubkey.SerializeCompressed())
|
||||
|
||||
congestionTreeLeaf := bitcointree.Receiver{
|
||||
Pubkey: userPubkeyStr,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
leaves := []bitcointree.Receiver{congestionTreeLeaf}
|
||||
|
||||
ephemeralKey, err := secp256k1.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cosigners := []*secp256k1.PublicKey{ephemeralKey.PubKey()} // TODO asp as cosigner
|
||||
|
||||
sharedOutputScript, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
|
||||
cosigners,
|
||||
aspPubkey,
|
||||
leaves,
|
||||
a.MinRelayFee,
|
||||
a.RoundLifetime,
|
||||
a.UnilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
|
||||
address, err := btcutil.NewAddressTaproot(sharedOutputScript[2:], &netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
onchainReceiver := NewBitcoinReceiver(
|
||||
address.EncodeAddress(), uint64(sharedOutputAmount),
|
||||
)
|
||||
|
||||
partialTx, err := a.sendOnchain(ctx, []Receiver{onchainReceiver})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(partialTx), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
txid := ptx.UnsignedTx.TxHash().String()
|
||||
|
||||
congestionTree, err := bitcointree.CraftCongestionTree(
|
||||
&wire.OutPoint{
|
||||
Hash: ptx.UnsignedTx.TxHash(),
|
||||
Index: 0,
|
||||
},
|
||||
cosigners,
|
||||
aspPubkey,
|
||||
leaves,
|
||||
a.MinRelayFee,
|
||||
a.RoundLifetime,
|
||||
a.UnilateralExitDelay,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: aspPubkey,
|
||||
Seconds: uint(a.RoundLifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
root := sweepTapTree.RootNode.TapHash()
|
||||
|
||||
signer := bitcointree.NewTreeSignerSession(
|
||||
ephemeralKey,
|
||||
congestionTree,
|
||||
int64(a.MinRelayFee),
|
||||
root.CloneBytes(),
|
||||
)
|
||||
|
||||
nonces, err := signer.GetNonces() // TODO send nonces to ASP
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
coordinator, err := bitcointree.NewTreeCoordinatorSession(
|
||||
congestionTree,
|
||||
int64(a.MinRelayFee),
|
||||
root.CloneBytes(),
|
||||
cosigners,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := coordinator.AddNonce(ephemeralKey.PubKey(), nonces); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aggregatedNonces, err := coordinator.AggregateNonces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signer.SetKeys(cosigners); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := signer.SetAggregatedNonces(aggregatedNonces); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sigs, err := signer.Sign()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := coordinator.AddSig(ephemeralKey.PubKey(), sigs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedTree, err := coordinator.SignTree()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.client.Onboard(
|
||||
ctx, partialTx, userPubkeyStr, signedTree,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) Balance(
|
||||
ctx context.Context, computeVtxoExpiration bool,
|
||||
) (*Balance, error) {
|
||||
offchainAddrs, onchainAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const nbWorkers = 3
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3 * len(offchainAddrs))
|
||||
wg.Add(nbWorkers * len(offchainAddrs))
|
||||
|
||||
chRes := make(chan balanceRes, 3)
|
||||
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
|
||||
for i := range offchainAddrs {
|
||||
offchainAddr := offchainAddrs[i]
|
||||
onchainAddr := onchainAddrs[i]
|
||||
boardingAddr := boardingAddrs[i]
|
||||
redeemAddr := redeemAddrs[i]
|
||||
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
balance, amountByExpiration, err := a.getOffchainBalance(
|
||||
@@ -325,17 +173,7 @@ func (a *covenantlessArkClient) Balance(
|
||||
}
|
||||
}(offchainAddr)
|
||||
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
balance, err := a.explorer.GetBalance(addr)
|
||||
if err != nil {
|
||||
chRes <- balanceRes{err: err}
|
||||
return
|
||||
}
|
||||
chRes <- balanceRes{onchainSpendableBalance: balance}
|
||||
}(onchainAddr)
|
||||
|
||||
go func(addr string) {
|
||||
getDelayedBalance := func(addr string) {
|
||||
defer wg.Done()
|
||||
|
||||
spendableBalance, lockedBalance, err := a.explorer.GetRedeemedVtxosBalance(
|
||||
@@ -351,7 +189,10 @@ func (a *covenantlessArkClient) Balance(
|
||||
onchainLockedBalance: lockedBalance,
|
||||
err: err,
|
||||
}
|
||||
}(redeemAddr)
|
||||
}
|
||||
|
||||
go getDelayedBalance(boardingAddr)
|
||||
go getDelayedBalance(redeemAddr)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -401,7 +242,7 @@ func (a *covenantlessArkClient) Balance(
|
||||
}
|
||||
|
||||
count++
|
||||
if count == 3 {
|
||||
if count == nbWorkers {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -603,7 +444,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
@@ -631,7 +472,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, receivers, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -751,9 +592,7 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
return signedRedeemTx, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) ClaimAsync(
|
||||
ctx context.Context,
|
||||
) (string, error) {
|
||||
func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
|
||||
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -764,10 +603,23 @@ func (a *covenantlessArkClient) ClaimAsync(
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var pendingBalance uint64
|
||||
for _, vtxo := range pendingVtxos {
|
||||
pendingBalance += vtxo.Amount
|
||||
}
|
||||
for _, vtxo := range boardingUtxos {
|
||||
pendingBalance += vtxo.Amount
|
||||
}
|
||||
if pendingBalance == 0 {
|
||||
return "", nil
|
||||
}
|
||||
@@ -776,7 +628,9 @@ func (a *covenantlessArkClient) ClaimAsync(
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, receiver)
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) sendOnchain(
|
||||
@@ -822,14 +676,14 @@ func (a *covenantlessArkClient) sendOnchain(
|
||||
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
|
||||
}
|
||||
|
||||
utxos, delayedUtxos, change, err := a.coinSelectOnchain(
|
||||
utxos, change, err := a.coinSelectOnchain(
|
||||
ctx, targetAmount, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
|
||||
if err := a.addInputs(ctx, updater, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -869,14 +723,14 @@ func (a *covenantlessArkClient) sendOnchain(
|
||||
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
|
||||
}
|
||||
// reselect the difference
|
||||
selected, delayedSelected, newChange, err := a.coinSelectOnchain(
|
||||
ctx, feeAmount-change, append(utxos, delayedUtxos...),
|
||||
selected, newChange, err := a.coinSelectOnchain(
|
||||
ctx, feeAmount-change, utxos,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
|
||||
if err := a.addInputs(ctx, updater, selected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -995,7 +849,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
@@ -1024,7 +878,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, receiversOutput, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1034,16 +888,17 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) addInputs(
|
||||
ctx context.Context, updater *psbt.Updater,
|
||||
utxos, delayedUtxos []explorer.Utxo, net *chaincfg.Params,
|
||||
ctx context.Context,
|
||||
updater *psbt.Updater,
|
||||
utxos []explorer.Utxo,
|
||||
) error {
|
||||
offchainAddr, onchainAddr, err := a.wallet.NewAddress(ctx, false)
|
||||
// TODO works only with single-key wallet
|
||||
offchain, _, err := a.wallet.NewAddress(ctx, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr, _ := btcutil.DecodeAddress(onchainAddr, net)
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(addr)
|
||||
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1054,107 +909,41 @@ func (a *covenantlessArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
sequence, err := utxo.Sequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
},
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
|
||||
|
||||
if err := updater.AddInWitnessUtxo(
|
||||
&wire.TxOut{
|
||||
Value: int64(utxo.Amount),
|
||||
PkScript: changeScript,
|
||||
},
|
||||
len(updater.Upsbt.UnsignedTx.TxIn)-1,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(delayedUtxos) > 0 {
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
|
||||
vtxoTapKey, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
|
||||
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := txscript.PayToAddrScript(p2tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, utxo := range delayedUtxos {
|
||||
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.addVtxoInput(
|
||||
updater,
|
||||
&wire.OutPoint{
|
||||
Hash: *previousHash,
|
||||
Index: utxo.Vout,
|
||||
},
|
||||
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())
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
|
||||
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: tapLeafProof.Script,
|
||||
LeafVersion: tapLeafProof.LeafVersion,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1164,6 +953,7 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
receivers []client.Output,
|
||||
roundEphemeralKey *secp256k1.PrivateKey,
|
||||
) (string, error) {
|
||||
@@ -1218,20 +1008,20 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
pingStop()
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, receivers,
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(signedForfeitTxs) <= 0 {
|
||||
if len(signedForfeitTxs) <= 0 && len(vtxosToSign) > 0 {
|
||||
log.Info("no forfeit txs to sign, waiting for the next round")
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("finalizing payment... ")
|
||||
if err := a.client.FinalizePayment(ctx, signedForfeitTxs); err != nil {
|
||||
if err := a.client.FinalizePayment(ctx, signedForfeitTxs, signedRoundTx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1311,15 +1101,29 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
|
||||
|
||||
func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, receivers []client.Output,
|
||||
) ([]string, error) {
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
if err := a.validateCongestionTree(event, receivers); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify congestion tree: %s", err)
|
||||
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
|
||||
}
|
||||
|
||||
return a.loopAndSign(
|
||||
if len(vtxos) > 0 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) validateCongestionTree(
|
||||
@@ -1516,24 +1320,49 @@ func (a *covenantlessArkClient) loopAndSign(
|
||||
|
||||
func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
|
||||
) ([]explorer.Utxo, []explorer.Utxo, uint64, error) {
|
||||
offchainAddrs, onchainAddrs, _, err := a.wallet.GetAddresses(ctx)
|
||||
) ([]explorer.Utxo, uint64, error) {
|
||||
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
net := utils.ToBitcoinNetwork(a.Network)
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
fetchedUtxos := make([]explorer.Utxo, 0)
|
||||
for _, onchainAddr := range onchainAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(onchainAddr)
|
||||
for _, addr := range boardingAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
fetchedUtxos = append(fetchedUtxos, utxos...)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos := make([]explorer.Utxo, 0)
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(boardingTimeout)
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected := make([]explorer.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
for _, utxo := range fetchedUtxos {
|
||||
if selectedAmount >= targetAmount {
|
||||
@@ -1546,70 +1375,51 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
}
|
||||
}
|
||||
|
||||
utxos = append(utxos, utxo)
|
||||
selected = append(selected, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
|
||||
if selectedAmount >= targetAmount {
|
||||
return utxos, nil, selectedAmount - targetAmount, nil
|
||||
return selected, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
fetchedUtxos = make([]explorer.Utxo, 0)
|
||||
for _, offchainAddr := range offchainAddrs {
|
||||
_, userPubkey, aspPubkey, _ := common.DecodeAddress(offchainAddr)
|
||||
|
||||
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
|
||||
)
|
||||
for _, addr := range redemptionAddrs {
|
||||
utxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
p2tr, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(vtxoTapKey), &net,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
addr := p2tr.EncodeAddress()
|
||||
|
||||
utxos, err = a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(uint(a.UnilateralExitDelay))
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
}
|
||||
fetchedUtxos = append(fetchedUtxos, utxos...)
|
||||
}
|
||||
|
||||
delayedUtxos := make([]explorer.Utxo, 0)
|
||||
for _, utxo := range fetchedUtxos {
|
||||
if selectedAmount >= targetAmount {
|
||||
break
|
||||
}
|
||||
|
||||
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
|
||||
time.Duration(a.UnilateralExitDelay) * time.Second,
|
||||
)
|
||||
if availableAt.After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, excluded := range exclude {
|
||||
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
delayedUtxos = append(delayedUtxos, utxo)
|
||||
selected = append(selected, utxo)
|
||||
selectedAmount += utxo.Amount
|
||||
}
|
||||
|
||||
if selectedAmount < targetAmount {
|
||||
return nil, nil, 0, fmt.Errorf(
|
||||
return nil, 0, fmt.Errorf(
|
||||
"not enough funds to cover amount %d", targetAmount,
|
||||
)
|
||||
}
|
||||
|
||||
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
|
||||
return selected, selectedAmount - targetAmount, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getRedeemBranches(
|
||||
@@ -1673,6 +1483,53 @@ func (a *covenantlessArkClient) getOffchainBalance(
|
||||
return balance, amountByExpiration, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
|
||||
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimable := make([]explorer.Utxo, 0)
|
||||
now := time.Now()
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingUtxos, err := a.explorer.GetUtxos(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
u := utxo.ToUtxo(boardingTimeout)
|
||||
if u.SpendableAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
claimable = append(claimable, u)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return claimable, nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) getVtxos(
|
||||
ctx context.Context, addr string, computeVtxoExpiration bool,
|
||||
) ([]client.Vtxo, []client.Vtxo, error) {
|
||||
@@ -1718,14 +1575,25 @@ func (a *covenantlessArkClient) getVtxos(
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
ctx context.Context, pendingVtxos []client.Vtxo, myself client.Output,
|
||||
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
|
||||
) (string, error) {
|
||||
inputs := make([]client.VtxoKey, 0, len(pendingVtxos))
|
||||
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo))
|
||||
|
||||
for _, coin := range pendingVtxos {
|
||||
inputs = append(inputs, coin.VtxoKey)
|
||||
}
|
||||
|
||||
for _, utxo := range boardingUtxo {
|
||||
fmt.Println(utxo)
|
||||
fmt.Println(boardingDescriptor)
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
Descriptor: boardingDescriptor,
|
||||
})
|
||||
}
|
||||
outputs := []client.Output{myself}
|
||||
|
||||
roundEphemeralKey, err := secp256k1.GeneratePrivateKey()
|
||||
@@ -1747,7 +1615,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
roundTxid, err := a.handleRoundStream(
|
||||
ctx, paymentID, pendingVtxos, outputs, roundEphemeralKey,
|
||||
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -38,31 +38,27 @@ func main() {
|
||||
defer aliceArkClient.Lock(ctx, password)
|
||||
|
||||
log.Info("alice is acquiring onchain funds...")
|
||||
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx)
|
||||
_, aliceBoardingAddr, err := aliceArkClient.Receive(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := runCommand("nigiri", "faucet", "--liquid", aliceOnchainAddr); err != nil {
|
||||
if _, err := runCommand("nigiri", "faucet", "--liquid", aliceBoardingAddr); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
onboardAmount := uint64(20000)
|
||||
onboardAmount := uint64(1_0000_0000) // 1 BTC
|
||||
log.Infof("alice is onboarding with %d sats offchain...", onboardAmount)
|
||||
txid, err := aliceArkClient.Onboard(ctx, onboardAmount)
|
||||
|
||||
log.Infof("alice claiming onboarding funds...")
|
||||
txid, err := aliceArkClient.Claim(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := generateBlock(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
log.Infof("alice onboarded with tx: %s", txid)
|
||||
log.Infof("onboarding completed in round tx: %s", txid)
|
||||
|
||||
aliceBalance, err := aliceArkClient.Balance(ctx, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,31 +38,19 @@ func main() {
|
||||
defer aliceArkClient.Lock(ctx, password)
|
||||
|
||||
log.Info("alice is acquiring onchain funds...")
|
||||
_, aliceOnchainAddr, err := aliceArkClient.Receive(ctx)
|
||||
_, boardingAddress, err := aliceArkClient.Receive(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := runCommand("nigiri", "faucet", aliceOnchainAddr); err != nil {
|
||||
if _, err := runCommand("nigiri", "faucet", boardingAddress); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
onboardAmount := uint64(20000)
|
||||
onboardAmount := uint64(1_0000_0000) // 1 BTC
|
||||
log.Infof("alice is onboarding with %d sats offchain...", onboardAmount)
|
||||
txid, err := aliceArkClient.Onboard(ctx, onboardAmount)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := generateBlock(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
log.Infof("alice onboarded with tx: %s", txid)
|
||||
|
||||
aliceBalance, err := aliceArkClient.Balance(ctx, false)
|
||||
if err != nil {
|
||||
@@ -72,6 +60,14 @@ func main() {
|
||||
log.Infof("alice onchain balance: %d", aliceBalance.OnchainBalance.SpendableAmount)
|
||||
log.Infof("alice offchain balance: %d", aliceBalance.OffchainBalance.Total)
|
||||
|
||||
log.Infof("alice claiming onboarding funds...")
|
||||
txid, err := aliceArkClient.Claim(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Infof("alice claimed onboarding funds in round %s", txid)
|
||||
|
||||
fmt.Println("")
|
||||
log.Info("bob is setting up his ark wallet...")
|
||||
bobArkClient, err := setupArkClient()
|
||||
@@ -137,7 +133,7 @@ func main() {
|
||||
|
||||
fmt.Println("")
|
||||
log.Info("bob is claiming the incoming payment...")
|
||||
roundTxid, err := bobArkClient.ClaimAsync(ctx)
|
||||
roundTxid, err := bobArkClient.Claim(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,35 @@ const (
|
||||
)
|
||||
|
||||
type Utxo struct {
|
||||
Txid string
|
||||
Vout uint32
|
||||
Amount uint64
|
||||
Asset string // liquid only
|
||||
Delay uint
|
||||
SpendableAt time.Time
|
||||
}
|
||||
|
||||
func (u *Utxo) Sequence() (uint32, error) {
|
||||
return common.BIP68EncodeAsNumber(u.Delay)
|
||||
}
|
||||
|
||||
func newUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
|
||||
utxoTime := explorerUtxo.Status.Blocktime
|
||||
if utxoTime == 0 {
|
||||
utxoTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return Utxo{
|
||||
Txid: explorerUtxo.Txid,
|
||||
Vout: explorerUtxo.Vout,
|
||||
Amount: explorerUtxo.Amount,
|
||||
Asset: explorerUtxo.Asset,
|
||||
Delay: delay,
|
||||
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
type ExplorerUtxo struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout uint32 `json:"vout"`
|
||||
Amount uint64 `json:"value"`
|
||||
@@ -35,10 +64,14 @@ type Utxo struct {
|
||||
} `json:"status"`
|
||||
}
|
||||
|
||||
func (e ExplorerUtxo) ToUtxo(delay uint) Utxo {
|
||||
return newUtxo(e, delay)
|
||||
}
|
||||
|
||||
type Explorer interface {
|
||||
GetTxHex(txid string) (string, error)
|
||||
Broadcast(txHex string) (string, error)
|
||||
GetUtxos(addr string) ([]Utxo, error)
|
||||
GetUtxos(addr string) ([]ExplorerUtxo, error)
|
||||
GetBalance(addr string) (uint64, error)
|
||||
GetRedeemedVtxosBalance(
|
||||
addr string, unilateralExitDelay int64,
|
||||
@@ -143,7 +176,7 @@ func (e *explorerSvc) Broadcast(txStr string) (string, error) {
|
||||
return txid, nil
|
||||
}
|
||||
|
||||
func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
|
||||
func (e *explorerSvc) GetUtxos(addr string) ([]ExplorerUtxo, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,7 +190,7 @@ func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(string(body))
|
||||
}
|
||||
payload := []Utxo{}
|
||||
payload := []ExplorerUtxo{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ module github.com/ark-network/ark/pkg/client-sdk
|
||||
|
||||
go 1.22.6
|
||||
|
||||
replace github.com/ark-network/ark/common => ../../common
|
||||
|
||||
require (
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87
|
||||
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87 h1:VBY4KqHqxE4q6NnmvEZTLvLZoNA0Q6NhMhjBs1hzy9Y=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240815203029-edc4534dfc87/go.mod h1:m5H86Dx+k8cQjLXeYL1MV+h3x/XnhJCXJP/PL3KgZqY=
|
||||
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87 h1:TIv00zlpxLKmY2LjFAIMF8RxNtn9rFqQsv73Lwoj2ds=
|
||||
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87/go.mod h1:aYAGDfoeBLofnZt9n85wusFyCkrS7hvwdo5TynBlkuY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
|
||||
@@ -29,6 +29,7 @@ type storeData struct {
|
||||
RoundLifetime string `json:"round_lifetime"`
|
||||
UnilateralExitDelay string `json:"unilateral_exit_delay"`
|
||||
MinRelayFee string `json:"min_relay_fee"`
|
||||
BoardingDescriptorTemplate string `json:"boarding_descriptor_template"`
|
||||
}
|
||||
|
||||
func (d storeData) isEmpty() bool {
|
||||
@@ -51,6 +52,7 @@ func (d storeData) decode() store.StoreData {
|
||||
RoundLifetime: int64(roundLifetime),
|
||||
UnilateralExitDelay: int64(unilateralExitDelay),
|
||||
MinRelayFee: uint64(minRelayFee),
|
||||
BoardingDescriptorTemplate: d.BoardingDescriptorTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ func (d storeData) asMap() map[string]string {
|
||||
"round_lifetime": d.RoundLifetime,
|
||||
"unilateral_exit_delay": d.UnilateralExitDelay,
|
||||
"min_relay_fee": d.MinRelayFee,
|
||||
"boarding_descriptor_template": d.BoardingDescriptorTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +111,7 @@ func (s *Store) AddData(ctx context.Context, data store.StoreData) error {
|
||||
RoundLifetime: fmt.Sprintf("%d", data.RoundLifetime),
|
||||
UnilateralExitDelay: fmt.Sprintf("%d", data.UnilateralExitDelay),
|
||||
MinRelayFee: fmt.Sprintf("%d", data.MinRelayFee),
|
||||
BoardingDescriptorTemplate: data.BoardingDescriptorTemplate,
|
||||
}
|
||||
|
||||
if err := s.write(sd); err != nil {
|
||||
|
||||
@@ -21,6 +21,7 @@ type StoreData struct {
|
||||
RoundLifetime int64
|
||||
UnilateralExitDelay int64
|
||||
MinRelayFee uint64
|
||||
BoardingDescriptorTemplate string
|
||||
}
|
||||
|
||||
type ConfigStore interface {
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestStore(t *testing.T) {
|
||||
RoundLifetime: 512,
|
||||
UnilateralExitDelay: 512,
|
||||
MinRelayFee: 300,
|
||||
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
||||
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
@@ -41,42 +41,42 @@ func NewBitcoinWallet(
|
||||
func (w *bitcoinWallet) GetAddresses(
|
||||
ctx context.Context,
|
||||
) ([]string, []string, []string, error) {
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := []string{offchainAddr}
|
||||
onchainAddrs := []string{onchainAddr}
|
||||
boardingAddrs := []string{boardingAddr}
|
||||
redemptionAddrs := []string{redemptionAddr}
|
||||
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
|
||||
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
|
||||
}
|
||||
|
||||
func (w *bitcoinWallet) NewAddress(
|
||||
ctx context.Context, _ bool,
|
||||
) (string, string, error) {
|
||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return offchainAddr, onchainAddr, nil
|
||||
return offchainAddr, boardingAddr, nil
|
||||
}
|
||||
|
||||
func (w *bitcoinWallet) NewAddresses(
|
||||
ctx context.Context, _ bool, num int,
|
||||
) ([]string, []string, error) {
|
||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := make([]string, 0, num)
|
||||
onchainAddrs := make([]string, 0, num)
|
||||
boardingAddrs := make([]string, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
offchainAddrs = append(offchainAddrs, offchainAddr)
|
||||
onchainAddrs = append(onchainAddrs, onchainAddr)
|
||||
boardingAddrs = append(boardingAddrs, boardingAddr)
|
||||
}
|
||||
return offchainAddrs, onchainAddrs, nil
|
||||
return offchainAddrs, boardingAddrs, nil
|
||||
}
|
||||
|
||||
func (s *bitcoinWallet) SignTransaction(
|
||||
@@ -92,11 +92,6 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := s.configStore.GetData(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, input := range updater.Upsbt.UnsignedTx.TxIn {
|
||||
if updater.Upsbt.Inputs[i].WitnessUtxo != nil {
|
||||
continue
|
||||
@@ -122,28 +117,11 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.PkScript[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(sighashType, i); err != nil {
|
||||
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
_, onchainAddr, _, err := s.getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
net := utils.ToBitcoinNetwork(data.Network)
|
||||
addr, _ := btcutil.DecodeAddress(onchainAddr, &net)
|
||||
onchainWalletScript, err := txscript.PayToAddrScript(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
prevouts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
for i, input := range updater.Upsbt.Inputs {
|
||||
@@ -158,36 +136,6 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
|
||||
|
||||
for i, input := range ptx.Inputs {
|
||||
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
|
||||
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
preimage, err := txscript.CalcWitnessSigHash(
|
||||
input.WitnessUtxo.PkScript,
|
||||
txsighashes,
|
||||
txscript.SigHashAll,
|
||||
updater.Upsbt.UnsignedTx,
|
||||
i,
|
||||
int64(input.WitnessUtxo.Value),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sig := ecdsa.Sign(s.privateKey, preimage)
|
||||
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||
|
||||
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
|
||||
{
|
||||
PubKey: s.walletData.Pubkey.SerializeCompressed(),
|
||||
Signature: signatureWithSighashType,
|
||||
},
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TaprootLeafScript) > 0 {
|
||||
pubkey := s.walletData.Pubkey
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
@@ -269,11 +217,6 @@ func (w *bitcoinWallet) getAddress(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(data.Network)
|
||||
|
||||
onchainAddr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(w.walletData.Pubkey.SerializeCompressed()), &netParams)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
|
||||
)
|
||||
@@ -289,5 +232,35 @@ func (w *bitcoinWallet) getAddress(
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return offchainAddr, onchainAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingAddr, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(boardingTapKey),
|
||||
&netParams,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return offchainAddr, boardingAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
|
||||
}
|
||||
|
||||
@@ -3,20 +3,21 @@ package singlekeywallet
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
||||
walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -40,42 +41,42 @@ func NewLiquidWallet(
|
||||
func (w *liquidWallet) GetAddresses(
|
||||
ctx context.Context,
|
||||
) ([]string, []string, []string, error) {
|
||||
offchainAddr, onchainAddr, redemptionAddr, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := []string{offchainAddr}
|
||||
onchainAddrs := []string{onchainAddr}
|
||||
boardingAddrs := []string{boardingAddr}
|
||||
redemptionAddrs := []string{redemptionAddr}
|
||||
return offchainAddrs, onchainAddrs, redemptionAddrs, nil
|
||||
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
|
||||
}
|
||||
|
||||
func (w *liquidWallet) NewAddress(
|
||||
ctx context.Context, _ bool,
|
||||
) (string, string, error) {
|
||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return offchainAddr, onchainAddr, nil
|
||||
return offchainAddr, boardingAddr, nil
|
||||
}
|
||||
|
||||
func (w *liquidWallet) NewAddresses(
|
||||
ctx context.Context, _ bool, num int,
|
||||
) ([]string, []string, error) {
|
||||
offchainAddr, onchainAddr, _, err := w.getAddress(ctx)
|
||||
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := make([]string, 0, num)
|
||||
onchainAddrs := make([]string, 0, num)
|
||||
boardingAddrs := make([]string, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
offchainAddrs = append(offchainAddrs, offchainAddr)
|
||||
onchainAddrs = append(onchainAddrs, onchainAddr)
|
||||
boardingAddrs = append(boardingAddrs, boardingAddr)
|
||||
}
|
||||
return offchainAddrs, onchainAddrs, nil
|
||||
return offchainAddrs, boardingAddrs, nil
|
||||
}
|
||||
|
||||
func (s *liquidWallet) SignTransaction(
|
||||
@@ -114,13 +115,7 @@ func (s *liquidWallet) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
|
||||
sighashType := txscript.SigHashAll
|
||||
|
||||
if utxo.Script[0] == txscript.OP_1 {
|
||||
sighashType = txscript.SigHashDefault
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(i, sighashType); err != nil {
|
||||
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -135,8 +130,6 @@ func (s *liquidWallet) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
liquidNet := utils.ToElementsNetwork(storeData.Network)
|
||||
p2wpkh := payment.FromPublicKey(s.walletData.Pubkey, &liquidNet, nil)
|
||||
onchainWalletScript := p2wpkh.WitnessScript
|
||||
|
||||
utx, err := pset.UnsignedTx()
|
||||
if err != nil {
|
||||
@@ -156,33 +149,6 @@ func (s *liquidWallet) SignTransaction(
|
||||
serializedPubKey := s.walletData.Pubkey.SerializeCompressed()
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
prevout := input.GetUtxo()
|
||||
|
||||
if bytes.Equal(prevout.Script, onchainWalletScript) {
|
||||
p, err := payment.FromScript(prevout.Script, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
preimage := utx.HashForWitnessV0(
|
||||
i, p.Script, prevout.Value, txscript.SigHashAll,
|
||||
)
|
||||
|
||||
sig := ecdsa.Sign(s.privateKey, preimage[:])
|
||||
|
||||
signatureWithSighashType := append(
|
||||
sig.Serialize(), byte(txscript.SigHashAll),
|
||||
)
|
||||
|
||||
err = signer.SignInput(
|
||||
i, signatureWithSighashType, serializedPubKey, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(input.TapLeafScript) > 0 {
|
||||
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
|
||||
if err != nil {
|
||||
@@ -276,12 +242,6 @@ func (w *liquidWallet) getAddress(
|
||||
|
||||
liquidNet := utils.ToElementsNetwork(data.Network)
|
||||
|
||||
p2wpkh := payment.FromPublicKey(w.walletData.Pubkey, &liquidNet, nil)
|
||||
onchainAddr, err := p2wpkh.WitnessPubKeyHash()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
|
||||
)
|
||||
@@ -289,5 +249,27 @@ func (w *liquidWallet) getAddress(
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return offchainAddr, onchainAddr, redemptionAddr, nil
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet,
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return offchainAddr, boardingAddr, redemptionAddr, nil
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type WalletService interface {
|
||||
IsLocked() bool
|
||||
GetAddresses(
|
||||
ctx context.Context,
|
||||
) (offchainAddresses, onchainAddresses, redemptionAddresses []string, err error)
|
||||
) (offchainAddresses, boardingAddresses, redemptionAddresses []string, err error)
|
||||
NewAddress(
|
||||
ctx context.Context, change bool,
|
||||
) (offchainAddr, onchainAddr string, err error)
|
||||
@@ -29,5 +29,5 @@ type WalletService interface {
|
||||
) (offchainAddresses, onchainAddresses []string, err error)
|
||||
SignTransaction(
|
||||
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
||||
) (singedTx string, err error)
|
||||
) (signedTx string, err error)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestWallet(t *testing.T) {
|
||||
RoundLifetime: 512,
|
||||
UnilateralExitDelay: 512,
|
||||
MinRelayFee: 300,
|
||||
BoardingDescriptorTemplate: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(USER)), and(older(604672), pk(USER)) })",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -27,7 +27,6 @@ func init() {
|
||||
js.Global().Set("lock", LockWrapper())
|
||||
js.Global().Set("locked", IsLockedWrapper())
|
||||
js.Global().Set("balance", BalanceWrapper())
|
||||
js.Global().Set("onboard", OnboardWrapper())
|
||||
js.Global().Set("receive", ReceiveWrapper())
|
||||
js.Global().Set("sendOnChain", SendOnChainWrapper())
|
||||
js.Global().Set("sendOffChain", SendOffChainWrapper())
|
||||
|
||||
@@ -140,33 +140,18 @@ func BalanceWrapper() js.Func {
|
||||
})
|
||||
}
|
||||
|
||||
func OnboardWrapper() js.Func {
|
||||
return JSPromise(func(args []js.Value) (interface{}, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, errors.New("invalid number of args")
|
||||
}
|
||||
amount := uint64(args[0].Int())
|
||||
|
||||
txID, err := arkSdkClient.Onboard(context.Background(), amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return js.ValueOf(txID), nil
|
||||
})
|
||||
}
|
||||
|
||||
func ReceiveWrapper() js.Func {
|
||||
return JSPromise(func(args []js.Value) (interface{}, error) {
|
||||
if arkSdkClient == nil {
|
||||
return nil, errors.New("ARK SDK client is not initialized")
|
||||
}
|
||||
offchainAddr, onchainAddr, err := arkSdkClient.Receive(context.Background())
|
||||
offchainAddr, boardingAddr, err := arkSdkClient.Receive(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"offchainAddr": offchainAddr,
|
||||
"onchainAddr": onchainAddr,
|
||||
"boardingAddr": boardingAddr,
|
||||
}
|
||||
return js.ValueOf(result), nil
|
||||
})
|
||||
|
||||
@@ -79,6 +79,7 @@ func mainAction(_ *cli.Context) error {
|
||||
BitcoindRpcUser: cfg.BitcoindRpcUser,
|
||||
BitcoindRpcPass: cfg.BitcoindRpcPass,
|
||||
BitcoindRpcHost: cfg.BitcoindRpcHost,
|
||||
BoardingExitDelay: cfg.BoardingExitDelay,
|
||||
}
|
||||
svc, err := grpcservice.NewService(svcConfig, appConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ module github.com/ark-network/ark/server
|
||||
|
||||
go 1.22.6
|
||||
|
||||
replace github.com/ark-network/ark/common => ../common
|
||||
|
||||
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
|
||||
require (
|
||||
|
||||
@@ -24,8 +24,6 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
|
||||
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
|
||||
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
|
||||
github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812233307-18e343b31899 h1:pUtYz5kx/hvm8EiF4aj1BNdLFKMGnV3g4s1lQST9jFg=
|
||||
github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812233307-18e343b31899/go.mod h1:8DXpdLoJXeIPh3JPd6AoANVTf7Rw63QLL8l+OJkNBlU=
|
||||
github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812233307-18e343b31899 h1:Vlc9pbGToiqeBbAx3q4Wovg/DK6DJXAE2k5YAQiqza4=
|
||||
|
||||
@@ -63,6 +63,7 @@ type Config struct {
|
||||
MinRelayFee uint64
|
||||
RoundLifetime int64
|
||||
UnilateralExitDelay int64
|
||||
BoardingExitDelay int64
|
||||
|
||||
EsploraURL string
|
||||
NeutrinoPeer string
|
||||
@@ -126,6 +127,12 @@ func (c *Config) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
if c.BoardingExitDelay < minAllowedSequence {
|
||||
return fmt.Errorf(
|
||||
"invalid boarding exit delay, must at least %d", minAllowedSequence,
|
||||
)
|
||||
}
|
||||
|
||||
if c.RoundLifetime%minAllowedSequence != 0 {
|
||||
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
|
||||
log.Infof(
|
||||
@@ -142,6 +149,14 @@ func (c *Config) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
if c.BoardingExitDelay%minAllowedSequence != 0 {
|
||||
c.BoardingExitDelay -= c.BoardingExitDelay % minAllowedSequence
|
||||
log.Infof(
|
||||
"boarding exit delay must be a multiple of %d, rounded to %d",
|
||||
minAllowedSequence, c.BoardingExitDelay,
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.repoManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,11 +290,11 @@ func (c *Config) txBuilderService() error {
|
||||
switch c.TxBuilderType {
|
||||
case "covenant":
|
||||
svc = txbuilder.NewTxBuilder(
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
)
|
||||
case "covenantless":
|
||||
svc = cltxbuilder.NewTxBuilder(
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("unknown tx builder type")
|
||||
@@ -323,7 +338,7 @@ func (c *Config) schedulerService() error {
|
||||
func (c *Config) appService() error {
|
||||
if common.IsLiquid(c.Network) {
|
||||
svc, err := application.NewCovenantService(
|
||||
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
|
||||
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -335,7 +350,7 @@ func (c *Config) appService() error {
|
||||
}
|
||||
|
||||
svc, err := application.NewCovenantlessService(
|
||||
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
|
||||
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,6 +29,7 @@ type Config struct {
|
||||
MinRelayFee uint64
|
||||
RoundLifetime int64
|
||||
UnilateralExitDelay int64
|
||||
BoardingExitDelay int64
|
||||
EsploraURL string
|
||||
NeutrinoPeer string
|
||||
BitcoindRpcUser string
|
||||
@@ -54,6 +55,7 @@ var (
|
||||
MinRelayFee = "MIN_RELAY_FEE"
|
||||
RoundLifetime = "ROUND_LIFETIME"
|
||||
UnilateralExitDelay = "UNILATERAL_EXIT_DELAY"
|
||||
BoardingExitDelay = "BOARDING_EXIT_DELAY"
|
||||
EsploraURL = "ESPLORA_URL"
|
||||
NeutrinoPeer = "NEUTRINO_PEER"
|
||||
BitcoindRpcUser = "BITCOIND_RPC_USER"
|
||||
@@ -79,6 +81,7 @@ var (
|
||||
defaultMinRelayFee = 30 // 0.1 sat/vbyte on Liquid
|
||||
defaultRoundLifetime = 604672
|
||||
defaultUnilateralExitDelay = 1024
|
||||
defaultBoardingExitDelay = 604672
|
||||
defaultNoMacaroons = false
|
||||
defaultNoTLS = false
|
||||
)
|
||||
@@ -104,6 +107,7 @@ func LoadConfig() (*Config, error) {
|
||||
viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay)
|
||||
viper.SetDefault(BlockchainScannerType, defaultBlockchainScannerType)
|
||||
viper.SetDefault(NoMacaroons, defaultNoMacaroons)
|
||||
viper.SetDefault(BoardingExitDelay, defaultBoardingExitDelay)
|
||||
|
||||
net, err := getNetwork()
|
||||
if err != nil {
|
||||
@@ -132,6 +136,7 @@ func LoadConfig() (*Config, error) {
|
||||
MinRelayFee: viper.GetUint64(MinRelayFee),
|
||||
RoundLifetime: viper.GetInt64(RoundLifetime),
|
||||
UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay),
|
||||
BoardingExitDelay: viper.GetInt64(BoardingExitDelay),
|
||||
EsploraURL: viper.GetString(EsploraURL),
|
||||
NeutrinoPeer: viper.GetString(NeutrinoPeer),
|
||||
BitcoindRpcUser: viper.GetString(BitcoindRpcUser),
|
||||
|
||||
@@ -10,14 +10,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,6 +34,7 @@ type covenantService struct {
|
||||
roundLifetime int64
|
||||
roundInterval int64
|
||||
unilateralExitDelay int64
|
||||
boardingExitDelay int64
|
||||
minRelayFee uint64
|
||||
|
||||
wallet ports.WalletService
|
||||
@@ -42,22 +47,21 @@ type covenantService struct {
|
||||
forfeitTxs *forfeitTxsMap
|
||||
|
||||
eventsCh chan domain.RoundEvent
|
||||
onboardingCh chan onboarding
|
||||
|
||||
lastEvent domain.RoundEvent
|
||||
currentRoundLock sync.Mutex
|
||||
currentRound *domain.Round
|
||||
lastEvent domain.RoundEvent
|
||||
}
|
||||
|
||||
func NewCovenantService(
|
||||
network common.Network,
|
||||
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
|
||||
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
|
||||
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
||||
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
||||
scheduler ports.SchedulerService,
|
||||
) (Service, error) {
|
||||
eventsCh := make(chan domain.RoundEvent)
|
||||
onboardingCh := make(chan onboarding)
|
||||
paymentRequests := newPaymentsMap(nil)
|
||||
paymentRequests := newPaymentsMap()
|
||||
|
||||
forfeitTxs := newForfeitTxsMap(builder)
|
||||
pubkey, err := walletSvc.GetPubkey(context.Background())
|
||||
@@ -69,9 +73,9 @@ func NewCovenantService(
|
||||
|
||||
svc := &covenantService{
|
||||
network, pubkey,
|
||||
roundLifetime, roundInterval, unilateralExitDelay, minRelayFee,
|
||||
roundLifetime, roundInterval, unilateralExitDelay, boardingExitDelay, minRelayFee,
|
||||
walletSvc, repoManager, builder, scanner, sweeper,
|
||||
paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, nil,
|
||||
paymentRequests, forfeitTxs, eventsCh, sync.Mutex{}, nil, nil,
|
||||
}
|
||||
repoManager.RegisterEventsHandler(
|
||||
func(round *domain.Round) {
|
||||
@@ -88,7 +92,6 @@ func NewCovenantService(
|
||||
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
|
||||
}
|
||||
go svc.listenToScannerNotifications()
|
||||
go svc.listenToOnboarding()
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
@@ -116,11 +119,33 @@ func (s *covenantService) Stop() {
|
||||
s.repoManager.Close()
|
||||
log.Debug("closed connection to db")
|
||||
close(s.eventsCh)
|
||||
close(s.onboardingCh)
|
||||
}
|
||||
|
||||
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
|
||||
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
|
||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) {
|
||||
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
|
||||
vtxosInputs := make([]domain.VtxoKey, 0)
|
||||
boardingInputs := make([]Input, 0)
|
||||
|
||||
for _, in := range inputs {
|
||||
if in.IsVtxo() {
|
||||
vtxosInputs = append(vtxosInputs, in.VtxoKey())
|
||||
continue
|
||||
}
|
||||
boardingInputs = append(boardingInputs, in)
|
||||
}
|
||||
|
||||
vtxos := make([]domain.Vtxo, 0)
|
||||
|
||||
if len(vtxosInputs) > 0 {
|
||||
var err error
|
||||
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -128,18 +153,130 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKe
|
||||
if v.Spent {
|
||||
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
|
||||
}
|
||||
|
||||
if v.Redeemed {
|
||||
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
|
||||
}
|
||||
|
||||
if v.Spent {
|
||||
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boardingTxs := make(map[string]string, 0) // txid -> txhex
|
||||
now := time.Now().Unix()
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
if _, ok := boardingTxs[in.Txid]; !ok {
|
||||
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
|
||||
}
|
||||
|
||||
if blocktime+int64(s.boardingExitDelay) < now {
|
||||
return "", fmt.Errorf("tx %s expired", in.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[in.Txid] = txhex
|
||||
}
|
||||
}
|
||||
|
||||
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
desc, err := in.GetDescriptor()
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("failed to parse boarding input descriptor")
|
||||
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
|
||||
}
|
||||
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("failed to create boarding input")
|
||||
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
|
||||
}
|
||||
|
||||
utxos = append(utxos, input)
|
||||
}
|
||||
|
||||
payment, err := domain.NewPayment(vtxos)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := s.paymentRequests.push(*payment); err != nil {
|
||||
if err := s.paymentRequests.push(*payment, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return payment.Id, nil
|
||||
}
|
||||
|
||||
func (s *covenantService) newBoardingInput(
|
||||
txhex string, vout uint32, desc descriptor.TaprootDescriptor,
|
||||
) (ports.BoardingInput, error) {
|
||||
tx, err := transaction.NewTxFromHex(txhex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse tx: %s", err)
|
||||
}
|
||||
|
||||
if len(tx.Outputs) <= int(vout) {
|
||||
return nil, fmt.Errorf("output not found")
|
||||
}
|
||||
|
||||
out := tx.Outputs[vout]
|
||||
script := out.Script
|
||||
|
||||
if len(out.RangeProof) > 0 || len(out.SurjectionProof) > 0 {
|
||||
return nil, fmt.Errorf("output is confidential")
|
||||
}
|
||||
|
||||
scriptFromDescriptor, err := tree.ComputeOutputScript(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compute output script: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(script, scriptFromDescriptor) {
|
||||
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
||||
}
|
||||
|
||||
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
|
||||
}
|
||||
|
||||
if timeout != uint(s.boardingExitDelay) {
|
||||
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
|
||||
}
|
||||
|
||||
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compute boarding script: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(script, expectedScript) {
|
||||
return nil, fmt.Errorf("output script mismatch expected script")
|
||||
}
|
||||
|
||||
value, err := elementsutil.ValueFromBytes(out.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse value: %s", err)
|
||||
}
|
||||
|
||||
return &boardingInput{
|
||||
txId: tx.TxHash(),
|
||||
vout: vout,
|
||||
boardingPubKey: pubkey,
|
||||
amount: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *covenantService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
|
||||
// Check credentials
|
||||
payment, ok := s.paymentRequests.view(creds)
|
||||
@@ -178,6 +315,19 @@ func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) er
|
||||
return s.forfeitTxs.sign(forfeitTxs)
|
||||
}
|
||||
|
||||
func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
|
||||
s.currentRoundLock.Lock()
|
||||
defer s.currentRoundLock.Unlock()
|
||||
|
||||
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.currentRound.UnsignedTx = combined
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
|
||||
pk := hex.EncodeToString(pubkey.SerializeCompressed())
|
||||
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
|
||||
@@ -209,50 +359,17 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
|
||||
RoundInterval: s.roundInterval,
|
||||
Network: s.network.Name,
|
||||
MinRelayFee: int64(s.minRelayFee),
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.BoardingDescriptorTemplate,
|
||||
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
"USER",
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *covenantService) Onboard(
|
||||
ctx context.Context, boardingTx string,
|
||||
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
ptx, err := psetv2.NewPsetFromBase64(boardingTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse boarding tx: %s", err)
|
||||
}
|
||||
|
||||
if err := tree.ValidateCongestionTree(
|
||||
congestionTree, boardingTx, s.pubkey, s.roundLifetime,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extracted, err := psetv2.Extract(ptx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract boarding tx: %s", err)
|
||||
}
|
||||
|
||||
boardingTxHex, err := extracted.ToHex()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert boarding tx to hex: %s", err)
|
||||
}
|
||||
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, boardingTxHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
|
||||
}
|
||||
|
||||
log.Debugf("broadcasted boarding tx %s", txid)
|
||||
|
||||
s.onboardingCh <- onboarding{
|
||||
tx: boardingTx,
|
||||
congestionTree: congestionTree,
|
||||
userPubkey: userPubkey,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *covenantService) RegisterCosignerPubkey(ctx context.Context, paymentId string, _ string) error {
|
||||
// if the user sends an ephemeral pubkey, something is going wrong client-side
|
||||
// we should delete the associated payment
|
||||
@@ -329,7 +446,7 @@ func (s *covenantService) startFinalization() {
|
||||
if num > paymentsThreshold {
|
||||
num = paymentsThreshold
|
||||
}
|
||||
payments, _ := s.paymentRequests.pop(num)
|
||||
payments, boardingInputs, _ := s.paymentRequests.pop(num)
|
||||
if _, err := round.RegisterPayments(payments); err != nil {
|
||||
round.Fail(fmt.Errorf("failed to register payments: %s", err))
|
||||
log.WithError(err).Warn("failed to register payments")
|
||||
@@ -343,7 +460,7 @@ func (s *covenantService) startFinalization() {
|
||||
return
|
||||
}
|
||||
|
||||
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds)
|
||||
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds)
|
||||
if err != nil {
|
||||
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
|
||||
log.WithError(err).Warn("failed to create pool tx")
|
||||
@@ -351,9 +468,18 @@ func (s *covenantService) startFinalization() {
|
||||
}
|
||||
log.Debugf("pool tx created for round %s", round.Id)
|
||||
|
||||
// TODO BTC make the senders sign the tree
|
||||
needForfeits := false
|
||||
for _, pay := range payments {
|
||||
if len(pay.Inputs) > 0 {
|
||||
needForfeits = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
|
||||
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")
|
||||
@@ -361,6 +487,7 @@ func (s *covenantService) startFinalization() {
|
||||
}
|
||||
|
||||
log.Debugf("forfeit transactions created for round %s", round.Id)
|
||||
}
|
||||
|
||||
if _, err := round.StartFinalization(
|
||||
connectorAddress, connectors, tree, unsignedPoolTx,
|
||||
@@ -401,73 +528,65 @@ func (s *covenantService) finalizeRound() {
|
||||
}
|
||||
|
||||
log.Debugf("signing round transaction %s\n", round.Id)
|
||||
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
|
||||
|
||||
boardingInputs := make([]int, 0)
|
||||
roundTx, err := psetv2.NewPsetFromBase64(round.UnsignedTx)
|
||||
if err != nil {
|
||||
log.Debugf("failed to parse round tx: %s", round.UnsignedTx)
|
||||
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to parse round tx")
|
||||
return
|
||||
}
|
||||
|
||||
for i, in := range roundTx.Inputs {
|
||||
if len(in.TapLeafScript) > 0 {
|
||||
if len(in.TapScriptSig) == 0 {
|
||||
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
|
||||
changes = round.Fail(err)
|
||||
log.WithError(err).Warn("missing boarding sig")
|
||||
return
|
||||
}
|
||||
|
||||
boardingInputs = append(boardingInputs, i)
|
||||
}
|
||||
}
|
||||
|
||||
signedRoundTx := round.UnsignedTx
|
||||
|
||||
if len(boardingInputs) > 0 {
|
||||
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to sign round tx")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to sign round tx")
|
||||
return
|
||||
}
|
||||
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
|
||||
if err != nil {
|
||||
log.Debugf("failed to broadcast round tx: %s", signedRoundTx)
|
||||
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
|
||||
log.WithError(err).Warn("failed to broadcast pool tx")
|
||||
return
|
||||
}
|
||||
|
||||
changes, _ = round.EndFinalization(forfeitTxs, txid)
|
||||
changes, err = round.EndFinalization(forfeitTxs, txid)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
|
||||
log.WithError(err).Warn("failed to finalize round")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
|
||||
}
|
||||
|
||||
func (s *covenantService) 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
|
||||
}
|
||||
}
|
||||
|
||||
func (s *covenantService) listenToScannerNotifications() {
|
||||
ctx := context.Background()
|
||||
chVtxos := s.scanner.GetNotificationChannel(ctx)
|
||||
@@ -867,23 +986,6 @@ func (s *covenantService) saveEvents(
|
||||
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
|
||||
}
|
||||
|
||||
func getPaymentsFromOnboardingLiquid(
|
||||
congestionTree tree.CongestionTree, userKey string,
|
||||
) []domain.Payment {
|
||||
leaves := congestionTree.Leaves()
|
||||
receivers := make([]domain.Receiver, 0, len(leaves))
|
||||
for _, node := range leaves {
|
||||
ptx, _ := psetv2.NewPsetFromBase64(node.Tx)
|
||||
receiver := domain.Receiver{
|
||||
Pubkey: userKey,
|
||||
Amount: ptx.Outputs[0].Value,
|
||||
}
|
||||
receivers = append(receivers, receiver)
|
||||
}
|
||||
payment := domain.NewPaymentUnsafe(nil, receivers)
|
||||
return []domain.Payment{*payment}
|
||||
}
|
||||
|
||||
func findForfeitTxLiquid(
|
||||
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
|
||||
) (string, error) {
|
||||
|
||||
@@ -11,12 +11,15 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -27,6 +30,7 @@ type covenantlessService struct {
|
||||
roundLifetime int64
|
||||
roundInterval int64
|
||||
unilateralExitDelay int64
|
||||
boardingExitDelay int64
|
||||
minRelayFee uint64
|
||||
|
||||
wallet ports.WalletService
|
||||
@@ -39,10 +43,10 @@ type covenantlessService struct {
|
||||
forfeitTxs *forfeitTxsMap
|
||||
|
||||
eventsCh chan domain.RoundEvent
|
||||
onboardingCh chan onboarding
|
||||
|
||||
// cached data for the current round
|
||||
lastEvent domain.RoundEvent
|
||||
currentRoundLock sync.Mutex
|
||||
currentRound *domain.Round
|
||||
treeSigningSessions map[string]*musigSigningSession
|
||||
asyncPaymentsCache map[domain.VtxoKey]struct {
|
||||
@@ -53,14 +57,13 @@ type covenantlessService struct {
|
||||
|
||||
func NewCovenantlessService(
|
||||
network common.Network,
|
||||
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
|
||||
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
|
||||
walletSvc ports.WalletService, repoManager ports.RepoManager,
|
||||
builder ports.TxBuilder, scanner ports.BlockchainScanner,
|
||||
scheduler ports.SchedulerService,
|
||||
) (Service, error) {
|
||||
eventsCh := make(chan domain.RoundEvent)
|
||||
onboardingCh := make(chan onboarding)
|
||||
paymentRequests := newPaymentsMap(nil)
|
||||
paymentRequests := newPaymentsMap()
|
||||
|
||||
forfeitTxs := newForfeitTxsMap(builder)
|
||||
pubkey, err := walletSvc.GetPubkey(context.Background())
|
||||
@@ -89,9 +92,10 @@ func NewCovenantlessService(
|
||||
paymentRequests: paymentRequests,
|
||||
forfeitTxs: forfeitTxs,
|
||||
eventsCh: eventsCh,
|
||||
onboardingCh: onboardingCh,
|
||||
currentRoundLock: sync.Mutex{},
|
||||
asyncPaymentsCache: asyncPaymentsCache,
|
||||
treeSigningSessions: make(map[string]*musigSigningSession),
|
||||
boardingExitDelay: boardingExitDelay,
|
||||
}
|
||||
|
||||
repoManager.RegisterEventsHandler(
|
||||
@@ -109,7 +113,6 @@ func NewCovenantlessService(
|
||||
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
|
||||
}
|
||||
go svc.listenToScannerNotifications()
|
||||
go svc.listenToOnboarding()
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
@@ -137,7 +140,6 @@ func (s *covenantlessService) Stop() {
|
||||
s.repoManager.Close()
|
||||
log.Debug("closed connection to db")
|
||||
close(s.eventsCh)
|
||||
close(s.onboardingCh)
|
||||
}
|
||||
|
||||
func (s *covenantlessService) CompleteAsyncPayment(
|
||||
@@ -242,8 +244,23 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
return res.RedeemTx, res.UnconditionalForfeitTxs, nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
|
||||
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
|
||||
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
|
||||
vtxosInputs := make([]domain.VtxoKey, 0)
|
||||
boardingInputs := make([]Input, 0)
|
||||
|
||||
for _, input := range inputs {
|
||||
if input.IsVtxo() {
|
||||
vtxosInputs = append(vtxosInputs, input.VtxoKey())
|
||||
continue
|
||||
}
|
||||
|
||||
boardingInputs = append(boardingInputs, input)
|
||||
}
|
||||
|
||||
vtxos := make([]domain.Vtxo, 0)
|
||||
if len(vtxosInputs) > 0 {
|
||||
var err error
|
||||
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -251,18 +268,123 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.Vt
|
||||
if v.Spent {
|
||||
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
|
||||
}
|
||||
|
||||
if v.Redeemed {
|
||||
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
|
||||
}
|
||||
|
||||
if v.Spent {
|
||||
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boardingTxs := make(map[string]string, 0) // txid -> txhex
|
||||
now := time.Now().Unix()
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
if _, ok := boardingTxs[in.Txid]; !ok {
|
||||
// check if the tx exists and is confirmed
|
||||
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
|
||||
}
|
||||
|
||||
// if the exit path is available, forbid registering the boarding utxo
|
||||
if blocktime+int64(s.boardingExitDelay) < now {
|
||||
return "", fmt.Errorf("tx %s expired", in.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[in.Txid] = txhex
|
||||
}
|
||||
}
|
||||
|
||||
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
desc, err := in.GetDescriptor()
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("failed to parse boarding input descriptor")
|
||||
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
|
||||
}
|
||||
|
||||
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("failed to create boarding input")
|
||||
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
|
||||
}
|
||||
|
||||
utxos = append(utxos, input)
|
||||
}
|
||||
|
||||
payment, err := domain.NewPayment(vtxos)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := s.paymentRequests.push(*payment); err != nil {
|
||||
if err := s.paymentRequests.push(*payment, utxos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return payment.Id, nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) newBoardingInput(txhex string, vout uint32, desc descriptor.TaprootDescriptor) (ports.BoardingInput, error) {
|
||||
var tx wire.MsgTx
|
||||
|
||||
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize tx: %s", err)
|
||||
}
|
||||
|
||||
if len(tx.TxOut) <= int(vout) {
|
||||
return nil, fmt.Errorf("output not found")
|
||||
}
|
||||
|
||||
out := tx.TxOut[vout]
|
||||
script := out.PkScript
|
||||
|
||||
scriptFromDescriptor, err := bitcointree.ComputeOutputScript(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compute output script: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(script, scriptFromDescriptor) {
|
||||
return nil, fmt.Errorf("descriptor does not match script in transaction output")
|
||||
}
|
||||
|
||||
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
|
||||
}
|
||||
|
||||
if timeout != uint(s.boardingExitDelay) {
|
||||
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
|
||||
}
|
||||
|
||||
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get boarding script: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(script, expectedScript) {
|
||||
return nil, fmt.Errorf("invalid boarding input output script")
|
||||
}
|
||||
|
||||
return &boardingInput{
|
||||
txId: tx.TxHash(),
|
||||
vout: vout,
|
||||
boardingPubKey: pubkey,
|
||||
amount: uint64(out.Value),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
|
||||
// Check credentials
|
||||
payment, ok := s.paymentRequests.view(creds)
|
||||
@@ -293,6 +415,19 @@ func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string
|
||||
return s.forfeitTxs.sign(forfeitTxs)
|
||||
}
|
||||
|
||||
func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
|
||||
s.currentRoundLock.Lock()
|
||||
defer s.currentRoundLock.Unlock()
|
||||
|
||||
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify and combine partial tx: %s", err)
|
||||
}
|
||||
|
||||
s.currentRound.UnsignedTx = combined
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
|
||||
pk := hex.EncodeToString(pubkey.SerializeCompressed())
|
||||
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
|
||||
@@ -324,50 +459,26 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
|
||||
RoundInterval: s.roundInterval,
|
||||
Network: s.network.Name,
|
||||
MinRelayFee: int64(s.minRelayFee),
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.BoardingDescriptorTemplate,
|
||||
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
"USER",
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO clArk changes the onboard flow (2 rounds ?)
|
||||
func (s *covenantlessService) Onboard(
|
||||
ctx context.Context, boardingTx string,
|
||||
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(boardingTx), true)
|
||||
func (s *covenantlessService) GetBoardingAddress(
|
||||
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
||||
) (string, error) {
|
||||
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse boarding tx: %s", err)
|
||||
return "", fmt.Errorf("failed to compute boarding script: %s", err)
|
||||
}
|
||||
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
congestionTree, boardingTx, s.pubkey, s.roundLifetime, int64(s.minRelayFee),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extracted, err := psbt.Extract(ptx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract boarding tx: %s", err)
|
||||
}
|
||||
|
||||
var serialized bytes.Buffer
|
||||
|
||||
if err := extracted.Serialize(&serialized); err != nil {
|
||||
return fmt.Errorf("failed to serialize boarding tx: %s", err)
|
||||
}
|
||||
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, hex.EncodeToString(serialized.Bytes()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
|
||||
}
|
||||
|
||||
log.Debugf("broadcasted boarding tx %s", txid)
|
||||
|
||||
s.onboardingCh <- onboarding{
|
||||
tx: boardingTx,
|
||||
congestionTree: congestionTree,
|
||||
userPubkey: userPubkey,
|
||||
}
|
||||
|
||||
return nil
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error {
|
||||
@@ -504,7 +615,7 @@ func (s *covenantlessService) startFinalization() {
|
||||
if num > paymentsThreshold {
|
||||
num = paymentsThreshold
|
||||
}
|
||||
payments, cosigners := s.paymentRequests.pop(num)
|
||||
payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
|
||||
if len(payments) > len(cosigners) {
|
||||
err := fmt.Errorf("missing ephemeral key for payments")
|
||||
round.Fail(fmt.Errorf("round aborted: %s", err))
|
||||
@@ -534,7 +645,7 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
cosigners = append(cosigners, ephemeralKey.PubKey())
|
||||
|
||||
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds, cosigners...)
|
||||
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds, cosigners...)
|
||||
if err != nil {
|
||||
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
|
||||
log.WithError(err).Warn("failed to create pool tx")
|
||||
@@ -683,14 +794,25 @@ func (s *covenantlessService) startFinalization() {
|
||||
tree = signedTree
|
||||
}
|
||||
|
||||
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
|
||||
needForfeits := false
|
||||
for _, pay := range payments {
|
||||
if len(pay.Inputs) > 0 {
|
||||
needForfeits = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var forfeitTxs, connectors []string
|
||||
|
||||
if needForfeits {
|
||||
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
|
||||
if err != nil {
|
||||
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
||||
log.WithError(err).Warn("failed to create connectors and forfeit txs")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("forfeit transactions created for round %s", round.Id)
|
||||
}
|
||||
|
||||
if _, err := round.StartFinalization(
|
||||
connectorAddress, connectors, tree, unsignedPoolTx,
|
||||
@@ -763,75 +885,63 @@ func (s *covenantlessService) finalizeRound() {
|
||||
}
|
||||
|
||||
log.Debugf("signing round transaction %s\n", round.Id)
|
||||
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
|
||||
|
||||
boardingInputs := make([]int, 0)
|
||||
roundTx, err := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to parse round tx")
|
||||
return
|
||||
}
|
||||
|
||||
for i, in := range roundTx.Inputs {
|
||||
if len(in.TaprootLeafScript) > 0 {
|
||||
if len(in.TaprootScriptSpendSig) == 0 {
|
||||
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
|
||||
changes = round.Fail(err)
|
||||
log.WithError(err).Warn("missing boarding sig")
|
||||
return
|
||||
}
|
||||
|
||||
boardingInputs = append(boardingInputs, i)
|
||||
}
|
||||
}
|
||||
|
||||
signedRoundTx := round.UnsignedTx
|
||||
|
||||
if len(boardingInputs) > 0 {
|
||||
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to sign round tx")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
|
||||
log.WithError(err).Warn("failed to sign round tx")
|
||||
return
|
||||
}
|
||||
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
|
||||
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
|
||||
log.WithError(err).Warn("failed to broadcast pool tx")
|
||||
return
|
||||
}
|
||||
|
||||
changes, _ = round.EndFinalization(forfeitTxs, txid)
|
||||
changes, err = round.EndFinalization(forfeitTxs, txid)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
|
||||
log.WithError(err).Warn("failed to finalize round")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
|
||||
}
|
||||
|
||||
func (s *covenantlessService) 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
|
||||
}
|
||||
}
|
||||
|
||||
func (s *covenantlessService) listenToScannerNotifications() {
|
||||
ctx := context.Background()
|
||||
chVtxos := s.scanner.GetNotificationChannel(ctx)
|
||||
@@ -1232,24 +1342,6 @@ func (s *covenantlessService) saveEvents(
|
||||
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
|
||||
}
|
||||
|
||||
func getPaymentsFromOnboardingBitcoin(
|
||||
congestionTree tree.CongestionTree, userKey string,
|
||||
) []domain.Payment {
|
||||
leaves := congestionTree.Leaves()
|
||||
receivers := make([]domain.Receiver, 0, len(leaves))
|
||||
for _, node := range leaves {
|
||||
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
|
||||
|
||||
receiver := domain.Receiver{
|
||||
Pubkey: userKey,
|
||||
Amount: uint64(ptx.UnsignedTx.TxOut[0].Value),
|
||||
}
|
||||
receivers = append(receivers, receiver)
|
||||
}
|
||||
payment := domain.NewPaymentUnsafe(nil, receivers)
|
||||
return []domain.Payment{*payment}
|
||||
}
|
||||
|
||||
func findForfeitTxBitcoin(
|
||||
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
|
||||
) (string, error) {
|
||||
|
||||
@@ -2,8 +2,9 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
@@ -16,9 +17,10 @@ var (
|
||||
type Service interface {
|
||||
Start() error
|
||||
Stop()
|
||||
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
|
||||
SpendVtxos(ctx context.Context, inputs []Input) (string, error)
|
||||
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
|
||||
SignVtxos(ctx context.Context, forfeitTxs []string) error
|
||||
SignRoundTx(ctx context.Context, roundTx string) error
|
||||
GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error)
|
||||
GetRoundById(ctx context.Context, id string) (*domain.Round, error)
|
||||
GetCurrentRound(ctx context.Context) (*domain.Round, error)
|
||||
@@ -30,10 +32,6 @@ type Service interface {
|
||||
ctx context.Context, pubkey *secp256k1.PublicKey,
|
||||
) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
|
||||
GetInfo(ctx context.Context) (*ServiceInfo, error)
|
||||
Onboard(
|
||||
ctx context.Context, boardingTx string,
|
||||
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
|
||||
) error
|
||||
// Async payments
|
||||
CreateAsyncPayment(
|
||||
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
|
||||
@@ -41,6 +39,7 @@ type Service interface {
|
||||
CompleteAsyncPayment(
|
||||
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
|
||||
) error
|
||||
GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error)
|
||||
// Tree signing methods
|
||||
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
|
||||
RegisterCosignerNonces(
|
||||
@@ -60,6 +59,7 @@ type ServiceInfo struct {
|
||||
RoundInterval int64
|
||||
Network string
|
||||
MinRelayFee int64
|
||||
BoardingDescriptorTemplate string
|
||||
}
|
||||
|
||||
type WalletStatus struct {
|
||||
@@ -68,10 +68,28 @@ type WalletStatus struct {
|
||||
IsSynced bool
|
||||
}
|
||||
|
||||
type onboarding struct {
|
||||
tx string
|
||||
congestionTree tree.CongestionTree
|
||||
userPubkey *secp256k1.PublicKey
|
||||
type Input struct {
|
||||
Txid string
|
||||
Index uint32
|
||||
Descriptor string
|
||||
}
|
||||
|
||||
func (i Input) IsVtxo() bool {
|
||||
return len(i.Descriptor) <= 0
|
||||
}
|
||||
|
||||
func (i Input) VtxoKey() domain.VtxoKey {
|
||||
return domain.VtxoKey{
|
||||
Txid: i.Txid,
|
||||
VOut: i.Index,
|
||||
}
|
||||
}
|
||||
|
||||
func (i Input) GetDescriptor() (*descriptor.TaprootDescriptor, error) {
|
||||
if i.IsVtxo() {
|
||||
return nil, fmt.Errorf("input is not a boarding input")
|
||||
}
|
||||
return descriptor.ParseTaprootDescriptor(i.Descriptor)
|
||||
}
|
||||
|
||||
type txOutpoint struct {
|
||||
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type timedPayment struct {
|
||||
domain.Payment
|
||||
boardingInputs []ports.BoardingInput
|
||||
timestamp time.Time
|
||||
pingTimestamp time.Time
|
||||
}
|
||||
@@ -26,11 +28,8 @@ type paymentsMap struct {
|
||||
ephemeralKeys map[string]*secp256k1.PublicKey
|
||||
}
|
||||
|
||||
func newPaymentsMap(payments []domain.Payment) *paymentsMap {
|
||||
func newPaymentsMap() *paymentsMap {
|
||||
paymentsById := make(map[string]*timedPayment)
|
||||
for _, p := range payments {
|
||||
paymentsById[p.Id] = &timedPayment{p, time.Now(), time.Time{}}
|
||||
}
|
||||
lock := &sync.RWMutex{}
|
||||
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
|
||||
}
|
||||
@@ -60,7 +59,7 @@ func (m *paymentsMap) delete(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *paymentsMap) push(payment domain.Payment) error {
|
||||
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
@@ -68,7 +67,7 @@ func (m *paymentsMap) push(payment domain.Payment) error {
|
||||
return fmt.Errorf("duplicated inputs")
|
||||
}
|
||||
|
||||
m.payments[payment.Id] = &timedPayment{payment, time.Now(), time.Time{}}
|
||||
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey) {
|
||||
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
@@ -109,8 +108,10 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
|
||||
}
|
||||
|
||||
payments := make([]domain.Payment, 0, num)
|
||||
boardingInputs := make([]ports.BoardingInput, 0)
|
||||
cosigners := make([]*secp256k1.PublicKey, 0, num)
|
||||
for _, p := range paymentsByTime[:num] {
|
||||
boardingInputs = append(boardingInputs, p.boardingInputs...)
|
||||
payments = append(payments, p.Payment)
|
||||
if pubkey, ok := m.ephemeralKeys[p.Payment.Id]; ok {
|
||||
cosigners = append(cosigners, pubkey)
|
||||
@@ -118,7 +119,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
|
||||
}
|
||||
delete(m.payments, p.Id)
|
||||
}
|
||||
return payments, cosigners
|
||||
return payments, boardingInputs, cosigners
|
||||
}
|
||||
|
||||
func (m *paymentsMap) update(payment domain.Payment) error {
|
||||
@@ -312,3 +313,26 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
|
||||
}
|
||||
return vtxos
|
||||
}
|
||||
|
||||
type boardingInput struct {
|
||||
txId chainhash.Hash
|
||||
vout uint32
|
||||
boardingPubKey *secp256k1.PublicKey
|
||||
amount uint64
|
||||
}
|
||||
|
||||
func (b boardingInput) GetHash() chainhash.Hash {
|
||||
return b.txId
|
||||
}
|
||||
|
||||
func (b boardingInput) GetIndex() uint32 {
|
||||
return b.vout
|
||||
}
|
||||
|
||||
func (b boardingInput) GetAmount() uint64 {
|
||||
return b.amount
|
||||
}
|
||||
|
||||
func (b boardingInput) GetBoardingPubkey() *secp256k1.PublicKey {
|
||||
return b.boardingPubKey
|
||||
}
|
||||
|
||||
@@ -28,14 +28,6 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewPaymentUnsafe(inputs []Vtxo, receivers []Receiver) *Payment {
|
||||
return &Payment{
|
||||
Id: uuid.New().String(),
|
||||
Inputs: inputs,
|
||||
Receivers: receivers,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
|
||||
if p.Receivers == nil {
|
||||
p.Receivers = make([]Receiver, 0)
|
||||
@@ -70,18 +62,13 @@ func (p Payment) validate(ignoreOuts bool) error {
|
||||
if len(p.Id) <= 0 {
|
||||
return fmt.Errorf("missing id")
|
||||
}
|
||||
if len(p.Inputs) <= 0 {
|
||||
return fmt.Errorf("missing inputs")
|
||||
}
|
||||
if ignoreOuts {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.Receivers) <= 0 {
|
||||
return fmt.Errorf("missing outputs")
|
||||
}
|
||||
// Check that input and output and output amounts match.
|
||||
inAmount := p.TotalInputAmount()
|
||||
outAmount := uint64(0)
|
||||
for _, r := range p.Receivers {
|
||||
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
|
||||
return fmt.Errorf("missing receiver destination")
|
||||
@@ -89,10 +76,6 @@ func (p Payment) validate(ignoreOuts bool) error {
|
||||
if r.Amount < dustAmount {
|
||||
return fmt.Errorf("receiver amount must be greater than dust")
|
||||
}
|
||||
outAmount += r.Amount
|
||||
}
|
||||
if inAmount != outAmount {
|
||||
return fmt.Errorf("input and output amounts mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ var inputs = []domain.Vtxo{
|
||||
|
||||
func TestPayment(t *testing.T) {
|
||||
t.Run("new_payment", func(t *testing.T) {
|
||||
t.Run("vaild", func(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
payment, err := domain.NewPayment(inputs)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, payment)
|
||||
@@ -30,24 +30,6 @@ func TestPayment(t *testing.T) {
|
||||
require.Exactly(t, inputs, payment.Inputs)
|
||||
require.Empty(t, payment.Receivers)
|
||||
})
|
||||
|
||||
t.Run("invaild", func(t *testing.T) {
|
||||
fixtures := []struct {
|
||||
inputs []domain.Vtxo
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
inputs: nil,
|
||||
expectedErr: "missing inputs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range fixtures {
|
||||
payment, err := domain.NewPayment(f.inputs)
|
||||
require.EqualError(t, err, f.expectedErr)
|
||||
require.Nil(t, payment)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("add_receivers", func(t *testing.T) {
|
||||
@@ -87,15 +69,6 @@ func TestPayment(t *testing.T) {
|
||||
},
|
||||
expectedErr: "receiver amount must be greater than dust",
|
||||
},
|
||||
{
|
||||
receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
expectedErr: "input and output amounts mismatch",
|
||||
},
|
||||
}
|
||||
|
||||
payment, err := domain.NewPayment(inputs)
|
||||
|
||||
@@ -60,39 +60,6 @@ func NewRound(dustAmount uint64) *Round {
|
||||
}
|
||||
}
|
||||
|
||||
func NewFinalizedRound(
|
||||
dustAmount uint64, userKey, poolTxid, poolTx string,
|
||||
congestionTree tree.CongestionTree, payments []Payment,
|
||||
) *Round {
|
||||
r := NewRound(dustAmount)
|
||||
events := []RoundEvent{
|
||||
RoundStarted{
|
||||
Id: r.Id,
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
PaymentsRegistered{
|
||||
Id: r.Id,
|
||||
Payments: payments,
|
||||
},
|
||||
RoundFinalizationStarted{
|
||||
Id: r.Id,
|
||||
CongestionTree: congestionTree,
|
||||
PoolTx: poolTx,
|
||||
},
|
||||
RoundFinalized{
|
||||
Id: r.Id,
|
||||
Txid: poolTxid,
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
r.raise(event)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func NewRoundFromEvents(events []RoundEvent) *Round {
|
||||
r := &Round{}
|
||||
|
||||
@@ -205,8 +172,12 @@ func (r *Round) StartFinalization(connectorAddress string, connectors []string,
|
||||
|
||||
func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent, error) {
|
||||
if len(forfeitTxs) <= 0 {
|
||||
for _, p := range r.Payments {
|
||||
if len(p.Inputs) > 0 {
|
||||
return nil, fmt.Errorf("missing list of signed forfeit txs")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(txid) <= 0 {
|
||||
return nil, fmt.Errorf("missing pool txid")
|
||||
}
|
||||
@@ -216,6 +187,10 @@ func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent,
|
||||
if r.Stage.Ended {
|
||||
return nil, fmt.Errorf("round already finalized")
|
||||
}
|
||||
if forfeitTxs == nil {
|
||||
forfeitTxs = make([]string, 0)
|
||||
}
|
||||
|
||||
event := RoundFinalized{
|
||||
Id: r.Id,
|
||||
Txid: txid,
|
||||
|
||||
@@ -449,6 +449,7 @@ func testEndFinalization(t *testing.T) {
|
||||
Stage: domain.Stage{
|
||||
Code: domain.FinalizationStage,
|
||||
},
|
||||
Payments: paymentsById,
|
||||
},
|
||||
forfeitTxs: nil,
|
||||
txid: txid,
|
||||
|
||||
@@ -16,9 +16,16 @@ type SweepInput interface {
|
||||
GetInternalKey() *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
type BoardingInput interface {
|
||||
GetAmount() uint64
|
||||
GetIndex() uint32
|
||||
GetHash() chainhash.Hash
|
||||
GetBoardingPubkey() *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
type TxBuilder interface {
|
||||
BuildPoolTx(
|
||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
|
||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, minRelayFee uint64, sweptRounds []domain.Round,
|
||||
cosigners ...*secp256k1.PublicKey,
|
||||
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
|
||||
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
|
||||
@@ -33,4 +40,6 @@ type TxBuilder interface {
|
||||
vtxosToSpend []domain.Vtxo,
|
||||
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64,
|
||||
) (*domain.AsyncPaymentTxs, error)
|
||||
GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (addr string, script []byte, err error)
|
||||
VerifyAndCombinePartialTx(dest string, src string) (string, error)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type WalletService interface {
|
||||
MainAccountBalance(ctx context.Context) (uint64, uint64, error)
|
||||
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)
|
||||
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
|
||||
GetTransaction(ctx context.Context, txid string) (string, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ func deserializeEvent(buf []byte) (domain.RoundEvent, error) {
|
||||
}
|
||||
{
|
||||
var event = domain.RoundFinalizationStarted{}
|
||||
if err := json.Unmarshal(buf, &event); err == nil && len(event.Connectors) > 0 {
|
||||
if err := json.Unmarshal(buf, &event); err == nil && len(event.PoolTx) > 0 {
|
||||
return event, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,15 @@ import (
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +32,7 @@ type txBuilder struct {
|
||||
net common.Network
|
||||
roundLifetime int64 // in seconds
|
||||
exitDelay int64 // in seconds
|
||||
boardingExitDelay int64 // in seconds
|
||||
}
|
||||
|
||||
func NewTxBuilder(
|
||||
@@ -36,8 +40,18 @@ func NewTxBuilder(
|
||||
net common.Network,
|
||||
roundLifetime int64,
|
||||
exitDelay int64,
|
||||
boardingExitDelay int64,
|
||||
) ports.TxBuilder {
|
||||
return &txBuilder{wallet, net, roundLifetime, exitDelay}
|
||||
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay}
|
||||
}
|
||||
|
||||
func (b *txBuilder) GetBoardingScript(owner, asp *secp256k1.PublicKey) (string, []byte, error) {
|
||||
addr, script, _, err := b.getBoardingTaproot(owner, asp)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return addr, script, nil
|
||||
}
|
||||
|
||||
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
|
||||
@@ -112,7 +126,11 @@ func (b *txBuilder) BuildForfeitTxs(
|
||||
}
|
||||
|
||||
func (b *txBuilder) BuildPoolTx(
|
||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
payments []domain.Payment,
|
||||
boardingInputs []ports.BoardingInput,
|
||||
minRelayFee uint64,
|
||||
sweptRounds []domain.Round,
|
||||
_ ...*secp256k1.PublicKey, // cosigners are not used in the covenant
|
||||
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
|
||||
// The creation of the tree and the pool tx are tightly coupled:
|
||||
@@ -146,7 +164,7 @@ func (b *txBuilder) BuildPoolTx(
|
||||
}
|
||||
|
||||
ptx, err := b.createPoolTx(
|
||||
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
|
||||
sharedOutputAmount, sharedOutputScript, payments, boardingInputs, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -197,9 +215,19 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
|
||||
|
||||
expirationTime := parentblocktime + lifetime
|
||||
|
||||
amount := uint64(0)
|
||||
for _, out := range pset.Outputs {
|
||||
amount += out.Value
|
||||
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
tx, err := transaction.NewTxFromHex(txhex)
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
sweepInput = &sweepLiquidInput{
|
||||
@@ -208,7 +236,7 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
|
||||
TxIndex: index,
|
||||
},
|
||||
sweepLeaf: sweepLeaf,
|
||||
amount: amount,
|
||||
amount: inputValue,
|
||||
}
|
||||
|
||||
return expirationTime, sweepInput, nil
|
||||
@@ -362,8 +390,11 @@ func (b *txBuilder) getLeafScriptAndTree(
|
||||
}
|
||||
|
||||
func (b *txBuilder) createPoolTx(
|
||||
sharedOutputAmount uint64, sharedOutputScript []byte,
|
||||
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
|
||||
sharedOutputAmount uint64,
|
||||
sharedOutputScript []byte,
|
||||
payments []domain.Payment,
|
||||
boardingInputs []ports.BoardingInput,
|
||||
aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
|
||||
sweptRounds []domain.Round,
|
||||
) (*psetv2.Pset, error) {
|
||||
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
|
||||
@@ -396,11 +427,13 @@ func (b *txBuilder) createPoolTx(
|
||||
})
|
||||
}
|
||||
|
||||
if connectorsAmount > 0 {
|
||||
outputs = append(outputs, psetv2.OutputArgs{
|
||||
Asset: b.onchainNetwork().AssetID,
|
||||
Amount: connectorsAmount,
|
||||
Script: connectorScript,
|
||||
})
|
||||
}
|
||||
|
||||
for _, receiver := range receivers {
|
||||
targetAmount += receiver.Amount
|
||||
@@ -417,6 +450,9 @@ func (b *txBuilder) createPoolTx(
|
||||
})
|
||||
}
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
targetAmount -= in.GetAmount()
|
||||
}
|
||||
ctx := context.Background()
|
||||
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
|
||||
if err != nil {
|
||||
@@ -447,6 +483,48 @@ func (b *txBuilder) createPoolTx(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
if err := updater.AddInputs(
|
||||
[]psetv2.InputArgs{
|
||||
{
|
||||
Txid: in.GetHash().String(),
|
||||
TxIndex: in.GetIndex(),
|
||||
},
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index := len(ptx.Inputs) - 1
|
||||
|
||||
assetBytes, err := elementsutil.AssetHashToBytes(b.onchainNetwork().AssetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert asset to bytes: %s", err)
|
||||
}
|
||||
|
||||
valueBytes, err := elementsutil.ValueToBytes(in.GetAmount())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert value to bytes: %s", err)
|
||||
}
|
||||
|
||||
_, script, tapLeafProof, err := b.getBoardingTaproot(in.GetBoardingPubkey(), aspPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := updater.AddInWitnessUtxo(index, transaction.NewTxOutput(assetBytes, valueBytes, script)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := updater.AddInTapLeafScript(index, psetv2.NewTapLeafScript(*tapLeafProof, tree.UnspendableKey())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := updater.AddInSighashType(index, txscript.SigHashDefault); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := addInputs(updater, utxos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -471,14 +549,23 @@ func (b *txBuilder) createPoolTx(
|
||||
if feeAmount == change {
|
||||
// fees = change, remove change output
|
||||
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
|
||||
ptx.Global.OutputCount--
|
||||
feeAmount += change
|
||||
} else if feeAmount < change {
|
||||
// change covers the fees, reduce change amount
|
||||
if change-feeAmount < dustLimit {
|
||||
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
|
||||
ptx.Global.OutputCount--
|
||||
feeAmount += change
|
||||
} else {
|
||||
ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount
|
||||
}
|
||||
} else {
|
||||
// change is not enough to cover fees, re-select utxos
|
||||
if change > 0 {
|
||||
// remove change output if present
|
||||
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
|
||||
ptx.Global.OutputCount--
|
||||
}
|
||||
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
|
||||
if err != nil {
|
||||
@@ -486,6 +573,9 @@ func (b *txBuilder) createPoolTx(
|
||||
}
|
||||
|
||||
if change > 0 {
|
||||
if change < dustLimit {
|
||||
feeAmount += change
|
||||
} else {
|
||||
if err := updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: b.onchainNetwork().AssetID,
|
||||
@@ -496,6 +586,7 @@ func (b *txBuilder) createPoolTx(
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := addInputs(updater, newUtxos); err != nil {
|
||||
return nil, err
|
||||
@@ -541,6 +632,77 @@ func (b *txBuilder) createPoolTx(
|
||||
return ptx, nil
|
||||
}
|
||||
|
||||
// This method aims to verify and add partial signature from boarding input
|
||||
func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, error) {
|
||||
roundPset, err := psetv2.NewPsetFromBase64(dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sourcePset, err := psetv2.NewPsetFromBase64(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
roundUtx, err := roundPset.UnsignedTx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sourceUtx, err := sourcePset.UnsignedTx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if roundUtx.TxHash().String() != sourceUtx.TxHash().String() {
|
||||
return "", fmt.Errorf("txid mismatch")
|
||||
}
|
||||
|
||||
roundSigner, err := psetv2.NewSigner(roundPset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, input := range sourcePset.Inputs {
|
||||
if len(input.TapScriptSig) == 0 || len(input.TapLeafScript) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
partialSig := input.TapScriptSig[0]
|
||||
|
||||
leafHash, err := chainhash.NewHash(partialSig.LeafHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
preimage, err := b.getTaprootPreimage(src, i, leafHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sig, err := schnorr.ParseSignature(partialSig.Signature)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubkey, err := schnorr.ParsePubKey(partialSig.PubKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !sig.Verify(preimage, pubkey) {
|
||||
return "", fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return roundSigner.Pset.ToBase64()
|
||||
}
|
||||
|
||||
func (b *txBuilder) createConnectors(
|
||||
poolTx string, payments []domain.Payment, connectorAddress string, minRelayFee uint64,
|
||||
) ([]*psetv2.Pset, error) {
|
||||
@@ -733,6 +895,52 @@ func (b *txBuilder) onchainNetwork() *network.Network {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *txBuilder) getBoardingTaproot(owner, asp *secp256k1.PublicKey) (string, []byte, *taproot.TapscriptElementsProof, error) {
|
||||
multisigClosure := tree.ForfeitClosure{
|
||||
Pubkey: owner,
|
||||
AspPubkey: asp,
|
||||
}
|
||||
|
||||
csvClosure := tree.CSVSigClosure{
|
||||
Pubkey: owner,
|
||||
Seconds: uint(b.boardingExitDelay),
|
||||
}
|
||||
|
||||
multisigLeaf, err := multisigClosure.Leaf()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
csvLeaf, err := csvClosure.Leaf()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
tapTree := taproot.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
|
||||
root := tapTree.RootNode.TapHash()
|
||||
tapKey := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), root[:])
|
||||
|
||||
p2tr, err := payment.FromTweakedKey(tapKey, b.onchainNetwork(), nil)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
addr, err := p2tr.TaprootAddress()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
tapLeaf, err := multisigClosure.Leaf()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
leafProofIndex := tapTree.LeafProofIndex[tapLeaf.TapHash()]
|
||||
leafProof := tapTree.LeafMerkleProofs[leafProofIndex]
|
||||
|
||||
return addr, p2tr.Script, &leafProof, nil
|
||||
}
|
||||
|
||||
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
|
||||
for _, leaf := range input.TapLeafScript {
|
||||
closure := &tree.CSVSigClosure{}
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
minRelayFee = uint64(30)
|
||||
roundLifetime = int64(1209344)
|
||||
unilateralExitDelay = int64(512)
|
||||
boardingExitDelay = int64(512)
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestBuildPoolTx(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Liquid, roundLifetime, unilateralExitDelay,
|
||||
wallet, common.Liquid, roundLifetime, unilateralExitDelay, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parsePoolTxFixtures()
|
||||
@@ -60,7 +61,7 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Valid {
|
||||
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
|
||||
pubkey, f.Payments, minRelayFee, []domain.Round{},
|
||||
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, poolTx)
|
||||
@@ -81,7 +82,7 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Invalid {
|
||||
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
|
||||
pubkey, f.Payments, minRelayFee, []domain.Round{},
|
||||
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
|
||||
)
|
||||
require.EqualError(t, err, f.ExpectedErr)
|
||||
require.Empty(t, poolTx)
|
||||
@@ -94,7 +95,7 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
|
||||
func TestBuildForfeitTxs(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Liquid, 1209344, unilateralExitDelay,
|
||||
wallet, common.Liquid, 1209344, unilateralExitDelay, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parseForfeitTxsFixtures()
|
||||
|
||||
@@ -229,6 +229,16 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
|
||||
return res, res2, args.Error(2)
|
||||
}
|
||||
|
||||
func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string, error) {
|
||||
args := m.Called(ctx, txid)
|
||||
|
||||
var res string
|
||||
if a := args.Get(0); a != nil {
|
||||
res = a.(string)
|
||||
}
|
||||
return res, args.Error(1)
|
||||
}
|
||||
|
||||
type mockedInput struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user