Skip to content

Commit 8041ec5

Browse files
authored
Merge pull request #544 from AmbireTech/get-leaf-route
GET `/v5/channel/:id/get-leaf` route + tests
2 parents a7db507 + 24b42af commit 8041ec5

File tree

6 files changed

+250
-26
lines changed

6 files changed

+250
-26
lines changed

primitives/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ name = "modify_campaign_request"
6363
[[example]]
6464
name = "spender_response"
6565

66+
[[example]]
67+
name = "get_leaf_response"
68+
6669
[[example]]
6770
name = "validator_messages_create_request"
6871

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use primitives::sentry::GetLeafResponse;
2+
use serde_json::{from_value, json};
3+
4+
fn main() {
5+
let json = json!({
6+
"merkleProof": "8ea7760ca2dbbe00673372afbf8b05048717ce8a305f1f853afac8c244182e0c",
7+
});
8+
9+
assert!(from_value::<GetLeafResponse>(json).is_ok());
10+
}

primitives/src/sentry.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,19 @@ pub struct ValidationErrorResponse {
722722
pub validation: Vec<String>,
723723
}
724724

725+
/// Get leaf response with the Merkle proof for the requested spender/earner.
726+
///
727+
/// # Examples
728+
///
729+
/// ```
730+
#[doc = include_str!("../examples/get_leaf_response.rs")]
731+
/// ```
732+
#[derive(Serialize, Deserialize, Debug)]
733+
#[serde(rename_all = "camelCase")]
734+
pub struct GetLeafResponse {
735+
pub merkle_proof: String,
736+
}
737+
725738
/// Request body for posting new [`Event`]s to a [`Campaign`](crate::Campaign).
726739
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
727740
#[serde(rename_all = "camelCase")]

sentry/src/routes.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Sentry REST API documentation
22
//!
3+
//! This module includes the documentation for all routes of the `Sentry`
4+
//! REST API and the corresponding requests, responses and parameters.
35
//!
46
//! All routes are listed below. Here is an overview and links to all of them:
57
//! - [Channel](#channel) routes
@@ -236,29 +238,36 @@
236238
#![doc = include_str!("../../primitives/examples/channel_pay_request.rs")]
237239
//! ```
238240
//!
239-
//!
240-
//! #### GET `/v5/channel/:id/get-leaf`
241-
//!
242-
//! TODO: implement and document as part of issue #382
241+
//! #### GET `/v5/channel/:id/get-leaf
243242
//!
244243
//! This route gets the latest approved state ([`NewState`]/[`ApproveState`] pair),
245-
//! and finds the given `spender`/`earner` in the balances tree, and produce a merkle proof for it.
244+
//! finds the given `spender` or `earner` in the balances tree and produces a Merkle proof for it.
246245
//! This is useful for the Platform to verify if a spender leaf really exists.
247246
//!
248-
//! The route is handled by `todo`.
247+
//! The route is handled by [`channel::get_leaf()`].
248+
//!
249+
//! Response: [`GetLeafResponse`](primitives::sentry::GetLeafResponse)
250+
//!
251+
//! ##### Routes:
252+
//!
253+
//! - GET `/v5/channel/:id/get-leaf/spender/:addr`
254+
//! - GET `/v5/channel/:id/get-leaf/earner/:addr`
249255
//!
250-
//! Request query parameters:
256+
//! ##### Examples:
251257
//!
252-
//! - `spender=[0x...]` or `earner=[0x...]` (required)
258+
//! URI for retrieving leaf of a Spender:
253259
//!
254-
//! Example Spender:
260+
//! `/v5/channel/0xf147fa3f1c5e5e06d359c15aa082442cc3e0380f306306022d1e9047c565a0f9/get-leaf/spender/0xDd589B43793934EF6Ad266067A0d1D4896b0dff0`
255261
//!
256-
//! `/get-leaf?spender=0x...`
262+
//! URI for retrieving leaf of an Earner:
257263
//!
258-
//! Example Earner:
264+
//! `/v5/channel/0xf147fa3f1c5e5e06d359c15aa082442cc3e0380f306306022d1e9047c565a0f9/get-leaf/earner/0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9`
259265
//!
260-
//! `/get-leaf?earner=0x....`
261-
//! This module includes all routes for `Sentry` and the documentation of each Request/Response.
266+
//! Response:
267+
//!
268+
//! ```
269+
#![doc = include_str!("../../primitives/examples/get_leaf_response.rs")]
270+
//! ```
262271
//!
263272
//! #### POST `/v5/channel/dummy-deposit` (auth required)
264273
//!

