tulipmania 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 +66 -0
- data/Rakefile +32 -0
- data/lib/tulipmania/block.rb +40 -0
- data/lib/tulipmania/blockchain.rb +47 -0
- data/lib/tulipmania/cache.rb +22 -0
- data/lib/tulipmania/exchange.rb +113 -0
- data/lib/tulipmania/ledger.rb +30 -0
- data/lib/tulipmania/node.rb +88 -0
- data/lib/tulipmania/transaction.rb +30 -0
- data/lib/tulipmania/version.rb +11 -0
- data/lib/tulipmania/views/_blockchain.erb +34 -0
- data/lib/tulipmania/views/_ledger.erb +15 -0
- data/lib/tulipmania/views/_peers.erb +24 -0
- data/lib/tulipmania/views/_pending_transactions.erb +23 -0
- data/lib/tulipmania/views/_wallet.erb +16 -0
- data/lib/tulipmania/views/index.erb +30 -0
- data/lib/tulipmania/views/style.scss +172 -0
- data/lib/tulipmania/wallet.rb +15 -0
- data/lib/tulipmania.rb +129 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 43c6117496ffb6839f9d060bd44fedbf675f6236
|
4
|
+
data.tar.gz: 9128fd6314ecd688f6769946d9b7c05c906d6c9e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5338475a3b1366689431aba1be3789e3a783b89f87a01ba28cfb85a02da74d632c5cdb124a425e9be324fc744e41fd7312711ac4e27caea6aa39cdb47a32277
|
7
|
+
data.tar.gz: 8cc2013daab3ece6ce8b1303045ab0bbb6166240114bf51bc2a98795044ba8caa6e6906570703007b25dade78d4d3bcc4272831679ba93d1ea941ae3027d330e
|
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/tulipmania.rb
|
6
|
+
lib/tulipmania/block.rb
|
7
|
+
lib/tulipmania/blockchain.rb
|
8
|
+
lib/tulipmania/cache.rb
|
9
|
+
lib/tulipmania/exchange.rb
|
10
|
+
lib/tulipmania/ledger.rb
|
11
|
+
lib/tulipmania/node.rb
|
12
|
+
lib/tulipmania/transaction.rb
|
13
|
+
lib/tulipmania/version.rb
|
14
|
+
lib/tulipmania/views/_blockchain.erb
|
15
|
+
lib/tulipmania/views/_ledger.erb
|
16
|
+
lib/tulipmania/views/_peers.erb
|
17
|
+
lib/tulipmania/views/_pending_transactions.erb
|
18
|
+
lib/tulipmania/views/_wallet.erb
|
19
|
+
lib/tulipmania/views/index.erb
|
20
|
+
lib/tulipmania/views/style.scss
|
21
|
+
lib/tulipmania/wallet.rb
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# tulipmania (anno 1673) library / gem and command line tool
|
2
|
+
|
3
|
+
|
4
|
+
tulips on the blockchain; learn by example from the real world (anno 1637) - buy! sell! hodl! enjoy the beauty of admiral of admirals, semper augustus, and more;
|
5
|
+
run your own hyper ledger tulip exchange nodes on the blockchain peer-to-peer over HTTP; revolutionize the world one block at a time
|
6
|
+
|
7
|
+
|
8
|
+
* home :: [github.com/openblockchains/tulipmania](https://github.com/openblockchains/tulipmania)
|
9
|
+
* bugs :: [github.com/openblockchains/tulipmania/issues](https://github.com/openblockchains/tulipmania/issues)
|
10
|
+
* gem :: [rubygems.org/gems/tulipmania](https://rubygems.org/gems/tulipmania)
|
11
|
+
* rdoc :: [rubydoc.info/gems/tulipmania](http://rubydoc.info/gems/tulipmania)
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
## Development
|
17
|
+
|
18
|
+
For local development - clone or download (and unzip) the tulipmania code repo.
|
19
|
+
Next install all dependencies using bundler with a Gemfile e.g.:
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
# Gemfile
|
23
|
+
|
24
|
+
source "https://rubygems.org"
|
25
|
+
|
26
|
+
gem 'sinatra'
|
27
|
+
gem 'sass'
|
28
|
+
gem 'blockchain-lite'
|
29
|
+
```
|
30
|
+
|
31
|
+
run
|
32
|
+
|
33
|
+
```
|
34
|
+
$ bundle ## will use the Gemfile (see above)
|
35
|
+
```
|
36
|
+
|
37
|
+
and now you're ready to run your own hyper ledger tulip exchange server node.
|
38
|
+
Use the [`config.ru`](config.ru) script for rack:
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
# config.ru
|
42
|
+
|
43
|
+
$LOAD_PATH << './lib'
|
44
|
+
|
45
|
+
require 'tulipmania'
|
46
|
+
|
47
|
+
run Tulipmania::Service
|
48
|
+
```
|
49
|
+
|
50
|
+
and startup the money printing machine using rackup - the rack command line tool:
|
51
|
+
|
52
|
+
```
|
53
|
+
$ rackup ## will use the config.ru - rackup configuration script (see above).
|
54
|
+
```
|
55
|
+
|
56
|
+
In your browser open up the page e.g. `http://localhost:9292`. Voila!
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+

|
64
|
+
|
65
|
+
The `tulipmania` scripts are dedicated to the public domain.
|
66
|
+
Use it as you please with no restrictions whatsoever.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
require './lib/tulipmania/version.rb'
|
3
|
+
|
4
|
+
Hoe.spec 'tulipmania' do
|
5
|
+
|
6
|
+
self.version = Tulipmania::VERSION
|
7
|
+
|
8
|
+
self.summary = 'tulipmania - tulips on the blockchain; learn by example from the real world (anno 1637) - buy! sell! hodl! enjoy the beauty of admiral of admirals, semper augustus, and more; run your own hyper ledger tulip exchange nodes on the blockchain peer-to-peer over HTTPprint 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/tulipmania']
|
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
|
+
['sass'], ## used for css style preprocessing (scss)
|
23
|
+
['blockchain-lite', '>=1.2'],
|
24
|
+
]
|
25
|
+
|
26
|
+
self.licenses = ['Public Domain']
|
27
|
+
|
28
|
+
self.spec_extras = {
|
29
|
+
required_ruby_version: '>= 2.3'
|
30
|
+
}
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
+
######
|
9
|
+
# add more methods
|
10
|
+
|
11
|
+
class Block
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{ index: @index,
|
15
|
+
timestamp: @timestamp,
|
16
|
+
nonce: @nonce,
|
17
|
+
transactions: @transactions.map { |tx| tx.to_h },
|
18
|
+
previous_hash: @previous_hash }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_h( h )
|
22
|
+
transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
|
23
|
+
|
24
|
+
## parse iso8601 format e.g 2017-10-05T22:26:12-04:00
|
25
|
+
timestamp = Time.parse( h['timestamp'] )
|
26
|
+
|
27
|
+
self.new( h['index'],
|
28
|
+
transactions,
|
29
|
+
h['previous_hash'],
|
30
|
+
timestamp: timestamp,
|
31
|
+
nonce: h['nonce'].to_i )
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
true ## for now always valid
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
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,113 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
class Exchange
|
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 Exchange
|
@@ -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,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>
|
@@ -0,0 +1,172 @@
|
|
1
|
+
|
2
|
+
body {
|
3
|
+
padding: 0;
|
4
|
+
margin: 0;
|
5
|
+
min-width: 960px;
|
6
|
+
|
7
|
+
font-family: 'menlo', monospace;
|
8
|
+
font-size: 14px;
|
9
|
+
|
10
|
+
background: #fff;
|
11
|
+
color: #2B2D2F;
|
12
|
+
}
|
13
|
+
|
14
|
+
|
15
|
+
.columns {
|
16
|
+
display: flex;
|
17
|
+
|
18
|
+
.left {
|
19
|
+
width: 66%;
|
20
|
+
}
|
21
|
+
|
22
|
+
.right {
|
23
|
+
width: 34%;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
h1 {
|
29
|
+
font-size: 24px;
|
30
|
+
font-weight: normal;
|
31
|
+
padding-left: 15px;
|
32
|
+
margin-bottom: 20px;
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
h2 {
|
37
|
+
font-size: 16px;
|
38
|
+
}
|
39
|
+
|
40
|
+
h2 span {
|
41
|
+
font-size: 14px;
|
42
|
+
color: #597898;
|
43
|
+
font-weight: normal;
|
44
|
+
}
|
45
|
+
|
46
|
+
label {
|
47
|
+
display: inline-block;
|
48
|
+
width: 80px;
|
49
|
+
text-align: right;
|
50
|
+
padding-right: 10px;
|
51
|
+
}
|
52
|
+
|
53
|
+
input[type=text] {
|
54
|
+
display: inline-block;
|
55
|
+
font-size: 14px;
|
56
|
+
padding: 8px;
|
57
|
+
border-radius: 0;
|
58
|
+
border: 0;
|
59
|
+
}
|
60
|
+
|
61
|
+
table {
|
62
|
+
border-spacing: 0;
|
63
|
+
border-collapse: collapse;
|
64
|
+
|
65
|
+
th {
|
66
|
+
text-align: left;
|
67
|
+
}
|
68
|
+
|
69
|
+
td {
|
70
|
+
vertical-align: top;
|
71
|
+
padding: 5px 15px 5px 0;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
|
76
|
+
ul {
|
77
|
+
list-style: none;
|
78
|
+
padding: 0;
|
79
|
+
margin: 0;
|
80
|
+
}
|
81
|
+
|
82
|
+
input[type=submit] {
|
83
|
+
font-size: 14px;
|
84
|
+
font-family: menlo, monospace;
|
85
|
+
border-radius: 5px;
|
86
|
+
padding: 8px 20px;
|
87
|
+
background: #FFDC00;
|
88
|
+
color: #2B2D2F;
|
89
|
+
border: 0;
|
90
|
+
}
|
91
|
+
|
92
|
+
input[type=submit].small {
|
93
|
+
font-size: 10px;
|
94
|
+
padding: 4px 10px;
|
95
|
+
}
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
.wallet {
|
100
|
+
padding: 15px;
|
101
|
+
background: #7FDBFF;
|
102
|
+
|
103
|
+
h2 {
|
104
|
+
margin-bottom: 0;
|
105
|
+
}
|
106
|
+
|
107
|
+
.balance {
|
108
|
+
font-size: 30px;
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
|
113
|
+
.pending-transactions {
|
114
|
+
padding: 15px;
|
115
|
+
background: #A3E6FF;
|
116
|
+
}
|
117
|
+
|
118
|
+
.peers {
|
119
|
+
padding: 15px;
|
120
|
+
background: #C6EFFF;
|
121
|
+
|
122
|
+
li form {
|
123
|
+
display: inline;
|
124
|
+
}
|
125
|
+
|
126
|
+
li {
|
127
|
+
padding: 5px 0px;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
|
132
|
+
.ledger {
|
133
|
+
padding: 15px;
|
134
|
+
background: #E3F7FF;
|
135
|
+
}
|
136
|
+
|
137
|
+
|
138
|
+
.blockchain {
|
139
|
+
padding: 15px;
|
140
|
+
position: relative;
|
141
|
+
background: #001F3F;
|
142
|
+
color: #fff;
|
143
|
+
|
144
|
+
form {
|
145
|
+
position: absolute;
|
146
|
+
top: 30px;
|
147
|
+
right: 15px;
|
148
|
+
}
|
149
|
+
|
150
|
+
.blocks {
|
151
|
+
border: 1px solid #597898;
|
152
|
+
border-bottom: 0;
|
153
|
+
|
154
|
+
.block {
|
155
|
+
margin: 0;
|
156
|
+
border-bottom: 2px dashed #597898;
|
157
|
+
padding: 10px;
|
158
|
+
|
159
|
+
.header {
|
160
|
+
text-align: center;
|
161
|
+
padding: 0 8px 8px 8px;
|
162
|
+
color: #597898;
|
163
|
+
border-bottom: 1px solid #354c63;
|
164
|
+
margin-bottom: 10px;
|
165
|
+
}
|
166
|
+
|
167
|
+
.id {
|
168
|
+
color: #597898;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|
data/lib/tulipmania.rb
ADDED
@@ -0,0 +1,129 @@
|
|
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
|
+
|
11
|
+
### 3rd party gems
|
12
|
+
require 'sinatra/base' # note: use "modular" sinatra app / service
|
13
|
+
require 'blockchain-lite/proof_of_work/block' # note: use proof-of-work block only (for now)
|
14
|
+
|
15
|
+
|
16
|
+
### our own code
|
17
|
+
require 'tulipmania/version' ## let version always go first
|
18
|
+
require 'tulipmania/block'
|
19
|
+
require 'tulipmania/cache'
|
20
|
+
require 'tulipmania/transaction'
|
21
|
+
require 'tulipmania/blockchain'
|
22
|
+
require 'tulipmania/exchange'
|
23
|
+
require 'tulipmania/ledger'
|
24
|
+
require 'tulipmania/wallet'
|
25
|
+
|
26
|
+
require 'tulipmania/node'
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
module Tulipmania
|
32
|
+
|
33
|
+
class Service < Sinatra::Base
|
34
|
+
|
35
|
+
def self.banner
|
36
|
+
"tulipmania/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
## - for now hard-code address e.g. Sepp
|
41
|
+
NODE = Node.new( address: 'Sepp' )
|
42
|
+
|
43
|
+
|
44
|
+
PUBLIC_FOLDER = "#{Tulipmania.root}/lib/tulipmania/public"
|
45
|
+
VIEWS_FOLDER = "#{Tulipmania.root}/lib/tulipmania/views"
|
46
|
+
|
47
|
+
puts "[tulipmania] boot - setting public folder to: #{PUBLIC_FOLDER}"
|
48
|
+
puts "[tulipmania] boot - setting views folder to: #{VIEWS_FOLDER}"
|
49
|
+
|
50
|
+
set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
|
51
|
+
set :views, VIEWS_FOLDER # set up the views dir
|
52
|
+
|
53
|
+
set :static, true # set up static file routing -- check - still needed?
|
54
|
+
|
55
|
+
|
56
|
+
set connections: []
|
57
|
+
|
58
|
+
|
59
|
+
get '/style.css' do
|
60
|
+
scss :style ## note: converts (pre-processes) style.scss to style.css
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
get '/' do
|
65
|
+
@node = NODE
|
66
|
+
erb :index
|
67
|
+
end
|
68
|
+
|
69
|
+
post '/send' do
|
70
|
+
NODE.on_send( params[:to], params[:amount].to_i )
|
71
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
72
|
+
redirect '/'
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
post '/transactions' do
|
77
|
+
if NODE.on_add_transaction(
|
78
|
+
params[:from],
|
79
|
+
params[:to],
|
80
|
+
params[:amount].to_i,
|
81
|
+
params[:id]
|
82
|
+
)
|
83
|
+
settings.connections.each { |out| out << "data: added transaction\n\n" }
|
84
|
+
end
|
85
|
+
redirect '/'
|
86
|
+
end
|
87
|
+
|
88
|
+
post '/mine' do
|
89
|
+
NODE.on_mine!
|
90
|
+
redirect '/'
|
91
|
+
end
|
92
|
+
|
93
|
+
post '/peers' do
|
94
|
+
NODE.on_add_peer( params[:host], params[:port].to_i )
|
95
|
+
redirect '/'
|
96
|
+
end
|
97
|
+
|
98
|
+
post '/peers/:index/delete' do
|
99
|
+
NODE.on_delete_peer( params[:index].to_i )
|
100
|
+
redirect '/'
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
post '/resolve' do
|
106
|
+
data = JSON.parse(request.body.read)
|
107
|
+
if data['chain'] && NODE.on_resolve( data['chain'] )
|
108
|
+
status 202 ### 202 Accepted; see httpstatuses.com/202
|
109
|
+
settings.connections.each { |out| out << "data: resolved\n\n" }
|
110
|
+
else
|
111
|
+
status 200 ### 200 OK
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
get '/events', provides: 'text/event-stream' do
|
117
|
+
stream :keep_open do |out|
|
118
|
+
settings.connections << out
|
119
|
+
out.callback { settings.connections.delete(out) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
end # class Service
|
125
|
+
end # module Tulipmania
|
126
|
+
|
127
|
+
|
128
|
+
# say hello
|
129
|
+
puts Tulipmania::Service.banner
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tulipmania
|
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-18 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: sass
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: blockchain-lite
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rdoc
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: hoe
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.16'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.16'
|
83
|
+
description: tulipmania - tulips on the blockchain; learn by example from the real
|
84
|
+
world (anno 1637) - buy! sell! hodl! enjoy the beauty of admiral of admirals, semper
|
85
|
+
augustus, and more; run your own hyper ledger tulip exchange nodes on the blockchain
|
86
|
+
peer-to-peer over HTTPprint your own money / cryptocurrency; run your own federated
|
87
|
+
central bank nodes on the blockchain peer-to-peer over HTTP; revolutionize the world
|
88
|
+
one block at a time
|
89
|
+
email: ruby-talk@ruby-lang.org
|
90
|
+
executables: []
|
91
|
+
extensions: []
|
92
|
+
extra_rdoc_files:
|
93
|
+
- HISTORY.md
|
94
|
+
- Manifest.txt
|
95
|
+
- README.md
|
96
|
+
files:
|
97
|
+
- HISTORY.md
|
98
|
+
- Manifest.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- lib/tulipmania.rb
|
102
|
+
- lib/tulipmania/block.rb
|
103
|
+
- lib/tulipmania/blockchain.rb
|
104
|
+
- lib/tulipmania/cache.rb
|
105
|
+
- lib/tulipmania/exchange.rb
|
106
|
+
- lib/tulipmania/ledger.rb
|
107
|
+
- lib/tulipmania/node.rb
|
108
|
+
- lib/tulipmania/transaction.rb
|
109
|
+
- lib/tulipmania/version.rb
|
110
|
+
- lib/tulipmania/views/_blockchain.erb
|
111
|
+
- lib/tulipmania/views/_ledger.erb
|
112
|
+
- lib/tulipmania/views/_peers.erb
|
113
|
+
- lib/tulipmania/views/_pending_transactions.erb
|
114
|
+
- lib/tulipmania/views/_wallet.erb
|
115
|
+
- lib/tulipmania/views/index.erb
|
116
|
+
- lib/tulipmania/views/style.scss
|
117
|
+
- lib/tulipmania/wallet.rb
|
118
|
+
homepage: https://github.com/openblockchains/tulipmania
|
119
|
+
licenses:
|
120
|
+
- Public Domain
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options:
|
124
|
+
- "--main"
|
125
|
+
- README.md
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '2.3'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.5.2
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: tulipmania - tulips on the blockchain; learn by example from the real world
|
144
|
+
(anno 1637) - buy! sell! hodl! enjoy the beauty of admiral of admirals, semper augustus,
|
145
|
+
and more; run your own hyper ledger tulip exchange nodes on the blockchain peer-to-peer
|
146
|
+
over HTTPprint your own money / cryptocurrency; run your own federated central bank
|
147
|
+
nodes on the blockchain peer-to-peer over HTTP; revolutionize the world one block
|
148
|
+
at a time
|
149
|
+
test_files: []
|