Skip to content

Commit aecadaa

Browse files
authored
Merge pull request #439 from AdExNetwork/issue-431-get-accounting-for-channel
Issue 431 get accounting for channel
2 parents 692bf08 + 524db4d commit aecadaa

File tree

3 files changed

+197
-6
lines changed

3 files changed

+197
-6
lines changed

sentry/src/db/accounting.rs

+14
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ pub async fn get_accounting(
8383
Ok(row.as_ref().map(Accounting::from))
8484
}
8585

86+
pub async fn get_all_accountings_for_channel(
87+
pool: DbPool,
88+
channel_id: ChannelId,
89+
) -> Result<Vec<Accounting>, PoolError> {
90+
let client = pool.get().await?;
91+
let statement = client.prepare("SELECT channel_id, side, address, amount, updated, created FROM accounting WHERE channel_id = $1").await?;
92+
93+
let rows = client.query(&statement, &[&channel_id]).await?;
94+
95+
let accountings = rows.iter().map(Accounting::from).collect();
96+
97+
Ok(accountings)
98+
}
99+
86100
/// Will update current Spender/Earner amount or insert a new Accounting record
87101
///
88102
/// See `UPDATE_ACCOUNTING_STATEMENT` static for full query.

sentry/src/lib.rs

+19-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use {
2525
campaign::{campaign_list, create_campaign, update_campaign},
2626
cfg::config,
2727
channel::{
28-
channel_list, create_validator_messages, get_all_spender_limits, get_spender_limits,
29-
last_approved,
28+
channel_list, create_validator_messages, get_accounting_for_channel,
29+
get_all_spender_limits, get_spender_limits, last_approved,
3030
},
3131
event_aggregate::list_channel_event_aggregates,
3232
validator_message::{extract_params, list_validator_messages},
@@ -81,6 +81,10 @@ static CHANNEL_ALL_SPENDER_LIMITS: Lazy<Regex> = Lazy::new(|| {
8181
Regex::new(r"^/v5/channel/0x([a-zA-Z0-9]{64})/spender/all/?$")
8282
.expect("The regex should be valid")
8383
});
84+
static CHANNEL_ACCOUNTING: Lazy<Regex> = Lazy::new(|| {
85+
Regex::new(r"^/v5/channel/0x([a-zA-Z0-9]{64})/accounting/?$")
86+
.expect("The regex should be valid")
87+
});
8488

8589
#[derive(Debug, Clone)]
8690
pub struct RouteParams(pub Vec<String>);
@@ -394,6 +398,19 @@ async fn channels_router<A: Adapter + 'static>(
394398
.await?;
395399

396400
get_all_spender_limits(req, app).await
401+
} else if let (Some(caps), &Method::GET) = (CHANNEL_ACCOUNTING.captures(&path), method) {
402+
let param = RouteParams(vec![caps
403+
.get(1)
404+
.map_or("".to_string(), |m| m.as_str().to_string())]);
405+
req.extensions_mut().insert(param);
406+
407+
req = Chain::new()
408+
.chain(AuthRequired)
409+
.chain(ChannelLoad)
410+
.apply(req, app)
411+
.await?;
412+
413+
get_accounting_for_channel(req, app).await
397414
} else {
398415
Err(ResponseError::NotFound)
399416
}

sentry/src/routes/channel.rs