sentry/src/routes/channel.rs

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ use serde::{Deserialize, Serialize};
77
use slog::{error, Logger};
88
use std::{any::Any, collections::HashMap, sync::Arc};
99

10-
use adapter::{client::Locked, Adapter, Dummy};
10+
use adapter::{
11+
client::Locked,
12+
util::{get_balance_leaf, get_signable_state_root},
13+
Adapter, Dummy,
14+
};
1115
use primitives::{
1216
balances::{Balances, CheckedState, UncheckedState},
17+
merkle_tree::MerkleTree,
1318
sentry::{
1419
channel_list::{ChannelListQuery, ChannelListResponse},
15-
AccountingResponse, AllSpendersQuery, AllSpendersResponse, ChannelPayRequest, LastApproved,
16-
LastApprovedQuery, LastApprovedResponse, SpenderResponse, SuccessResponse,
20+
AccountingResponse, AllSpendersQuery, AllSpendersResponse, ChannelPayRequest,
21+
GetLeafResponse, LastApproved, LastApprovedQuery, LastApprovedResponse, SpenderResponse,
22+
SuccessResponse,
1723
},
1824
spender::{Spendable, Spender},
1925
validator::NewState,
@@ -32,7 +38,7 @@ use crate::{
3238
DbPool,
3339
},
3440
response::ResponseError,
35-
routes::campaign::fetch_campaign_ids_for_channel,
41+
routes::{campaign::fetch_campaign_ids_for_channel, routers::LeafFor},
3642
Application, Auth,
3743
};
3844

@@ -498,6 +504,73 @@ pub async fn channel_payout<C: Locked + 'static>(
498504
Ok(Json(SuccessResponse { success: true }))
499505
}
500506

