diff --git a/itest/tests/swap_pb2.py b/itest/tests/swap_pb2.py index 2d4e948..5fcea2f 100644 --- a/itest/tests/swap_pb2.py +++ b/itest/tests/swap_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\nswap.proto\x12\x04swap"8\n\x11\x43reateSwapRequest\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x15\n\rrefund_pubkey\x18\x02 \x01(\x0c"P\n\x12\x43reateSwapResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x14\n\x0c\x63laim_pubkey\x18\x02 \x01(\x0c\x12\x13\n\x0block_height\x18\x03 \x01(\r")\n\x0ePaySwapRequest\x12\x17\n\x0fpayment_request\x18\x01 \x01(\t"\x11\n\x0fPaySwapResponse"a\n\x11RefundSwapRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x13\n\x0btransaction\x18\x02 \x01(\x0c\x12\x13\n\x0binput_index\x18\x03 \x01(\r\x12\x11\n\tpub_nonce\x18\x04 \x01(\x0c"B\n\x12RefundSwapResponse\x12\x11\n\tpub_nonce\x18\x01 \x01(\x0c\x12\x19\n\x11partial_signature\x18\x02 \x01(\x0c\x32\xc9\x01\n\x07Swapper\x12\x41\n\nCreateSwap\x12\x17.swap.CreateSwapRequest\x1a\x18.swap.CreateSwapResponse"\x00\x12\x38\n\x07PaySwap\x12\x14.swap.PaySwapRequest\x1a\x15.swap.PaySwapResponse"\x00\x12\x41\n\nRefundSwap\x12\x17.swap.RefundSwapRequest\x1a\x18.swap.RefundSwapResponse"\x00\x62\x06proto3' + b'\n\nswap.proto\x12\x04swap"8\n\x11\x43reateSwapRequest\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x15\n\rrefund_pubkey\x18\x02 \x01(\x0c"z\n\x12\x43reateSwapResponse\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x14\n\x0c\x63laim_pubkey\x18\x02 \x01(\x0c\x12\x13\n\x0block_height\x18\x03 \x01(\r\x12(\n\nparameters\x18\x04 \x01(\x0b\x32\x14.swap.SwapParameters")\n\x0ePaySwapRequest\x12\x17\n\x0fpayment_request\x18\x01 \x01(\t"\x11\n\x0fPaySwapResponse"a\n\x11RefundSwapRequest\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x13\n\x0btransaction\x18\x02 \x01(\x0c\x12\x13\n\x0binput_index\x18\x03 \x01(\r\x12\x11\n\tpub_nonce\x18\x04 \x01(\x0c"B\n\x12RefundSwapResponse\x12\x11\n\tpub_nonce\x18\x01 \x01(\x0c\x12\x19\n\x11partial_signature\x18\x02 \x01(\x0c"z\n\x0eSwapParameters\x12\x11\n\tlock_time\x18\x01 \x01(\r\x12\x1b\n\x13max_swap_amount_sat\x18\x02 \x01(\x04\x12\x1b\n\x13min_swap_amount_sat\x18\x03 \x01(\x04\x12\x1b\n\x13min_utxo_amount_sat\x18\x04 \x01(\x04"\x17\n\x15SwapParametersRequest"B\n\x16SwapParametersResponse\x12(\n\nparameters\x18\x01 \x01(\x0b\x32\x14.swap.SwapParameters2\x98\x02\n\x07Swapper\x12\x41\n\nCreateSwap\x12\x17.swap.CreateSwapRequest\x1a\x18.swap.CreateSwapResponse"\x00\x12\x38\n\x07PaySwap\x12\x14.swap.PaySwapRequest\x1a\x15.swap.PaySwapResponse"\x00\x12\x41\n\nRefundSwap\x12\x17.swap.RefundSwapRequest\x1a\x18.swap.RefundSwapResponse"\x00\x12M\n\x0eSwapParameters\x12\x1b.swap.SwapParametersRequest\x1a\x1c.swap.SwapParametersResponse"\x00\x62\x06proto3' ) _globals = globals() @@ -30,15 +30,21 @@ _globals["_CREATESWAPREQUEST"]._serialized_start = 20 _globals["_CREATESWAPREQUEST"]._serialized_end = 76 _globals["_CREATESWAPRESPONSE"]._serialized_start = 78 - _globals["_CREATESWAPRESPONSE"]._serialized_end = 158 - _globals["_PAYSWAPREQUEST"]._serialized_start = 160 - _globals["_PAYSWAPREQUEST"]._serialized_end = 201 - _globals["_PAYSWAPRESPONSE"]._serialized_start = 203 - _globals["_PAYSWAPRESPONSE"]._serialized_end = 220 - _globals["_REFUNDSWAPREQUEST"]._serialized_start = 222 - _globals["_REFUNDSWAPREQUEST"]._serialized_end = 319 - _globals["_REFUNDSWAPRESPONSE"]._serialized_start = 321 - _globals["_REFUNDSWAPRESPONSE"]._serialized_end = 387 - _globals["_SWAPPER"]._serialized_start = 390 - _globals["_SWAPPER"]._serialized_end = 591 + _globals["_CREATESWAPRESPONSE"]._serialized_end = 200 + _globals["_PAYSWAPREQUEST"]._serialized_start = 202 + _globals["_PAYSWAPREQUEST"]._serialized_end = 243 + _globals["_PAYSWAPRESPONSE"]._serialized_start = 245 + _globals["_PAYSWAPRESPONSE"]._serialized_end = 262 + _globals["_REFUNDSWAPREQUEST"]._serialized_start = 264 + _globals["_REFUNDSWAPREQUEST"]._serialized_end = 361 + _globals["_REFUNDSWAPRESPONSE"]._serialized_start = 363 + _globals["_REFUNDSWAPRESPONSE"]._serialized_end = 429 + _globals["_SWAPPARAMETERS"]._serialized_start = 431 + _globals["_SWAPPARAMETERS"]._serialized_end = 553 + _globals["_SWAPPARAMETERSREQUEST"]._serialized_start = 555 + _globals["_SWAPPARAMETERSREQUEST"]._serialized_end = 578 + _globals["_SWAPPARAMETERSRESPONSE"]._serialized_start = 580 + _globals["_SWAPPARAMETERSRESPONSE"]._serialized_end = 646 + _globals["_SWAPPER"]._serialized_start = 649 + _globals["_SWAPPER"]._serialized_end = 929 # @@protoc_insertion_point(module_scope) diff --git a/itest/tests/swap_pb2.pyi b/itest/tests/swap_pb2.pyi index 684598e..0dcc37a 100644 --- a/itest/tests/swap_pb2.pyi +++ b/itest/tests/swap_pb2.pyi @@ -1,6 +1,11 @@ from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional +from typing import ( + ClassVar as _ClassVar, + Mapping as _Mapping, + Optional as _Optional, + Union as _Union, +) DESCRIPTOR: _descriptor.FileDescriptor @@ -15,18 +20,21 @@ class CreateSwapRequest(_message.Message): ) -> None: ... class CreateSwapResponse(_message.Message): - __slots__ = ("address", "claim_pubkey", "lock_height") + __slots__ = ("address", "claim_pubkey", "lock_height", "parameters") ADDRESS_FIELD_NUMBER: _ClassVar[int] CLAIM_PUBKEY_FIELD_NUMBER: _ClassVar[int] LOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + PARAMETERS_FIELD_NUMBER: _ClassVar[int] address: str claim_pubkey: bytes lock_height: int + parameters: SwapParameters def __init__( self, address: _Optional[str] = ..., claim_pubkey: _Optional[bytes] = ..., lock_height: _Optional[int] = ..., + parameters: _Optional[_Union[SwapParameters, _Mapping]] = ..., ) -> None: ... class PaySwapRequest(_message.Message): @@ -68,3 +76,38 @@ class RefundSwapResponse(_message.Message): pub_nonce: _Optional[bytes] = ..., partial_signature: _Optional[bytes] = ..., ) -> None: ... + +class SwapParameters(_message.Message): + __slots__ = ( + "lock_time", + "max_swap_amount_sat", + "min_swap_amount_sat", + "min_utxo_amount_sat", + ) + LOCK_TIME_FIELD_NUMBER: _ClassVar[int] + MAX_SWAP_AMOUNT_SAT_FIELD_NUMBER: _ClassVar[int] + MIN_SWAP_AMOUNT_SAT_FIELD_NUMBER: _ClassVar[int] + MIN_UTXO_AMOUNT_SAT_FIELD_NUMBER: _ClassVar[int] + lock_time: int + max_swap_amount_sat: int + min_swap_amount_sat: int + min_utxo_amount_sat: int + def __init__( + self, + lock_time: _Optional[int] = ..., + max_swap_amount_sat: _Optional[int] = ..., + min_swap_amount_sat: _Optional[int] = ..., + min_utxo_amount_sat: _Optional[int] = ..., + ) -> None: ... + +class SwapParametersRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SwapParametersResponse(_message.Message): + __slots__ = ("parameters",) + PARAMETERS_FIELD_NUMBER: _ClassVar[int] + parameters: SwapParameters + def __init__( + self, parameters: _Optional[_Union[SwapParameters, _Mapping]] = ... + ) -> None: ... diff --git a/itest/tests/swap_pb2_grpc.py b/itest/tests/swap_pb2_grpc.py index ba5f04f..f3ab0d1 100644 --- a/itest/tests/swap_pb2_grpc.py +++ b/itest/tests/swap_pb2_grpc.py @@ -55,6 +55,12 @@ def __init__(self, channel): response_deserializer=swap__pb2.RefundSwapResponse.FromString, _registered_method=True, ) + self.SwapParameters = channel.unary_unary( + "/swap.Swapper/SwapParameters", + request_serializer=swap__pb2.SwapParametersRequest.SerializeToString, + response_deserializer=swap__pb2.SwapParametersResponse.FromString, + _registered_method=True, + ) class SwapperServicer(object): @@ -78,6 +84,12 @@ def RefundSwap(self, request, context): context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") + def SwapParameters(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + def add_SwapperServicer_to_server(servicer, server): rpc_method_handlers = { @@ -96,6 +108,11 @@ def add_SwapperServicer_to_server(servicer, server): request_deserializer=swap__pb2.RefundSwapRequest.FromString, response_serializer=swap__pb2.RefundSwapResponse.SerializeToString, ), + "SwapParameters": grpc.unary_unary_rpc_method_handler( + servicer.SwapParameters, + request_deserializer=swap__pb2.SwapParametersRequest.FromString, + response_serializer=swap__pb2.SwapParametersResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( "swap.Swapper", rpc_method_handlers @@ -197,3 +214,33 @@ def RefundSwap( metadata, _registered_method=True, ) + + @staticmethod + def SwapParameters( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/swap.Swapper/SwapParameters", + swap__pb2.SwapParametersRequest.SerializeToString, + swap__pb2.SwapParametersResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True, + ) diff --git a/swapd/proto/swap/swap.proto b/swapd/proto/swap/swap.proto index 0d8f160..684150e 100644 --- a/swapd/proto/swap/swap.proto +++ b/swapd/proto/swap/swap.proto @@ -5,6 +5,7 @@ service Swapper { rpc CreateSwap (CreateSwapRequest) returns (CreateSwapResponse) {} rpc PaySwap (PaySwapRequest) returns (PaySwapResponse) {} rpc RefundSwap (RefundSwapRequest) returns (RefundSwapResponse) {} + rpc SwapParameters (SwapParametersRequest) returns (SwapParametersResponse) {} } message CreateSwapRequest { @@ -16,6 +17,7 @@ message CreateSwapResponse { string address = 1; bytes claim_pubkey = 2; uint32 lock_height = 3; + SwapParameters parameters = 4; } message PaySwapRequest { @@ -34,4 +36,17 @@ message RefundSwapRequest { message RefundSwapResponse { bytes pub_nonce = 1; bytes partial_signature = 2; -} \ No newline at end of file +} + +message SwapParameters { + uint32 lock_time = 1; + uint64 max_swap_amount_sat = 2; + uint64 min_swap_amount_sat = 3; + uint64 min_utxo_amount_sat = 4; +} + +message SwapParametersRequest {} + +message SwapParametersResponse { + SwapParameters parameters = 1; +} diff --git a/swapd/src/public_server.rs b/swapd/src/public_server.rs index 3bd0bf1..f0ea802 100644 --- a/swapd/src/public_server.rs +++ b/swapd/src/public_server.rs @@ -31,7 +31,8 @@ use crate::swap::{ }; use swap_api::{ swapper_server::Swapper, CreateSwapRequest, CreateSwapResponse, PaySwapRequest, - PaySwapResponse, RefundSwapRequest, RefundSwapResponse, + PaySwapResponse, RefundSwapRequest, RefundSwapResponse, SwapParameters, SwapParametersRequest, + SwapParametersResponse, }; pub mod swap_api { @@ -39,6 +40,8 @@ pub mod swap_api { } const FAKE_PREIMAGE: [u8; 32] = [0; 32]; +const MIN_SWAP_AMOUNT_CONF_TARGET: i32 = 12; + pub struct SwapServerParams where C: ChainClient, @@ -123,6 +126,22 @@ where fee_estimator: params.fee_estimator, } } + + async fn get_swap_parameters(&self) -> Result { + let fee_estimate = self + .fee_estimator + .estimate_fee(MIN_SWAP_AMOUNT_CONF_TARGET) + .await?; + // Assume a transaction weight of 1000. + let min_utxo_amount_sat = (fee_estimate.sat_per_kw as u64) * 3 / 2; + + Ok(SwapParameters { + lock_time: self.swap_service.lock_time(), + max_swap_amount_sat: self.max_swap_amount_sat, + min_swap_amount_sat: min_utxo_amount_sat, + min_utxo_amount_sat, + }) + } } #[tonic::async_trait] impl Swapper for SwapServer @@ -172,11 +191,12 @@ where "new swap created" ); - // TODO: Add min/max allowed here? + let parameters = self.get_swap_parameters().await?; Ok(Response::new(CreateSwapResponse { address: swap.public.address.to_string(), claim_pubkey: swap.public.claim_pubkey.serialize().to_vec(), lock_height: swap.public.lock_height, + parameters: Some(parameters), })) } @@ -185,7 +205,6 @@ where &self, request: Request, ) -> Result, Status> { - // TODO: Ensure swap is not refunded and cannot be refunded at the same time. debug!("pay_swap request"); let req = request.into_inner(); let invoice: Bolt11Invoice = req.payment_request.parse().map_err(|e| { @@ -211,15 +230,23 @@ where )); } - if amount_sat > self.max_swap_amount_sat { + let parameters = self.get_swap_parameters().await?; + if amount_sat > parameters.max_swap_amount_sat { trace!( amount_sat, - max_swap_amount_sat = self.max_swap_amount_sat, + max_swap_amount_sat = parameters.max_swap_amount_sat, "invoice amount exceeds max swap amount" ); - return Err(Status::invalid_argument( - "amount exceeds maximum allowed deposit", - )); + return Err(Status::invalid_argument("amount exceeds max swap amount")); + } + + if amount_sat < parameters.min_swap_amount_sat { + trace!( + amount_sat, + min_swap_amount_sat = parameters.min_swap_amount_sat, + "invoice amount is below min swap amount" + ); + return Err(Status::invalid_argument("amount is below min swap amount")); } let hash = invoice.payment_hash(); @@ -279,6 +306,16 @@ where return false; } + if utxo.tx_out.value.to_sat() < parameters.min_utxo_amount_sat { + debug!( + outpoint = field::display(utxo.outpoint), + utxo_amount_sat = utxo.tx_out.value.to_sat(), + min_utxo_amount_sat = parameters.min_utxo_amount_sat, + "utxo value is below min_utxo_amount_sat" + ); + return false; + } + trace!( outpoint = field::display(utxo.outpoint), confirmations, @@ -570,6 +607,16 @@ where pub_nonce: our_pub_nonce.serialize().to_vec(), })) } + + async fn swap_parameters( + &self, + _request: Request, + ) -> Result, Status> { + let parameters = self.get_swap_parameters().await?; + Ok(Response::new(SwapParametersResponse { + parameters: Some(parameters), + })) + } } impl From for Status { diff --git a/swapd/src/swap/swap_service.rs b/swapd/src/swap/swap_service.rs index bd102b1..b05e5b5 100644 --- a/swapd/src/swap/swap_service.rs +++ b/swapd/src/swap/swap_service.rs @@ -288,6 +288,10 @@ where Ok(tx) } + pub fn lock_time(&self) -> u32 { + self.lock_time + } + pub fn partial_sign_refund_tx( &self, swap: &Swap,