Skip to content

Commit 602763a

Browse files
committed
Merge branch 'gakonst-asset-scale' into gakonst-precision-loss
2 parents 754c764 + fa69cd6 commit 602763a

File tree

1 file changed

+164
-1
lines changed

1 file changed

+164
-1
lines changed

crates/interledger-service-util/src/exchange_rates_service.rs

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ where
9999

100100
match outgoing_amount {
101101
Ok(outgoing_amount) => {
102-
// f64 which cannot fit in u64 gets cast as 0
102+
// The conversion succeeded, but the produced f64
103+
// is larger than the maximum value for a u64.
104+
// When it gets cast to a u64, it will end up being 0.
103105
if outgoing_amount != 0.0 && outgoing_amount as u64 == 0 {
104106
return Box::new(err(RejectBuilder {
105107
code: ErrorCode::F08_AMOUNT_TOO_LARGE,
@@ -119,6 +121,10 @@ where
119121
outgoing_amount, request.to.asset_code(), request.to.asset_scale(), request.to.id());
120122
}
121123
Err(_) => {
124+
// This branch gets executed when the `Convert` trait
125+
// returns an error. Happens due to float
126+
// multiplication overflow .
127+
// (float overflow in Rust produces +inf)
122128
return Box::new(err(RejectBuilder {
123129
code: ErrorCode::F08_AMOUNT_TOO_LARGE,
124130
message: format!(
@@ -141,3 +147,160 @@ where
141147
Box::new(self.next.send_request(request))
142148
}
143149
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
use futures::{future::ok, Future};
155+
use interledger_ildcp::IldcpAccount;
156+
use interledger_packet::{Address, FulfillBuilder, PrepareBuilder};
157+
use interledger_service::{outgoing_service_fn, Account};
158+
use std::collections::HashMap;
159+
use std::str::FromStr;
160+
use std::{
161+
sync::{Arc, Mutex},
162+
time::SystemTime,
163+
};
164+
165+
#[test]
166+
fn exchange_rate_ok() {
167+
let ret = exchange_rate(100, 1, 1.0, 1, 2.0);
168+
assert_eq!(ret.1[0].prepare.amount(), 200);
169+
170+
let ret = exchange_rate(1_000_000, 1, 3.0, 1, 2.0);
171+
assert_eq!(ret.1[0].prepare.amount(), 666_666);
172+
}
173+
174+
#[test]
175+
fn exchange_conversion_error() {
176+
// rejects f64 that does not fit in u64
177+
let ret = exchange_rate(std::u64::MAX, 1, 1.0, 1, 2.0);
178+
let reject = ret.0.unwrap_err();
179+
assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE);
180+
assert!(reject.message().starts_with(b"Could not cast"));
181+
182+
// `Convert` errored
183+
let ret = exchange_rate(std::u64::MAX, 1, 1.0, 255, std::f64::MAX);
184+
let reject = ret.0.unwrap_err();
185+
assert_eq!(reject.code(), ErrorCode::F08_AMOUNT_TOO_LARGE);
186+
assert!(reject.message().starts_with(b"Could not convert"));
187+
}
188+
189+
// Instantiates an exchange rate service and returns the fulfill/reject
190+
// packet and the outgoing request after performing an asset conversion
191+
fn exchange_rate(
192+
amount: u64,
193+
scale1: u8,
194+
rate1: f64,
195+
scale2: u8,
196+
rate2: f64,
197+
) -> (Result<Fulfill, Reject>, Vec<OutgoingRequest<TestAccount>>) {
198+
let requests = Arc::new(Mutex::new(Vec::new()));
199+
let requests_clone = requests.clone();
200+
let outgoing = outgoing_service_fn(move |request| {
201+
requests_clone.lock().unwrap().push(request);
202+
Box::new(ok(FulfillBuilder {
203+
fulfillment: &[0; 32],
204+
data: b"hello!",
205+
}
206+
.build()))
207+
});
208+
let mut service = test_service(rate1, rate2, outgoing);
209+
let result = service
210+
.send_request(OutgoingRequest {
211+
from: TestAccount::new("ABC".to_owned(), scale1),
212+
to: TestAccount::new("XYZ".to_owned(), scale2),
213+
original_amount: amount,
214+
prepare: PrepareBuilder {
215+
destination: Address::from_str("example.destination").unwrap(),
216+
amount,
217+
expires_at: SystemTime::now(),
218+
execution_condition: &[1; 32],
219+
data: b"hello",
220+
}
221+
.build(),
222+
})
223+
.wait();
224+
225+
let reqs = requests.lock().unwrap();
226+
(result, reqs.clone())
227+
}
228+
229+
#[derive(Debug, Clone)]
230+
struct TestAccount {
231+
ilp_address: Address,
232+
asset_code: String,
233+
asset_scale: u8,
234+
}
235+
impl TestAccount {
236+
fn new(asset_code: String, asset_scale: u8) -> Self {
237+
TestAccount {
238+
ilp_address: Address::from_str("example.alice").unwrap(),
239+
asset_code,
240+
asset_scale,
241+
}
242+
}
243+
}
244+
245+
impl Account for TestAccount {
246+
type AccountId = u64;
247+
248+
fn id(&self) -> u64 {
249+
0
250+
}
251+
}
252+
253+
impl IldcpAccount for TestAccount {
254+
fn asset_code(&self) -> &str {
255+
&self.asset_code
256+
}
257+
258+
fn asset_scale(&self) -> u8 {
259+
self.asset_scale
260+
}
261+
262+
fn client_address(&self) -> &Address {
263+
&self.ilp_address
264+
}
265+
}
266+
267+
#[derive(Debug, Clone)]
268+
struct TestStore {
269+
rates: HashMap<Vec<String>, (f64, f64)>,
270+
}
271+
272+
impl ExchangeRateStore for TestStore {
273+
fn get_exchange_rates(&self, asset_codes: &[&str]) -> Result<Vec<f64>, ()> {
274+
let mut ret = Vec::new();
275+
let key = vec![asset_codes[0].to_owned(), asset_codes[1].to_owned()];
276+
let v = self.rates.get(&key);
277+
if let Some(v) = v {
278+
ret.push(v.0);
279+
ret.push(v.1);
280+
} else {
281+
return Err(());
282+
}
283+
Ok(ret)
284+
}
285+
}
286+
287+
fn test_store(rate1: f64, rate2: f64) -> TestStore {
288+
let mut rates = HashMap::new();
289+
rates.insert(vec!["ABC".to_owned(), "XYZ".to_owned()], (rate1, rate2));
290+
TestStore { rates }
291+
}
292+
293+
fn test_service(
294+
rate1: f64,
295+
rate2: f64,
296+
handler: impl OutgoingService<TestAccount> + Clone + Send + Sync,
297+
) -> ExchangeRateService<
298+
TestStore,
299+
impl OutgoingService<TestAccount> + Clone + Send + Sync,
300+
TestAccount,
301+
> {
302+
let store = test_store(rate1, rate2);
303+
ExchangeRateService::new(Address::from_str("example.bob").unwrap(), store, handler)
304+
}
305+
306+
}

0 commit comments

Comments
 (0)