507+
/// GET `/v5/channel/0xXXX.../get-leaf` requests
508+
///
509+
/// # Routes:
510+
///
511+
/// - GET `/v5/channel/:id/get-leaf/spender/:addr`
512+
/// - GET `/v5/channel/:id/get-leaf/earner/:addr`
513+
///
514+
/// Response: [`GetLeafResponse`]
515+
pub async fn get_leaf<C: Locked + 'static>(
516+
Extension(app): Extension<Arc<Application<C>>>,
517+
Extension(channel_context): Extension<ChainOf<Channel>>,
518+
Extension(leaf_for): Extension<LeafFor>,
519+
Path(params): Path<(ChannelId, Address)>,
520+
) -> Result<Json<GetLeafResponse>, ResponseError> {
521+
let channel = channel_context.context;
522+
523+
let approve_state = latest_approve_state(&app.pool, &channel)
524+
.await?
525+
.ok_or(ResponseError::NotFound)?;
526+
527+
let state_root = approve_state.msg.state_root.clone();
528+
529+
let new_state = latest_new_state(&app.pool, &channel, &state_root)
530+
.await?
531+
.ok_or_else(|| ResponseError::BadRequest("No NewState message for spender".to_string()))?;
532+
533+
let addr = params.1;
534+
535+
let element = match leaf_for {
536+
LeafFor::Spender => {
537+
let amount = new_state
538+
.msg
539+
.balances
540+
.spenders
541+
.get(&addr)
542+
.ok_or(ResponseError::NotFound)?;
543+
544+
get_balance_leaf(
545+
true,
546+
&addr,
547+
&amount.to_precision(channel_context.token.precision.get()),
548+
)?
549+
}
550+
LeafFor::Earner => {
551+
let amount = new_state
552+
.msg
553+
.balances
554+
.earners
555+
.get(&addr)
556+
.ok_or(ResponseError::NotFound)?;
557+
558+
get_balance_leaf(
559+
false,
560+
&addr,
561+
&amount.to_precision(channel_context.token.precision.get()),
562+
)?
563+
}
564+
};
565+
let merkle_tree = MerkleTree::new(&[element])?;
566+
567+
let signable_state_root = get_signable_state_root(channel.id().as_bytes(), &merkle_tree.root());
568+
569+
let merkle_proof = hex::encode(signable_state_root);
570+
571+
Ok(Json(GetLeafResponse { merkle_proof }))
572+
}
573+
501574
/// POST `/v5/channel/dummy-deposit` request
502575
///
503576
/// Full details about the route's API and intend can be found in the [`routes`](crate::routes#post-v5channeldummy-deposit-auth-required) module
@@ -669,26 +742,30 @@ pub mod validator_message {
669742

670743
#[cfg(test)]
671744
mod test {
672-
use std::str::FromStr;
673-
745+
use super::*;
746+
use crate::{
747+
db::{
748+
insert_campaign, insert_channel, validator_message::insert_validator_message,
749+
CampaignRemaining,
750+
},
751+
test_util::setup_dummy_app,
752+
};
674753
use adapter::{
675754
ethereum::test_util::{GANACHE_INFO_1, GANACHE_INFO_1337},
755+
prelude::Unlocked,
676756
primitives::Deposit as AdapterDeposit,
677757
};
678758
use primitives::{
759+
balances::UncheckedState,
679760
channel::Nonce,
680761
test_util::{
681762
ADVERTISER, CREATOR, DUMMY_CAMPAIGN, FOLLOWER, GUARDIAN, IDS, LEADER, LEADER_2,
682763
PUBLISHER, PUBLISHER_2,
683764
},
765+
validator::{ApproveState, MessageTypes, NewState},
684766
BigNum, ChainId, Deposit, UnifiedMap, ValidatorId,
685767
};
686-
687-
use super::*;
688-
use crate::{
689-
db::{insert_campaign, insert_channel, CampaignRemaining},
690-
test_util::setup_dummy_app,
691-
};
768+
use std::str::FromStr;
692769

693770
#[tokio::test]
694771
async fn create_and_fetch_spendable() {
@@ -1407,4 +1484,102 @@ mod test {
14071484
);
14081485
}
14091486
}
1487+
1488+
#[tokio::test]
1489+
async fn get_spender_and_earner_leafs() {
1490+
let mut balances: Balances<CheckedState> = Balances::new();
1491+
balances
1492+
.spend(*ADVERTISER, *PUBLISHER, UnifiedNum::from_u64(1000))
1493+
.expect("should spend");
1494+
balances
1495+
.spend(*ADVERTISER, *PUBLISHER_2, UnifiedNum::from_u64(1000))
1496+
.expect("should spend");
1497+
balances
1498+
.spend(*CREATOR, *PUBLISHER, UnifiedNum::from_u64(1000))
1499+
.expect("should spend");
1500+
balances
1501+
.spend(*CREATOR, *PUBLISHER_2, UnifiedNum::from_u64(1000))
1502+
.expect("should spend");
1503+
1504+
let app_guard = setup_dummy_app().await;
1505+
let app = Extension(Arc::new(app_guard.app.clone()));
1506+
1507+
let channel_context = Extension(
1508+
app.config
1509+
.find_chain_of(DUMMY_CAMPAIGN.channel.token)
1510+
.expect("Dummy channel Token should be present in config!")
1511+
.with(DUMMY_CAMPAIGN.channel),
1512+
);
1513+
let channel = channel_context.context;
1514+
1515+
insert_channel(&app.pool, &channel_context)
1516+
.await
1517+
.expect("should insert channel");
1518+
1519+
// Setting up the validator messages
1520+
let state_root =
1521+
"b1a4fc6c1a1e1ab908a487e504006edcebea297f61b4b8ce6cad3b29e29454cc".to_string();
1522+
let signature = app
1523+
.adapter
1524+
.clone()
1525+
.unlock()
1526+
.expect("should unlock")
1527+
.sign(&state_root.clone())
1528+
.expect("should sign");
1529+
let new_state: NewState<UncheckedState> = NewState {
1530+
state_root: state_root.clone(),
1531+
signature: signature.clone(),
1532+
balances: balances.into_unchecked(),
1533+
};
1534+
let approve_state = ApproveState {
1535+
state_root,
1536+
signature,
1537+
is_healthy: true,
1538+
};
1539+
1540+
insert_validator_message(
1541+
&app.pool,
1542+
&channel,
1543+
&channel.leader,
1544+
&MessageTypes::NewState(new_state),
1545+
)
1546+
.await
1547+
.expect("Should insert NewState msg");
1548+
insert_validator_message(
1549+
&app.pool,
1550+
&channel,
1551+
&channel.follower,
1552+
&MessageTypes::ApproveState(approve_state),
1553+
)
1554+
.await
1555+
.expect("Should insert NewState msg");
1556+
1557+
// hardcoded proofs
1558+
let spender_proof =
1559+
"8ea7760ca2dbbe00673372afbf8b05048717ce8a305f1f853afac8c244182e0c".to_string();
1560+
let earner_proof =
1561+
"dc94141cb41550df047ba3a965ce36d98eb6098eb952ca3cb6fd9682e5810b51".to_string();
1562+
1563+
// call functions
1564+
let spender_leaf = get_leaf(
1565+
app.clone(),
1566+
channel_context.clone(),
1567+
Extension(LeafFor::Spender),
1568+
Path((channel.id(), *ADVERTISER)),
1569+
)
1570+
.await
1571+
.expect("should get spender leaf");
1572+
let earner_leaf = get_leaf(
1573+
app.clone(),
1574+
channel_context.clone(),
1575+
Extension(LeafFor::Earner),
1576+
Path((channel.id(), *PUBLISHER)),
1577+
)
1578+
.await
1579+
.expect("should get earner leaf");
1580+
1581+
// compare results
1582+
assert_eq!(spender_proof, spender_leaf.merkle_proof);
1583+
assert_eq!(earner_proof, earner_leaf.merkle_proof);
1584+
}
14101585
}

