Exonum — это фреймворк с открытым исходным кодом для создания приложений на основе блокчейна. Он ориентирован на работу с закрытыми блокчейнами и применим в любых сферах: FinTech, GovTech и LegalTech.
Сегодня мы проведем небольшой обзор решения, а также в рамках образовательного формата разберемся с тем, как построить простую криптовалюту с использованием Exonum. Весь код, приведенный ниже, вы найдете в репозитории на GitHub.
/ Exonum. Your next step to blockchain / Exonum
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"
}
К сожалению, не доступен сервер mySQL