shilling 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/HISTORY.md +3 -0
- data/LICENSE.md +116 -0
- data/Manifest.txt +26 -0
- data/README.md +115 -0
- data/Rakefile +32 -0
- data/bin/shilling +17 -0
- data/lib/shilling.rb +103 -0
- data/lib/shilling/bank.rb +109 -0
- data/lib/shilling/block.rb +44 -0
- data/lib/shilling/blockchain.rb +47 -0
- data/lib/shilling/cache.rb +22 -0
- data/lib/shilling/ledger.rb +30 -0
- data/lib/shilling/node.rb +82 -0
- data/lib/shilling/pool.rb +42 -0
- data/lib/shilling/service.rb +113 -0
- data/lib/shilling/tool.rb +66 -0
- data/lib/shilling/transaction.rb +30 -0
- data/lib/shilling/version.rb +11 -0
- data/lib/shilling/views/_blockchain.erb +37 -0
- data/lib/shilling/views/_ledger.erb +15 -0
- data/lib/shilling/views/_peers.erb +24 -0
- data/lib/shilling/views/_pending_transactions.erb +23 -0
- data/lib/shilling/views/_wallet.erb +16 -0
- data/lib/shilling/views/index.erb +30 -0
- data/lib/shilling/views/style.scss +172 -0
- data/lib/shilling/wallet.rb +15 -0
- metadata +152 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
Block = BlockchainLite::ProofOfWork::Block
|
4
|
+
|
5
|
+
## see https://github.com/openblockchains/blockchain.lite.rb/blob/master/lib/blockchain-lite/proof_of_work/block.rb
|
6
|
+
|
7
|
+
######
|
8
|
+
## add more methods
|
9
|
+
|
10
|
+
class Block
|
11
|
+
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{ index: @index,
|
15
|
+
timestamp: @timestamp,
|
16
|
+
nonce: @nonce,
|
17
|
+
transactions: @transactions.map { |tx| tx.to_h },
|
18
|
+
transactions_hash: @transactions_hash,
|
19
|
+
previous_hash: @previous_hash,
|
20
|
+
hash: @hash }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_h( h )
|
24
|
+
transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
|
25
|
+
|
26
|
+
## todo: use hash and transactions_hash to check integrity of block - why? why not?
|
27
|
+
|
28
|
+
## parse iso8601 format e.g 2017-10-05T22:26:12-04:00
|
29
|
+
timestamp = Time.parse( h['timestamp'] )
|
30
|
+
|
31
|
+
self.new( h['index'],
|
32
|
+
transactions,
|
33
|
+
h['previous_hash'],
|
34
|
+
timestamp: timestamp,
|
35
|
+
nonce: h['nonce'].to_i )
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def valid?
|
40
|
+
true ## for now always valid
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end # class Block
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
class Blockchain
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@chain, :[], :size, :each, :empty?, :any?, :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:utf-8' ) do |f|
|
10
|
+
f.write JSON.pretty_generate( data )
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def read
|
15
|
+
if File.exists?( @name )
|
16
|
+
data = File.open( @name, 'r:bom|utf-8' ).read
|
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 Shilling.config.coinbase?( wallet )
|
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 Shilling.config.coinbase?( tx.from )
|
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,82 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Node
|
4
|
+
attr_reader :id, :peers, :wallet, :bank
|
5
|
+
|
6
|
+
def initialize( address: )
|
7
|
+
@id = SecureRandom.uuid
|
8
|
+
@peers = []
|
9
|
+
@wallet = Wallet.new( address )
|
10
|
+
@bank = Bank.new @wallet.address
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
def on_add_peer( host, port )
|
16
|
+
@peers << [host, port]
|
17
|
+
@peers.uniq!
|
18
|
+
# TODO/FIX: no need to send to every peer, just the new one
|
19
|
+
send_chain_to_peers
|
20
|
+
@bank.pending.each { |tx| send_transaction_to_peers( tx ) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_delete_peer( index )
|
24
|
+
@peers.delete_at( index )
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def on_add_transaction( from, to, amount, id )
|
29
|
+
## note: for now must always pass in id - why? why not? possible tx without id???
|
30
|
+
tx = Tx.new( from, to, amount, id )
|
31
|
+
if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
|
32
|
+
send_transaction_to_peers( tx )
|
33
|
+
return true
|
34
|
+
else
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_send( to, amount )
|
40
|
+
tx = @wallet.generate_transaction( to, amount )
|
41
|
+
if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
|
42
|
+
send_transaction_to_peers( tx )
|
43
|
+
return true
|
44
|
+
else
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def on_mine!
|
51
|
+
@bank.mine_block!
|
52
|
+
send_chain_to_peers
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_resolve( data )
|
56
|
+
chain_new = Blockchain.from_json( data )
|
57
|
+
if @bank.resolve!( chain_new )
|
58
|
+
send_chain_to_peers
|
59
|
+
return true
|
60
|
+
else
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def send_chain_to_peers
|
70
|
+
data = JSON.pretty_generate( @bank.as_json ) ## payload in json
|
71
|
+
@peers.each do |(host, port)|
|
72
|
+
Net::HTTP.post(URI::HTTP.build(host: host, port: port, path: '/resolve'), data )
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_transaction_to_peers( tx )
|
77
|
+
@peers.each do |(host, port)|
|
78
|
+
Net::HTTP.post_form(URI::HTTP.build(host: host, port: port, path: '/transactions'), tx.to_h )
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end ## class Node
|
@@ -0,0 +1,42 @@
|
|
1
|
+
####################################
|
2
|
+
# pending (unconfirmed) transactions (mem) pool
|
3
|
+
|
4
|
+
class Pool
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@transactions, :[], :size, :each, :empty?, :any?
|
7
|
+
|
8
|
+
|
9
|
+
def initialize( transactions=[] )
|
10
|
+
@transactions = transactions
|
11
|
+
end
|
12
|
+
|
13
|
+
def transactions() @transactions; end
|
14
|
+
|
15
|
+
def <<( tx )
|
16
|
+
@transactions << tx
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def update!( txns_confirmed )
|
21
|
+
## find a better name?
|
22
|
+
## remove confirmed transactions from pool
|
23
|
+
|
24
|
+
## document - keep only pending transaction not yet (confirmed) in blockchain ????
|
25
|
+
@transactions = @transactions.select do |tx_unconfirmed|
|
26
|
+
txns_confirmed.none? { |tx_confirmed| tx_confirmed.id == tx_unconfirmed.id }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
def as_json
|
33
|
+
@transactions.map { |tx| tx.to_h }
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.from_json( data )
|
37
|
+
## note: assumes data is an array of block records/objects in json
|
38
|
+
transactions = data.map { |h| Tx.from_h( h ) }
|
39
|
+
self.new( transactions )
|
40
|
+
end
|
41
|
+
|
42
|
+
end # class Pool
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Shilling
|
4
|
+
|
5
|
+
class Service < Sinatra::Base
|
6
|
+
|
7
|
+
def self.banner
|
8
|
+
"shilling/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
PUBLIC_FOLDER = "#{Shilling.root}/lib/shilling/public"
|
13
|
+
VIEWS_FOLDER = "#{Shilling.root}/lib/shilling/views"
|
14
|
+
|
15
|
+
set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
|
16
|
+
set :views, VIEWS_FOLDER # set up the views dir
|
17
|
+
|
18
|
+
set :static, true # set up static file routing -- check - still needed?
|
19
|
+
|
20
|
+
|
21
|
+
set connections: []
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
get '/style.css' do
|
26
|
+
scss :style ## note: converts (pre-processes) style.scss to style.css
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
get '/' do
|
31
|
+
@node = node ## todo: pass along node as hash varialbe / assigns to erb
|
32
|
+
erb :index
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
post '/send' do
|
37
|
+
node.on_send( params[:to], params[:amount].to_i )
|
38
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
39
|
+
redirect '/'
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
post '/transactions' do
|
44
|
+
if node.on_add_transaction(
|
45
|
+
params[:from],
|
46
|
+
params[:to],
|
47
|
+
params[:amount].to_i,
|
48
|
+
params[:id]
|
49
|
+
)
|
50
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
51
|
+
end
|
52
|
+
redirect '/'
|
53
|
+
end
|
54
|
+
|
55
|
+
post '/mine' do
|
56
|
+
node.on_mine!
|
57
|
+
redirect '/'
|
58
|
+
end
|
59
|
+
|
60
|
+
post '/peers' do
|
61
|
+
node.on_add_peer( params[:host], params[:port].to_i )
|
62
|
+
redirect '/'
|
63
|
+
end
|
64
|
+
|
65
|
+
post '/peers/:index/delete' do
|
66
|
+
node.on_delete_peer( params[:index].to_i )
|
67
|
+
redirect '/'
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
post '/resolve' do
|
73
|
+
data = JSON.parse(request.body.read)
|
74
|
+
if data['chain'] && node.on_resolve( data['chain'] )
|
75
|
+
status 202 ### 202 Accepted; see httpstatuses.com/202
|
76
|
+
settings.connections.each { |out| out << "data: resolved\n\n" }
|
77
|
+
else
|
78
|
+
status 200 ### 200 OK
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
get '/events', provides: 'text/event-stream' do
|
84
|
+
stream :keep_open do |out|
|
85
|
+
settings.connections << out
|
86
|
+
out.callback { settings.connections.delete(out) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
#########
|
93
|
+
## return network node (built and configured on first use)
|
94
|
+
## fix: do NOT use @@ - use a class level method or something
|
95
|
+
def node
|
96
|
+
if defined?( @@node )
|
97
|
+
@@node
|
98
|
+
else
|
99
|
+
puts "[debug] shilling - build (network) node (address: #{Shilling.config.address})"
|
100
|
+
@@node = Node.new( address: Shilling.config.address )
|
101
|
+
@@node
|
102
|
+
end
|
103
|
+
####
|
104
|
+
## check why this is a syntax error:
|
105
|
+
## @node ||= do
|
106
|
+
## puts "[debug] shilling - build (network) node (address: #{Shilling.config.address})"
|
107
|
+
## @node = Node.new( address: Shilling.config.address )
|
108
|
+
## end
|
109
|
+
end
|
110
|
+
|
111
|
+
end # class Service
|
112
|
+
|
113
|
+
end # module Shilling
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
module Shilling
|
5
|
+
|
6
|
+
class Tool
|
7
|
+
|
8
|
+
def run( args )
|
9
|
+
opts = {}
|
10
|
+
|
11
|
+
parser = OptionParser.new do |cmd|
|
12
|
+
cmd.banner = "Usage: shilling [options]"
|
13
|
+
|
14
|
+
cmd.separator ""
|
15
|
+
cmd.separator " Wallet options:"
|
16
|
+
|
17
|
+
cmd.on("-n", "--name=NAME", "Address name (default: Theresa)") do |name|
|
18
|
+
## use -a or --adr or --address as option flag - why? why not?
|
19
|
+
## note: default now picks a random address from WALLET_ADDRESSES
|
20
|
+
opts[:address] = name
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
cmd.separator ""
|
25
|
+
cmd.separator " Server (node) options:"
|
26
|
+
|
27
|
+
cmd.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |host|
|
28
|
+
opts[:Host] = host ## note: rack server handler expects :Host
|
29
|
+
end
|
30
|
+
|
31
|
+
cmd.on("-p", "--port PORT", "use PORT (default: 4567)") do |port|
|
32
|
+
opts[:Port] = port ## note: rack server handler expects :Post
|
33
|
+
end
|
34
|
+
|
35
|
+
cmd.on("-h", "--help", "Prints this help") do
|
36
|
+
puts cmd
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
parser.parse!( args )
|
42
|
+
pp opts
|
43
|
+
|
44
|
+
|
45
|
+
###################
|
46
|
+
## startup server (via rack interface/handler)
|
47
|
+
|
48
|
+
app_class = Service ## use app = Service.new -- why? why not?
|
49
|
+
host = opts[:Host] || '0.0.0.0'
|
50
|
+
port = opts[:Port] || '4567'
|
51
|
+
|
52
|
+
Shilling.configure do |config|
|
53
|
+
config.address = opts[:address] || 'Theresa'
|
54
|
+
end
|
55
|
+
|
56
|
+
Rack::Handler::WEBrick.run( app_class, Host: host, Port: port ) do |server|
|
57
|
+
## todo: add traps here - why, why not??
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end ## method run
|
62
|
+
|
63
|
+
|
64
|
+
end ## class Tool
|
65
|
+
|
66
|
+
end ## module Shilling
|