sentry/src/routes/routers.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ use crate::{
3939
campaign,
4040
channel::{
4141
add_spender_leaf, channel_dummy_deposit, channel_list, channel_payout,
42-
get_accounting_for_channel, get_all_spender_limits, get_spender_limits, last_approved,
42+
get_accounting_for_channel, get_all_spender_limits, get_leaf, get_spender_limits,
43+
last_approved,
4344
validator_message::{create_validator_messages, list_validator_messages},
4445
},
4546
units_for_slot::post_units_for_slot,
@@ -66,6 +67,12 @@ async fn if_dummy_adapter<C: Locked + 'static, B>(
6667
}
6768
}
6869

70+
#[derive(Clone)]
71+
pub enum LeafFor {
72+
Earner,
73+
Spender,
74+
}
75+
6976
/// `/v5/channel` router
7077
pub fn channels_router<C: Locked + 'static>() -> Router {
7178
let spender_routes = Router::new()
@@ -79,6 +86,12 @@ pub fn channels_router<C: Locked + 'static>() -> Router {
7986
ServiceBuilder::new().layer(middleware::from_fn(authentication_required::<C, _>)),
8087
);
8188

89+
let get_leaf_routes = Router::new()
90+
.route("/spender/:addr", get(get_leaf::<C>))
91+
.route_layer(Extension(LeafFor::Spender))
92+
.route("/earner/:addr", get(get_leaf::<C>))
93+
.route_layer(Extension(LeafFor::Earner));
94+
8295
let channel_routes = Router::new()
8396
.route(
8497
"/pay",
@@ -88,6 +101,7 @@ pub fn channels_router<C: Locked + 'static>() -> Router {
88101
.route("/accounting", get(get_accounting_for_channel::<C>))
89102
.route("/last-approved", get(last_approved::<C>))
90103
.nest("/spender", spender_routes)
104+
.nest("/get-leaf", get_leaf_routes)
91105
.route(
92106
"/validator-messages",
93107
post(create_validator_messages::<C>)

0 commit comments

Comments
 (0)