-
Notifications
You must be signed in to change notification settings - Fork 63
Improve Asset Scale conversions #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ba54167
fix(settlement-client): do not convert scale
gakonst 5e576ab
feat(settlement): Implement asset scale conversion for u256
gakonst a04832a
feat(settlement): Normalize the amount properly when being notified b…
gakonst 4fc57a2
fix(eth-se): Scale the amount to settle for based on the body and eng…
gakonst f02937d
improve tester
gakonst 9395f53
test(eth-se): adjust test for proper scale conversion
gakonst 0041bd7
test(eth-xrp) Set ETHXRP exchange rate
gakonst c32f27c
test(eth-xrp): Switch back to BTP
gakonst 5b1e2a5
fix(crate): Remove settlement_engine_asset_scale from account
gakonst a5874a9
improvement(settlement/engines): use BigUInt to handle big numbers
gakonst 61ecec5
test(settlement-engines): use asset_scale local variables per test
gakonst 642a29b
test(engine-eth-xrp): elaborate on how results are produced
gakonst 725b9f0
fix(settlement): Convert asset scale properly
gakonst ca46d74
fix comment
gakonst 56237a6
Revert "test(eth-xrp): Switch back to BTP"
gakonst 1f6f9a4
feat(settlement/engine): Return None when idempotent data not found i…
gakonst 9de2c7f
docs: fix readme in interoperability test
gakonst b5b62ec
fix(settlement): Make u64 conversions overflow-safe, and warn for f64…
gakonst 1c3a182
docs: fix review comments
gakonst 449e4a6
improvement(settlement): return Error on overflow during conversion
gakonst d227963
improvement(exchange-rate): make the scale conversion after the excha…
gakonst fa69cd6
test(exchange-rate): Add tests which verify that rate conversions fai…
gakonst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,15 +90,217 @@ where | |
.build())); | ||
}; | ||
|
||
let scaled_rate = rate.normalize_scale(ConvertDetails { | ||
from: request.to.asset_scale(), | ||
to: request.from.asset_scale(), | ||
// Can we overflow here? | ||
let outgoing_amount = (request.prepare.amount() as f64) * rate; | ||
let outgoing_amount = outgoing_amount.normalize_scale(ConvertDetails { | ||
from: request.from.asset_scale(), | ||
to: request.to.asset_scale(), | ||
}); | ||
let outgoing_amount = (request.prepare.amount() as f64) * scaled_rate; | ||
request.prepare.set_amount(outgoing_amount as u64); | ||
trace!("Converted incoming amount of: {} {} (scale {}) from account {} to outgoing amount of: {} {} (scale {}) for account {}", request.original_amount, request.from.asset_code(), request.from.asset_scale(), request.from.id(), outgoing_amount, request.to.asset_code(), request.to.asset_scale(), request.to.id()); | ||
|
||
match outgoing_amount { | ||
Ok(outgoing_amount) => { | ||
// The conversion succeeded, but the produced f64 | ||
// is larger than the maximum value for a u64. | ||
// When it gets cast to a u64, it will end up being 0. | ||
if outgoing_amount != 0.0 && outgoing_amount as u64 == 0 { | ||
return Box::new(err(RejectBuilder { | ||
code: ErrorCode::F08_AMOUNT_TOO_LARGE, | ||
message: format!( | ||
"Could not cast outgoing amount to u64 {}", | ||
outgoing_amount, | ||
) | ||
.as_bytes(), | ||
triggered_by: Some(&self.ilp_address), | ||
data: &[], | ||
} | ||
.build())); | ||
} | ||
request.prepare.set_amount(outgoing_amount as u64); | ||
trace!("Converted incoming amount of: {} {} (scale {}) from account {} to outgoing amount of: {} {} (scale {}) for account {}", | ||
request.original_amount, request.from.asset_code(), request.from.asset_scale(), request.from.id(), | ||
outgoing_amount, request.to.asset_code(), request.to.asset_scale(), request.to.id()); | ||
} | ||
Err(_) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would this return an error versus turning into 0? This code is kind of confusing so it would definitely be helpful to put some comments describing what's going on |
||
// This branch gets executed when the `Convert` trait | ||
// returns an error. Happens due to float | ||
// multiplication overflow . | ||
// (float overflow in Rust produces +inf) | ||
return Box::new(err(RejectBuilder { | ||
code: ErrorCode::F08_AMOUNT_TOO_LARGE, | ||
message: format!( | ||
"Could not convert exchange rate from {}:{} to: {}:{}. Got incoming amount: {}", | ||
request.from.asset_code(), | ||
request.from.asset_scale(), | ||
request.to.asset_code(), | ||
request.to.asset_scale(), | ||
request.prepare.amount(), | ||
) | ||
.as_bytes(), | ||
triggered_by: Some(&self.ilp_address), | ||
data: &[], | ||
} | ||
.build())); | ||
} | ||
} | ||
} | ||
|
||
Box::new(self.next.send_request(request)) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use futures::{future::ok, Future}; | ||
use interledger_ildcp::IldcpAccount; | ||
use interledger_packet::{Address, FulfillBuilder, PrepareBuilder}; | ||
use interledger_service::{outgoing_service_fn, Account}; | ||
use std::collections::HashMap; | ||
use std::str::FromStr; | ||
use std::{ | ||
sync::{Arc, Mutex}, | ||
time::SystemTime, | ||
}; | ||
|
||
#[test] | ||
fn exchange_rate_ok() { | ||
let ret = exchange_rate(100, 1, 1.0, 1, 2.0); | ||
assert_eq!(ret.1[0].prepare.amount(), 200); | ||
|
||
let ret = exchange_rate(1_000_000, 1, 3.0, 1, 2.0); | ||
assert_eq!(ret.1[0].prepare.amount(), 666_666); | ||
} | ||
|
||
#[test] | ||
fn exchange_conversion_error() { | ||
// rejects f64 that does not fit in u64 | ||
let ret = exchange_rate(std::u64::MAX, 1, 1.0, 1, 2.0); | ||
let reject = ret.0.unwrap_err(); | ||
assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE); | ||
assert!(reject.message().starts_with(b"Could not cast")); | ||
|
||
// `Convert` errored | ||
let ret = exchange_rate(std::u64::MAX, 1, 1.0, 255, std::f64::MAX); | ||
let reject = ret.0.unwrap_err(); | ||
assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE); | ||
assert!(reject.message().starts_with(b"Could not convert")); | ||
} | ||
|
||
// Instantiates an exchange rate service and returns the fulfill/reject | ||
// packet and the outgoing request after performing an asset conversion | ||
fn exchange_rate( | ||
amount: u64, | ||
scale1: u8, | ||
rate1: f64, | ||
scale2: u8, | ||
rate2: f64, | ||
) -> (Result<Fulfill, Reject>, Vec<OutgoingRequest<TestAccount>>) { | ||
let requests = Arc::new(Mutex::new(Vec::new())); | ||
let requests_clone = requests.clone(); | ||
let outgoing = outgoing_service_fn(move |request| { | ||
requests_clone.lock().unwrap().push(request); | ||
Box::new(ok(FulfillBuilder { | ||
fulfillment: &[0; 32], | ||
data: b"hello!", | ||
} | ||
.build())) | ||
}); | ||
let mut service = test_service(rate1, rate2, outgoing); | ||
let result = service | ||
.send_request(OutgoingRequest { | ||
from: TestAccount::new("ABC".to_owned(), scale1), | ||
to: TestAccount::new("XYZ".to_owned(), scale2), | ||
original_amount: amount, | ||
prepare: PrepareBuilder { | ||
destination: Address::from_str("example.destination").unwrap(), | ||
amount, | ||
expires_at: SystemTime::now(), | ||
execution_condition: &[1; 32], | ||
data: b"hello", | ||
} | ||
.build(), | ||
}) | ||
.wait(); | ||
|
||
let reqs = requests.lock().unwrap(); | ||
(result, reqs.clone()) | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct TestAccount { | ||
ilp_address: Address, | ||
asset_code: String, | ||
asset_scale: u8, | ||
} | ||
impl TestAccount { | ||
fn new(asset_code: String, asset_scale: u8) -> Self { | ||
TestAccount { | ||
ilp_address: Address::from_str("example.alice").unwrap(), | ||
asset_code, | ||
asset_scale, | ||
} | ||
} | ||
} | ||
|
||
impl Account for TestAccount { | ||
type AccountId = u64; | ||
|
||
fn id(&self) -> u64 { | ||
0 | ||
} | ||
} | ||
|
||
impl IldcpAccount for TestAccount { | ||
fn asset_code(&self) -> &str { | ||
&self.asset_code | ||
} | ||
|
||
fn asset_scale(&self) -> u8 { | ||
self.asset_scale | ||
} | ||
|
||
fn client_address(&self) -> &Address { | ||
&self.ilp_address | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct TestStore { | ||
rates: HashMap<Vec<String>, (f64, f64)>, | ||
} | ||
|
||
impl ExchangeRateStore for TestStore { | ||
fn get_exchange_rates(&self, asset_codes: &[&str]) -> Result<Vec<f64>, ()> { | ||
let mut ret = Vec::new(); | ||
let key = vec![asset_codes[0].to_owned(), asset_codes[1].to_owned()]; | ||
let v = self.rates.get(&key); | ||
if let Some(v) = v { | ||
ret.push(v.0); | ||
ret.push(v.1); | ||
} else { | ||
return Err(()); | ||
} | ||
Ok(ret) | ||
} | ||
} | ||
|
||
fn test_store(rate1: f64, rate2: f64) -> TestStore { | ||
let mut rates = HashMap::new(); | ||
rates.insert(vec!["ABC".to_owned(), "XYZ".to_owned()], (rate1, rate2)); | ||
TestStore { rates } | ||
} | ||
|
||
fn test_service( | ||
rate1: f64, | ||
rate2: f64, | ||
handler: impl OutgoingService<TestAccount> + Clone + Send + Sync, | ||
) -> ExchangeRateService< | ||
TestStore, | ||
impl OutgoingService<TestAccount> + Clone + Send + Sync, | ||
TestAccount, | ||
> { | ||
let store = test_store(rate1, rate2); | ||
ExchangeRateService::new(Address::from_str("example.bob").unwrap(), store, handler) | ||
} | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this check really work? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a test which shows that it does