{"id":17272900,"url":"https://github.com/tomusdrw/sub0-offchain-workshop","last_synced_at":"2026-07-14T11:30:17.064Z","repository":{"id":37183055,"uuid":"225667190","full_name":"tomusdrw/sub0-offchain-workshop","owner":"tomusdrw","description":"Code for offchain workshop.","archived":false,"fork":false,"pushed_at":"2023-04-03T20:02:04.000Z","size":4397,"stargazers_count":26,"open_issues_count":15,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2023-04-10T03:12:47.292Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tomusdrw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/CONTRIBUTING.adoc","funding":null,"license":"LICENSE","code_of_conduct":"docs/CODE_OF_CONDUCT.adoc","threat_model":null,"audit":null,"citation":null,"codeowners":"docs/CODEOWNERS","security":"docs/SECURITY.md","support":null}},"created_at":"2019-12-03T16:33:20.000Z","updated_at":"2023-04-10T03:12:47.293Z","dependencies_parsed_at":"2023-02-12T06:00:42.154Z","dependency_job_id":null,"html_url":"https://github.com/tomusdrw/sub0-offchain-workshop","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomusdrw%2Fsub0-offchain-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomusdrw%2Fsub0-offchain-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomusdrw%2Fsub0-offchain-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomusdrw%2Fsub0-offchain-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tomusdrw","download_url":"https://codeload.github.com/tomusdrw/sub0-offchain-workshop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219844712,"owners_count":16556482,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T08:49:44.740Z","updated_at":"2026-07-14T11:30:16.995Z","avatar_url":"https://github.com/tomusdrw.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"### 1. Clone repo:\n```bash\n$ git clone --depth 1 --branch td-submit-transactions https://github.com/paritytech/substrate\n```\n\n### 2. Add offchain worker that prints `Hello World!'\nCompile \u0026 run\n```bash\n$ cargo run -p node-template -- --dev -lruntime=trace\n```\n\n### 3. Either print or `debug::warn`, also show logger\n```rust\n debug::RuntimeLogger::init();\n debug::print!(\"Hello world!\");\n```\n\n### 4. Read `Something` from the storage and display block hash/number\n\n```rust\nfn offchain_worker(block_number: T::BlockNumber) {\n\t//debug::RuntimeLogger::init();\n\tlet something = Something::get();\n\tdebug::warn!(\"Hello World from offchain workers!\");\n\tdebug::warn!(\"Something is: {:?}\", something);\n\n\tlet block_hash = \u003csystem::Module\u003cT\u003e\u003e::block_hash(block_number - 1);\n\tdebug::warn!(\"Current block is: {:?} (parent: {:?})\", block_number, block_hash);\n}\n```\n\n\n### 5. Make an HTTP request.\n\n#### 5.1. Handle response.\n\n```rust\nlet price = match Self::fetch_btc_price() {\n    Ok(price) =\u003e {\n      debug::info!(\"Got BTC price: {} cents\", price);\n      price\n    },\n    _ =\u003e {\n      debug::error!(\"Error fetching BTC price.\", e);\n      return\n    }\n};\n```\n\n#### 5.2. Make a request and check response.\n\n```rust\n  impl\u003cT: Trait\u003e Module\u003cT\u003e {\n    fn fetch_btc_price() -\u003e Result\u003cu64, http::Error\u003e {\n      let pending = http::Request::get(\n        \"https://min-api.cryptocompare.com/data/price?fsym=BTC\u0026tsyms=USD\"\n      ).send().map_err(|_| http::Error::IoError)?;\n\n      let response = pending.wait()?;\n      if response.code != 200 {\n        debug::warn!(\"Unexpected status code: {}\", response.code);\n        return Err(http::Error::Unknown);\n      }\n\n      let body = response.body().collect::\u003cVec\u003cu8\u003e\u003e();\n      debug::warn!(\"Body: {:?}\", core::str::from_utf8(\u0026body).ok());\n\n      Ok(5)\n    }\n  }\n```\n\n#### 5.3. Parse JSON.\n\n```rust\n\tconst START_IDX: usize = \"{\\\"USD\\\":\".len();\n\tlet body = response.body().collect::\u003cVec\u003cu8\u003e\u003e();\n\tlet json = match core::str::from_utf8(\u0026body) {\n\t\tOk(json) if json.len() \u003e START_IDX =\u003e json,\n\t\t_ =\u003e {\n\t\t\tdebug::warn!(\"Unexpected (non-utf8 or too short) response received: {:?}\", body);\n\t\t\treturn Err(http::Error::Unknown);\n\t\t}\n\t};\n```\n\n#### 5.4. Parse number:\n\n```rust\n\tlet price = \u0026json[START_IDX .. json.len() - 1];\n\tlet pricef: f64 = match price.parse() {\n\t\tOk(pricef) =\u003e pricef,\n\t\tErr(_) =\u003e {\n\t\t\tdebug::warn!(\"Unparsable price: {:?}\", price);\n\t\t\treturn Err(http::Error::Unknown);\n\t\t}\n\t};\n\n\tOk((pricef * 100.) as u64)\n```\n\n### 6. Create signed transaction.\n\n#### 6.1. Add type SubmitTransaction, type Call to the trait.\n\n```rust\n\t/// The overarching event type.\n\ttype Call: From\u003cCall\u003cSelf\u003e\u003e;\n\n\t/// Transaction submitter.\n\ttype SubmitTransaction: system::offchain::SubmitSignedTransaction\u003cSelf, \u003cSelf as Trait\u003e::Call\u003e;\n```\n\n#### 6.2. Add app-crypto stuff.\n\n```rust\n  use primitives::crypto::KeyTypeId;\n  pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b\"btc!\");\n\n  pub mod crypto {\n    use super::KEY_TYPE;\n    use sp_runtime::app_crypto::{app_crypto, sr25519};\n    app_crypto!(sr25519, KEY_TYPE);\n  }\n```\n\n#### 6.3. Implement CreateTransaction for the runtime.\n\n```rust\n  /// The payload being signed in transactions.\n  pub type SignedPayload = generic::SignedPayload\u003cCall, SignedExtra\u003e;\n\n  impl system::offchain::CreateTransaction\u003cRuntime, UncheckedExtrinsic\u003e for Runtime {\n    type Public = \u003cSignature as sp_runtime::traits::Verify\u003e::Signer;\n    type Signature = Signature;\n\n    fn create_transaction\u003cTSigner: system::offchain::Signer\u003cSelf::Public, Self::Signature\u003e\u003e(\n      call: Call,\n      public: Self::Public,\n      account: AccountId,\n      index: Index,\n    ) -\u003e Option\u003c(Call, \u003cUncheckedExtrinsic as sp_runtime::traits::Extrinsic\u003e::SignaturePayload)\u003e {\n      let period = 1 \u003c\u003c 8;\n      let current_block = System::block_number() as u64;\n      let tip = 0;\n      let extra: SignedExtra = (\n        system::CheckVersion::\u003cRuntime\u003e::new(),\n        system::CheckGenesis::\u003cRuntime\u003e::new(),\n        system::CheckEra::\u003cRuntime\u003e::from(generic::Era::mortal(period, current_block)),\n        system::CheckNonce::\u003cRuntime\u003e::from(index),\n        system::CheckWeight::\u003cRuntime\u003e::new(),\n        transaction_payment::ChargeTransactionPayment::\u003cRuntime\u003e::from(tip),\n      );\n      let raw_payload = SignedPayload::new(call, extra).ok()?;\n      let signature = TSigner::sign(public, \u0026raw_payload)?;\n      let address = Indices::unlookup(account);\n      let (call, extra, _) = raw_payload.deconstruct();\n      Some((call, (address, signature, extra)))\n    }\n  }\n```\n\n#### 6.4. Implement submission logic.\n\n```rust\nfn submit_btc_price_on_chain(price: u32) {\n\tuse system::offchain::SubmitSignedTransaction;\n\n\tlet call = Call::do_something(price);\n\n\tlet res = T::SubmitTransaction::submit_signed(\n\t\tcall\n\t);\n\n\tif res.is_empty() {\n\t\tdebug::error!(\"No local accounts found.\");\n\t}\n}\n```\n\n### 7. Testing\n\n#### 7.1. Generate a new account\n\n```bash\n$ subkey -s generate\n```\n\n#### 7.2. Submit  a new key via RPC\n\n```bash\n$ 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\"]'\n```\n\n#### 7.3. Transfer the balance\n\nIn the UI https://polkadot.js.org/apps/#/explorer\n\n#### 7.4. Alternatively insert account (Alice) to the keystore during CLI init.\n\nhttps://github.com/gnunicorn/substrate-offchain-cb/blob/df0dbca/src/service.rs#L116\n\n### 8. Implement handling the incoming prices.\n\n```rust\n\tpub fn submit_btc_price(origin, price: u32) -\u003e dispatch::Result {\n\t\tlet who = ensure_signed(origin)?;\n\n\t\tdebug::info!(\"Adding to the average: {}\", price);\n\t\tlet average = Prices::mutate(|prices| {\n\t\t\tconst MAX_LEN: usize = 64;\n\n\t\t\tif prices.len() \u003c MAX_LEN {\n\t\t\t\tprices.push(price);\n\t\t\t} else {\n\t\t\t\tprices[price as usize % MAX_LEN] = price;\n\t\t\t}\n\n\t\t\t// TODO Whatchout for overflows\n\t\t\tprices.iter().sum::\u003cu32\u003e() / prices.len() as u32\n\t\t});\n\t\tdebug::info!(\"Current average price is: {}\", average);\n\t\t// here we are raising the Something event\n\t\tSelf::deposit_event(RawEvent::NewPrice(price, who));\n\t\tOk(())\n\t}\n\n\tfn offchain_worker(block_number: T::BlockNumber) {\n\t\tdebug::RuntimeLogger::init();\n\t\tlet average: Option\u003cu32\u003e = {\n\t\t\tlet prices = Prices::get();\n\t\t\tif prices.is_empty() {\n\t\t\t\tNone\n\t\t\t} else {\n\t\t\t\tSome(prices.iter().sum::\u003cu32\u003e() / prices.len() as u32)\n\t\t\t}\n\t\t};\n\t\tdebug::warn!(\"Hello World from offchain workers!\");\n\t\tdebug::warn!(\"Current price of BTC is: {:?}\", average);\n\n\t\tlet block_hash = \u003csystem::Module\u003cT\u003e\u003e::block_hash(block_number);\n\t\tdebug::warn!(\"Current block is: {:?} ({:?})\", block_number, block_hash);\n\n\t\tlet price = match Self::fetch_btc_price() {\n\t\t\tOk(price) =\u003e {\n\t\t\t\tdebug::warn!(\"Got BTC price: {} cents\", price);\n\t\t\t\tprice\n\t\t\t},\n\t\t\tErr(_) =\u003e {\n\t\t\t\tdebug::warn!(\"Error fetching BTC price.\");\n\t\t\t\t// TODO [ToDr] What to do here?\n\t\t\t\treturn\n\t\t\t}\n\t\t};\n\n\t\tSelf::submit_btc_price_on_chain(price);\n\t}\n```\n\n\n##### TODO substrate\n1. Move sr_primitives::offchain to offchain-primitives\n2. Re-export types from `primitives/core/offchain/mod.rs` (HttpError)?\n3. Inconsistency in error types (HttpError vs Error), make the former internal?\n5. Annoying WASM warnings.\n\n4. Make vector of accounts optional - use all accounts.\n6. Add must_use to submit_signed return value.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomusdrw%2Fsub0-offchain-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftomusdrw%2Fsub0-offchain-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomusdrw%2Fsub0-offchain-workshop/lists"}