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.
@@ -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