diff --git a/substrate/.gitlab-ci.yml b/substrate/.gitlab-ci.yml index 8791690d89..8083e34ea9 100644 --- a/substrate/.gitlab-ci.yml +++ b/substrate/.gitlab-ci.yml @@ -214,6 +214,24 @@ test-frame-staking: - WASM_BUILD_NO_COLOR=1 time cargo test --release --verbose --no-default-features --features std - sccache -s +test-frame-examples-compile-to-wasm: + stage: test + <<: *docker-env + variables: + # Enable debug assertions since we are running optimized builds for testing + # but still want to have debug assertions. + RUSTFLAGS: -Cdebug-assertions=y + RUST_BACKTRACE: 1 + except: + variables: + - $DEPLOY_TAG + script: + - cd frame/example-offchain-worker/ + - cargo +nightly build --target=wasm32-unknown-unknown --no-default-features + - cd ../example + - cargo +nightly build --target=wasm32-unknown-unknown --no-default-features + - sccache -s + test-wasmtime: stage: test <<: *docker-env diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index dc3530103a..8a55a09772 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3063,6 +3063,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lite-json" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa835713bb12ba5204013497da16caf2dd2eee25ca829d0efaa054fb38c4ddd" +dependencies = [ + "paste", +] + [[package]] name = "lock_api" version = "0.3.3" @@ -4147,9 +4156,9 @@ version = "2.0.0-alpha.4" dependencies = [ "frame-support", "frame-system", + "lite-json", "parity-scale-codec", "serde", - "serde_json", "sp-core", "sp-io", "sp-runtime", @@ -6718,18 +6727,18 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +checksum = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8" dependencies = [ "proc-macro2", "quote", diff --git a/substrate/frame/example-offchain-worker/Cargo.toml b/substrate/frame/example-offchain-worker/Cargo.toml index 3c705138f4..a700c6fad7 100644 --- a/substrate/frame/example-offchain-worker/Cargo.toml +++ b/substrate/frame/example-offchain-worker/Cargo.toml @@ -17,7 +17,7 @@ sp-core = { version = "2.0.0-alpha.4", default-features = false, path = "../../p sp-io = { version = "2.0.0-alpha.4", default-features = false, path = "../../primitives/io" } sp-runtime = { version = "2.0.0-alpha.4", default-features = false, path = "../../primitives/runtime" } sp-std = { version = "2.0.0-alpha.4", default-features = false, path = "../../primitives/std" } -serde_json = { version = "1.0.46", default-features = false, features = ["alloc"] } +lite-json = { version = "0.1", default-features = false } [features] default = ["std"] @@ -26,6 +26,7 @@ std = [ "frame-support/std", "frame-system/std", "serde", + "lite-json/std", "sp-core/std", "sp-io/std", "sp-runtime/std", diff --git a/substrate/frame/example-offchain-worker/src/lib.rs b/substrate/frame/example-offchain-worker/src/lib.rs index b05d7c38da..5a417fa078 100644 --- a/substrate/frame/example-offchain-worker/src/lib.rs +++ b/substrate/frame/example-offchain-worker/src/lib.rs @@ -47,13 +47,14 @@ use frame_support::{ weights::SimpleDispatchInfo, }; use frame_system::{self as system, ensure_signed, ensure_none, offchain}; -use serde_json as json; use sp_core::crypto::KeyTypeId; use sp_runtime::{ offchain::{http, Duration, storage::StorageValueRef}, traits::Zero, transaction_validity::{InvalidTransaction, ValidTransaction, TransactionValidity}, }; +use sp_std::{vec, vec::Vec}; +use lite_json::json::JsonValue; #[cfg(test)] mod tests; @@ -320,7 +321,7 @@ impl Module { } /// A helper function to fetch the price and send signed transaction. - fn fetch_price_and_send_signed() -> Result<(), String> { + fn fetch_price_and_send_signed() -> Result<(), &'static str> { use system::offchain::SubmitSignedTransaction; // Firstly we check if there are any accounts in the local keystore that are capable of // signing the transaction. @@ -334,7 +335,7 @@ impl Module { // Make an external HTTP request to fetch the current price. // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|e| format!("{:?}", e))?; + let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; // Received price is wrapped into a call to `submit_price` public function of this pallet. // This means that the transaction, when executed, will simply call that function passing @@ -357,20 +358,18 @@ impl Module { } /// A helper function to fetch the price and send unsigned transaction. - fn fetch_price_and_send_unsigned(block_number: T::BlockNumber) -> Result<(), String> { + fn fetch_price_and_send_unsigned(block_number: T::BlockNumber) -> Result<(), &'static str> { use system::offchain::SubmitUnsignedTransaction; // Make sure we don't fetch the price if unsigned transaction is going to be rejected // anyway. let next_unsigned_at = >::get(); if next_unsigned_at > block_number { - return Err( - format!("Too early to send unsigned transaction. Next at: {:?}", next_unsigned_at) - )? + return Err("Too early to send unsigned transaction") } // Make an external HTTP request to fetch the current price. // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|e| format!("{:?}", e))?; + let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; // Received price is wrapped into a call to `submit_price_unsigned` public function of this // pallet. This means that the transaction, when executed, will simply call that function @@ -428,21 +427,17 @@ impl Module { // Note that the return object allows you to read the body in chunks as well // with a way to control the deadline. let body = response.body().collect::>(); - // Next we parse the response using `serde_json`. Even though it's possible to use - // `serde_derive` and deserialize to a struct it's not recommended due to blob size - // overhead introduced by such code. Deserializing to `json::Value` is much more - // lightweight and should be preferred, especially if we only care about a small number - // of properties from the response. - let val: Result = json::from_slice(&body); - // Let's parse the price as float value. Note that you should avoid using floats in the - // runtime, it's fine to do that in the offchain worker, but we do convert it to an integer - // before submitting on-chain. - let price = val.ok().and_then(|v| v.get("USD").and_then(|v| v.as_f64())); - let price = match price { - Some(pricef) => Ok((pricef * 100.) as u32), + + // Create a str slice from the body. + let body_str = sp_std::str::from_utf8(&body).map_err(|_| { + debug::warn!("No UTF8 body"); + http::Error::Unknown + })?; + + let price = match Self::parse_price(body_str) { + Some(price) => Ok(price), None => { - let s = core::str::from_utf8(&body); - debug::warn!("Unable to extract price from the response: {:?}", s); + debug::warn!("Unable to extract price from the response: {:?}", body_str); Err(http::Error::Unknown) } }?; @@ -452,6 +447,28 @@ impl Module { Ok(price) } + /// Parse the price from the given JSON string using `lite-json`. + /// + /// Returns `None` when parsing failed or `Some(price in cents)` when parsing is successful. + fn parse_price(price_str: &str) -> Option { + let val = lite_json::parse_json(price_str); + let price = val.ok().and_then(|v| match v { + JsonValue::Object(obj) => { + let mut chars = "USD".chars(); + obj.into_iter() + .find(|(k, _)| k.iter().all(|k| Some(*k) == chars.next())) + .and_then(|v| match v.1 { + JsonValue::Number(number) => Some(number), + _ => None, + }) + }, + _ => None + })?; + + let exp = price.fraction_length.checked_sub(2).unwrap_or(0); + Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32) + } + /// Add new price to the list. fn add_price(who: T::AccountId, price: u32) { debug::info!("Adding to the average: {}", price); diff --git a/substrate/frame/example-offchain-worker/src/tests.rs b/substrate/frame/example-offchain-worker/src/tests.rs index 9b6a567a17..f64503b0a9 100644 --- a/substrate/frame/example-offchain-worker/src/tests.rs +++ b/substrate/frame/example-offchain-worker/src/tests.rs @@ -132,7 +132,7 @@ fn should_make_http_call_and_parse_result() { // when let price = Example::fetch_price().unwrap(); // then - assert_eq!(price, 15522); + assert_eq!(price, 15523); }); } @@ -164,7 +164,7 @@ fn should_submit_signed_transaction_on_chain() { assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature.unwrap().0, 0); - assert_eq!(tx.call, Call::submit_price(15522)); + assert_eq!(tx.call, Call::submit_price(15523)); }); } @@ -186,7 +186,7 @@ fn should_submit_unsigned_transaction_on_chain() { assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); - assert_eq!(tx.call, Call::submit_price_unsigned(1, 15522)); + assert_eq!(tx.call, Call::submit_price_unsigned(1, 15523)); }); } @@ -208,3 +208,19 @@ fn price_oracle_response(state: &mut testing::OffchainState) { ..Default::default() }); } + +#[test] +fn parse_price_works() { + let test_data = vec![ + ("{\"USD\":6536.92}", Some(653692)), + ("{\"USD\":65.92}", Some(6592)), + ("{\"USD\":6536.924565}", Some(653692)), + ("{\"USD\":6536}", Some(653600)), + ("{\"USD2\":6536}", None), + ("{\"USD\":\"6432\"}", None), + ]; + + for (json, expected) in test_data { + assert_eq!(expected, Example::parse_price(json)); + } +} diff --git a/substrate/frame/example/Cargo.toml b/substrate/frame/example/Cargo.toml index ee1b870e03..5db9aca45b 100644 --- a/substrate/frame/example/Cargo.toml +++ b/substrate/frame/example/Cargo.toml @@ -20,7 +20,7 @@ sp-std = { version = "2.0.0-alpha.4", default-features = false, path = "../../pr sp-io = { version = "2.0.0-alpha.4", default-features = false, path = "../../primitives/io" } [dev-dependencies] -sp-core = { version = "2.0.0-alpha.4", path = "../../primitives/core" } +sp-core = { version = "2.0.0-alpha.4", path = "../../primitives/core", default-features = false } [features] default = ["std"]