├── web
├── .gitignore
├── public
│ ├── favicon.png
│ ├── index.html
│ ├── build
│ │ ├── bundle.css
│ │ └── bundle.css.map
│ └── global.css
├── src
│ ├── main.js
│ ├── jsonrpc.js
│ ├── App.svelte
│ ├── Wallet.svelte
│ └── Explorer.svelte
├── package.json
├── rollup.config.js
├── README.md
├── scripts
│ └── setupTypeScript.js
└── package-lock.json
├── .gitignore
├── demo.gif
├── config
└── default.toml
├── src
├── lib.rs
├── web.rs
├── block.rs
├── settings.rs
├── crypto.rs
├── tx.rs
├── miner.rs
├── bin
│ ├── nibbled.rs
│ └── nibble-cli.rs
├── rpc.rs
├── storage.rs
├── node.rs
└── p2p.rs
├── Cargo.toml
├── README.md
└── Cargo.lock
/web/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | data
3 | *.config.toml
4 | *_data/
5 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monokh/nibble/HEAD/demo.gif
--------------------------------------------------------------------------------
/web/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monokh/nibble/HEAD/web/public/favicon.png
--------------------------------------------------------------------------------
/config/default.toml:
--------------------------------------------------------------------------------
1 | rpc_port = 1337
2 | tcp_port = 1338
3 | web_port = 1339
4 | data_dir = "data"
5 | miner_enabled = false
--------------------------------------------------------------------------------
/web/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | var app = new App({
4 | target: document.body
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod node;
2 | pub mod settings;
3 | pub mod block;
4 | pub mod tx;
5 | pub mod crypto;
6 | pub mod storage;
7 | pub mod miner;
8 | pub mod rpc;
9 | pub mod p2p;
10 | pub mod web;
--------------------------------------------------------------------------------
/web/src/jsonrpc.js:
--------------------------------------------------------------------------------
1 | export async function call (rpc, method, params) {
2 | return fetch(rpc, {
3 | method: 'POST',
4 | body: JSON.stringify({
5 | jsonrpc: "2.0",
6 | id: 1,
7 | method,
8 | params
9 | }),
10 | headers: {
11 | 'Content-Type': 'application/json'
12 | },
13 | }).then(function(response) {
14 | return response.json();
15 | }).then(function(json) {
16 | return json.result;
17 | })
18 | }
--------------------------------------------------------------------------------
/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "build": "rollup -c",
6 | "dev": "rollup -c -w",
7 | "start": "sirv public"
8 | },
9 | "devDependencies": {
10 | "@rollup/plugin-commonjs": "^14.0.0",
11 | "@rollup/plugin-node-resolve": "^8.0.0",
12 | "rollup": "^2.3.4",
13 | "rollup-plugin-livereload": "^2.0.0",
14 | "rollup-plugin-svelte": "^6.0.0",
15 | "rollup-plugin-terser": "^7.0.0",
16 | "svelte": "^3.0.0"
17 | },
18 | "dependencies": {
19 | "sirv-cli": "^1.0.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/web.rs:
--------------------------------------------------------------------------------
1 | use colored::*;
2 | use rouille::Response;
3 |
4 | pub fn run_server (port: u32) {
5 | let path = format!("127.0.0.1:{}", port);
6 | println!("{} Wallet, Explorer running on {}", "WEB:".green(), path);
7 | rouille::start_server(path, move |request| {
8 | let response = rouille::match_assets(&request, "web/public");
9 | if response.is_success() {
10 | return response;
11 | } else if request.url() == "/" {
12 | return rouille::match_assets(&request, "web/public/index.html");
13 | } else {
14 | return Response::text("404").with_status_code(404);
15 | }
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/web/src/App.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
31 |
32 |
33 |
NIBBLE
34 |
35 | Node:
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/block.rs:
--------------------------------------------------------------------------------
1 | use crate::tx;
2 |
3 | use serde::{Serialize, Deserialize};
4 |
5 | #[derive(Debug, Clone, Serialize, Deserialize)]
6 | pub struct Block {
7 | pub hash: String,
8 | pub prev_block: String,
9 | pub nonce: u32,
10 | pub transactions: Vec,
11 | }
12 |
13 | impl Block {
14 | pub fn serialize(&self) -> String {
15 | let txs = self.transactions.iter().fold(String::new(), |a, b| a + &b.to_string());
16 | return format!("{}{}{}", self.prev_block, txs, self.nonce)
17 | }
18 | }
19 |
20 | pub struct ProposedBlock {
21 | pub prev_block: String,
22 | pub transactions: Vec
23 | }
24 |
25 | impl ProposedBlock {
26 | pub fn serialize(&self) -> String {
27 | let txs = self.transactions.iter().fold(String::new(), |a, b| a + &b.to_string());
28 | return format!("{}{}", self.prev_block, txs)
29 | }
30 | }
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nibble"
3 | version = "0.2.0"
4 | authors = ["monokh "]
5 | edition = "2018"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | regex = "1"
11 | hex = "0.4.2"
12 | toml = "0.5.6"
13 | config = "0.10.1"
14 | sha2 = "0.9.1"
15 | generic-array = "0.14.2"
16 | typenum = "1.12.0"
17 | clap = "2.33.1"
18 | rouille = "3.0.0"
19 | colored = "2.0.0"
20 | serde = { version = "1.0.114", features = ["derive"] }
21 | serde_json = "1.0.57"
22 | jsonrpc-core = "14.2.0"
23 | jsonrpc-http-server = "14.2.0"
24 | jsonrpc-derive = "14.2.1"
25 | rocksdb = "0.14.0"
26 | reqwest = { version = "0.10.7", features = ["json"] }
27 | tokio = { version = "0.2.22", features = ["full"] }
28 | jsonrpc-client-http = "0.5.0"
29 | jsonrpc-client-core = "0.5.0"
30 | jsonrpc-core-client = { version = "14.2.0", features = ["http"] }
31 |
32 | [dependencies.secp256k1]
33 | version = "0.17.2"
34 | features = ["rand-std", "serde"]
35 |
--------------------------------------------------------------------------------
/src/settings.rs:
--------------------------------------------------------------------------------
1 | use config::{ConfigError, Config, File};
2 | use serde::Deserialize;
3 | use std::env;
4 |
5 | #[derive(Deserialize)]
6 | pub struct Settings {
7 | pub rpc_port: u32,
8 | pub tcp_port: u32,
9 | pub web_port: u32,
10 | pub data_dir: String,
11 | pub miner_enabled: bool,
12 | pub bootstrap_node: Option,
13 | }
14 |
15 | impl Settings {
16 | pub fn new() -> Result {
17 | let mut s = Config::new();
18 |
19 | // Start off by merging in the "default" configuration file
20 | s.merge(File::with_name("config/default"))?;
21 |
22 | // Config in home dir
23 | s.merge(File::with_name("~/.nibble/config.toml").required(false))?;
24 |
25 | // Config in local dir
26 | let local_config = env::args().nth(1).unwrap_or("config".to_string());
27 | s.merge(File::with_name(&local_config).required(false))?;
28 |
29 | // You can deserialize (and thus freeze) the entire configuration as
30 | s.try_into()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nibble :cookie:
2 |
3 | Nibble is a re creation of Bitcoin as a concept. It is intended to create a simple view of Bitcoin with code. It will be built from simple data structures and processes, eventually becoming ~~a network that anyone can run~~ **FAILED**
4 |
5 | 
6 |
7 | ## Run
8 |
9 | `cargo run --bin nibbled`
10 |
11 | RPC interface: `http://localhost:1337`
12 |
13 | Explorer and Wallet UI: `http://localhost:1339`
14 |
15 | ## Guide
16 |
17 | Data Structures: https://monokh.com/posts/bitcoin-from-scratch-part-1
18 |
19 | The Node: https://monokh.com/posts/bitcoin-from-scratch-part-2
20 |
21 | The Network: https://monokh.com/posts/bitcoin-from-scratch-part-3
22 |
23 | ## CLI
24 |
25 | ```
26 | USAGE:
27 | nibble-cli [OPTIONS] [SUBCOMMAND]
28 |
29 | FLAGS:
30 | -h, --help Prints help information
31 | -V, --version Prints version information
32 |
33 | OPTIONS:
34 | -r, --rpc-url RPC Url to connect to [default: http://localhost:1337]
35 |
36 | SUBCOMMANDS:
37 | balances Returns balances for each pubkey known by the node
38 | blockheight Get the current block height
39 | getbalance Pubkey
40 | getblock Block Number
41 | getpubkey Get node's public key
42 | help Prints this message or the help of the given subcommand(s)
43 | mempool Returns the transactions in the mempool
44 | newpubkey Generate a random pubkey
45 | send Amount to send
46 | ```
47 |
--------------------------------------------------------------------------------
/web/public/build/bundle.css:
--------------------------------------------------------------------------------
1 | .container.svelte-jdyva7{width:1100px;margin:0 auto;display:grid;grid-template-columns:33% 67%;grid-template-rows:auto auto auto;grid-template-areas:"header rpc"
2 | "wallet explorer"}h1.svelte-jdyva7{color:#000000;grid-area:header}.rpc.svelte-jdyva7{grid-area:rpc;text-align:right;align-self:center}
3 | .explorer.svelte-19rqxfk.svelte-19rqxfk{padding:0 20px 20px 20px;margin-left:20px;border:1px solid #b9b9b9}.table.svelte-19rqxfk.svelte-19rqxfk{width:100%;table-layout:fixed;border-collapse:collapse}.table.svelte-19rqxfk thead.svelte-19rqxfk,.table.svelte-19rqxfk tr.svelte-19rqxfk{border-bottom:1px solid #9e9e9e}.table.svelte-19rqxfk td.svelte-19rqxfk{padding-top:10px;padding-right:10px}.table.svelte-19rqxfk thead.svelte-19rqxfk{font-weight:bold}.table.svelte-19rqxfk thead td.svelte-19rqxfk:nth-child(3){width:100px}.table.svelte-19rqxfk td.svelte-19rqxfk{padding-bottom:10px}.table.svelte-19rqxfk tr td.svelte-19rqxfk{overflow:hidden;text-overflow:ellipsis}.table.svelte-19rqxfk tr td.svelte-19rqxfk:hover{overflow:show;text-overflow:initial;overflow-wrap:break-word}.blocks.svelte-19rqxfk.svelte-19rqxfk{display:flex;flex-direction:row}.block-list.svelte-19rqxfk.svelte-19rqxfk{align-items:center;height:400px;overflow-y:scroll;padding-right:10px}.block-list.svelte-19rqxfk button.svelte-19rqxfk{display:block;width:100px;padding:10px 10px 8px;margin-bottom:10px}.block-detail.svelte-19rqxfk.svelte-19rqxfk{flex:1;padding-left:20px}
4 | .wallet.svelte-yz4xpt{padding:0 20px 20px 20px;border:1px solid #b9b9b9}.grid.svelte-yz4xpt{display:grid;grid-template-rows:auto auto;grid-template-columns:auto 1fr;grid-gap:10px;align-items:center}
5 |
6 | /*# sourceMappingURL=bundle.css.map */
--------------------------------------------------------------------------------
/src/crypto.rs:
--------------------------------------------------------------------------------
1 | use sha2::{Sha256, Digest};
2 | use secp256k1::{Secp256k1, Message};
3 | use secp256k1::rand;
4 | use secp256k1::All;
5 | use secp256k1::Signature;
6 | use std::str::FromStr;
7 |
8 | pub mod key {
9 | pub use secp256k1::key::PublicKey;
10 | pub use secp256k1::key::SecretKey;
11 | }
12 |
13 | pub fn sha256 (payload: String) -> Vec {
14 | let mut hasher = Sha256::new();
15 | hasher.update(payload);
16 | return hasher.finalize().as_slice().to_vec()
17 | }
18 |
19 | pub struct KeyPair {
20 | secp: Secp256k1, // TODO: how to make this global?
21 | pub public_key: key::PublicKey,
22 | pub private_key: key::SecretKey,
23 | }
24 |
25 | impl KeyPair {
26 | pub fn new() -> KeyPair {
27 | #[allow(deprecated)]
28 | let mut rand = rand::OsRng::new().unwrap();
29 | let secp = Secp256k1::new();
30 | let (private_key, public_key) = secp.generate_keypair(&mut rand);
31 | KeyPair {
32 | secp,
33 | public_key,
34 | private_key,
35 | }
36 | }
37 |
38 | pub fn from(key: String) -> Result {
39 | let secp = Secp256k1::new();
40 | let private_key = key::SecretKey::from_str(&key)?;
41 | let public_key = key::PublicKey::from_secret_key(&secp, &private_key);
42 | return Ok(KeyPair {
43 | secp,
44 | public_key,
45 | private_key,
46 | });
47 | }
48 |
49 | pub fn sign(&self, message: &[u8]) -> Signature {
50 | let message = Message::from_slice(message).expect("message from slice");
51 | return self.secp.sign(&message, &self.private_key);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/web/public/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | font-size: 14px;
9 | color: #333;
10 | margin: 0;
11 | padding: 20px;
12 | box-sizing: border-box;
13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
14 | }
15 |
16 | a {
17 | color: rgb(0,100,200);
18 | text-decoration: none;
19 | }
20 |
21 | a:hover {
22 | text-decoration: underline;
23 | }
24 |
25 | a:visited {
26 | color: rgb(0,80,160);
27 | }
28 |
29 | label {
30 | display: block;
31 | }
32 |
33 | input, button, select, textarea {
34 | font-family: inherit;
35 | font-size: inherit;
36 | -webkit-padding: 0.4em 0;
37 | padding: 0.4em;
38 | box-sizing: border-box;
39 | border: 1px solid #ccc;
40 | border-radius: 2px;
41 | }
42 |
43 | input:disabled {
44 | color: #ccc;
45 | }
46 |
47 | button {
48 | cursor: pointer;
49 | position:relative;
50 | width: auto;
51 | display:inline-block;
52 | color:#ecf0f1;
53 | text-decoration:none;
54 | border-radius:5px;
55 | border:solid 1px #9c9c9c;
56 | background:#8d8680;
57 | text-align:center;
58 | padding:6px 10px 4px;
59 |
60 | -webkit-transition: all 0.1s;
61 | -moz-transition: all 0.1s;
62 | transition: all 0.1s;
63 |
64 | -webkit-box-shadow: 0px 6px 0px #6b6b6b;
65 | -moz-box-shadow: 0px 6px 0px #6b6b6b;
66 | box-shadow: 0px 6px 0px #6b6b6b;
67 | }
68 |
69 | button:active, button.selected {
70 | -webkit-box-shadow: 0px 2px 0px #6b6b6b;
71 | -moz-box-shadow: 0px 2px 0px #6b6b6b;
72 | box-shadow: 0px 2px 0px #6b6b6b;
73 | position:relative;
74 | top:4px;
75 | }
76 |
77 | button:focus {
78 | outline: none;
79 |
80 | }
81 |
82 | h1, h2, h3, h4, h5 {
83 | color: #5a5a5a;
84 | }
85 |
86 | hr {
87 | height: 1px;
88 | background: rgb(216, 216, 216);
89 | border: 0;
90 | margin: 20px 0;
91 | }
--------------------------------------------------------------------------------
/web/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 |
7 | const production = !process.env.ROLLUP_WATCH;
8 |
9 | function serve() {
10 | let server;
11 |
12 | function toExit() {
13 | if (server) server.kill(0);
14 | }
15 |
16 | return {
17 | writeBundle() {
18 | if (server) return;
19 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
20 | stdio: ['ignore', 'inherit', 'inherit'],
21 | shell: true
22 | });
23 |
24 | process.on('SIGTERM', toExit);
25 | process.on('exit', toExit);
26 | }
27 | };
28 | }
29 |
30 | export default {
31 | input: 'src/main.js',
32 | output: {
33 | sourcemap: true,
34 | format: 'iife',
35 | name: 'app',
36 | file: 'public/build/bundle.js'
37 | },
38 | plugins: [
39 | svelte({
40 | // enable run-time checks when not in production
41 | dev: !production,
42 | // we'll extract any component CSS out into
43 | // a separate file - better for performance
44 | css: css => {
45 | css.write('bundle.css');
46 | }
47 | }),
48 |
49 | // If you have external dependencies installed from
50 | // npm, you'll most likely need these plugins. In
51 | // some cases you'll need additional configuration -
52 | // consult the documentation for details:
53 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
54 | resolve({
55 | browser: true,
56 | dedupe: ['svelte']
57 | }),
58 | commonjs(),
59 |
60 | // In dev mode, call `npm run start` once
61 | // the bundle has been generated
62 | !production && serve(),
63 |
64 | // Watch the `public` directory and refresh the
65 | // browser on changes when not in production
66 | !production && livereload('public'),
67 |
68 | // If we're building for production (npm run build
69 | // instead of npm run dev), minify
70 | production && terser()
71 | ],
72 | watch: {
73 | clearScreen: false
74 | }
75 | };
76 |
--------------------------------------------------------------------------------
/src/tx.rs:
--------------------------------------------------------------------------------
1 | use crate::crypto;
2 |
3 | use crypto::key;
4 | use secp256k1::key::PublicKey;
5 | use secp256k1::{Secp256k1, Signature, Message};
6 | use std::str::FromStr;
7 | use std::fmt;
8 | use serde::{Serialize, Deserialize};
9 |
10 | #[derive(Copy, Clone, Serialize, Deserialize)]
11 | pub struct Transaction {
12 | pub from: PublicKey,
13 | pub to: PublicKey,
14 | pub amount: u32
15 | }
16 |
17 | impl fmt::Debug for Transaction {
18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 | f.debug_struct("Transaction")
20 | .field("from", &format!("{}", self.from))
21 | .field("to", &format!("{}", self.to))
22 | .field("amount", &self.amount)
23 | .finish()
24 | }
25 | }
26 |
27 | impl Transaction{
28 | pub fn serialize(&self) -> String {
29 | return format!("{}{}{}", self.from, &self.to, hex::encode(&format!("{}", self.amount)));
30 | }
31 |
32 | pub fn hash(&self) -> Vec {
33 | return crypto::sha256(self.serialize())
34 | }
35 | }
36 |
37 | #[derive(Debug, Clone, Serialize, Deserialize)]
38 | pub struct SignedTransaction {
39 | pub transaction: Transaction,
40 | pub sig: String,
41 | }
42 |
43 | impl SignedTransaction{
44 | pub fn to_string(& self) -> String {
45 | return format!("{}{}", self.transaction.serialize(), self.sig);
46 | }
47 |
48 | pub fn is_sig_valid(& self) -> bool {
49 | let secp = Secp256k1::verification_only();
50 | let unsigned_tx_hash = Message::from_slice(self.transaction.hash().as_slice()).expect("message from slice");
51 | let sig = Signature::from_str(self.sig.as_str()).expect("signature from string");
52 | return secp.verify(&unsigned_tx_hash, &sig, &self.transaction.from).is_ok();
53 | }
54 | }
55 |
56 | pub fn create_signed(keypair: &crypto::KeyPair, to: key::PublicKey, amount: u32) -> SignedTransaction {
57 | let tx = Transaction {
58 | from: keypair.public_key,
59 | to,
60 | amount,
61 | };
62 |
63 | let sig = keypair.sign(&tx.hash());
64 |
65 | return SignedTransaction{
66 | transaction: tx,
67 | sig: sig.to_string(),
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/miner.rs:
--------------------------------------------------------------------------------
1 | use crate::node;
2 | use crate::block;
3 | use crate::crypto;
4 |
5 | use colored::*;
6 | use std::time;
7 | use std::sync::mpsc;
8 | use std::thread;
9 | use std::sync::{Arc, Mutex};
10 |
11 | pub fn start_miner(node_ref: Arc>, interrupt_rx: mpsc::Receiver<()>) {
12 | let (out_tx, out_rx) = mpsc::channel();
13 | let (in_tx, in_rx) = mpsc::channel();
14 | thread::spawn(move || {
15 | loop {
16 | if let Ok(proposed_block) = out_rx.try_recv() {
17 | let proposed_block: block::ProposedBlock = proposed_block;
18 |
19 | let mut nonce: u32 = 0;
20 | let block_string = proposed_block.serialize();
21 | loop {
22 | if let Ok(()) = interrupt_rx.try_recv() {
23 | in_tx.send(None).unwrap();
24 | break;
25 | }
26 | let block = format!("{}{}", block_string, nonce);
27 | let block_hash = hex::encode(crypto::sha256(block.clone()));
28 | if block_hash.starts_with(&"0".repeat(node::DIFFICULTY)) {
29 | let mined_block = block::Block {
30 | hash: block_hash,
31 | nonce,
32 | prev_block: proposed_block.prev_block,
33 | transactions: proposed_block.transactions
34 | };
35 | in_tx.send(Some(mined_block)).unwrap();
36 | break;
37 | }
38 | // Artificially limit hash rate to avoid unnecessary cpu usage
39 | thread::sleep(time::Duration::from_millis(80));
40 | nonce += 1;
41 | }
42 | }
43 | thread::sleep(time::Duration::from_millis(500));
44 | }
45 | });
46 | {
47 | let mut node = node_ref.lock().unwrap();
48 | let proposed_block = node.get_proposed_block().unwrap();
49 | out_tx.send(proposed_block).unwrap();
50 | }
51 | loop {
52 | if let Ok(block) = in_rx.try_recv() {
53 | let mut node = node_ref.lock().unwrap();
54 | if let Some(block) = block {
55 | println!("{} {}", "Mined Block:".green(), block.hash);
56 | node.receive_block(block).unwrap();
57 | }
58 | let proposed_block = node.get_proposed_block().unwrap();
59 | out_tx.send(proposed_block).unwrap();
60 | };
61 | thread::sleep(time::Duration::from_millis(1000));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/bin/nibbled.rs:
--------------------------------------------------------------------------------
1 | use nibble::node::Node;
2 | use nibble::miner;
3 | use nibble::rpc;
4 | use nibble::p2p;
5 | use nibble::web;
6 | use nibble::settings;
7 |
8 | use colored::*;
9 | use std::sync::{Arc, Mutex};
10 | use std::sync::mpsc;
11 | use std::thread;
12 |
13 | fn main() -> std::io::Result<()> {
14 | let config = settings::Settings::new().unwrap();
15 |
16 | let p2p_data = p2p::P2PData::new();
17 | let p2p_data_ref = Arc::new(Mutex::new(p2p_data));
18 |
19 | // Broadcasting blocks and transactions
20 | let (block_tx, block_rx) = mpsc::channel();
21 | let (transaction_tx, transaction_rx) = mpsc::channel();
22 |
23 | // Interrupt the miner when new blocks are received through the network
24 | let (miner_interrupt_tx, miner_interrupt_rx) = mpsc::channel();
25 |
26 | let receiver_p2p_data_ref = p2p_data_ref.clone();
27 | let receiver_thread = thread::spawn(move || {
28 | p2p::run_receiver(receiver_p2p_data_ref, block_rx, transaction_rx);
29 | });
30 |
31 | let node = Node::new(block_tx, transaction_tx);
32 | let node_ref = Arc::new(Mutex::new(node));
33 | {
34 | let mut node_inst = node_ref.lock().unwrap();
35 | node_inst.start().expect("Start failed");
36 | println!("{}", format!("Your Public Key: {}", node_inst.keypair.public_key).yellow());
37 | }
38 |
39 | let rpc_node_ref = node_ref.clone();
40 | let rpc_port = config.rpc_port.clone();
41 | let rpc_thread = thread::spawn(move || {
42 | rpc::run_server(rpc_node_ref, rpc_port)
43 | });
44 |
45 | let p2p_node_ref = node_ref.clone();
46 | let tcp_port = config.tcp_port.clone();
47 | let server_p2p_data_ref = p2p_data_ref.clone();
48 | let host_addr = format!("127.0.0.1:{}", tcp_port); // TODO: real address
49 | let run_server_host_addr = host_addr.clone();
50 | let p2p_miner_interrupt_tx = miner_interrupt_tx.clone();
51 | let p2p_thread = thread::spawn(move || {
52 | p2p::run_server(p2p_node_ref, server_p2p_data_ref, run_server_host_addr, p2p_miner_interrupt_tx).unwrap();
53 | });
54 |
55 | let p2p_node_ref = node_ref.clone();
56 | let miner_thread = if config.miner_enabled {
57 | let miner_node_ref = node_ref.clone();
58 | Some(thread::spawn(move || {
59 | miner::start_miner(miner_node_ref, miner_interrupt_rx);
60 | }))
61 | } else { None };
62 |
63 | let p2p_data_ref = p2p_data_ref.clone();
64 | let init_host_addr = host_addr.clone();
65 | p2p::init(p2p_node_ref, p2p_data_ref, miner_interrupt_tx, init_host_addr, config.bootstrap_node).unwrap();
66 |
67 | let web_port = config.web_port.clone();
68 | let web_thread = thread::spawn(move || {
69 | web::run_server(web_port)
70 | });
71 |
72 | rpc_thread.join().unwrap();
73 | p2p_thread.join().unwrap();
74 | receiver_thread.join().unwrap();
75 | match miner_thread {
76 | Some(thread) => thread.join().unwrap(),
77 | None => (),
78 | }
79 | web_thread.join().unwrap();
80 |
81 | return Ok(());
82 | }
83 |
--------------------------------------------------------------------------------
/web/src/Wallet.svelte:
--------------------------------------------------------------------------------
1 |
49 |
50 |
64 |
65 |
66 |
Wallet
67 |
68 |
Balance:
{#await balance then balance}{balance} Nibble {/await}
69 |
Public Key:
{#await pubKey then pubKey}{/await}
70 |
71 |
72 |
Send
73 |
74 |
To:
75 |
Amount:
76 |
77 |
78 |
Random Public Key
79 |
{#await randomPubKey then randomPubKey}{/await}
80 |
81 |
Get Balance
82 |
83 |
Public Key:
84 |
{#if !isNaN(pubKeyBalance)}{pubKeyBalance} Nibble{/if}
85 |
86 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | *Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
2 |
3 | ---
4 |
5 | # svelte app
6 |
7 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
8 |
9 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
10 |
11 | ```bash
12 | npx degit sveltejs/template svelte-app
13 | cd svelte-app
14 | ```
15 |
16 | *Note that you will need to have [Node.js](https://nodejs.org) installed.*
17 |
18 |
19 | ## Get started
20 |
21 | Install the dependencies...
22 |
23 | ```bash
24 | cd svelte-app
25 | npm install
26 | ```
27 |
28 | ...then start [Rollup](https://rollupjs.org):
29 |
30 | ```bash
31 | npm run dev
32 | ```
33 |
34 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
35 |
36 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
37 |
38 |
39 | ## Building and running in production mode
40 |
41 | To create an optimised version of the app:
42 |
43 | ```bash
44 | npm run build
45 | ```
46 |
47 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
48 |
49 |
50 | ## Single-page app mode
51 |
52 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
53 |
54 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
55 |
56 | ```js
57 | "start": "sirv public --single"
58 | ```
59 |
60 | ## Using TypeScript
61 |
62 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
63 |
64 | ```bash
65 | node scripts/setupTypeScript.js
66 | ```
67 |
68 | Or remove the script via:
69 |
70 | ```bash
71 | rm scripts/setupTypeScript.js
72 | ```
73 |
74 | ## Deploying to the web
75 |
76 | ### With [Vercel](https://vercel.com)
77 |
78 | Install `vercel` if you haven't already:
79 |
80 | ```bash
81 | npm install -g vercel
82 | ```
83 |
84 | Then, from within your project folder:
85 |
86 | ```bash
87 | cd public
88 | vercel deploy --name my-project
89 | ```
90 |
91 | ### With [surge](https://surge.sh/)
92 |
93 | Install `surge` if you haven't already:
94 |
95 | ```bash
96 | npm install -g surge
97 | ```
98 |
99 | Then, from within your project folder:
100 |
101 | ```bash
102 | npm run build
103 | surge public my-project.surge.sh
104 | ```
105 |
--------------------------------------------------------------------------------
/src/rpc.rs:
--------------------------------------------------------------------------------
1 | use jsonrpc_http_server::jsonrpc_core::{IoHandler};
2 | use jsonrpc_http_server::{ServerBuilder};
3 |
4 | use jsonrpc_core::{Result};
5 | use jsonrpc_derive::rpc;
6 |
7 | use crate::crypto;
8 | use crate::node;
9 | use crate::block;
10 | use crate::tx;
11 | use crate::storage;
12 |
13 | use crypto::key;
14 | use colored::*;
15 | use std::collections::HashMap;
16 | use std::sync::{Arc, Mutex};
17 |
18 | #[rpc(server, client)]
19 | pub trait Rpc {
20 | #[rpc(name = "protocolVersion")]
21 | fn protocol_version(&self) -> Result;
22 |
23 | #[rpc(name = "send")]
24 | fn send(&self, pubkey: key::PublicKey, amount: u32) -> Result;
25 |
26 | #[rpc(name = "newpubkey")]
27 | fn newpubkey(&self) -> Result;
28 |
29 | #[rpc(name = "getpubkey")]
30 | fn getpubkey(&self) -> Result;
31 |
32 | #[rpc(name = "blockheight")]
33 | fn blockheight(&self) -> Result;
34 |
35 | #[rpc(name = "getblock")]
36 | fn getblock(&self, block_number: u32) -> Result;
37 |
38 | #[rpc(name = "balances")]
39 | fn balances(&self) -> Result>;
40 |
41 | #[rpc(name = "getbalance")]
42 | fn getbalance(&self, pubkey: key::PublicKey) -> Result;
43 |
44 | #[rpc(name = "mempool")]
45 | fn mempool(&self) -> Result>;
46 | }
47 |
48 | struct RpcImpl {
49 | node_ref: Arc>,
50 | }
51 |
52 | impl RpcImpl {
53 | fn new(node_ref: Arc>) -> RpcImpl {
54 | return RpcImpl {
55 | node_ref
56 | }
57 | }
58 | }
59 |
60 | impl Rpc for RpcImpl {
61 | fn protocol_version(&self) -> Result {
62 | Ok("1.0".into())
63 | }
64 |
65 | fn send(&self, pubkey: key::PublicKey, amount: u32) -> Result {
66 | let mut node = self.node_ref.lock().unwrap();
67 | let tx = node.send_transaction(pubkey, amount).unwrap();
68 | return Ok(tx);
69 | }
70 |
71 | fn newpubkey(&self) -> Result {
72 | let random_key = crypto::KeyPair::new();
73 | return Ok(random_key.public_key.to_string());
74 | }
75 |
76 | fn getpubkey(&self) -> Result {
77 | let node = self.node_ref.lock().unwrap();
78 | return Ok(node.keypair.public_key.to_string());
79 | }
80 |
81 | fn blockheight(&self) -> Result {
82 | let blockheight = storage::get_latest_block_number(&storage::db::blocks_md(true)).unwrap();
83 | return Ok(blockheight);
84 | }
85 |
86 | fn getblock(&self, block_number: u32) -> Result {
87 | let block_hash = storage::get_block_hash(&storage::db::blocks_md(true), block_number).unwrap().unwrap();
88 | let block = storage::get_block(&storage::db::blocks(true), &block_hash).unwrap().unwrap();
89 | return Ok(block)
90 | }
91 |
92 | fn getbalance(&self, pubkey: key::PublicKey) -> Result {
93 | let balance = storage::get_balance(&storage::db::balances(true), pubkey).unwrap();
94 | return Ok(balance.unwrap_or(0));
95 | }
96 |
97 | fn balances(&self) -> Result> {
98 | let balances = storage::get_balances(&storage::db::balances(true)).unwrap();
99 | return Ok(balances);
100 | }
101 |
102 | fn mempool(&self) -> Result> {
103 | let node = self.node_ref.lock().unwrap();
104 | return Ok(node.mempool.clone());
105 | }
106 | }
107 |
108 | pub fn run_server (node_ref: Arc>, port: u32) {
109 | let mut io = IoHandler::new();
110 | let rpc = RpcImpl::new(node_ref);
111 | io.extend_with(rpc.to_delegate());
112 |
113 | let rpc_path = format!("127.0.0.1:{}", port);
114 |
115 | let server = ServerBuilder::new(io)
116 | .threads(3)
117 | .start_http(&rpc_path.parse().unwrap())
118 | .unwrap();
119 |
120 | println!("{} Listening on {}", "RPC:".green(), rpc_path);
121 |
122 | server.wait();
123 | }
--------------------------------------------------------------------------------
/web/scripts/setupTypeScript.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** This script modifies the project to support TS code in .svelte files like:
4 |
5 |
8 |
9 | As well as validating the code for CI.
10 | */
11 |
12 | /** To work on this script:
13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
14 | */
15 |
16 | const fs = require("fs")
17 | const path = require("path")
18 | const { argv } = require("process")
19 |
20 | const projectRoot = argv[2] || path.join(__dirname, "..")
21 |
22 | // Add deps to pkg.json
23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
25 | "svelte-check": "^1.0.0",
26 | "svelte-preprocess": "^4.0.0",
27 | "@rollup/plugin-typescript": "^6.0.0",
28 | "typescript": "^3.9.3",
29 | "tslib": "^2.0.0",
30 | "@tsconfig/svelte": "^1.0.0"
31 | })
32 |
33 | // Add script for checking
34 | packageJSON.scripts = Object.assign(packageJSON.scripts, {
35 | "validate": "svelte-check"
36 | })
37 |
38 | // Write the package JSON
39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
40 |
41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
44 | fs.renameSync(beforeMainJSPath, afterMainTSPath)
45 |
46 | // Switch the app.svelte file to use TS
47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte")
48 | let appFile = fs.readFileSync(appSveltePath, "utf8")
49 | appFile = appFile.replace("
42 |
43 |
113 |
114 |
115 |
Mempool
116 |
117 | {#if mempool.length}
118 |
119 |
120 | | From |
121 | To |
122 | Value |
123 |
124 | {#each mempool as item}
125 |
126 | | {item.transaction.from} |
127 | {item.transaction.to} |
128 | {item.transaction.amount} |
129 |
130 | {/each}
131 |
132 | {:else}
133 | Empty
134 | {/if}
135 |
136 |
137 |
Blocks
138 |
139 |
140 | {#each blocks.slice().reverse() as block (block)}
141 |
146 | {/each}
147 |
148 | {#if selectedBlock}
149 |
150 |
{selectedBlock.hash}
151 |
152 |
153 | | From |
154 | To |
155 | Value |
156 |
157 | {#each selectedBlock.transactions as item}
158 |
159 | | {item.transaction.from} |
160 | {item.transaction.to} |
161 | {item.transaction.amount} |
162 |
163 | {/each}
164 |
165 |
166 | {/if}
167 |
168 |
--------------------------------------------------------------------------------
/src/storage.rs:
--------------------------------------------------------------------------------
1 | use crate::block;
2 | use crate::crypto;
3 |
4 | use crypto::key;
5 | use rocksdb::{DB};
6 | use serde_json;
7 | use std::collections::HashMap;
8 | use std::str::FromStr;
9 |
10 | pub mod db {
11 | use rocksdb::{DB, Options};
12 | use crate::settings;
13 |
14 | static BLOCKS_DB_PATH: &str = "/blocks";
15 | static BLOCKS_METADATA_DB_PATH: &str = "/blocksmetadata";
16 | static BALANCES_DB_PATH: &str = "/balances";
17 |
18 | pub fn open(path: &str, read_only: bool) -> DB {
19 | let config = settings::Settings::new().unwrap();
20 | let full_path = format!("{}{}", config.data_dir, path);
21 | if read_only {
22 | return DB::open_for_read_only(&Options::default(), full_path, true).unwrap();
23 | } else {
24 | return DB::open_default(full_path).unwrap();
25 | }
26 | }
27 |
28 | pub fn blocks(read_only: bool) -> DB {
29 | return open(BLOCKS_DB_PATH, read_only);
30 | }
31 |
32 | pub fn blocks_md(read_only: bool) -> DB {
33 | return open(BLOCKS_METADATA_DB_PATH, read_only);
34 | }
35 |
36 | pub fn balances(read_only: bool) -> DB {
37 | return open(BALANCES_DB_PATH, read_only);
38 | }
39 | }
40 |
41 | pub fn add_block(db: &DB, block: &block::Block) -> Result<(), String> {
42 | let json = serde_json::to_string(block).map_err(|e| e.to_string())?;
43 | db.put(block.hash.clone(), json).map_err(|e| e.to_string())?;
44 | return Ok(());
45 | }
46 |
47 | pub fn get_block(db: &DB, block_hash: &String) -> Result