cargo new --bin cryptocurrency
[package] name = "cryptocurrency" version = "0.3.0" authors = ["Your Name <your@email.com>"] [dependencies] iron = "0.5.1" bodyparser = "0.7.0" router = "0.5.1" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" exonum = "0.3.0"
extern crate serde; extern crate serde_json; #[macro_use] extern crate serde_derive; #[macro_use] extern crate exonum; extern crate router; extern crate bodyparser; extern crate iron; use exonum::blockchain::{Blockchain, Service, GenesisConfig, ValidatorKeys, Transaction, ApiContext}; use exonum::node::{Node, NodeConfig, NodeApiConfig, TransactionSend, ApiSender }; use exonum::messages::{RawTransaction, FromRaw, Message}; use exonum::storage::{Fork, MemoryDB, MapIndex}; use exonum::crypto::{PublicKey, Hash, HexValue}; use exonum::encoding::{self, Field}; use exonum::api::{Api, ApiError}; use iron::prelude::*; use iron::Handler; use router::Router;
// Service identifier const SERVICE_ID: u16 = 1; // Identifier for wallet creation transaction type const TX_CREATE_WALLET_ID: u16 = 1; // Identifier for coins transfer transaction type const TX_TRANSFER_ID: u16 = 2; // Starting balance of a newly created wallet const INIT_BALANCE: u64 = 100;
fn main() { exonum::helpers::init_logger().unwrap(); }
let db = MemoryDB::new(); let services: Vec<Box<Service>> = vec![ ]; let blockchain = Blockchain::new(Box::new(db), services);
let validator_keys = ValidatorKeys { consensus_key: consensus_public_key, service_key: service_public_key, }; let genesis = GenesisConfig::new(vec![validator_keys].into_iter());
let api_address = "0.0.0.0:8000".parse().unwrap(); let api_cfg = NodeApiConfig { public_api_address: Some(api_address), ..Default::default() }; let peer_address = "0.0.0.0:2000".parse().unwrap(); // Complete node configuration let node_cfg = NodeConfig { listen_address: peer_address, peers: vec![], service_public_key, service_secret_key, consensus_public_key, consensus_secret_key, genesis, external_address: None, network: Default::default(), whitelist: Default::default(), api: api_cfg, mempool: Default::default(), services_configs: Default::default(), }; let node = Node::new(blockchain, node_cfg); node.run().unwrap();
encoding_struct! { struct Wallet { const SIZE = 48; field pub_key: &PublicKey [00 => 32] field name: &str [32 => 40] field balance: u64 [40 => 48] } }
impl Wallet { pub fn increase(self, amount: u64) -> Self { let balance = self.balance() + amount; Self::new(self.pub_key(), self.name(), balance) } pub fn decrease(self, amount: u64) -> Self { let balance = self.balance() - amount; Self::new(self.pub_key(), self.name(), balance) } }
pub struct CurrencySchema<'a> { view: &'a mut Fork, }
impl<'a> CurrencySchema<'a> { pub fn wallets(&mut self) -> MapIndex<&mut Fork, PublicKey, Wallet> { let prefix = blockchain::gen_prefix(SERVICE_ID, 0, &()); MapIndex::new("cryptocurrency.wallets", self.view) } // Utility method to quickly get a separate wallet from the storage pub fn wallet(&mut self, pub_key: &PublicKey) -> Option<Wallet> { self.wallets().get(pub_key) } }
message! { struct TxCreateWallet { const TYPE = SERVICE_ID; const ID = TX_CREATE_WALLET_ID; const SIZE = 40; field pub_key: &PublicKey [00 => 32] field name: &str [32 => 40] } }
impl Transaction for TxCreateWallet { fn verify(&self) -> bool { self.verify_signature(self.pub_key()) } fn execute(&self, view: &mut Fork) { let mut schema = CurrencySchema { view }; if schema.wallet(self.pub_key()).is_none() { let wallet = Wallet::new(self.pub_key(), self.name(), INIT_BALANCE); println!("Create the wallet: {:?}", wallet); schema.wallets().put(self.pub_key(), wallet) } } }
message! { struct TxTransfer { const TYPE = SERVICE_ID; const ID = TX_TRANSFER_ID; const SIZE = 80; field from: &PublicKey [00 => 32] field to: &PublicKey [32 => 64] field amount: u64 [64 => 72] field seed: u64 [72 => 80] } }
impl Transaction for TxTransfer { fn verify(&self) -> bool { (*self.from() != *self.to()) && self.verify_signature(self.from()) } fn execute(&self, view: &mut Fork) { let mut schema = CurrencySchema { view }; let sender = schema.wallet(self.from()); let receiver = schema.wallet(self.to()); if let (Some(mut sender), Some(mut receiver)) = (sender, receiver) { let amount = self.amount(); if sender.balance() >= amount { let sender.decrease(amount); let receiver.increase(amount); println!("Transfer between wallets: {:?} => {:?}", sender, receiver); let mut wallets = schema.wallets(); wallets.put(self.from(), sender); wallets.put(self.to(), receiver); } } } }
impl Transaction for TxCreateWallet { // `verify()` and `execute()` code... fn info(&self) -> serde_json::Value { serde_json::to_value(&self) .expect("Cannot serialize transaction to JSON") } }
#[derive(Clone)] struct CryptocurrencyApi { channel: ApiSender, blockchain: Blockchain, }
#[serde(untagged)] #[derive(Clone, Serialize, Deserialize)] enum TransactionRequest { CreateWallet(TxCreateWallet), Transfer(TxTransfer), } impl Into<Box<Transaction>> for TransactionRequest { fn into(self) -> Box<Transaction> { match self { TransactionRequest::CreateWallet(trans) => Box::new(trans), TransactionRequest::Transfer(trans) => Box::new(trans), } } } #[derive(Serialize, Deserialize)] struct TransactionResponse { tx_hash: Hash, }
impl Api for CryptocurrencyApi { fn wire(&self, router: &mut Router) { let self_ = self.clone(); let tx_handler = move |req: &mut Request| -> IronResult<Response> { match req.get::<bodyparser::Struct<TransactionRequest>>() { Ok(Some(tx)) => { let tx: Box<Transaction> = tx.into(); let tx_hash = tx.hash(); self_.channel.send(tx).map_err(ApiError::from)?; let json = TransactionResponse { tx_hash }; self_.ok_response(&serde_json::to_value(&json).unwrap()) } Ok(None) => Err(ApiError::IncorrectRequest( "Empty request body".into()))?, Err(e) => Err(ApiError::IncorrectRequest(Box::new(e)))?, } }; // (Read request processing skipped) // Bind the transaction handler to a specific route. router.post("/v1/wallets/transaction", transaction, "transaction"); // (Read request binding skipped) } }
impl CryptocurrencyApi { fn get_wallet(&self, pub_key: &PublicKey) -> Option<Wallet> { let mut view = self.blockchain.fork(); let mut schema = CurrencySchema { view: &mut view }; schema.wallet(pub_key) } fn get_wallets(&self) -> Option<Vec<Wallet>> { let mut view = self.blockchain.fork(); let mut schema = CurrencySchema { view: &mut view }; let idx = schema.wallets(); let wallets: Vec<Wallet> = idx.values().collect(); if wallets.is_empty() { None } else { Some(wallets) } } }
impl Api for CryptocurrencyApi { fn wire(&self, router: &mut Router) { let self_ = self.clone(); // (Transaction processing skipped) // Gets status of all wallets in the database. let self_ = self.clone(); let wallets_info = move |_: &mut Request| -> IronResult<Response> { if let Some(wallets) = self_.get_wallets() { self_.ok_response(&serde_json::to_value(wallets).unwrap()) } else { self_.not_found_response( &serde_json::to_value("Wallets database is empty") .unwrap(), ) } }; // Gets status of the wallet corresponding to the public key. let self_ = self.clone(); let wallet_info = move |req: &mut Request| -> IronResult<Response> { // Get the hex public key as the last URL component; // return an error if the public key cannot be parsed. let path = req.url.path(); let wallet_key = path.last().unwrap(); let public_key = PublicKey::from_hex(wallet_key) .map_err(ApiError::FromHex)?; if let Some(wallet) = self_.get_wallet(&public_key) { self_.ok_response(&serde_json::to_value(wallet).unwrap()) } else { self_.not_found_response( &serde_json::to_value("Wallet not found").unwrap(), ) } }; // (Transaction binding skipped) // Bind read request endpoints. router.get("/v1/wallets", wallets_info, "wallets_info"); router.get("/v1/wallet/:pub_key", wallet_info, "wallet_info"); }
impl Service for CurrencyService { fn service_name(&self) -> &'static str { "cryptocurrency" } fn service_id(&self) -> u16 { SERVICE_ID } fn tx_from_raw(&self, raw: RawTransaction) -> Result<Box<Transaction>, encoding::Error> { let trans: Box<Transaction> = match raw.message_type() { TX_TRANSFER_ID => Box::new(TxTransfer::from_raw(raw)?), TX_CREATE_WALLET_ID => Box::new(TxCreateWallet::from_raw(raw)?), _ => { return Err(encoding::Error::IncorrectMessageType { message_type: raw.message_type() }); }, }; Ok(trans) } fn public_api_handler(&self, ctx: &ApiContext) -> Option<Box<Handler>> { let mut router = Router::new(); let api = CryptocurrencyApi { channel: ctx.node_channel().clone(), blockchain: ctx.blockchain().clone(), }; api.wire(&mut router); Some(Box::new(router)) } }
let services: Vec<Box<Service>> = vec![ Box::new(CurrencyService), ]; cargo run
let s = sandbox_with_services(vec![Box::new(CurrencyService::new()), Box::new(ConfigUpdateService::new())]);
{ "body": { "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472", "name": "Johnny Doe" }, "network_id": 0, "protocol_version": 0, "service_id": 1, "message_id": 1, "signature": "ad5efdb52e48309df9aa582e67372bb3ae67828c5eaa1a7a5e387597174055d315eaa7879912d0509acf17f06a23b7f13f242017b354f682d85930fa28240402" }
curl -H "Content-Type: application/json" -X POST -d @create-wallet-1.json \ http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction
Create the wallet: Wallet { pub_key: PublicKey(3E657AE), name: "Johnny Doe", balance: 100 }
{ "body": { "from": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472", "to": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819", "amount": "10", "seed": "12623766328194547469" }, "network_id": 0, "protocol_version": 0, "service_id": 1, "message_id": 2, "signature": "2c5e9eee1b526299770b3677ffd0d727f693ee181540e1914f5a84801dfd410967fce4c22eda621701c2b9c676ed62bc48df9c973462a8514ffb32bec202f103" }
curl -H "Content-Type: application/json" -X POST -d @transfer-funds.json \ http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction
Transfer between wallets: Wallet { pub_key: PublicKey(3E657AE), name: "Johnny Doe", balance: 90 } => Wallet { pub_key: PublicKey(D1E87747), name: "Janie Roe", balance: 110 }
curl http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets
[ { "balance": "90", "name": "Johnny Doe", "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472" }, { "balance": "110", "name": "Janie Roe", "pub_key": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819" } ]
curl "http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallet/\ 03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472"
{ "balance": "90", "name": "Johnny Doe", "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472" }
Source: https://habr.com/ru/post/342208/