centralbank 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY.md +3 -0
- data/Manifest.txt +21 -0
- data/README.md +9 -0
- data/Rakefile +30 -0
- data/lib/centralbank.rb +128 -0
- data/lib/centralbank/bank.rb +113 -0
- data/lib/centralbank/block.rb +56 -0
- data/lib/centralbank/blockchain.rb +47 -0
- data/lib/centralbank/cache.rb +22 -0
- data/lib/centralbank/ledger.rb +30 -0
- data/lib/centralbank/node.rb +88 -0
- data/lib/centralbank/public/style.css +163 -0
- data/lib/centralbank/transaction.rb +30 -0
- data/lib/centralbank/version.rb +11 -0
- data/lib/centralbank/views/_blockchain.erb +34 -0
- data/lib/centralbank/views/_ledger.erb +15 -0
- data/lib/centralbank/views/_peers.erb +24 -0
- data/lib/centralbank/views/_pending_transactions.erb +23 -0
- data/lib/centralbank/views/_wallet.erb +16 -0
- data/lib/centralbank/views/index.erb +30 -0
- data/lib/centralbank/wallet.rb +15 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f068e40d44f33359ab2e4e52a1cc4225a5f8889f
|
4
|
+
data.tar.gz: 9e5e516e4ad710ca788cec1127b38ff195963a4e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '0810cd371d9696b64d1e03db898da57878c93f10918f4498742b71cb79e792ba032e8700450636870ba64006b02ca33b8702816c623e6c532fa9a98aa1327b98'
|
7
|
+
data.tar.gz: 7a5fc1f5039caa7b62a3439cbbb47660b905145b92922e363ed9fbd3afadf5db13440096e06dfbee40e3270cb665ac8a53c7ff0d37d5989e92970bb60c26fd59
|
data/HISTORY.md
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
HISTORY.md
|
2
|
+
Manifest.txt
|
3
|
+
README.md
|
4
|
+
Rakefile
|
5
|
+
lib/centralbank.rb
|
6
|
+
lib/centralbank/bank.rb
|
7
|
+
lib/centralbank/block.rb
|
8
|
+
lib/centralbank/blockchain.rb
|
9
|
+
lib/centralbank/cache.rb
|
10
|
+
lib/centralbank/ledger.rb
|
11
|
+
lib/centralbank/node.rb
|
12
|
+
lib/centralbank/public/style.css
|
13
|
+
lib/centralbank/transaction.rb
|
14
|
+
lib/centralbank/version.rb
|
15
|
+
lib/centralbank/views/_blockchain.erb
|
16
|
+
lib/centralbank/views/_ledger.erb
|
17
|
+
lib/centralbank/views/_peers.erb
|
18
|
+
lib/centralbank/views/_pending_transactions.erb
|
19
|
+
lib/centralbank/views/_wallet.erb
|
20
|
+
lib/centralbank/views/index.erb
|
21
|
+
lib/centralbank/wallet.rb
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
require './lib/centralbank/version.rb'
|
3
|
+
|
4
|
+
Hoe.spec 'centralbank' do
|
5
|
+
|
6
|
+
self.version = Centralbank::VERSION
|
7
|
+
|
8
|
+
self.summary = 'centralbank - print your own money / cryptocurrency; run your own federated central bank nodes on the blockchain peer-to-peer over HTTP; revolutionize the world one block at a time'
|
9
|
+
self.description = summary
|
10
|
+
|
11
|
+
self.urls = ['https://github.com/openblockchains/centralbank']
|
12
|
+
|
13
|
+
self.author = 'Gerald Bauer'
|
14
|
+
self.email = 'ruby-talk@ruby-lang.org'
|
15
|
+
|
16
|
+
# switch extension to .markdown for gihub formatting
|
17
|
+
self.readme_file = 'README.md'
|
18
|
+
self.history_file = 'History.md'
|
19
|
+
|
20
|
+
self.extra_deps = [
|
21
|
+
['sinatra', '>=2.0']
|
22
|
+
]
|
23
|
+
|
24
|
+
self.licenses = ['Public Domain']
|
25
|
+
|
26
|
+
self.spec_extras = {
|
27
|
+
required_ruby_version: '>= 2.3'
|
28
|
+
}
|
29
|
+
|
30
|
+
end
|
data/lib/centralbank.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# stdlibs
|
4
|
+
require 'json'
|
5
|
+
require 'digest'
|
6
|
+
require 'net/http'
|
7
|
+
require 'set'
|
8
|
+
require 'pp'
|
9
|
+
|
10
|
+
## require 'blockchain-lite'
|
11
|
+
## Block = BlockchainLite::Basic::Block
|
12
|
+
|
13
|
+
|
14
|
+
### 3rd party gems
|
15
|
+
require 'sinatra/base' # note: use "modular" sinatra app / service
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
### our own code
|
20
|
+
require 'centralbank/version' ## let version always go first
|
21
|
+
require 'centralbank/block'
|
22
|
+
require 'centralbank/cache'
|
23
|
+
require 'centralbank/transaction'
|
24
|
+
require 'centralbank/blockchain'
|
25
|
+
require 'centralbank/bank'
|
26
|
+
require 'centralbank/ledger'
|
27
|
+
require 'centralbank/wallet'
|
28
|
+
|
29
|
+
require 'centralbank/node'
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
module Centralbank
|
35
|
+
|
36
|
+
class Service < Sinatra::Base
|
37
|
+
|
38
|
+
def self.banner
|
39
|
+
"centralbank/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
## - for now hard-code address e.g. Sepp
|
44
|
+
NODE = Node.new( address: 'Sepp' )
|
45
|
+
|
46
|
+
|
47
|
+
PUBLIC_FOLDER = "#{Centralbank.root}/lib/centralbank/public"
|
48
|
+
VIEWS_FOLDER = "#{Centralbank.root}/lib/centralbank/views"
|
49
|
+
|
50
|
+
puts "[centralbank] boot - setting public folder to: #{PUBLIC_FOLDER}"
|
51
|
+
puts "[centralbank] boot - setting views folder to: #{VIEWS_FOLDER}"
|
52
|
+
|
53
|
+
set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
|
54
|
+
set :views, VIEWS_FOLDER # set up the views dir
|
55
|
+
|
56
|
+
set :static, true # set up static file routing -- check - still needed?
|
57
|
+
|
58
|
+
|
59
|
+
set connections: []
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
get '/' do
|
64
|
+
@node = NODE
|
65
|
+
erb :index
|
66
|
+
end
|
67
|
+
|
68
|
+
post '/send' do
|
69
|
+
NODE.on_send( params[:to], params[:amount].to_i )
|
70
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
71
|
+
redirect '/'
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
post '/transactions' do
|
76
|
+
if NODE.on_add_transaction(
|
77
|
+
params[:from],
|
78
|
+
params[:to],
|
79
|
+
params[:amount].to_i,
|
80
|
+
params[:id]
|
81
|
+
)
|
82
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
83
|
+
end
|
84
|
+
redirect '/'
|
85
|
+
end
|
86
|
+
|
87
|
+
post '/mine' do
|
88
|
+
NODE.on_mine!
|
89
|
+
redirect '/'
|
90
|
+
end
|
91
|
+
|
92
|
+
post '/peers' do
|
93
|
+
NODE.on_add_peer( params[:host], params[:port].to_i )
|
94
|
+
redirect '/'
|
95
|
+
end
|
96
|
+
|
97
|
+
post '/peers/:index/delete' do
|
98
|
+
NODE.on_delete_peer( params[:index].to_i )
|
99
|
+
redirect '/'
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
post '/resolve' do
|
105
|
+
data = JSON.parse(request.body.read)
|
106
|
+
if data['chain'] && NODE.on_resolve( data['chain'] )
|
107
|
+
status 202 ### 202 Accepted; see httpstatuses.com/202
|
108
|
+
settings.connections.each { |out| out << "data: resolved\n\n" }
|
109
|
+
else
|
110
|
+
status 200 ### 200 OK
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
get '/events', provides: 'text/event-stream' do
|
116
|
+
stream :keep_open do |out|
|
117
|
+
settings.connections << out
|
118
|
+
out.callback { settings.connections.delete(out) }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
end # class Service
|
124
|
+
end # module Centralbank
|
125
|
+
|
126
|
+
|
127
|
+
# say hello
|
128
|
+
puts Centralbank::Service.banner
|
@@ -0,0 +1,113 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
class Bank
|
6
|
+
attr_reader :pending, :chain, :ledger
|
7
|
+
|
8
|
+
COINBASE = "COINBASE"
|
9
|
+
MINING_REWARD = 5
|
10
|
+
|
11
|
+
|
12
|
+
def initialize( address )
|
13
|
+
@address = address
|
14
|
+
|
15
|
+
@cache = Cache.new( 'data.json' )
|
16
|
+
h = @cache.read
|
17
|
+
if h
|
18
|
+
## restore blockchain
|
19
|
+
@chain = Blockchain.from_json( h['chain'] )
|
20
|
+
## restore pending transactions too
|
21
|
+
@pending = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
|
22
|
+
else
|
23
|
+
@chain = Blockchain.new
|
24
|
+
@chain << [Tx.new( COINBASE, @address, MINING_REWARD )] # genesis (big bang!) starter block
|
25
|
+
@pending = []
|
26
|
+
end
|
27
|
+
|
28
|
+
## update ledger (balances) with confirmed transactions
|
29
|
+
@ledger = Ledger.new( @chain )
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
def mine_block!
|
35
|
+
add_transaction( Tx.new( COINBASE, @address, MINING_REWARD ))
|
36
|
+
|
37
|
+
## add mined (w/ computed/calculated hash) block
|
38
|
+
@chain << @pending
|
39
|
+
@pending = []
|
40
|
+
|
41
|
+
## update ledger (balances) with new confirmed transactions
|
42
|
+
@ledger = Ledger.new( @chain )
|
43
|
+
|
44
|
+
@cache.write as_json
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def sufficient_funds?( wallet, amount )
|
49
|
+
## (convenience) delegate for ledger
|
50
|
+
## todo/check: use address instead of wallet - why? why not?
|
51
|
+
## for now single address wallet (that is, wallet==address)
|
52
|
+
@ledger.sufficient_funds?( wallet, amount )
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def add_transaction( tx )
|
57
|
+
if tx.valid? && transaction_is_new?( tx )
|
58
|
+
@pending << tx
|
59
|
+
@cache.write as_json
|
60
|
+
return true
|
61
|
+
else
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
##
|
68
|
+
# check - how to name incoming chain - chain_new, chain_candidate - why? why not?
|
69
|
+
# what's an intuitive name - what's gets used most often???
|
70
|
+
|
71
|
+
def resolve!( chain_new )
|
72
|
+
# TODO this does not protect against invalid block shapes (bogus COINBASE transactions for example)
|
73
|
+
|
74
|
+
if !chain_new.empty? && chain_new.last.valid? && chain_new.size > @chain.size
|
75
|
+
@chain = chain_new
|
76
|
+
## update ledger (balances) with new confirmed transactions
|
77
|
+
@ledger = Ledger.new( @chain )
|
78
|
+
|
79
|
+
|
80
|
+
_transactions = @chain.transactions ## use a copy for reference (optimization) in inner loop
|
81
|
+
## todo: cleanup ??? -- use tx2 for t ???
|
82
|
+
## document - keep only pending transaction not yet in blockchain ????
|
83
|
+
@pending = @pending.select do |tx|
|
84
|
+
_transactions.none? { |tx_confirmed| tx_confirmed.id == tx.id }
|
85
|
+
end
|
86
|
+
@cache.write as_json
|
87
|
+
return true
|
88
|
+
else
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
def as_json
|
96
|
+
{ chain: @chain.as_json,
|
97
|
+
transactions: @pending.map { |tx| tx.to_h }
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def transaction_is_new?( tx_new )
|
106
|
+
## check if tx exists already in blockchain or pending tx pool
|
107
|
+
|
108
|
+
## todo: use chain.include? to check for include
|
109
|
+
## avoid loop and create new array for check!!!
|
110
|
+
(@chain.transactions + @pending).none? { |tx| tx_new.id == tx.id }
|
111
|
+
end
|
112
|
+
|
113
|
+
end ## class Bank
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
##################
|
3
|
+
# todo: add proof-of-work (nounce calculation) !!!!
|
4
|
+
#
|
5
|
+
|
6
|
+
class Block
|
7
|
+
|
8
|
+
attr_reader :index, :timestamp, :transactions, :previous_hash, :hash
|
9
|
+
|
10
|
+
def initialize( index, transactions, previous_hash )
|
11
|
+
@index = index
|
12
|
+
@timestamp = Time.now.utc ## note: use coordinated universal time (utc)
|
13
|
+
@transactions = transactions
|
14
|
+
@previous_hash = previous_hash
|
15
|
+
@hash = calc_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def calc_hash
|
19
|
+
sha = Digest::SHA256.new
|
20
|
+
sha.update( @index.to_s + @time.to_s + @transactions.to_s + @previous_hash )
|
21
|
+
sha.hexdigest
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def self.first( transactions ) # create genesis (big bang! first) block
|
26
|
+
## uses index zero (0) and arbitrary previous_hash ('0')
|
27
|
+
self.new( 0, transactions, '0' )
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.next( previous, transactions )
|
31
|
+
self.new( previous.index+1, transactions, previous.hash )
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
{ index: @index,
|
38
|
+
timestamp: @timestamp,
|
39
|
+
transactions: @transactions.map { |tx| tx.to_h },
|
40
|
+
previous_hash: @previous_hash }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_h( h )
|
44
|
+
transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
|
45
|
+
self.new( h['index'],
|
46
|
+
## h['timestamp'], -- fix!!!: change use keyword params (make timestamp optional)
|
47
|
+
transactions,
|
48
|
+
h['previous_hash'] )
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid?
|
52
|
+
true ## for now always valid
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end # class Block
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
class Blockchain
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@chain, :[], :size, :each, :empty?, :last
|
7
|
+
|
8
|
+
|
9
|
+
def initialize( chain=[] )
|
10
|
+
@chain = chain
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<( txs )
|
14
|
+
## todo: check if is block or array
|
15
|
+
## if array (of transactions) - auto-add (build) block
|
16
|
+
## allow block - why? why not?
|
17
|
+
## for now just use transactions (keep it simple :-)
|
18
|
+
|
19
|
+
if @chain.size == 0
|
20
|
+
block = Block.first( txs )
|
21
|
+
else
|
22
|
+
block = Block.next( @chain.last, txs )
|
23
|
+
end
|
24
|
+
@chain << block
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
def as_json
|
30
|
+
@chain.map { |block| block.to_h }
|
31
|
+
end
|
32
|
+
|
33
|
+
def transactions
|
34
|
+
## "accumulate" get all transactions from all blocks "reduced" into a single array
|
35
|
+
@chain.reduce( [] ) { |acc, block| acc + block.transactions }
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
def self.from_json( data )
|
41
|
+
## note: assumes data is an array of block records/objects in json
|
42
|
+
chain = data.map { |h| Block.from_h( h ) }
|
43
|
+
self.new( chain )
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end # class Blockchain
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Cache
|
4
|
+
def initialize( name )
|
5
|
+
@name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def write( data )
|
9
|
+
File.open( @name, 'w' ) do |f| ## use utf8 - why? why not??
|
10
|
+
f.write JSON.pretty_generate( data )
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def read
|
15
|
+
if File.exists?( @name )
|
16
|
+
data = File.read( @name ) ## use utf8 - why? why not??
|
17
|
+
JSON.parse( data )
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end ## class Cache
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
class Ledger
|
3
|
+
attr_reader :wallets ## use addresses - why? why not? for now single address wallet (wallet==address)
|
4
|
+
|
5
|
+
def initialize( chain=[] )
|
6
|
+
@wallets = {}
|
7
|
+
chain.each do |block|
|
8
|
+
apply_transactions( block.transactions )
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def sufficient_funds?( wallet, amount )
|
13
|
+
return true if wallet == Bank::COINBASE ### use Config::COINBASE why? why not ???
|
14
|
+
@wallets.has_key?( wallet ) && @wallets[wallet] - amount >= 0
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def apply_transactions( transactions )
|
21
|
+
transactions.each do |tx|
|
22
|
+
if sufficient_funds?(tx.from, tx.amount)
|
23
|
+
@wallets[tx.from] -= tx.amount unless tx.from == Bank::COINBASE ### use Config::COINBASE why? why not ???
|
24
|
+
@wallets[tx.to] ||= 0
|
25
|
+
@wallets[tx.to] += tx.amount
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end ## class Ledger
|
@@ -0,0 +1,88 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Node
|
4
|
+
attr_reader :id, :peers, :wallet, :bank
|
5
|
+
|
6
|
+
|
7
|
+
WALLET_ADDRESSES = %w[Sepp Franz Sissi Maria Eva Ferdl Max Adam]
|
8
|
+
|
9
|
+
def initialize( address: nil )
|
10
|
+
## pick "random" address if nil (none passed in)
|
11
|
+
address ||= WALLET_ADDRESSES[rand( WALLET_ADDRESSES.size )]
|
12
|
+
|
13
|
+
@id = SecureRandom.uuid
|
14
|
+
@peers = []
|
15
|
+
@wallet = Wallet.new( address )
|
16
|
+
@bank = Bank.new @wallet.address
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
def on_add_peer( host, port )
|
22
|
+
@peers << [host, port]
|
23
|
+
@peers.uniq!
|
24
|
+
# TODO/FIX: no need to send to every peer, just the new one
|
25
|
+
send_chain_to_peers
|
26
|
+
@bank.pending.each { |tx| send_transaction_to_peers( tx ) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_delete_peer( index )
|
30
|
+
@peers.delete_at( index )
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def on_add_transaction( from, to, amount, id )
|
35
|
+
## note: for now must always pass in id - why? why not? possible tx without id???
|
36
|
+
tx = Tx.new( from, to, amount, id )
|
37
|
+
if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
|
38
|
+
send_transaction_to_peers( tx )
|
39
|
+
return true
|
40
|
+
else
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_send( to, amount )
|
46
|
+
tx = @wallet.generate_transaction( to, amount )
|
47
|
+
if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
|
48
|
+
send_transaction_to_peers( tx )
|
49
|
+
return true
|
50
|
+
else
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def on_mine!
|
57
|
+
@bank.mine_block!
|
58
|
+
send_chain_to_peers
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_resolve( data )
|
62
|
+
chain_new = Blockchain.from_json( data )
|
63
|
+
if @bank.resolve!( chain_new )
|
64
|
+
send_chain_to_peers
|
65
|
+
return true
|
66
|
+
else
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def send_chain_to_peers
|
76
|
+
data = JSON.pretty_generate( @bank.as_json ) ## payload in json
|
77
|
+
@peers.each do |(host, port)|
|
78
|
+
Net::HTTP.post(URI::HTTP.build(host: host, port: port, path: '/resolve'), data )
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def send_transaction_to_peers( tx )
|
83
|
+
@peers.each do |(host, port)|
|
84
|
+
Net::HTTP.post_form(URI::HTTP.build(host: host, port: port, path: '/transactions'), tx.to_h )
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end ## class Node
|
@@ -0,0 +1,163 @@
|
|
1
|
+
body {
|
2
|
+
background: #fff;
|
3
|
+
font-family: 'menlo', monospace;
|
4
|
+
color: #2B2D2F;
|
5
|
+
padding: 0;
|
6
|
+
margin: 0;
|
7
|
+
font-size: 14px;
|
8
|
+
min-width: 960px;
|
9
|
+
}
|
10
|
+
|
11
|
+
.columns {
|
12
|
+
display: flex;
|
13
|
+
}
|
14
|
+
|
15
|
+
.left {
|
16
|
+
width: 66%;
|
17
|
+
}
|
18
|
+
|
19
|
+
.right {
|
20
|
+
width: 34%;
|
21
|
+
}
|
22
|
+
|
23
|
+
h1 {
|
24
|
+
font-size: 24px;
|
25
|
+
font-weight: normal;
|
26
|
+
padding-left: 15px;
|
27
|
+
margin-bottom: 20px;
|
28
|
+
}
|
29
|
+
|
30
|
+
h2 {
|
31
|
+
font-size: 16px;
|
32
|
+
}
|
33
|
+
|
34
|
+
h2 span {
|
35
|
+
font-size: 14px;
|
36
|
+
color: #597898;
|
37
|
+
font-weight: normal;
|
38
|
+
}
|
39
|
+
|
40
|
+
label {
|
41
|
+
display: inline-block;
|
42
|
+
width: 80px;
|
43
|
+
text-align: right;
|
44
|
+
padding-right: 10px;
|
45
|
+
}
|
46
|
+
|
47
|
+
input[type=text] {
|
48
|
+
display: inline-block;
|
49
|
+
font-size: 14px;
|
50
|
+
padding: 8px;
|
51
|
+
border-radius: 0;
|
52
|
+
border: 0;
|
53
|
+
}
|
54
|
+
|
55
|
+
table {
|
56
|
+
border-spacing: 0;
|
57
|
+
border-collapse: collapse;
|
58
|
+
}
|
59
|
+
|
60
|
+
th {
|
61
|
+
text-align: left;
|
62
|
+
}
|
63
|
+
|
64
|
+
td {
|
65
|
+
vertical-align: top;
|
66
|
+
padding: 5px 15px 5px 0;
|
67
|
+
}
|
68
|
+
|
69
|
+
ul {
|
70
|
+
list-style: none;
|
71
|
+
padding: 0;
|
72
|
+
margin: 0;
|
73
|
+
}
|
74
|
+
|
75
|
+
input[type=submit] {
|
76
|
+
font-size: 14px;
|
77
|
+
font-family: menlo, monospace;
|
78
|
+
border-radius: 5px;
|
79
|
+
padding: 8px 20px;
|
80
|
+
background: #FFDC00;
|
81
|
+
color: #2B2D2F;
|
82
|
+
border: 0;
|
83
|
+
}
|
84
|
+
|
85
|
+
input[type=submit].small {
|
86
|
+
font-size: 10px;
|
87
|
+
padding: 4px 10px;
|
88
|
+
}
|
89
|
+
|
90
|
+
.wallet,
|
91
|
+
.pending-transactions,
|
92
|
+
.peers,
|
93
|
+
.ledger,
|
94
|
+
.blockchain {
|
95
|
+
padding: 15px;
|
96
|
+
}
|
97
|
+
|
98
|
+
.wallet {
|
99
|
+
background: #7FDBFF;
|
100
|
+
}
|
101
|
+
|
102
|
+
.wallet h2 {
|
103
|
+
margin-bottom: 0;
|
104
|
+
}
|
105
|
+
|
106
|
+
.balance {
|
107
|
+
font-size: 30px;
|
108
|
+
}
|
109
|
+
|
110
|
+
.pending-transactions {
|
111
|
+
background: #A3E6FF;
|
112
|
+
}
|
113
|
+
|
114
|
+
.peers {
|
115
|
+
background: #C6EFFF;
|
116
|
+
}
|
117
|
+
|
118
|
+
.peers li form {
|
119
|
+
display: inline;
|
120
|
+
}
|
121
|
+
|
122
|
+
.ledger {
|
123
|
+
background: #E3F7FF;
|
124
|
+
}
|
125
|
+
|
126
|
+
.blockchain {
|
127
|
+
position: relative;
|
128
|
+
background: #001F3F;
|
129
|
+
color: #fff;
|
130
|
+
}
|
131
|
+
|
132
|
+
.blockchain form {
|
133
|
+
position: absolute;
|
134
|
+
top: 30px;
|
135
|
+
right: 15px;
|
136
|
+
}
|
137
|
+
|
138
|
+
.blocks {
|
139
|
+
border: 1px solid #597898;
|
140
|
+
border-bottom: 0;
|
141
|
+
}
|
142
|
+
|
143
|
+
.block {
|
144
|
+
margin: 0;
|
145
|
+
border-bottom: 2px dashed #597898;
|
146
|
+
padding: 10px;
|
147
|
+
}
|
148
|
+
|
149
|
+
.block .header {
|
150
|
+
text-align: center;
|
151
|
+
padding: 0 8px 8px 8px;
|
152
|
+
color: #597898;
|
153
|
+
border-bottom: 1px solid #354c63;
|
154
|
+
margin-bottom: 10px;
|
155
|
+
}
|
156
|
+
|
157
|
+
.block .id {
|
158
|
+
color: #597898;
|
159
|
+
}
|
160
|
+
|
161
|
+
.peers li {
|
162
|
+
padding: 5px 0px;
|
163
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Transaction
|
4
|
+
|
5
|
+
attr_reader :from, :to, :amount, :id
|
6
|
+
|
7
|
+
def initialize( from, to, amount, id=SecureRandom.uuid )
|
8
|
+
@from = from
|
9
|
+
@to = to
|
10
|
+
@amount = amount
|
11
|
+
@id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_h( hash )
|
15
|
+
self.new *hash.values_at( 'from', 'to', 'amount', 'id' )
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
{ from: @from, to: @to, amount: @amount, id: @id }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
## check signature in the future; for now always true
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Transaction
|
29
|
+
|
30
|
+
Tx = Transaction ## add Tx shortcut / alias
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<div class="blockchain">
|
2
|
+
<h2>
|
3
|
+
Blockchain<br>
|
4
|
+
<span><%= @node.bank.chain.size %> blocks</span>
|
5
|
+
</h2>
|
6
|
+
<form action="/mine" method="post">
|
7
|
+
<input type="submit" class="button" value="Mine a Block">
|
8
|
+
</form>
|
9
|
+
|
10
|
+
<div class="blocks">
|
11
|
+
<% @node.bank.chain.last(10).reverse.each do |block| %>
|
12
|
+
<div class="block">
|
13
|
+
<div class="header">
|
14
|
+
<%= block.index %> — <%= block.timestamp %><br>
|
15
|
+
</div>
|
16
|
+
<table>
|
17
|
+
<% block.transactions.each do |tx| %>
|
18
|
+
<tr>
|
19
|
+
<td class="id">
|
20
|
+
<%= tx.id[0..2] %>
|
21
|
+
</td>
|
22
|
+
<td>
|
23
|
+
$<%= tx.amount %>
|
24
|
+
</td>
|
25
|
+
<td>
|
26
|
+
<%= tx.from[0..7] %> → <%= tx.to[0..7] %>
|
27
|
+
</td>
|
28
|
+
</tr>
|
29
|
+
<% end %>
|
30
|
+
</table>
|
31
|
+
</div>
|
32
|
+
<% end %>
|
33
|
+
</div>
|
34
|
+
</div>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="ledger">
|
2
|
+
<h2>Ledger</h2>
|
3
|
+
<table>
|
4
|
+
<tr>
|
5
|
+
<th>Address</th>
|
6
|
+
<th>Balance</th>
|
7
|
+
</tr>
|
8
|
+
<% @node.bank.ledger.wallets.each do |address, amount| %>
|
9
|
+
<tr>
|
10
|
+
<td><%= address[0..15] %></td>
|
11
|
+
<td>$<%= amount %></td>
|
12
|
+
</tr>
|
13
|
+
<% end %>
|
14
|
+
</table>
|
15
|
+
</div>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<div class="peers">
|
2
|
+
<h2>Peers</h2>
|
3
|
+
<% if @node.peers.any? %>
|
4
|
+
<ul>
|
5
|
+
<% @node.peers.each_with_index do |(host, port), i| %>
|
6
|
+
<li>
|
7
|
+
http://<%= host %>:<%= port %>
|
8
|
+
<form action="/peers/<%= i %>/delete" method="post">
|
9
|
+
<input type="submit" class="small" value="Remove" />
|
10
|
+
</form>
|
11
|
+
</li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
<% else %>
|
15
|
+
<i>No peers</i>
|
16
|
+
<% end %>
|
17
|
+
<form action="/peers" method="post" class="peers-form">
|
18
|
+
<label for="host">http://</label>
|
19
|
+
<input type="text" name="host" id="host" placeholder="host" />
|
20
|
+
<label for="port">Port</label>
|
21
|
+
<input type="text" name="port" id="port" />
|
22
|
+
<input type="submit" class="button" value="Add Peer" />
|
23
|
+
</form>
|
24
|
+
</div>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<div class="pending-transactions">
|
2
|
+
<h2>Pending Transactions</h2>
|
3
|
+
<% if @node.bank.pending.any? %>
|
4
|
+
<table>
|
5
|
+
<tr>
|
6
|
+
<th>From</th>
|
7
|
+
<th>To</th>
|
8
|
+
<th>$</th>
|
9
|
+
<th>Id</th>
|
10
|
+
</tr>
|
11
|
+
<% @node.bank.pending.each do |tx| %>
|
12
|
+
<tr>
|
13
|
+
<td><%= tx.from[0..15] %></td>
|
14
|
+
<td><%= tx.to[0..15] %></td>
|
15
|
+
<td><%= tx.amount %></td>
|
16
|
+
<td><%= tx.id[0..2] %></td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
19
|
+
</table>
|
20
|
+
<% else %>
|
21
|
+
<i>No pending transactions</i>
|
22
|
+
<% end %>
|
23
|
+
</div>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
<div class="wallet">
|
3
|
+
<div class="details">
|
4
|
+
<h2>Address</h2>
|
5
|
+
<div><%= @node.wallet.address %></div>
|
6
|
+
<h2>Balance</h2>
|
7
|
+
<div class="balance">$<%= @node.bank.ledger.wallets[@node.wallet.address] || 0 %></div>
|
8
|
+
</div>
|
9
|
+
<form action="/send" method="post" class="transaction-form">
|
10
|
+
<label for="to">To</label>
|
11
|
+
<input type="text" name="to" id="to" placeholder="address" />
|
12
|
+
<label for="amount">Amount</label>
|
13
|
+
<input type="text" name="amount" id="amount" />
|
14
|
+
<input type="submit" class="button" value="Send" /><br>
|
15
|
+
</form>
|
16
|
+
</div>
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Central Bank Node</title>
|
5
|
+
<link rel="stylesheet" href="style.css">
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
|
9
|
+
<h1>Central Bank Node</h1>
|
10
|
+
|
11
|
+
<div class="columns">
|
12
|
+
<div class="left">
|
13
|
+
<%= erb :'_wallet' %>
|
14
|
+
<%= erb :'_pending_transactions' %>
|
15
|
+
<%= erb :'_peers' %>
|
16
|
+
<%= erb :'_ledger' %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<div class="right">
|
20
|
+
<%= erb :'_blockchain' %>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<script>
|
25
|
+
var es = new EventSource('/events');
|
26
|
+
es.onmessage = function(e) { window.location.reload(false); };
|
27
|
+
</script>
|
28
|
+
|
29
|
+
</body>
|
30
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: centralbank
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gerald Bauer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-12-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rdoc
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hoe
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.16'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.16'
|
55
|
+
description: centralbank - print your own money / cryptocurrency; run your own federated
|
56
|
+
central bank nodes on the blockchain peer-to-peer over HTTP; revolutionize the world
|
57
|
+
one block at a time
|
58
|
+
email: ruby-talk@ruby-lang.org
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files:
|
62
|
+
- HISTORY.md
|
63
|
+
- Manifest.txt
|
64
|
+
- README.md
|
65
|
+
files:
|
66
|
+
- HISTORY.md
|
67
|
+
- Manifest.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- lib/centralbank.rb
|
71
|
+
- lib/centralbank/bank.rb
|
72
|
+
- lib/centralbank/block.rb
|
73
|
+
- lib/centralbank/blockchain.rb
|
74
|
+
- lib/centralbank/cache.rb
|
75
|
+
- lib/centralbank/ledger.rb
|
76
|
+
- lib/centralbank/node.rb
|
77
|
+
- lib/centralbank/public/style.css
|
78
|
+
- lib/centralbank/transaction.rb
|
79
|
+
- lib/centralbank/version.rb
|
80
|
+
- lib/centralbank/views/_blockchain.erb
|
81
|
+
- lib/centralbank/views/_ledger.erb
|
82
|
+
- lib/centralbank/views/_peers.erb
|
83
|
+
- lib/centralbank/views/_pending_transactions.erb
|
84
|
+
- lib/centralbank/views/_wallet.erb
|
85
|
+
- lib/centralbank/views/index.erb
|
86
|
+
- lib/centralbank/wallet.rb
|
87
|
+
homepage: https://github.com/openblockchains/centralbank
|
88
|
+
licenses:
|
89
|
+
- Public Domain
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options:
|
93
|
+
- "--main"
|
94
|
+
- README.md
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '2.3'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.5.2
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: centralbank - print your own money / cryptocurrency; run your own federated
|
113
|
+
central bank nodes on the blockchain peer-to-peer over HTTP; revolutionize the world
|
114
|
+
one block at a time
|
115
|
+
test_files: []
|