+164-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::db::{
2+
accounting::{get_all_accountings_for_channel, Side},
23
event_aggregate::{latest_approve_state_v5, latest_heartbeats, latest_new_state_v5},
34
insert_channel, insert_validator_messages, list_channels,
45
spendable::{fetch_spendable, get_all_spendables_for_channel, update_spendable},
@@ -9,11 +10,11 @@ use futures::future::try_join_all;
910
use hyper::{Body, Request, Response};
1011
use primitives::{
1112
adapter::Adapter,
12-
balances::{CheckedState, UncheckedState},
13+
balances::{Balances, CheckedState, UncheckedState},
1314
config::TokenInfo,
1415
sentry::{
15-
channel_list::ChannelListQuery, AllSpendersResponse, LastApproved, LastApprovedQuery,
16-
LastApprovedResponse, Pagination, SpenderResponse, SuccessResponse,
16+
channel_list::ChannelListQuery, AccountingResponse, AllSpendersResponse, LastApproved,
17+
LastApprovedQuery, LastApprovedResponse, Pagination, SpenderResponse, SuccessResponse,
1718
},
1819
spender::{Spendable, Spender, SpenderLeaf},
1920
validator::{MessageTypes, NewState},
@@ -341,13 +342,54 @@ async fn get_corresponding_new_state(
341342
new_state
342343
}
343344

345+
pub async fn get_accounting_for_channel<A: Adapter + 'static>(
346+
req: Request<Body>,
347+
app: &Application<A>,
348+
) -> Result<Response<Body>, ResponseError> {
349+
let channel = req
350+
.extensions()
351+
.get::<Channel>()
352+
.expect("Request should have Channel")
353+
.to_owned();
354+
355+
let accountings = get_all_accountings_for_channel(app.pool.clone(), channel.id()).await?;
356+
357+
let mut unchecked_balances: Balances<UncheckedState> = Balances::default();
358+
359+
for accounting in accountings {
360+
match accounting.side {
361+
Side::Earner => unchecked_balances
362+
.earners
363+
.insert(accounting.address, accounting.amount),
364+
Side::Spender => unchecked_balances
365+
.spenders
366+
.insert(accounting.address, accounting.amount),
367+
};
368+
}
369+
370+
let balances = match unchecked_balances.check() {
371+
Ok(balances) => balances,
372+
Err(error) => {
373+
error!(&app.logger, "{}", &error; "module" => "channel_accounting");
374+
return Err(ResponseError::FailedValidation(
375+
"Earners sum is not equal to spenders sum for channel".to_string(),
376+
));
377+
}
378+
};
379+
380+
let res = AccountingResponse::<CheckedState> { balances };
381+
Ok(success_response(serde_json::to_string(&res)?))
382+
}
383+
344384
#[cfg(test)]
345385
mod test {
346386
use super::*;
387+
use crate::db::{accounting::spend_amount, insert_channel};
347388
use crate::test_util::setup_dummy_app;
389+
use hyper::StatusCode;
348390
use primitives::{
349391
adapter::Deposit,
350-
util::tests::prep_db::{ADDRESSES, DUMMY_CAMPAIGN},
392+
util::tests::prep_db::{ADDRESSES, DUMMY_CAMPAIGN, IDS},
351393
BigNum,
352394
};
353395

@@ -433,4 +475,122 @@ mod test {
433475
);
434476
assert_eq!(updated_spendable.spender, ADDRESSES["creator"]);
435477
}
478+
479+
async fn res_to_accounting_response(res: Response<Body>) -> AccountingResponse<CheckedState> {
480+
let json = hyper::body::to_bytes(res.into_body())
481+
.await
482+
.expect("Should get json");
483+
484+
let accounting_response: AccountingResponse<CheckedState> =
485+
serde_json::from_slice(&json).expect("Should get AccouuntingResponse");
486+
accounting_response
487+
}
488+
489+
#[tokio::test]
490+
async fn get_accountings_for_channel() {
491+
let app = setup_dummy_app().await;
492+
let channel = DUMMY_CAMPAIGN.channel.clone();
493+
insert_channel(&app.pool, channel)
494+
.await
495+
.expect("should insert channel");
496+
let build_request = |channel: Channel| {
497+
Request::builder()
498+
.extension(channel)
499+
.body(Body::empty())
500+
.expect("Should build Request")
501+
};
502+
// Testing for no accounting yet
503+
{
504+
let res = get_accounting_for_channel(build_request(channel.clone()), &app)
505+
.await
506+
.expect("should get response");
507+
assert_eq!(StatusCode::OK, res.status());
508+
509+
let accounting_response = res_to_accounting_response(res).await;
510+
assert_eq!(accounting_response.balances.earners.len(), 0);
511+
assert_eq!(accounting_response.balances.spenders.len(), 0);
512+
}
513+
514+
// Testing for 2 accountings - first channel
515+
{
516+
let mut balances = Balances::<CheckedState>::new();
517+
balances
518+
.spend(
519+
ADDRESSES["creator"],
520+
ADDRESSES["publisher"],
521+
UnifiedNum::from_u64(200),
522+
)
523+
.expect("should not overflow");
524+
balances
525+
.spend(
526+
ADDRESSES["tester"],
527+
ADDRESSES["publisher2"],
528+
UnifiedNum::from_u64(100),
529+
)
530+
.expect("Should not overflow");
531+
spend_amount(app.pool.clone(), channel.id(), balances.clone())
532+
.await
533+
.expect("should spend");
534+
535+
let res = get_accounting_for_channel(build_request(channel.clone()), &app)
536+
.await
537+
.expect("should get response");
538+
assert_eq!(StatusCode::OK, res.status());
539+
540+
let accounting_response = res_to_accounting_response(res).await;
541+
542+
assert_eq!(balances, accounting_response.balances);
543+
}
544+
545+
// Testing for 2 accountings - second channel (same address is both an earner and a spender)
546+
{
547+
let mut second_channel = DUMMY_CAMPAIGN.channel.clone();
548+
second_channel.leader = IDS["user"]; // channel.id() will be different now
549+
insert_channel(&app.pool, second_channel)
550+
.await
551+
.expect("should insert channel");
552+
553+
let mut balances = Balances::<CheckedState>::new();
554+
balances
555+
.spend(ADDRESSES["tester"], ADDRESSES["publisher"], 300.into())
556+
.expect("Should not overflow");
557+
558+
balances
559+
.spend(ADDRESSES["publisher"], ADDRESSES["user"], 300.into())
560+
.expect("Should not overflow");
561+
562+
spend_amount(app.pool.clone(), second_channel.id(), balances.clone())
563+
.await
564+
.expect("should spend");
565+
566+
let res = get_accounting_for_channel(build_request(second_channel.clone()), &app)
567+
.await
568+
.expect("should get response");
569+
assert_eq!(StatusCode::OK, res.status());
570+
571+
let accounting_response = res_to_accounting_response(res).await;
572+
573+
assert_eq!(balances, accounting_response.balances)
574+
}
575+
576+
// Testing for when sums don't match on first channel - Error case
577+
{
578+
let mut balances = Balances::<CheckedState>::new();
579+
balances
580+
.earners
581+
.insert(ADDRESSES["publisher"], UnifiedNum::from_u64(100));
582+
balances
583+
.spenders
584+
.insert(ADDRESSES["creator"], UnifiedNum::from_u64(200));
585+
spend_amount(app.pool.clone(), channel.id(), balances)
586+
.await
587+
.expect("should spend");
588+
589+
let res = get_accounting_for_channel(build_request(channel.clone()), &app).await;
590+
let expected = ResponseError::FailedValidation(
591+
"Earners sum is not equal to spenders sum for channel".to_string(),
592+
);
593+
assert_eq!(expected, res.expect_err("Should return an error"));
594+
}
595+
}
436596
}

0 commit comments

Comments
 (0)