Skip to content

Commit

Permalink
Initial pay endpoint setup
Browse files Browse the repository at this point in the history
  • Loading branch information
orbitalturtle committed Apr 10, 2024
1 parent f530565 commit d44914f
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 46 deletions.
15 changes: 14 additions & 1 deletion teos-common/proto/common/teos/v2/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ message RegisterRequest {
string subscription_signature = 5;
}

message PayRequest {
// Requests an invoice from the tower so that the user can make a payment.

bytes user_id = 1;
}

message PayResponse {
// Response to a PayRequest containing an invoice for the client to pay.

bytes user_id = 1;
string invoice = 2;
}

message GetSubscriptionInfoRequest {
// Request to get a specific user's subscription info.

Expand All @@ -29,4 +42,4 @@ message GetSubscriptionInfoResponse {
uint32 available_slots = 1;
uint32 subscription_expiry = 2;
repeated bytes locators = 3;
}
}
3 changes: 2 additions & 1 deletion teos/proto/teos/v2/tower_services.proto
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ service PublicTowerServices {
// Public tower services, only reachable from the public API.

rpc register(common.teos.v2.RegisterRequest) returns (common.teos.v2.RegisterResponse) {}
rpc pay(common.teos.v2.PayRequest) returns (common.teos.v2.PayResponse) {}
rpc add_appointment(common.teos.v2.AddAppointmentRequest) returns (common.teos.v2.AddAppointmentResponse) {}
rpc get_appointment(common.teos.v2.GetAppointmentRequest) returns (common.teos.v2.GetAppointmentResponse) {}
rpc get_subscription_info(common.teos.v2.GetSubscriptionInfoRequest) returns (common.teos.v2.GetSubscriptionInfoResponse) {}
Expand All @@ -47,4 +48,4 @@ service PrivateTowerServices {
rpc get_users(google.protobuf.Empty) returns (GetUsersResponse) {}
rpc get_user(GetUserRequest) returns (GetUserResponse) {}
rpc stop(google.protobuf.Empty) returns (google.protobuf.Empty) {}
}
}
12 changes: 6 additions & 6 deletions teos/src/api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ mod tests_methods {
#[tokio::test]
async fn test_register_max_slots() {
let (server_addr, _, _s) =
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION, false)).await;
let user_id = get_random_user_id();

// Register once, this should go trough and set slots to the limit
Expand Down Expand Up @@ -724,7 +724,7 @@ mod tests_methods {
#[tokio::test]
async fn test_register_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable(),
)
.await;
let user_id = get_random_user_id();
Expand Down Expand Up @@ -819,7 +819,7 @@ mod tests_methods {
async fn test_add_appointment_already_triggered() {
// Get the InternalAPI so we can mess with the inner state
let (server_addr, internal_api, _s) =
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION, false)).await;

// Register
let (user_sk, user_pk) = cryptography::get_random_keypair();
Expand Down Expand Up @@ -870,7 +870,7 @@ mod tests_methods {
#[tokio::test]
async fn test_add_appointment_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable(),
)
.await;
let (user_sk, _) = cryptography::get_random_keypair();
Expand Down Expand Up @@ -1031,7 +1031,7 @@ mod tests_methods {
#[tokio::test]
async fn test_get_appointment_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable(),
)
.await;

