install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/quip-protocol-rs" ~/.claude/skills/aradotso-trending-skills-quip-protocol-rs && rm -rf "$T"
manifest:
skills/quip-protocol-rs/SKILL.mdsource content
--- name: quip-protocol-rs description: Rust implementation of the Quip Protocol blockchain node, forked from Substrate/Polkadot SDK solochain template triggers: - build a substrate node in rust - quip protocol blockchain - substrate solochain template - create a custom pallet in substrate - run a substrate development chain - substrate FRAME runtime development - polkadot sdk rust blockchain - substrate node template setup --- # Quip Protocol RS > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. A Rust implementation of the Quip Protocol blockchain node, forked from the [Substrate](https://substrate.io/) / Polkadot SDK solochain template. It provides a ready-to-hack blockchain node with FRAME pallets, AURA block authoring, GRANDPA finality, and a JSON-RPC server. --- ## What This Project Does - Runs a standalone Substrate-based blockchain node (solochain) - Uses FRAME to compose a runtime from pallets (modules) - Exposes an RPC server (default `ws://localhost:9944`) - Ships with a template pallet for custom business logic - Supports single-node dev chains and multi-node testnets --- ## Project Structure
quip-protocol-rs/ ├── node/ │ └── src/ │ ├── chain_spec.rs # Genesis state configuration │ ├── service.rs # Node implementation (consensus, networking) │ └── main.rs ├── runtime/ │ └── src/ │ └── lib.rs # FRAME runtime, pallet composition ├── pallets/ │ └── template/ │ └── src/ │ └── lib.rs # Example custom pallet └── Cargo.toml
--- ## Installation & Prerequisites ### System Dependencies ```bash # Ubuntu/Debian sudo apt update && sudo apt install -y \ build-essential clang curl git libssl-dev \ llvm libudev-dev make protobuf-compiler # macOS brew install cmake protobuf
Rust Toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env rustup default stable rustup update rustup target add wasm32-unknown-unknown rustup component add rust-src
Clone & Build
git clone https://github.com/QuipNetwork/quip-protocol-rs.git cd quip-protocol-rs # Full release build cargo build --release # Dev build (faster, for iteration) cargo build
Key CLI Commands
# Start single-node dev chain (no state persistence) ./target/release/solochain-template-node --dev # Start dev chain with persistent state mkdir -p ./my-chain-state ./target/release/solochain-template-node --dev --base-path ./my-chain-state/ # Purge dev chain state ./target/release/solochain-template-node purge-chain --dev # Detailed logging RUST_BACKTRACE=1 ./target/release/solochain-template-node -ldebug --dev # Custom log targets RUST_LOG=runtime=debug,txpool=trace ./target/release/solochain-template-node --dev # Show all CLI options ./target/release/solochain-template-node --help # Generate and open Rust docs cargo +nightly doc --open
Writing a Custom Pallet
Pallets live in
pallets/<name>/src/lib.rs. The template pallet is the canonical starting point.
Minimal Pallet Example
// pallets/my_pallet/src/lib.rs #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; #[frame_support::pallet] pub mod pallet { use frame_support::{ pallet_prelude::*, traits::Currency, }; use frame_system::pallet_prelude::*; type BalanceOf<T> = <<T as Config>::Currency as Currency< <T as frame_system::Config>::AccountId, >>::Balance; #[pallet::config] pub trait Config: frame_system::Config { /// The runtime event type. type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// Currency used for staking. type Currency: Currency<Self::AccountId>; /// Max length of a stored value. #[pallet::constant] type MaxValueLength: Get<u32>; } #[pallet::pallet] pub struct Pallet<T>(_); // Storage: map AccountId -> BoundedVec<u8> #[pallet::storage] #[pallet::getter(fn stored_value)] pub type StoredValue<T: Config> = StorageMap< _, Blake2_128Concat, T::AccountId, BoundedVec<u8, T::MaxValueLength>, OptionQuery, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { /// A value was stored. [who, value] ValueStored { who: T::AccountId, value: BoundedVec<u8, T::MaxValueLength> }, /// A value was cleared. [who] ValueCleared { who: T::AccountId }, } #[pallet::error] pub enum Error<T> { /// Value exceeds maximum allowed length. ValueTooLong, /// No value found for this account. NothingStored, } #[pallet::call] impl<T: Config> Pallet<T> { /// Store a value for the caller. #[pallet::call_index(0)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn store_value( origin: OriginFor<T>, value: Vec<u8>, ) -> DispatchResult { let who = ensure_signed(origin)?; let bounded: BoundedVec<u8, T::MaxValueLength> = value.try_into().map_err(|_| Error::<T>::ValueTooLong)?; StoredValue::<T>::insert(&who, &bounded); Self::deposit_event(Event::ValueStored { who, value: bounded }); Ok(()) } /// Clear the caller's stored value. #[pallet::call_index(1)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn clear_value(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(StoredValue::<T>::contains_key(&who), Error::<T>::NothingStored); StoredValue::<T>::remove(&who); Self::deposit_event(Event::ValueCleared { who }); Ok(()) } } }
Adding the Pallet to Cargo.toml
Cargo.toml# pallets/my_pallet/Cargo.toml [package] name = "pallet-my-pallet" version = "0.1.0" edition = "2021" [dependencies] codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", optional = true, default-features = false } frame-support = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false } frame-system = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false } [features] default = ["std"] std = [ "codec/std", "frame-support/std", "frame-system/std", "scale-info/std", ] runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] try-runtime = ["frame-support/try-runtime"]
Wiring the Pallet into the Runtime
// runtime/src/lib.rs // 1. Declare the pallet parameter types parameter_types! { pub const MaxValueLength: u32 = 256; } // 2. Implement Config for your pallet impl pallet_my_pallet::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type MaxValueLength = MaxValueLength; } // 3. Add to the #[runtime] macro construct_runtime! block #[runtime] mod runtime { // ... existing pallets ... #[runtime::pallet_index(42)] pub type MyPallet = pallet_my_pallet::Pallet<Runtime>; }
Chain Specification (Genesis Config)
// node/src/chain_spec.rs use sc_service::ChainType; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_keyring::AccountKeyring; pub fn development_config() -> Result<ChainSpec, String> { Ok(ChainSpec::builder( WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, None, ) .with_name("Development") .with_id("dev") .with_chain_type(ChainType::Development) .with_genesis_config_patch(testnet_genesis( // Initial authorities (Aura, Grandpa) vec![authority_keys_from_seed("Alice")], // Sudo account AccountKeyring::Alice.to_account_id(), // Pre-funded accounts vec![ AccountKeyring::Alice.to_account_id(), AccountKeyring::Bob.to_account_id(), AccountKeyring::AliceStash.to_account_id(), AccountKeyring::BobStash.to_account_id(), ], true, )) .build()) } fn testnet_genesis( initial_authorities: Vec<(AuraId, GrandpaId)>, root_key: AccountId, endowed_accounts: Vec<AccountId>, _enable_println: bool, ) -> serde_json::Value { serde_json::json!({ "balances": { "balances": endowed_accounts.iter().cloned() .map(|k| (k, 1u64 << 60)) .collect::<Vec<_>>(), }, "aura": { "authorities": initial_authorities.iter().map(|x| (x.0.clone())).collect::<Vec<_>>(), }, "grandpa": { "authorities": initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect::<Vec<_>>(), }, "sudo": { "key": Some(root_key) }, }) }
Pallet Unit Tests
// pallets/my_pallet/src/tests.rs use crate::{mock::*, Error, Event}; use frame_support::{assert_noop, assert_ok}; #[test] fn store_value_works() { new_test_ext().execute_with(|| { System::set_block_number(1); assert_ok!(MyPallet::store_value( RuntimeOrigin::signed(1), b"hello world".to_vec() )); assert_eq!(MyPallet::stored_value(1).unwrap().as_slice(), b"hello world"); System::assert_last_event( Event::ValueStored { who: 1, value: b"hello world".to_vec().try_into().unwrap(), }.into() ); }); } #[test] fn store_value_fails_when_too_long() { new_test_ext().execute_with(|| { let too_long = vec![0u8; 257]; // MaxValueLength = 256 assert_noop!( MyPallet::store_value(RuntimeOrigin::signed(1), too_long), Error::<Test>::ValueTooLong ); }); } #[test] fn clear_value_fails_when_nothing_stored() { new_test_ext().execute_with(|| { assert_noop!( MyPallet::clear_value(RuntimeOrigin::signed(1)), Error::<Test>::NothingStored ); }); }
Mock Runtime for Tests
// pallets/my_pallet/src/mock.rs use crate as pallet_my_pallet; use frame_support::{ construct_runtime, parameter_types, traits::{ConstU16, ConstU32, ConstU64}, }; use sp_runtime::{traits::{BlakeTwo256, IdentityLookup}, BuildStorage}; type Block = frame_system::mocking::MockBlock<Test>; construct_runtime!( pub enum Test { System: frame_system, Balances: pallet_balances, MyPallet: pallet_my_pallet, } ); parameter_types! { pub const MaxValueLength: u32 = 256; } impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type Nonce = u64; type Hash = sp_core::H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; type Version = (); type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData<u64>; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = ConstU16<42>; type OnSetCode = (); type MaxConsumers = ConstU32<16>; } impl pallet_balances::Config for Test { type MaxLocks = ConstU32<50>; type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type Balance = u64; type RuntimeEvent = RuntimeEvent; type DustRemoval = (); type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); type FreezeIdentifier = (); type MaxFreezes = (); type RuntimeHoldReason = (); type MaxHolds = (); } impl pallet_my_pallet::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type MaxValueLength = MaxValueLength; } pub fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::<Test>::default() .build_storage() .unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext }
Running Tests
# Run all tests cargo test # Run tests for a specific pallet cargo test -p pallet-my-pallet # Run with output cargo test -- --nocapture # Run a specific test cargo test -p pallet-my-pallet store_value_works
RPC / Frontend Integration
Connect to the running node via Polkadot-JS Apps:
- Hosted: https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944
- Default WebSocket endpoint:
ws://127.0.0.1:9944 - Default HTTP endpoint:
http://127.0.0.1:9933
Interacting via @polkadot/api
(JavaScript)
@polkadot/apiimport { ApiPromise, WsProvider } from '@polkadot/api'; const provider = new WsProvider('ws://127.0.0.1:9944'); const api = await ApiPromise.create({ provider }); // Query storage const value = await api.query.myPallet.storedValue('5GrwvaEF...'); // Submit extrinsic const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice'); await api.tx.myPallet .storeValue(Buffer.from('hello')) .signAndSend(alice);
Docker
# Build Docker image (follows upstream Substrate Docker instructions) docker build -f ./docker/Dockerfile -t quip-node . # Run dev node in Docker docker run --rm -p 9944:9944 -p 9933:9933 quip-node \ --dev --ws-external --rpc-external
Nix Development Environment
# Install nix + nix-direnv, then: direnv allow # All Rust/WASM dependencies are automatically available
Common Patterns & Tips
Accessing Storage in a Pallet
// Single value #[pallet::storage] pub type MyValue<T> = StorageValue<_, u32, ValueQuery>; // Map #[pallet::storage] pub type MyMap<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>; // Double map #[pallet::storage] pub type MyDoubleMap<T: Config> = StorageDoubleMap< _, Blake2_128Concat, T::AccountId, Blake2_128Concat, u32, Vec<u8>, OptionQuery, >;
Emitting Events
// In dispatchable: Self::deposit_event(Event::SomethingHappened { who, value });
Weight Annotations
// Simple constant weight #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] // Using benchmarked weights (recommended for production) #[pallet::weight(<T as Config>::WeightInfo::my_call())]
Ensuring Origin
let who = ensure_signed(origin)?; // any signed account ensure_root(origin)?; // sudo/root only ensure_none(origin)?; // unsigned
Troubleshooting
| Issue | Fix |
|---|---|
target missing | |
| Install (Linux) or Xcode CLI tools (macOS) |
not found | or |
| Node won't start: port in use | Kill existing process or use , , flags |
| State corruption on restart | Run to wipe dev state |
| WASM blob outdated after runtime change | to recompile |
on multi-node | Ensure all nodes use identical chain spec and genesis |
# Check currently installed Rust targets rustup show # Verbose build output for debugging cargo build --release -v 2>&1 | grep error # Check node version ./target/release/solochain-template-node --version