1. Clone repo:
$ git clone --depth 1 --branch td-submit-transactions https://github.com/paritytech/substrate
2. Add offchain worker that prints `Hello World!'
Compile & run
$ cargo run -p node-template -- --dev -lruntime=trace
debug::warn
, also show logger
3. Either print or debug::RuntimeLogger::init();
debug::print!("Hello world!");
Something
from the storage and display block hash/number
4. Read fn offchain_worker(block_number: T::BlockNumber) {
//debug::RuntimeLogger::init();
let something = Something::get();
debug::warn!("Hello World from offchain workers!");
debug::warn!("Something is: {:?}", something);
let block_hash = <system::Module<T>>::block_hash(block_number - 1);
debug::warn!("Current block is: {:?} (parent: {:?})", block_number, block_hash);
}
5. Make an HTTP request.
5.1. Handle response.
let price = match Self::fetch_btc_price() {
Ok(price) => {
debug::info!("Got BTC price: {} cents", price);
price
},
_ => {
debug::error!("Error fetching BTC price.", e);
return
}
};
5.2. Make a request and check response.
impl<T: Trait> Module<T> {
fn fetch_btc_price() -> Result<u64, http::Error> {
let pending = http::Request::get(
"https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD"
).send().map_err(|_| http::Error::IoError)?;
let response = pending.wait()?;
if response.code != 200 {
debug::warn!("Unexpected status code: {}", response.code);
return Err(http::Error::Unknown);
}
let body = response.body().collect::<Vec<u8>>();
debug::warn!("Body: {:?}", core::str::from_utf8(&body).ok());
Ok(5)
}
}
5.3. Parse JSON.
const START_IDX: usize = "{\"USD\":".len();
let body = response.body().collect::<Vec<u8>>();
let json = match core::str::from_utf8(&body) {
Ok(json) if json.len() > START_IDX => json,
_ => {
debug::warn!("Unexpected (non-utf8 or too short) response received: {:?}", body);
return Err(http::Error::Unknown);
}
};
5.4. Parse number:
let price = &json[START_IDX .. json.len() - 1];
let pricef: f64 = match price.parse() {
Ok(pricef) => pricef,
Err(_) => {
debug::warn!("Unparsable price: {:?}", price);
return Err(http::Error::Unknown);
}
};
Ok((pricef * 100.) as u64)
6. Create signed transaction.
6.1. Add type SubmitTransaction, type Call to the trait.
/// The overarching event type.
type Call: From<Call<Self>>;
/// Transaction submitter.
type SubmitTransaction: system::offchain::SubmitSignedTransaction<Self, <Self as Trait>::Call>;
6.2. Add app-crypto stuff.
use primitives::crypto::KeyTypeId;
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!");
pub mod crypto {
use super::KEY_TYPE;
use sp_runtime::app_crypto::{app_crypto, sr25519};
app_crypto!(sr25519, KEY_TYPE);
}
6.3. Implement CreateTransaction for the runtime.
/// The payload being signed in transactions.
pub type SignedPayload = generic::SignedPayload<Call, SignedExtra>;
impl system::offchain::CreateTransaction<Runtime, UncheckedExtrinsic> for Runtime {
type Public = <Signature as sp_runtime::traits::Verify>::Signer;
type Signature = Signature;
fn create_transaction<TSigner: system::offchain::Signer<Self::Public, Self::Signature>>(
call: Call,
public: Self::Public,
account: AccountId,
index: Index,
) -> Option<(Call, <UncheckedExtrinsic as sp_runtime::traits::Extrinsic>::SignaturePayload)> {
let period = 1 << 8;
let current_block = System::block_number() as u64;
let tip = 0;
let extra: SignedExtra = (
system::CheckVersion::<Runtime>::new(),
system::CheckGenesis::<Runtime>::new(),
system::CheckEra::<Runtime>::from(generic::Era::mortal(period, current_block)),
system::CheckNonce::<Runtime>::from(index),
system::CheckWeight::<Runtime>::new(),
transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
);
let raw_payload = SignedPayload::new(call, extra).ok()?;
let signature = TSigner::sign(public, &raw_payload)?;
let address = Indices::unlookup(account);
let (call, extra, _) = raw_payload.deconstruct();
Some((call, (address, signature, extra)))
}
}
6.4. Implement submission logic.
fn submit_btc_price_on_chain(price: u32) {
use system::offchain::SubmitSignedTransaction;
let call = Call::do_something(price);
let res = T::SubmitTransaction::submit_signed(
call
);
if res.is_empty() {
debug::error!("No local accounts found.");
}
}
7. Testing
7.1. Generate a new account
$ subkey -s generate
7.2. Submit a new key via RPC
$ http localhost:9933 jsonrpc=2.0 id=1 method=author_insertKey params:='["btc!", "garment disorder company wasp craft dinosaur street crucial salad door maid document", "0xc44c1627a435c00e40bced87e2361236ced5b8db8aa6c1dd248926fe743f832f"]'
7.3. Transfer the balance
In the UI https://polkadot.js.org/apps/#/explorer
7.4. Alternatively insert account (Alice) to the keystore during CLI init.
https://github.com/gnunicorn/substrate-offchain-cb/blob/df0dbca/src/service.rs#L116
8. Implement handling the incoming prices.
pub fn submit_btc_price(origin, price: u32) -> dispatch::Result {
let who = ensure_signed(origin)?;
debug::info!("Adding to the average: {}", price);
let average = Prices::mutate(|prices| {
const MAX_LEN: usize = 64;
if prices.len() < MAX_LEN {
prices.push(price);
} else {
prices[price as usize % MAX_LEN] = price;
}
// TODO Whatchout for overflows
prices.iter().sum::<u32>() / prices.len() as u32
});
debug::info!("Current average price is: {}", average);
// here we are raising the Something event
Self::deposit_event(RawEvent::NewPrice(price, who));
Ok(())
}
fn offchain_worker(block_number: T::BlockNumber) {
debug::RuntimeLogger::init();
let average: Option<u32> = {
let prices = Prices::get();
if prices.is_empty() {
None
} else {
Some(prices.iter().sum::<u32>() / prices.len() as u32)
}
};
debug::warn!("Hello World from offchain workers!");
debug::warn!("Current price of BTC is: {:?}", average);
let block_hash = <system::Module<T>>::block_hash(block_number);
debug::warn!("Current block is: {:?} ({:?})", block_number, block_hash);
let price = match Self::fetch_btc_price() {
Ok(price) => {
debug::warn!("Got BTC price: {} cents", price);
price
},
Err(_) => {
debug::warn!("Error fetching BTC price.");
// TODO [ToDr] What to do here?
return
}
};
Self::submit_btc_price_on_chain(price);
}
TODO substrate
-
Move sr_primitives::offchain to offchain-primitives
-
Re-export types from
primitives/core/offchain/mod.rs
(HttpError)? -
Inconsistency in error types (HttpError vs Error), make the former internal?
-
Annoying WASM warnings.
-
Make vector of accounts optional - use all accounts.
-
Add must_use to submit_signed return value.