#[macro_use]
extern crate exonum;
#[macro_use]
extern crate exonum_testkit;
#[macro_use]
extern crate serde_derive;
use std::collections::BTreeSet;
use std::iter::FromIterator;
use exonum::crypto::{self, PublicKey, SecretKey};
use exonum::messages::Message;
use exonum_testkit::{ApiKind, ComparableSnapshot, TestKit, TestKitApi, TestKitBuilder};
mod cryptocurrency {
extern crate bodyparser;
extern crate iron;
extern crate router;
extern crate serde;
extern crate serde_json;
use exonum::blockchain::{ApiContext, Blockchain, Service, Transaction};
use exonum::node::{ApiSender, TransactionSend};
use exonum::messages::{Message, RawTransaction};
use exonum::storage::{Fork, MapIndex, Snapshot};
use exonum::crypto::{Hash, PublicKey};
use exonum::encoding;
use exonum::encoding::serialize::FromHex;
use exonum::api::{Api, ApiError};
use self::iron::prelude::*;
use self::iron::headers::ContentType;
use self::iron::{Handler, IronError};
use self::iron::status::Status;
use self::router::Router;
const SERVICE_ID: u16 = 1;
const TX_CREATE_WALLET_ID: u16 = 1;
const TX_TRANSFER_ID: u16 = 2;
pub const INIT_BALANCE: u64 = 100;
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<S> {
view: S,
}
impl<S: AsRef<Snapshot>> CurrencySchema<S> {
pub fn new(view: S) -> Self {
CurrencySchema { view }
}
pub fn wallets(&self) -> MapIndex<&Snapshot, PublicKey, Wallet> {
MapIndex::new("cryptocurrency.wallets", self.view.as_ref())
}
pub fn wallet(&self, pub_key: &PublicKey) -> Option<Wallet> {
self.wallets().get(pub_key)
}
}
impl<'a> CurrencySchema<&'a mut Fork> {
pub fn wallets_mut(&mut self) -> MapIndex<&mut Fork, PublicKey, Wallet> {
MapIndex::new("cryptocurrency.wallets", self.view)
}
}
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]
}
}
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 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);
schema.wallets_mut().put(self.pub_key(), wallet)
}
}
fn info(&self) -> serde_json::Value {
serde_json::to_value(&self).expect("Cannot serialize transaction to JSON")
}
}
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(sender), Some(receiver)) = (sender, receiver) {
let amount = self.amount();
if sender.balance() >= amount {
let sender = sender.decrease(amount);
let receiver = receiver.increase(amount);
let mut wallets = schema.wallets_mut();
wallets.put(self.from(), sender);
wallets.put(self.to(), receiver);
}
}
}
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,
}
#[derive(Serialize, Deserialize)]
pub struct TransactionResponse {
pub tx_hash: Hash,
}
impl CryptocurrencyApi {
fn wallet(&self, pub_key: &PublicKey) -> Option<Wallet> {
let view = self.blockchain.snapshot();
let schema = CurrencySchema::new(view);
schema.wallet(pub_key)
}
fn wallets(&self) -> Vec<Wallet> {
let view = self.blockchain.snapshot();
let schema = CurrencySchema::new(view);
let wallets = schema.wallets();
let wallets = wallets.values();
wallets.collect()
}
fn post_transaction(&self, req: &mut Request) -> IronResult<Response> {
#[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),
}
}
}
match req.get::<bodyparser::Struct<TransactionRequest>>() {
Ok(Some(transaction)) => {
let transaction: Box<Transaction> = transaction.into();
let tx_hash = transaction.hash();
self.channel.send(transaction).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)))?,
}
}
fn get_wallet(&self, req: &mut Request) -> IronResult<Response> {
use self::iron::modifiers::Header;
let path = req.url.path();
let wallet_key = path.last().unwrap();
let public_key = PublicKey::from_hex(wallet_key).map_err(|e| {
IronError::new(ApiError::FromHex(e), (
Status::BadRequest,
Header(ContentType::json()),
"\"Invalid request param: `pub_key`\"",
))
})?;
if let Some(wallet) = self.wallet(&public_key) {
self.ok_response(&serde_json::to_value(wallet).unwrap())
} else {
Err(IronError::new(ApiError::NotFound, (
Status::NotFound,
Header(ContentType::json()),
"\"Wallet not found\"",
)))
}
}
fn get_wallets(&self, _: &mut Request) -> IronResult<Response> {
self.ok_response(&serde_json::to_value(&self.wallets()).unwrap())
}
}
impl Api for CryptocurrencyApi {
fn wire(&self, router: &mut Router) {
let self_ = self.clone();
let post_transaction = move |req: &mut Request| self_.post_transaction(req);
let self_ = self.clone();
let get_wallets = move |req: &mut Request| self_.get_wallets(req);
let self_ = self.clone();
let get_wallet = move |req: &mut Request| self_.get_wallet(req);
router.post(
"/v1/wallets/transaction",
post_transaction,
"post_transaction",
);
router.get("/v1/wallets", get_wallets, "get_wallets");
router.get("/v1/wallet/:pub_key", get_wallet, "get_wallet");
}
}
pub struct CurrencyService;
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))
}
}
}
use cryptocurrency::{CurrencySchema, CurrencyService, TransactionResponse, TxCreateWallet,
TxTransfer, Wallet};
fn init_testkit() -> TestKit {
TestKitBuilder::validator()
.with_validators(4)
.with_service(CurrencyService)
.create()
}
fn create_wallet(api: &TestKitApi, name: &str) -> (TxCreateWallet, SecretKey) {
let (pubkey, key) = crypto::gen_keypair();
let tx = TxCreateWallet::new(&pubkey, name, &key);
let tx_info: TransactionResponse = api.post(
ApiKind::Service("cryptocurrency"),
"v1/wallets/transaction",
&tx,
);
assert_eq!(tx_info.tx_hash, tx.hash());
(tx, key)
}
fn transfer(api: &TestKitApi, tx: &TxTransfer) {
let tx_info: TransactionResponse = api.post(
ApiKind::Service("cryptocurrency"),
"v1/wallets/transaction",
tx,
);
assert_eq!(tx_info.tx_hash, tx.hash());
}
fn get_wallet(api: &TestKitApi, pubkey: &PublicKey) -> Wallet {
api.get(
ApiKind::Service("cryptocurrency"),
&format!("v1/wallet/{}", pubkey.to_string()),
)
}
fn get_all_wallets(api: &TestKitApi) -> Vec<Wallet> {
api.get(ApiKind::Service("cryptocurrency"), "v1/wallets")
}
#[test]
fn test_create_wallet() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx, _) = create_wallet(&api, "Alice");
testkit.create_block();
let wallet = get_wallet(&api, tx.pub_key());
assert_eq!(wallet.pub_key(), tx.pub_key());
assert_eq!(wallet.name(), tx.name());
assert_eq!(wallet.balance(), 100);
}
#[test]
fn test_transfer() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key_alice) = create_wallet(&api, "Alice");
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit.create_block();
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 100);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 100);
let tx = TxTransfer::new(
tx_alice.pub_key(),
tx_bob.pub_key(),
10, 0, &key_alice,
);
transfer(&api, &tx);
testkit.create_block();
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 90);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 110);
}
#[test]
fn test_snapshot_completeness() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, _) = create_wallet(&api, "Alice");
testkit.create_block();
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit
.probe(tx_bob)
.compare(testkit.snapshot())
.map(CurrencySchema::new)
.map(|schema| schema.wallet(tx_alice.pub_key()))
.assert_inv("Alice's wallet is there", Option::is_some)
.map(|w| w.as_ref().unwrap().balance())
.assert_eq("Alice's balance hasn't changed");
}
#[test]
fn test_transfer_from_nonexisting_wallet() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key_alice) = create_wallet(&api, "Alice");
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit.create_block_with_tx_hashes(&[tx_bob.hash()]);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 100);
let tx = TxTransfer::new(
tx_alice.pub_key(),
tx_bob.pub_key(),
10, 0, &key_alice,
);
let comp = testkit.probe(tx).compare(testkit.snapshot());
let comp = comp.map(CurrencySchema::new);
comp.map(|s| s.wallet(tx_alice.pub_key())).assert_inv(
"No Alice's wallet",
Option::is_none,
);
comp.map(|s| {
s.wallet(tx_bob.pub_key())
.expect("No Bob's wallet!")
.balance()
}).assert_eq("Bob's balance hasn't changed");
}
#[test]
fn test_transfer_to_nonexisting_wallet() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key_alice) = create_wallet(&api, "Alice");
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit.create_block_with_tx_hashes(&[tx_alice.hash()]);
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 100);
let tx = TxTransfer::new(
tx_alice.pub_key(),
tx_bob.pub_key(),
10, 0, &key_alice,
);
transfer(&api, &tx);
let old_snapshot = testkit.snapshot();
testkit.create_block_with_tx_hashes(&[tx.hash()]);
let comp = testkit.snapshot().compare(old_snapshot);
let comp = comp.map(CurrencySchema::new);
comp.map(|s| s.wallet(tx_bob.pub_key())).assert_inv(
"No Bob's wallet",
Option::is_none,
);
comp.map(|s| {
s.wallet(tx_alice.pub_key())
.expect("No Alice's wallet!")
.balance()
}).assert_eq("Alice's balance hasn't changed");
}
#[test]
fn test_transfer_overcharge() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key_alice) = create_wallet(&api, "Alice");
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit.create_block();
let tx = TxTransfer::new(
tx_alice.pub_key(),
tx_bob.pub_key(),
110, 0, &key_alice,
);
transfer(&api, &tx);
testkit.create_block();
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 100);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 100);
}
#[test]
fn test_transfers_in_single_block() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key_alice) = create_wallet(&api, "Alice");
let (tx_bob, key_bob) = create_wallet(&api, "Bob");
testkit.create_block();
let tx_a_to_b = TxTransfer::new(
tx_alice.pub_key(),
tx_bob.pub_key(),
90, 0, &key_alice,
);
let tx_b_to_a = TxTransfer::new(
tx_bob.pub_key(),
tx_alice.pub_key(),
120, 0, &key_bob,
);
{
let comp = testkit
.probe_all(vec![
Box::new(tx_b_to_a.clone()),
Box::new(tx_a_to_b.clone()),
])
.compare(testkit.snapshot());
let comp = comp.map(CurrencySchema::new);
comp.map(|s| s.wallet(tx_alice.pub_key()).unwrap().balance())
.assert("Alice's balance decreases", |&old, &new| new == old - 90);
comp.map(|s| s.wallet(tx_bob.pub_key()).unwrap().balance())
.assert("Bob's balance increases", |&old, &new| new == old + 90);
}
transfer(&api, &tx_a_to_b);
transfer(&api, &tx_b_to_a);
testkit.create_block_with_tx_hashes(&[tx_a_to_b.hash(), tx_b_to_a.hash()]);
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 130);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 70);
let wallets = get_all_wallets(&api);
assert_eq!(wallets.len(), 2);
assert_eq!(wallets.iter().fold(0, |acc, w| acc + w.balance()), 200);
assert_eq!(
BTreeSet::from_iter(wallets.iter().map(|w| *w.pub_key())),
BTreeSet::from_iter(vec![*tx_alice.pub_key(), *tx_bob.pub_key()])
);
}
#[test]
fn test_malformed_wallet_request() {
let testkit = TestKitBuilder::validator()
.with_validators(4)
.with_service(CurrencyService)
.create();
let api = testkit.api();
let info: String = api.get_err(ApiKind::Service("cryptocurrency"), "v1/wallet/c0ffee");
assert!(info.starts_with("Invalid request param"));
}
#[test]
fn test_unknown_wallet_request() {
let testkit = init_testkit();
let api = testkit.api();
let (tx_alice, _) = create_wallet(&api, "Alice");
let info: String = api.get_err(
ApiKind::Service("cryptocurrency"),
&format!("v1/wallet/{}", tx_alice.pub_key().to_string()),
);
assert_eq!(info, "Wallet not found".to_string());
}
#[test]
fn test_nonverified_transaction_in_create_block() {
let mut testkit = init_testkit();
let api = testkit.api();
let (create_tx, key) = create_wallet(&api, "Alice");
testkit.create_block();
let transfer_tx = TxTransfer::new(create_tx.pub_key(), create_tx.pub_key(), 10, 0, &key);
testkit.create_block_with_transactions(txvec![transfer_tx]);
let wallet = get_wallet(&api, create_tx.pub_key());
assert_eq!(wallet.balance(), 100);
}
#[test]
fn test_nonsigned_transaction_in_create_block() {
let mut testkit = init_testkit();
let api = testkit.api();
let (tx_alice, key) = create_wallet(&api, "Alice");
let (tx_bob, _) = create_wallet(&api, "Bob");
testkit.create_block();
let transfer_tx = TxTransfer::new(tx_alice.pub_key(), tx_bob.pub_key(), 10, 0, &key);
let bogus_transfer_tx = TxTransfer::new_with_signature(
tx_alice.pub_key(),
tx_bob.pub_key(),
20,
0,
&crypto::Signature::new([0; 64]),
);
testkit.create_block_with_transactions(txvec![bogus_transfer_tx, transfer_tx]);
let wallet = get_wallet(&api, tx_alice.pub_key());
assert_eq!(wallet.balance(), 90);
let wallet = get_wallet(&api, tx_bob.pub_key());
assert_eq!(wallet.balance(), 110);
}