shilling 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/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
|