Expand Down Expand Up @@ -1130,7 +1130,7 @@ mod tests_methods {
async fn test_get_subscription_info_service_unavailable() {
let (user_sk, _) = cryptography::get_random_keypair();
let (server_addr, _, _s) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable(),
)
.await;

Expand Down
128 changes: 116 additions & 12 deletions teos/src/api/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use tonic::{Code, Request, Response, Status};
use triggered::Trigger;

use crate::extended_appointment::UUID;
use crate::fees::ValidatePayment;
use crate::protos as msgs;
use crate::protos::private_tower_services_server::PrivateTowerServices;
use crate::protos::public_tower_services_server::PublicTowerServices;
Expand All @@ -28,6 +29,8 @@ pub struct InternalAPI {
bitcoind_reachable: Arc<(Mutex<bool>, Condvar)>,
/// A signal indicating the tower is shuting down.
shutdown_trigger: Trigger,
/// If fee mode is on, this validates payments made to the watchtower.
validator: Option<Arc<Mutex<dyn ValidatePayment + Send>>>,
}

impl InternalAPI {
Expand All @@ -37,12 +40,14 @@ impl InternalAPI {
addresses: Vec<msgs::NetworkAddress>,
bitcoind_reachable: Arc<(Mutex<bool>, Condvar)>,
shutdown_trigger: Trigger,
validator: Option<Arc<Mutex<dyn ValidatePayment + Send>>>,
) -> Self {
Self {
watcher,
addresses,
bitcoind_reachable,
shutdown_trigger,
validator,
}
}

Expand Down Expand Up @@ -97,6 +102,45 @@ impl PublicTowerServices for Arc<InternalAPI> {
}
}

/// Pay endpoint. Part of the public API. Internally calls the payment validator to generate an
/// invoice.
async fn pay(
&self,
request: Request<common_msgs::PayRequest>,
) -> Result<Response<common_msgs::PayResponse>, Status> {
self.check_service_unavailable()?;
let req_data = request.into_inner();

if self.validator.is_none() {
return Err(Status::new(
Code::Unimplemented,
"Paying for watchtower service is unsupported",
));
};

let user_id = UserId::from_slice(&req_data.user_id).map_err(|_| {
Status::new(
Code::InvalidArgument,
"Provided public key does not match expected format (33-byte compressed key)",
)
})?;

let invoice = match self.watcher.pay(user_id) {
Ok(invoice) => invoice,
Err(_) => {
return Err(Status::new(
Code::Unimplemented,
"Paying for watchtower service is unsupported",
));
}
};

return Ok(Response::new(common_msgs::PayResponse {
user_id: req_data.user_id,
invoice: invoice.to_string(),
}));
}

/// Add appointment endpoint. Part of the public API. Internally calls [Watcher::add_appointment].
async fn add_appointment(
&self,
Expand Down Expand Up @@ -846,7 +890,8 @@ mod tests_public_api {

#[tokio::test]
async fn test_register_max_slots() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
let (internal_api, _s) =
create_api_with_config(ApiConfig::new(u32::MAX, DURATION, false)).await;

let (_, user_pk) = get_random_keypair();
let user_id = UserId(user_pk).to_vec();
Expand Down Expand Up @@ -874,8 +919,10 @@ mod tests_public_api {

#[tokio::test]
async fn test_register_service_unavailable() {
let (internal_api, _s) =
create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await;
let (internal_api, _s) = create_api_with_config(
ApiConfig::new(u32::MAX, DURATION, false).bitcoind_unreachable(),
)
.await;

let (_, user_pk) = get_random_keypair();
let user_id = UserId(user_pk).to_vec();
Expand All @@ -892,6 +939,59 @@ mod tests_public_api {
}
}

#[tokio::test]
async fn test_pay() {
let cfg = ApiConfig::new(SLOTS, DURATION, true);
let (internal_api, _s) = create_api_with_config(cfg).await;
let (_, user_pk) = get_random_keypair();

let response = internal_api
.pay(Request::new(common_msgs::PayRequest {
user_id: UserId(user_pk).to_vec(),
}))
.await
.unwrap()
.into_inner();

assert!(matches!(response, common_msgs::PayResponse { .. }));

// Check that when we hit it a second time, we get the same invoice as before.
let response2 = internal_api
.pay(Request::new(common_msgs::PayRequest {
user_id: UserId(user_pk).to_vec(),
}))
.await
.unwrap()
.into_inner();

assert_eq!(response.invoice, response2.invoice);
}

#[tokio::test]
async fn test_pay_no_payments() {
// We'll test that if the tower payments aren't turned on, but a user tries to hit the pay endpoint,
// we get the expected error.
let cfg = ApiConfig::new(SLOTS, DURATION, false);
let (internal_api, _s) = create_api_with_config(cfg).await;
let (_, user_pk) = get_random_keypair();

match internal_api
.pay(Request::new(common_msgs::PayRequest {
user_id: UserId(user_pk).to_vec(),
}))
.await
{
Err(status) => {
assert_eq!(status.code(), Code::Unimplemented);
assert_eq!(
status.message(),
"Paying for watchtower service is unsupported"
)
}
_ => panic!("Test should have returned Err"),
}
}

#[tokio::test]
async fn test_add_appointment() {
let (internal_api, _s) = create_api().await;
Expand Down Expand Up @@ -948,7 +1048,7 @@ mod tests_public_api {

#[tokio::test]
async fn test_add_appointment_not_enough_slots() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(0, DURATION)).await;
let (internal_api, _s) = create_api_with_config(ApiConfig::new(0, DURATION, false)).await;

// User is registered but has no slots
let (user_sk, user_pk) = get_random_keypair();
Expand Down Expand Up @@ -977,7 +1077,7 @@ mod tests_public_api {

#[tokio::test]
async fn test_add_appointment_subscription_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0, false)).await;

// User is registered but subscription is expired
let (user_sk, user_pk) = get_random_keypair();
Expand Down Expand Up @@ -1042,8 +1142,10 @@ mod tests_public_api {

#[tokio::test]
async fn test_add_appointment_service_unavailable() {
let (internal_api, _s) =
create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await;
let (internal_api, _s) = create_api_with_config(
ApiConfig::new(u32::MAX, DURATION, false).bitcoind_unreachable(),
)
.await;

let (user_sk, _) = get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
Expand Down Expand Up @@ -1154,7 +1256,7 @@ mod tests_public_api {

#[tokio::test]
async fn test_get_appointment_subscription_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0, false)).await;

// Register the user
let (user_sk, user_pk) = get_random_keypair();
Expand Down Expand Up @@ -1183,7 +1285,8 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_appointment_service_unavailable() {
let (internal_api, _s) =
create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await;
create_api_with_config(ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable())
.await;

let (user_sk, _) = get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
Expand Down Expand Up @@ -1229,7 +1332,7 @@ mod tests_public_api {

#[tokio::test]
async fn test_get_subscription_info_non_registered() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0, false)).await;

// The user is not registered
let (user_sk, _) = get_random_keypair();
Expand All @@ -1252,7 +1355,7 @@ mod tests_public_api {

#[tokio::test]
async fn test_get_subscription_info_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0, false)).await;

// The user is registered but the subscription has expired
let (user_sk, user_pk) = get_random_keypair();
Expand All @@ -1277,7 +1380,8 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_subscription_info_service_unavailable() {
let (internal_api, _s) =
create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await;
create_api_with_config(ApiConfig::new(SLOTS, DURATION, false).bitcoind_unreachable())
.await;

let (user_sk, _) = get_random_keypair();
let message = "get subscription info".to_string();
Expand Down
17 changes: 17 additions & 0 deletions teos/src/fees.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use core::fmt::Debug;
use lightning_invoice::Invoice;

/// This trait specifies the functionality that needs to be implemented to
/// accept and validate a payment from a user.
pub trait ValidatePayment {
// Generates an invoice for the user to pay.
fn get_invoice(&self) -> Invoice;
// Validates that the payment was paid.
fn validate(&self, invoice: Invoice) -> bool;
}

impl Debug for dyn ValidatePayment + std::marker::Send + 'static {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "hello")
}
}
Loading

0 comments on commit d44914f

Please sign in to comment.