diff --git a/README.md b/README.md
index 85ab8a8..4633528 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,10 @@ Gems:
- [p2p](p2p) - build your own peer-to-peer (p2p) networks; run your own peer-to-peer (p2p) nodes over HTTP
+
+
+- [centralbank](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
+- [tulipmania](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 HTTP; revolutionize the world one block at a time
diff --git a/centralbank/.gitignore b/centralbank/.gitignore
new file mode 100644
index 0000000..91ce1dc
--- /dev/null
+++ b/centralbank/.gitignore
@@ -0,0 +1,70 @@
+###########
+# cached / saved state / data
+
+data.json
+data.*.json
+
+
+
+##########
+# ignore Gemfile.lock for now
+
+Gemfile.lock
+
+
+####
+# ignore sass cache
+.sass-cache
+
+
+
+*.gem
+*.rbc
+/.config
+/coverage/
+/InstalledFiles
+/pkg/
+/spec/reports/
+/spec/examples.txt
+/test/tmp/
+/test/version_tmp/
+/tmp/
+
+# Used by dotenv library to load environment variables.
+# .env
+
+## Specific to RubyMotion:
+.dat*
+.repl_history
+build/
+*.bridgesupport
+build-iPhoneOS/
+build-iPhoneSimulator/
+
+## Specific to RubyMotion (use of CocoaPods):
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# vendor/Pods/
+
+## Documentation cache and generated files:
+/.yardoc/
+/_yardoc/
+/doc/
+/rdoc/
+
+## Environment normalization:
+/.bundle/
+/vendor/bundle
+/lib/bundler/man/
+
+# for a library or gem, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# Gemfile.lock
+# .ruby-version
+# .ruby-gemset
+
+# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
+.rvmrc
diff --git a/centralbank/Dockerfile b/centralbank/Dockerfile
new file mode 100644
index 0000000..cce61de
--- /dev/null
+++ b/centralbank/Dockerfile
@@ -0,0 +1,9 @@
+FROM ruby
+
+RUN mkdir /centralbank
+WORKDIR /centralbank
+COPY . /centralbank/
+RUN bundle install --quiet
+EXPOSE 9292
+CMD [ "rackup", "--host", "0.0.0.0", "-p", "9292"]
+
diff --git a/centralbank/Gemfile b/centralbank/Gemfile
new file mode 100644
index 0000000..ab78f40
--- /dev/null
+++ b/centralbank/Gemfile
@@ -0,0 +1,7 @@
+# Gemfile
+
+source "https://rubygems.org"
+
+gem 'sinatra'
+gem 'sass'
+gem 'blockchain-lite'
diff --git a/centralbank/HISTORY.md b/centralbank/HISTORY.md
new file mode 100644
index 0000000..75146df
--- /dev/null
+++ b/centralbank/HISTORY.md
@@ -0,0 +1,3 @@
+### 0.1.0 / 2017-12-14
+
+* Everything is new. First release.
diff --git a/centralbank/Manifest.txt b/centralbank/Manifest.txt
new file mode 100644
index 0000000..6d8dbfe
--- /dev/null
+++ b/centralbank/Manifest.txt
@@ -0,0 +1,26 @@
+HISTORY.md
+LICENSE.md
+Manifest.txt
+README.md
+Rakefile
+bin/centralbank
+lib/centralbank.rb
+lib/centralbank/bank.rb
+lib/centralbank/block.rb
+lib/centralbank/blockchain.rb
+lib/centralbank/cache.rb
+lib/centralbank/ledger.rb
+lib/centralbank/node.rb
+lib/centralbank/pool.rb
+lib/centralbank/service.rb
+lib/centralbank/tool.rb
+lib/centralbank/transaction.rb
+lib/centralbank/version.rb
+lib/centralbank/views/_blockchain.erb
+lib/centralbank/views/_ledger.erb
+lib/centralbank/views/_peers.erb
+lib/centralbank/views/_pending_transactions.erb
+lib/centralbank/views/_wallet.erb
+lib/centralbank/views/index.erb
+lib/centralbank/views/style.scss
+lib/centralbank/wallet.rb
diff --git a/centralbank/NOTES.md b/centralbank/NOTES.md
new file mode 100644
index 0000000..8b1527e
--- /dev/null
+++ b/centralbank/NOTES.md
@@ -0,0 +1,35 @@
+# Notes
+
+
+## Todos
+
+- [ ] add favicon.png - why? why not? (see webservice gem)
+- [ ] add a Pool class (for pending transaction pool) !!!!!
+- [ ] add secure versions with signature e.g. SecureWallet, SecureTransaction, etc.
+
+```
+Source: https://www.reddit.com/r/ruby/comments/7ly337/day_24_ruby_advent_calendar_2017_centralbank/
+
+I'm playing with your Ruby blockchain stuff. It's very good. It's giving me a way to learn it without having to learn Go or C++ at the same time. But...
+
+When I try running central bank from the command line as described in blockchains section 6. using
+
+centralbank
+it doesn't work because centralbank is in the bin/ directory, so I tried (after setting chmod +x etc)
+
+bin/centralbank
+and then got an error I've never seen before
+
+env: ruby\r: No such file or directory
+So I wrote added one space at the end of the shebang header line That removed the extra \r and I then progressed to
+
+bin/centralbank
+Traceback (most recent call last):
+ (LoadError)h file or directory --
+Because it doesn't know where the library path is. If I run with
+
+ ruby -Ilib bin/centralbank
+Then it's fine.
+
+So you need to do something to remove that \r at the end of the shebang line, and add the library path before the require.
+```
diff --git a/centralbank/README.md b/centralbank/README.md
new file mode 100644
index 0000000..db8a258
--- /dev/null
+++ b/centralbank/README.md
@@ -0,0 +1,118 @@
+# centralbank library / gem and command line tool
+
+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
+
+
+* home :: [github.com/openblockchains/centralbank](https://github.com/openblockchains/centralbank)
+* bugs :: [github.com/openblockchains/centralbank/issues](https://github.com/openblockchains/centralbank/issues)
+* gem :: [rubygems.org/gems/centralbank](https://rubygems.org/gems/centralbank)
+* rdoc :: [rubydoc.info/gems/centralbank](http://rubydoc.info/gems/centralbank)
+
+
+## Command Line
+
+Use the `centralbank` command line tool. Try:
+
+```
+$ centralbank -h
+```
+
+resulting in:
+
+```
+Usage: centralbank [options]
+
+ Wallet options:
+ -n, --name=NAME Address name (default: Alice)
+
+ Server (node) options:
+ -o, --host HOST listen on HOST (default: 0.0.0.0)
+ -p, --port PORT use PORT (default: 4567)
+ -h, --help Prints this help
+```
+
+To start a new (network) node using the default wallet
+address (that is, Alice) and the default server host and port settings
+use:
+
+```
+$ centralbank
+```
+
+Stand back ten feets :-) while starting up the machinery.
+Ready to print (mine) money on the blockchain?
+In your browser open up the page e.g. `http://localhost:4567`. Voila!
+
+
+
+
+
+Note: You can start a second node on your computer -
+make sure to use a different port (use the `-p/--port` option)
+and (recommended)
+a different wallet address (use the `-n/--name` option).
+Example:
+
+```
+$ centralbank -p 5678 -n Bob
+```
+
+Happy mining!
+
+
+
+## Local Development Setup
+
+For local development - clone or download (and unzip) the centralbank code repo.
+Next install all dependencies using bundler with a Gemfile e.g.:
+
+``` ruby
+# Gemfile
+
+source "https://rubygems.org"
+
+gem 'sinatra'
+gem 'sass'
+gem 'blockchain-lite'
+```
+
+run
+
+```
+$ bundle ## will use the Gemfile (see above)
+```
+
+and now you're ready to run your own centralbank server node. Use the [`config.ru`](config.ru) script for rack:
+
+``` ruby
+# config.ru
+
+$LOAD_PATH << './lib'
+
+require 'centralbank'
+
+run Centralbank::Service
+```
+
+and startup the money printing machine using rackup - the rack command line tool:
+
+```
+$ rackup ## will use the config.ru - rackup configuration script (see above).
+```
+
+In your browser open up the page e.g. `http://localhost:9292`. Voila! Happy mining!
+
+
+
+
+## References
+
+[**Programming Cryptocurrencies and Blockchains (in Ruby)**](http://yukimotopress.github.io/blockchains) by Gerald Bauer et al, 2018, Yuki & Moto Press
+
+
+## License
+
+
+
+The `centralbank` scripts are dedicated to the public domain.
+Use it as you please with no restrictions whatsoever.
diff --git a/centralbank/Rakefile b/centralbank/Rakefile
new file mode 100644
index 0000000..f42fab8
--- /dev/null
+++ b/centralbank/Rakefile
@@ -0,0 +1,32 @@
+require 'hoe'
+require './lib/centralbank/version.rb'
+
+Hoe.spec 'centralbank' do
+
+ self.version = Centralbank::VERSION
+
+ 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'
+ self.description = summary
+
+ self.urls = ['https://github.com/openblockchains/centralbank']
+
+ self.author = 'Gerald Bauer'
+ self.email = 'ruby-talk@ruby-lang.org'
+
+ # switch extension to .markdown for gihub formatting
+ self.readme_file = 'README.md'
+ self.history_file = 'History.md'
+
+ self.extra_deps = [
+ ['sinatra', '>=2.0'],
+ ['sass'], ## used for css style preprocessing (scss)
+ ['blockchain-lite', '>=1.3.1'],
+ ]
+
+ self.licenses = ['Public Domain']
+
+ self.spec_extras = {
+ required_ruby_version: '>= 2.3'
+ }
+
+end
diff --git a/centralbank/bin/centralbank b/centralbank/bin/centralbank
new file mode 100644
index 0000000..5eae7d5
--- /dev/null
+++ b/centralbank/bin/centralbank
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+###################
+# == DEV TIPS:
+#
+# For local testing run like:
+#
+# ruby -Ilib bin/centralbank
+#
+# Set the executable bit in Linux. Example:
+#
+# % chmod a+x bin/centralbank
+#
+
+require 'centralbank'
+
+Centralbank.main
diff --git a/centralbank/centralbank.png b/centralbank/centralbank.png
new file mode 100644
index 0000000..f8eb0ae
Binary files /dev/null and b/centralbank/centralbank.png differ
diff --git a/centralbank/config.ru b/centralbank/config.ru
new file mode 100644
index 0000000..623694a
--- /dev/null
+++ b/centralbank/config.ru
@@ -0,0 +1,11 @@
+### note: for local testing - add to load path ./lib
+## to test / run use:
+## $ rackup
+
+
+$LOAD_PATH << './lib'
+
+require 'centralbank'
+
+
+run Centralbank::Service
diff --git a/centralbank/lib/centralbank.rb b/centralbank/lib/centralbank.rb
new file mode 100644
index 0000000..d2848a9
--- /dev/null
+++ b/centralbank/lib/centralbank.rb
@@ -0,0 +1,103 @@
+# encoding: utf-8
+
+# stdlibs
+require 'json'
+require 'digest'
+require 'net/http'
+require 'set'
+require 'pp'
+require 'optparse' ## note: used for command line tool (see Tool in tool.rb)
+
+
+### 3rd party gems
+require 'sinatra/base' # note: use "modular" sinatra app / service
+
+require 'merkletree'
+require 'blockchain-lite/proof_of_work/block' # note: use proof-of-work block only (for now)
+
+
+### our own code
+require 'centralbank/version' ## let version always go first
+require 'centralbank/block'
+require 'centralbank/cache'
+require 'centralbank/transaction'
+require 'centralbank/blockchain'
+require 'centralbank/pool'
+require 'centralbank/bank'
+require 'centralbank/ledger'
+require 'centralbank/wallet'
+
+require 'centralbank/node'
+require 'centralbank/service'
+
+require 'centralbank/tool' ## add (optional) command line tool
+
+
+
+
+
+module Centralbank
+
+
+ class Configuration
+ ## user/node settings
+ attr_accessor :address ## single wallet address (for now "clear" name e.g.Sepp, Franz, etc.)
+
+ WALLET_ADDRESSES = %w[Alice Bob Max Franz Maria Ferdl Lisi Adam Eva]
+
+ ## system/blockchain settings
+ attr_accessor :coinbase
+ attr_accessor :mining_reward
+
+ ## note: add a (†) coinbase marker
+ COINBASE = ['Everest†', 'Aconcagua†', 'Denali†',
+ 'Kilimanjaro†', 'Elbrus†', 'Vinson†',
+ 'Puncak Jaya†', 'Kosciuszko†',
+ 'Mont Blanc†'
+ ]
+
+
+ def initialize
+ ## try default setup via ENV variables
+ ## pick "random" address if nil (none passed in)
+ @address = ENV[ 'CENTRALBANK_NAME'] || rand_address()
+
+ @coinbase = COINBASE ## use a different name - why? why not?
+ ## note: for now is an array (multiple coinbases)
+ @mining_reward = 5
+ end
+
+ def rand_address() WALLET_ADDRESSES[rand( WALLET_ADDRESSES.size )]; end
+ def rand_coinbase() @coinbase[rand( @coinbase.size )]; end
+
+ def coinbase?( address ) ## check/todo: use wallet - why? why not? (for now wallet==address)
+ @coinbase.include?( address )
+ end
+
+ end # class Configuration
+
+
+ ## lets you use
+ ## Centralbank.configure do |config|
+ ## config.address = 'Sepp'
+ ## end
+
+ def self.configure
+ yield( config )
+ end
+
+ def self.config
+ @config ||= Configuration.new
+ end
+
+
+ ## add command line binary (tool) e.g. $ try centralbank -h
+ def self.main
+ Tool.new.run(ARGV)
+ end
+
+end # module Centralbank
+
+
+# say hello
+puts Centralbank::Service.banner
diff --git a/centralbank/lib/centralbank/bank.rb b/centralbank/lib/centralbank/bank.rb
new file mode 100644
index 0000000..a922eca
--- /dev/null
+++ b/centralbank/lib/centralbank/bank.rb
@@ -0,0 +1,109 @@
+
+
+class Bank
+ attr_reader :pending, :chain, :ledger
+
+
+ def initialize( address )
+ @address = address
+
+ ## note: add address name for now to cache
+ ## allows to start more nodes in same folder / directory
+ @cache = Cache.new( "data.#{address.downcase}.json" )
+ h = @cache.read
+ if h
+ ## restore blockchain
+ @chain = Blockchain.from_json( h['chain'] )
+ ## restore pending (unconfirmed) transactions pool too
+ @pending = Pool.from_json( h['transactions'] )
+ else
+ @chain = Blockchain.new
+ @chain << [Tx.new( Centralbank.config.rand_coinbase,
+ @address,
+ Centralbank.config.mining_reward )] # genesis (big bang!) starter block
+ @pending = Pool.new
+ end
+
+ ## update ledger (balances) with confirmed transactions
+ @ledger = Ledger.new( @chain )
+ end
+
+
+
+ def mine_block!
+ add_transaction( Tx.new( Centralbank.config.rand_coinbase,
+ @address,
+ Centralbank.config.mining_reward ))
+
+ ## add mined (w/ computed/calculated hash) block
+ @chain << @pending.transactions
+ @pending = Pool.new ## clear out/ empty pool (just create a new one for now)
+
+ ## update ledger (balances) with new confirmed transactions
+ @ledger = Ledger.new( @chain )
+
+ @cache.write as_json
+ end
+
+
+ def sufficient_funds?( wallet, amount )
+ ## (convenience) delegate for ledger
+ ## todo/check: use address instead of wallet - why? why not?
+ ## for now single address wallet (that is, wallet==address)
+ @ledger.sufficient_funds?( wallet, amount )
+ end
+
+
+ def add_transaction( tx )
+ if tx.valid? && transaction_is_new?( tx )
+ @pending << tx
+ @cache.write as_json
+ return true
+ else
+ return false
+ end
+ end
+
+
+ ##
+ # check - how to name incoming chain - chain_new, chain_candidate - why? why not?
+ # what's an intuitive name - what's gets used most often???
+
+ def resolve!( chain_new )
+ # TODO this does not protect against invalid block shapes (bogus COINBASE transactions for example)
+
+ if !chain_new.empty? && chain_new.last.valid? && chain_new.size > @chain.size
+ @chain = chain_new
+ ## update ledger (balances) with new confirmed transactions
+ @ledger = Ledger.new( @chain )
+
+ ## document - keep only pending transaction not yet (confirmed) in (new) blockchain ????
+ @pending.update!( @chain.transactions )
+ @cache.write as_json
+ return true
+ else
+ return false
+ end
+ end
+
+
+
+ def as_json
+ { chain: @chain.as_json,
+ transactions: @pending.as_json
+ }
+ end
+
+
+
+private
+
+ def transaction_is_new?( tx_new )
+ ## check if tx exists already in blockchain or pending tx pool
+
+ ## todo: use chain.include? to check for include
+ ## avoid loop and create new array for check!!!
+ (@chain.transactions + @pending.transactions).none? { |tx| tx_new.id == tx.id }
+ end
+
+end ## class Bank
diff --git a/centralbank/lib/centralbank/block.rb b/centralbank/lib/centralbank/block.rb
new file mode 100644
index 0000000..caaebe3
--- /dev/null
+++ b/centralbank/lib/centralbank/block.rb
@@ -0,0 +1,44 @@
+
+
+Block = BlockchainLite::ProofOfWork::Block
+
+## see https://github.com/openblockchains/blockchain.lite.rb/blob/master/lib/blockchain-lite/proof_of_work/block.rb
+
+######
+## add more methods
+
+class Block
+
+
+def to_h
+ { index: @index,
+ timestamp: @timestamp,
+ nonce: @nonce,
+ transactions: @transactions.map { |tx| tx.to_h },
+ transactions_hash: @transactions_hash,
+ previous_hash: @previous_hash,
+ hash: @hash }
+end
+
+def self.from_h( h )
+ transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
+
+ ## todo: use hash and transactions_hash to check integrity of block - why? why not?
+
+ ## parse iso8601 format e.g 2017-10-05T22:26:12-04:00
+ timestamp = Time.parse( h['timestamp'] )
+
+ self.new( h['index'],
+ transactions,
+ h['previous_hash'],
+ timestamp: timestamp,
+ nonce: h['nonce'].to_i )
+end
+
+
+def valid?
+ true ## for now always valid
+end
+
+
+end # class Block
diff --git a/centralbank/lib/centralbank/blockchain.rb b/centralbank/lib/centralbank/blockchain.rb
new file mode 100644
index 0000000..a040e64
--- /dev/null
+++ b/centralbank/lib/centralbank/blockchain.rb
@@ -0,0 +1,47 @@
+
+
+
+class Blockchain
+ extend Forwardable
+ def_delegators :@chain, :[], :size, :each, :empty?, :any?, :last
+
+
+ def initialize( chain=[] )
+ @chain = chain
+ end
+
+ def <<( txs )
+ ## todo: check if is block or array
+ ## if array (of transactions) - auto-add (build) block
+ ## allow block - why? why not?
+ ## for now just use transactions (keep it simple :-)
+
+ if @chain.size == 0
+ block = Block.first( txs )
+ else
+ block = Block.next( @chain.last, txs )
+ end
+ @chain << block
+ end
+
+
+
+ def as_json
+ @chain.map { |block| block.to_h }
+ end
+
+ def transactions
+ ## "accumulate" get all transactions from all blocks "reduced" into a single array
+ @chain.reduce( [] ) { |acc, block| acc + block.transactions }
+ end
+
+
+
+ def self.from_json( data )
+ ## note: assumes data is an array of block records/objects in json
+ chain = data.map { |h| Block.from_h( h ) }
+ self.new( chain )
+ end
+
+
+end # class Blockchain
diff --git a/centralbank/lib/centralbank/cache.rb b/centralbank/lib/centralbank/cache.rb
new file mode 100644
index 0000000..b7f520f
--- /dev/null
+++ b/centralbank/lib/centralbank/cache.rb
@@ -0,0 +1,22 @@
+
+
+class Cache
+ def initialize( name )
+ @name = name
+ end
+
+ def write( data )
+ File.open( @name, 'w:utf-8' ) do |f|
+ f.write JSON.pretty_generate( data )
+ end
+ end
+
+ def read
+ if File.exists?( @name )
+ data = File.open( @name, 'r:bom|utf-8' ).read
+ JSON.parse( data )
+ else
+ nil
+ end
+ end
+end ## class Cache
diff --git a/centralbank/lib/centralbank/ledger.rb b/centralbank/lib/centralbank/ledger.rb
new file mode 100644
index 0000000..bd5ebea
--- /dev/null
+++ b/centralbank/lib/centralbank/ledger.rb
@@ -0,0 +1,30 @@
+
+class Ledger
+ attr_reader :wallets ## use addresses - why? why not? for now single address wallet (wallet==address)
+
+ def initialize( chain=[] )
+ @wallets = {}
+ chain.each do |block|
+ apply_transactions( block.transactions )
+ end
+ end
+
+ def sufficient_funds?( wallet, amount )
+ return true if Centralbank.config.coinbase?( wallet )
+ @wallets.has_key?( wallet ) && @wallets[wallet] - amount >= 0
+ end
+
+
+private
+
+ def apply_transactions( transactions )
+ transactions.each do |tx|
+ if sufficient_funds?(tx.from, tx.amount)
+ @wallets[tx.from] -= tx.amount unless Centralbank.config.coinbase?( tx.from )
+ @wallets[tx.to] ||= 0
+ @wallets[tx.to] += tx.amount
+ end
+ end
+ end
+
+end ## class Ledger
diff --git a/centralbank/lib/centralbank/node.rb b/centralbank/lib/centralbank/node.rb
new file mode 100644
index 0000000..cec219d
--- /dev/null
+++ b/centralbank/lib/centralbank/node.rb
@@ -0,0 +1,82 @@
+
+
+class Node
+ attr_reader :id, :peers, :wallet, :bank
+
+ def initialize( address: )
+ @id = SecureRandom.uuid
+ @peers = []
+ @wallet = Wallet.new( address )
+ @bank = Bank.new @wallet.address
+ end
+
+
+
+ def on_add_peer( host, port )
+ @peers << [host, port]
+ @peers.uniq!
+ # TODO/FIX: no need to send to every peer, just the new one
+ send_chain_to_peers
+ @bank.pending.each { |tx| send_transaction_to_peers( tx ) }
+ end
+
+ def on_delete_peer( index )
+ @peers.delete_at( index )
+ end
+
+
+ def on_add_transaction( from, to, amount, id )
+ ## note: for now must always pass in id - why? why not? possible tx without id???
+ tx = Tx.new( from, to, amount, id )
+ if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
+ send_transaction_to_peers( tx )
+ return true
+ else
+ return false
+ end
+ end
+
+ def on_send( to, amount )
+ tx = @wallet.generate_transaction( to, amount )
+ if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
+ send_transaction_to_peers( tx )
+ return true
+ else
+ return false
+ end
+ end
+
+
+ def on_mine!
+ @bank.mine_block!
+ send_chain_to_peers
+ end
+
+ def on_resolve( data )
+ chain_new = Blockchain.from_json( data )
+ if @bank.resolve!( chain_new )
+ send_chain_to_peers
+ return true
+ else
+ return false
+ end
+ end
+
+
+
+private
+
+ def send_chain_to_peers
+ data = JSON.pretty_generate( @bank.as_json ) ## payload in json
+ @peers.each do |(host, port)|
+ Net::HTTP.post(URI::HTTP.build(host: host, port: port, path: '/resolve'), data )
+ end
+ end
+
+ def send_transaction_to_peers( tx )
+ @peers.each do |(host, port)|
+ Net::HTTP.post_form(URI::HTTP.build(host: host, port: port, path: '/transactions'), tx.to_h )
+ end
+ end
+
+end ## class Node
diff --git a/centralbank/lib/centralbank/pool.rb b/centralbank/lib/centralbank/pool.rb
new file mode 100644
index 0000000..d9002b1
--- /dev/null
+++ b/centralbank/lib/centralbank/pool.rb
@@ -0,0 +1,42 @@
+####################################
+# pending (unconfirmed) transactions (mem) pool
+
+class Pool
+ extend Forwardable
+ def_delegators :@transactions, :[], :size, :each, :empty?, :any?
+
+
+ def initialize( transactions=[] )
+ @transactions = transactions
+ end
+
+ def transactions() @transactions; end
+
+ def <<( tx )
+ @transactions << tx
+ end
+
+
+ def update!( txns_confirmed )
+ ## find a better name?
+ ## remove confirmed transactions from pool
+
+ ## document - keep only pending transaction not yet (confirmed) in blockchain ????
+ @transactions = @transactions.select do |tx_unconfirmed|
+ txns_confirmed.none? { |tx_confirmed| tx_confirmed.id == tx_unconfirmed.id }
+ end
+ end
+
+
+
+ def as_json
+ @transactions.map { |tx| tx.to_h }
+ end
+
+ def self.from_json( data )
+ ## note: assumes data is an array of block records/objects in json
+ transactions = data.map { |h| Tx.from_h( h ) }
+ self.new( transactions )
+ end
+
+end # class Pool
diff --git a/centralbank/lib/centralbank/service.rb b/centralbank/lib/centralbank/service.rb
new file mode 100644
index 0000000..59e8290
--- /dev/null
+++ b/centralbank/lib/centralbank/service.rb
@@ -0,0 +1,113 @@
+# encoding: utf-8
+
+module Centralbank
+
+ class Service < Sinatra::Base
+
+ def self.banner
+ "centralbank/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
+ end
+
+
+ PUBLIC_FOLDER = "#{Centralbank.root}/lib/centralbank/public"
+ VIEWS_FOLDER = "#{Centralbank.root}/lib/centralbank/views"
+
+ set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
+ set :views, VIEWS_FOLDER # set up the views dir
+
+ set :static, true # set up static file routing -- check - still needed?
+
+
+ set connections: []
+
+
+
+ get '/style.css' do
+ scss :style ## note: converts (pre-processes) style.scss to style.css
+ end
+
+
+ get '/' do
+ @node = node ## todo: pass along node as hash varialbe / assigns to erb
+ erb :index
+ end
+
+
+ post '/send' do
+ node.on_send( params[:to], params[:amount].to_i )
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
+ redirect '/'
+ end
+
+
+ post '/transactions' do
+ if node.on_add_transaction(
+ params[:from],
+ params[:to],
+ params[:amount].to_i,
+ params[:id]
+ )
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
+ end
+ redirect '/'
+ end
+
+ post '/mine' do
+ node.on_mine!
+ redirect '/'
+ end
+
+ post '/peers' do
+ node.on_add_peer( params[:host], params[:port].to_i )
+ redirect '/'
+ end
+
+ post '/peers/:index/delete' do
+ node.on_delete_peer( params[:index].to_i )
+ redirect '/'
+ end
+
+
+
+ post '/resolve' do
+ data = JSON.parse(request.body.read)
+ if data['chain'] && node.on_resolve( data['chain'] )
+ status 202 ### 202 Accepted; see httpstatuses.com/202
+ settings.connections.each { |out| out << "data: resolved\n\n" }
+ else
+ status 200 ### 200 OK
+ end
+ end
+
+
+ get '/events', provides: 'text/event-stream' do
+ stream :keep_open do |out|
+ settings.connections << out
+ out.callback { settings.connections.delete(out) }
+ end
+ end
+
+private
+
+#########
+## return network node (built and configured on first use)
+## fix: do NOT use @@ - use a class level method or something
+def node
+ if defined?( @@node )
+ @@node
+ else
+ puts "[debug] centralbank - build (network) node (address: #{Centralbank.config.address})"
+ @@node = Node.new( address: Centralbank.config.address )
+ @@node
+ end
+ ####
+ ## check why this is a syntax error:
+ ## @node ||= do
+ ## puts "[debug] centralbank - build (network) node (address: #{Centralbank.config.address})"
+ ## @node = Node.new( address: Centralbank.config.address )
+ ## end
+end
+
+ end # class Service
+
+end # module Centralbank
diff --git a/centralbank/lib/centralbank/tool.rb b/centralbank/lib/centralbank/tool.rb
new file mode 100644
index 0000000..846b720
--- /dev/null
+++ b/centralbank/lib/centralbank/tool.rb
@@ -0,0 +1,66 @@
+# encoding: utf-8
+
+
+module Centralbank
+
+class Tool
+
+def run( args )
+ opts = {}
+
+ parser = OptionParser.new do |cmd|
+ cmd.banner = "Usage: centralbank [options]"
+
+ cmd.separator ""
+ cmd.separator " Wallet options:"
+
+ cmd.on("-n", "--name=NAME", "Address name (default: Alice)") do |name|
+ ## use -a or --adr or --address as option flag - why? why not?
+ ## note: default now picks a random address from WALLET_ADDRESSES
+ opts[:address] = name
+ end
+
+
+ cmd.separator ""
+ cmd.separator " Server (node) options:"
+
+ cmd.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |host|
+ opts[:Host] = host ## note: rack server handler expects :Host
+ end
+
+ cmd.on("-p", "--port PORT", "use PORT (default: 4567)") do |port|
+ opts[:Port] = port ## note: rack server handler expects :Post
+ end
+
+ cmd.on("-h", "--help", "Prints this help") do
+ puts cmd
+ exit
+ end
+ end
+
+ parser.parse!( args )
+ pp opts
+
+
+ ###################
+ ## startup server (via rack interface/handler)
+
+ app_class = Service ## use app = Service.new -- why? why not?
+ host = opts[:Host] || '0.0.0.0'
+ port = opts[:Port] || '4567'
+
+ Centralbank.configure do |config|
+ config.address = opts[:address] || 'Alice'
+ end
+
+ Rack::Handler::WEBrick.run( app_class, Host: host, Port: port ) do |server|
+ ## todo: add traps here - why, why not??
+ end
+
+
+end ## method run
+
+
+end ## class Tool
+
+end ## module Centralbank
diff --git a/centralbank/lib/centralbank/transaction.rb b/centralbank/lib/centralbank/transaction.rb
new file mode 100644
index 0000000..77ab849
--- /dev/null
+++ b/centralbank/lib/centralbank/transaction.rb
@@ -0,0 +1,30 @@
+
+
+class Transaction
+
+ attr_reader :from, :to, :amount, :id
+
+ def initialize( from, to, amount, id=SecureRandom.uuid )
+ @from = from
+ @to = to
+ @amount = amount
+ @id = id
+ end
+
+ def self.from_h( hash )
+ self.new *hash.values_at( 'from', 'to', 'amount', 'id' )
+ end
+
+ def to_h
+ { from: @from, to: @to, amount: @amount, id: @id }
+ end
+
+
+ def valid?
+ ## check signature in the future; for now always true
+ true
+ end
+
+end # class Transaction
+
+Tx = Transaction ## add Tx shortcut / alias
diff --git a/centralbank/lib/centralbank/version.rb b/centralbank/lib/centralbank/version.rb
new file mode 100644
index 0000000..b650d1d
--- /dev/null
+++ b/centralbank/lib/centralbank/version.rb
@@ -0,0 +1,11 @@
+# encoding: utf-8
+
+module Centralbank
+
+ VERSION = '0.2.1'
+
+ def self.root
+ "#{File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )}"
+ end
+
+end # module Centralbank
diff --git a/centralbank/lib/centralbank/views/_blockchain.erb b/centralbank/lib/centralbank/views/_blockchain.erb
new file mode 100644
index 0000000..9b5b648
--- /dev/null
+++ b/centralbank/lib/centralbank/views/_blockchain.erb
@@ -0,0 +1,37 @@
+
+
+ Blockchain
+ <%= @node.bank.chain.size %> blocks
+
+
+
+
+ <% @node.bank.chain.last(10).reverse.each do |block| %>
+
+
+
+ <% block.transactions.each do |tx| %>
+
+
+ <%= tx.id[0..2] %>
+ |
+
+ $<%= tx.amount %>
+ |
+
+ <%= tx.from[0..15] %> → <%= tx.to[0..15] %>
+ |
+
+ <% end %>
+
+
+ <% end %>
+
+
+ †: Miner Transaction - New $$ on the Market!
+
+
diff --git a/centralbank/lib/centralbank/views/_ledger.erb b/centralbank/lib/centralbank/views/_ledger.erb
new file mode 100644
index 0000000..4de92af
--- /dev/null
+++ b/centralbank/lib/centralbank/views/_ledger.erb
@@ -0,0 +1,15 @@
+
+
Ledger
+
+
+ Address |
+ Balance |
+
+ <% @node.bank.ledger.wallets.each do |address, amount| %>
+
+ <%= address[0..15] %> |
+ $<%= amount %> |
+
+ <% end %>
+
+
diff --git a/centralbank/lib/centralbank/views/_peers.erb b/centralbank/lib/centralbank/views/_peers.erb
new file mode 100644
index 0000000..0c1a972
--- /dev/null
+++ b/centralbank/lib/centralbank/views/_peers.erb
@@ -0,0 +1,24 @@
+
+
Peers
+ <% if @node.peers.any? %>
+
+ <% @node.peers.each_with_index do |(host, port), i| %>
+ -
+ http://<%= host %>:<%= port %>
+
+
+ <% end %>
+
+ <% else %>
+
No peers
+ <% end %>
+
+
diff --git a/centralbank/lib/centralbank/views/_pending_transactions.erb b/centralbank/lib/centralbank/views/_pending_transactions.erb
new file mode 100644
index 0000000..7cdef8d
--- /dev/null
+++ b/centralbank/lib/centralbank/views/_pending_transactions.erb
@@ -0,0 +1,23 @@
+
+
Pending Transactions
+ <% if @node.bank.pending.any? %>
+
+
+ From |
+ To |
+ $ |
+ Id |
+
+ <% @node.bank.pending.each do |tx| %>
+
+ <%= tx.from[0..15] %> |
+ <%= tx.to[0..15] %> |
+ <%= tx.amount %> |
+ <%= tx.id[0..2] %> |
+
+ <% end %>
+
+ <% else %>
+
No pending transactions
+ <% end %>
+
diff --git a/centralbank/lib/centralbank/views/_wallet.erb b/centralbank/lib/centralbank/views/_wallet.erb
new file mode 100644
index 0000000..3129cda
--- /dev/null
+++ b/centralbank/lib/centralbank/views/_wallet.erb
@@ -0,0 +1,16 @@
+
+
+
+
Address
+
<%= @node.wallet.address %>
+
Balance
+
$<%= @node.bank.ledger.wallets[@node.wallet.address] || 0 %>
+
+
+
diff --git a/centralbank/lib/centralbank/views/index.erb b/centralbank/lib/centralbank/views/index.erb
new file mode 100644
index 0000000..446e85e
--- /dev/null
+++ b/centralbank/lib/centralbank/views/index.erb
@@ -0,0 +1,30 @@
+
+
+
+ Central Bank Node
+
+
+
+
+ Central Bank Node
+
+
+
+ <%= erb :'_wallet' %>
+ <%= erb :'_pending_transactions' %>
+ <%= erb :'_peers' %>
+ <%= erb :'_ledger' %>
+
+
+
+ <%= erb :'_blockchain' %>
+
+
+
+
+
+
+
diff --git a/centralbank/lib/centralbank/views/style.scss b/centralbank/lib/centralbank/views/style.scss
new file mode 100644
index 0000000..120e9cf
--- /dev/null
+++ b/centralbank/lib/centralbank/views/style.scss
@@ -0,0 +1,172 @@
+
+body {
+ padding: 0;
+ margin: 0;
+ min-width: 960px;
+
+ font-family: 'menlo', monospace;
+ font-size: 14px;
+
+ background: #fff;
+ color: #2B2D2F;
+}
+
+
+.columns {
+ display: flex;
+
+ .left {
+ width: 66%;
+ }
+
+ .right {
+ width: 34%;
+ }
+}
+
+
+h1 {
+ font-size: 24px;
+ font-weight: normal;
+ padding-left: 15px;
+ margin-bottom: 20px;
+}
+
+
+h2 {
+ font-size: 16px;
+}
+
+h2 span {
+ font-size: 14px;
+ color: #597898;
+ font-weight: normal;
+}
+
+label {
+ display: inline-block;
+ width: 80px;
+ text-align: right;
+ padding-right: 10px;
+}
+
+input[type=text] {
+ display: inline-block;
+ font-size: 14px;
+ padding: 8px;
+ border-radius: 0;
+ border: 0;
+}
+
+table {
+ border-spacing: 0;
+ border-collapse: collapse;
+
+ th {
+ text-align: left;
+ }
+
+ td {
+ vertical-align: top;
+ padding: 5px 15px 5px 0;
+ }
+}
+
+
+ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+input[type=submit] {
+ font-size: 14px;
+ font-family: menlo, monospace;
+ border-radius: 5px;
+ padding: 8px 20px;
+ background: #FFDC00;
+ color: #2B2D2F;
+ border: 0;
+}
+
+input[type=submit].small {
+ font-size: 10px;
+ padding: 4px 10px;
+}
+
+
+
+.wallet {
+ padding: 15px;
+ background: #7FDBFF;
+
+ h2 {
+ margin-bottom: 0;
+ }
+
+ .balance {
+ font-size: 30px;
+ }
+}
+
+
+.pending-transactions {
+ padding: 15px;
+ background: #A3E6FF;
+}
+
+.peers {
+ padding: 15px;
+ background: #C6EFFF;
+
+ li form {
+ display: inline;
+ }
+
+ li {
+ padding: 5px 0px;
+ }
+}
+
+
+.ledger {
+ padding: 15px;
+ background: #E3F7FF;
+}
+
+
+.blockchain {
+ padding: 15px;
+ position: relative;
+ background: #001F3F;
+ color: #fff;
+
+ form {
+ position: absolute;
+ top: 30px;
+ right: 15px;
+ }
+
+ .blocks {
+ border: 1px solid #597898;
+ border-bottom: 0;
+
+ .block {
+ margin: 0;
+ border-bottom: 2px dashed #597898;
+ padding: 10px;
+
+ .header {
+ text-align: center;
+ padding: 0 8px 8px 8px;
+ color: #597898;
+ border-bottom: 1px solid #354c63;
+ margin-bottom: 10px;
+ }
+
+ .id {
+ color: #597898;
+ }
+ }
+ }
+}
diff --git a/centralbank/lib/centralbank/wallet.rb b/centralbank/lib/centralbank/wallet.rb
new file mode 100644
index 0000000..94b09cf
--- /dev/null
+++ b/centralbank/lib/centralbank/wallet.rb
@@ -0,0 +1,15 @@
+###########
+# Single Address Wallet
+
+class Wallet
+ attr_reader :address
+
+ def initialize( address )
+ @address = address
+ end
+
+ def generate_transaction( to, amount )
+ Tx.new( address, to, amount )
+ end
+
+end # class Wallet
diff --git a/tulipmania/.gitignore b/tulipmania/.gitignore
new file mode 100644
index 0000000..654d0f3
--- /dev/null
+++ b/tulipmania/.gitignore
@@ -0,0 +1,69 @@
+###########
+# cached / saved state / data
+
+data.json
+data.*.json
+
+
+##########
+# ignore Gemfile.lock for now
+
+Gemfile.lock
+
+
+####
+# ignore sass cache
+.sass-cache
+
+
+
+*.gem
+*.rbc
+/.config
+/coverage/
+/InstalledFiles
+/pkg/
+/spec/reports/
+/spec/examples.txt
+/test/tmp/
+/test/version_tmp/
+/tmp/
+
+# Used by dotenv library to load environment variables.
+# .env
+
+## Specific to RubyMotion:
+.dat*
+.repl_history
+build/
+*.bridgesupport
+build-iPhoneOS/
+build-iPhoneSimulator/
+
+## Specific to RubyMotion (use of CocoaPods):
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# vendor/Pods/
+
+## Documentation cache and generated files:
+/.yardoc/
+/_yardoc/
+/doc/
+/rdoc/
+
+## Environment normalization:
+/.bundle/
+/vendor/bundle
+/lib/bundler/man/
+
+# for a library or gem, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# Gemfile.lock
+# .ruby-version
+# .ruby-gemset
+
+# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
+.rvmrc
diff --git a/tulipmania/HISTORY.md b/tulipmania/HISTORY.md
new file mode 100644
index 0000000..3302a16
--- /dev/null
+++ b/tulipmania/HISTORY.md
@@ -0,0 +1,3 @@
+### 0.1.0 / 2017-12-18
+
+* Everything is new. First release.
diff --git a/tulipmania/Manifest.txt b/tulipmania/Manifest.txt
new file mode 100644
index 0000000..eaa61b6
--- /dev/null
+++ b/tulipmania/Manifest.txt
@@ -0,0 +1,26 @@
+HISTORY.md
+LICENSE.md
+Manifest.txt
+README.md
+Rakefile
+bin/tulipmania
+lib/tulipmania.rb
+lib/tulipmania/block.rb
+lib/tulipmania/blockchain.rb
+lib/tulipmania/cache.rb
+lib/tulipmania/exchange.rb
+lib/tulipmania/ledger.rb
+lib/tulipmania/node.rb
+lib/tulipmania/pool.rb
+lib/tulipmania/service.rb
+lib/tulipmania/tool.rb
+lib/tulipmania/transaction.rb
+lib/tulipmania/version.rb
+lib/tulipmania/views/_blockchain.erb
+lib/tulipmania/views/_ledger.erb
+lib/tulipmania/views/_peers.erb
+lib/tulipmania/views/_pending_transactions.erb
+lib/tulipmania/views/_wallet.erb
+lib/tulipmania/views/index.erb
+lib/tulipmania/views/style.scss
+lib/tulipmania/wallet.rb
diff --git a/tulipmania/NOTES.md b/tulipmania/NOTES.md
new file mode 100644
index 0000000..7e16a77
--- /dev/null
+++ b/tulipmania/NOTES.md
@@ -0,0 +1,6 @@
+# Notes
+
+
+## Todos
+
+- [ ] add favicon.png - why? why not? (see webservice gem)
diff --git a/tulipmania/README.md b/tulipmania/README.md
new file mode 100644
index 0000000..46b667b
--- /dev/null
+++ b/tulipmania/README.md
@@ -0,0 +1,120 @@
+# tulipmania (anno 1673) library / gem and command line tool
+
+
+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 HTTP; revolutionize the world one block at a time
+
+
+* home :: [github.com/openblockchains/tulipmania](https://github.com/openblockchains/tulipmania)
+* bugs :: [github.com/openblockchains/tulipmania/issues](https://github.com/openblockchains/tulipmania/issues)
+* gem :: [rubygems.org/gems/tulipmania](https://rubygems.org/gems/tulipmania)
+* rdoc :: [rubydoc.info/gems/tulipmania](http://rubydoc.info/gems/tulipmania)
+
+
+
+## Command Line
+
+Use the `tulipmania` command line tool. Try:
+
+```
+$ tulipmania -h
+```
+
+resulting in:
+
+```
+Usage: tulipmania [options]
+
+ Wallet options:
+ -n, --name=NAME Address name (default: Anne)
+
+ Server (node) options:
+ -o, --host HOST listen on HOST (default: 0.0.0.0)
+ -p, --port PORT use PORT (default: 4567)
+ -h, --help Prints this help
+```
+
+To start a new (network) node using the default wallet
+address (that is, Anne) and the default server host and port settings
+use:
+
+```
+$ tulipmania
+```
+
+Stand back ten feets :-) while starting up the machinery.
+Ready to exchange tulips on the blockchain?
+In your browser open up the page e.g. `http://localhost:4567`. Voila!
+
+
+
+
+
+Note: You can start a second node on your computer -
+make sure to use a different port (use the `-p/--port` option)
+and (recommended)
+a different wallet address (use the `-n/--name` option).
+Example:
+
+```
+$ tulipmania -p 5678 -n Vincent
+```
+
+Happy mining!
+
+
+
+## Local Development Setup
+
+For local development - clone or download (and unzip) the tulipmania code repo.
+Next install all dependencies using bundler with a Gemfile e.g.:
+
+``` ruby
+# Gemfile
+
+source "https://rubygems.org"
+
+gem 'sinatra'
+gem 'sass'
+gem 'blockchain-lite'
+```
+
+run
+
+```
+$ bundle ## will use the Gemfile (see above)
+```
+
+and now you're ready to run your own tulipmania server node. Use the [`config.ru`](config.ru) script for rack:
+
+``` ruby
+# config.ru
+
+$LOAD_PATH << './lib'
+
+require 'tulipmania'
+
+run Tulipmania::Service
+```
+
+and startup the tulip exchange machinery using rackup - the rack command line tool:
+
+```
+$ rackup ## will use the config.ru - rackup configuration script (see above).
+```
+
+In your browser open up the page e.g. `http://localhost:9292`. Voila! Happy mining!
+
+
+## References
+
+[**Programming Cryptocurrencies and Blockchains (in Ruby)**](http://yukimotopress.github.io/blockchains) by Gerald Bauer et al, 2018, Yuki & Moto Press
+
+
+
+## License
+
+
+
+The `tulipmania` scripts are dedicated to the public domain.
+Use it as you please with no restrictions whatsoever.
diff --git a/tulipmania/Rakefile b/tulipmania/Rakefile
new file mode 100644
index 0000000..ae8cecd
--- /dev/null
+++ b/tulipmania/Rakefile
@@ -0,0 +1,32 @@
+require 'hoe'
+require './lib/tulipmania/version.rb'
+
+Hoe.spec 'tulipmania' do
+
+ self.version = Tulipmania::VERSION
+
+ 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 HTTP; revolutionize the world one block at a time'
+ self.description = summary
+
+ self.urls = ['https://github.com/openblockchains/tulipmania']
+
+ self.author = 'Gerald Bauer'
+ self.email = 'ruby-talk@ruby-lang.org'
+
+ # switch extension to .markdown for gihub formatting
+ self.readme_file = 'README.md'
+ self.history_file = 'History.md'
+
+ self.extra_deps = [
+ ['sinatra', '>=2.0'],
+ ['sass'], ## used for css style preprocessing (scss)
+ ['blockchain-lite', '>=1.3.1'],
+ ]
+
+ self.licenses = ['Public Domain']
+
+ self.spec_extras = {
+ required_ruby_version: '>= 2.3'
+ }
+
+end
diff --git a/tulipmania/bin/tulipmania b/tulipmania/bin/tulipmania
new file mode 100644
index 0000000..599908c
--- /dev/null
+++ b/tulipmania/bin/tulipmania
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+###################
+# == DEV TIPS:
+#
+# For local testing run like:
+#
+# ruby -Ilib bin/tulipmania
+#
+# Set the executable bit in Linux. Example:
+#
+# % chmod a+x bin/tulipmania
+#
+
+require 'tulipmania'
+
+Tulipmania.main
diff --git a/tulipmania/config.ru b/tulipmania/config.ru
new file mode 100644
index 0000000..d8be143
--- /dev/null
+++ b/tulipmania/config.ru
@@ -0,0 +1,11 @@
+### note: for local testing - add to load path ./lib
+## to test / run use:
+## $ rackup
+
+
+$LOAD_PATH << './lib'
+
+require 'tulipmania'
+
+
+run Tulipmania::Service
diff --git a/tulipmania/lib/tulipmania.rb b/tulipmania/lib/tulipmania.rb
new file mode 100644
index 0000000..95197d8
--- /dev/null
+++ b/tulipmania/lib/tulipmania.rb
@@ -0,0 +1,110 @@
+# encoding: utf-8
+
+# stdlibs
+require 'json'
+require 'digest'
+require 'net/http'
+require 'set'
+require 'optparse'
+require 'pp'
+
+
+### 3rd party gems
+require 'sinatra/base' # note: use "modular" sinatra app / service
+
+require 'merkletree'
+require 'blockchain-lite/proof_of_work/block' # note: use proof-of-work block only (for now)
+
+
+### our own code
+require 'tulipmania/version' ## let version always go first
+require 'tulipmania/block'
+require 'tulipmania/cache'
+require 'tulipmania/transaction'
+require 'tulipmania/blockchain'
+require 'tulipmania/pool'
+require 'tulipmania/exchange'
+require 'tulipmania/ledger'
+require 'tulipmania/wallet'
+
+require 'tulipmania/node'
+require 'tulipmania/service'
+
+require 'tulipmania/tool'
+
+
+module Tulipmania
+
+ class Configuration
+ ## user/node settings
+ attr_accessor :address ## single wallet address (for now "clear" name e.g. Anne, Vincent, etc.)
+
+ WALLET_ADDRESSES = ['Anne', 'Vicent', 'Ruben', 'Julia', 'Luuk',
+ 'Daisy', 'Max', 'Martijn', 'Naomi', 'Mina',
+ 'Isabel'
+ ]
+
+ ## system/blockchain settings
+ attr_accessor :coinbase
+ attr_accessor :mining_reward
+ attr_accessor :tulips ## rename to assets/commodities/etc. - why? why not?
+
+ ## note: add a (†) coinbase / grower marker
+ TULIP_GROWERS = ['Dutchgrown†', 'Keukenhof†', 'Flowers†',
+ 'Bloom & Blossom†', 'Teleflora†'
+ ]
+
+ TULIPS = ['Semper Augustus',
+ 'Admiral van Eijck',
+ 'Admiral of Admirals',
+ 'Red Impression',
+ 'Bloemendaal Sunset',
+ ]
+
+ def initialize
+ ## try default setup via ENV variables
+ ## pick "random" address if nil (none passed in)
+ @address = ENV[ 'TULIPMANIA_NAME'] || rand_address()
+
+ @coinbase = TULIP_GROWERS ## use a different name for coinbase - why? why not?
+ ## note: for now is an array (multiple growsers)
+
+ @tulips = TULIPS ## change name to commodities or assets - why? why not?
+ @mining_reward = 5
+ end
+
+
+ def rand_address() WALLET_ADDRESSES[rand( WALLET_ADDRESSES.size )]; end
+ def rand_tulip() @tulips[rand( @tulips.size )]; end
+ def rand_coinbase() @coinbase[rand( @coinbase.size )]; end
+
+ def coinbase?( address ) ## check/todo: use wallet - why? why not? (for now wallet==address)
+ @coinbase.include?( address )
+ end
+ end # class Configuration
+
+
+ ## lets you use
+ ## Tulipmania.configure do |config|
+ ## config.address = 'Anne'
+ ## end
+
+ def self.configure
+ yield( config )
+ end
+
+ def self.config
+ @config ||= Configuration.new
+ end
+
+
+ ## add command line binary (tool) e.g. $ try centralbank -h
+ def self.main
+ Tool.new.run(ARGV)
+ end
+end # module Tulipmania
+
+
+
+# say hello
+puts Tulipmania::Service.banner
diff --git a/tulipmania/lib/tulipmania/block.rb b/tulipmania/lib/tulipmania/block.rb
new file mode 100644
index 0000000..8f4b1de
--- /dev/null
+++ b/tulipmania/lib/tulipmania/block.rb
@@ -0,0 +1,40 @@
+
+
+Block = BlockchainLite::ProofOfWork::Block
+
+## see https://github.com/openblockchains/blockchain.lite.rb/blob/master/lib/blockchain-lite/proof_of_work/block.rb
+
+
+######
+# add more methods
+
+class Block
+
+def to_h
+ { index: @index,
+ timestamp: @timestamp,
+ nonce: @nonce,
+ transactions: @transactions.map { |tx| tx.to_h },
+ previous_hash: @previous_hash }
+end
+
+def self.from_h( h )
+ transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
+
+ ## parse iso8601 format e.g 2017-10-05T22:26:12-04:00
+ timestamp = Time.parse( h['timestamp'] )
+
+ self.new( h['index'],
+ transactions,
+ h['previous_hash'],
+ timestamp: timestamp,
+ nonce: h['nonce'].to_i )
+end
+
+
+def valid?
+ true ## for now always valid
+end
+
+
+end # class Block
diff --git a/tulipmania/lib/tulipmania/blockchain.rb b/tulipmania/lib/tulipmania/blockchain.rb
new file mode 100644
index 0000000..da882c4
--- /dev/null
+++ b/tulipmania/lib/tulipmania/blockchain.rb
@@ -0,0 +1,56 @@
+
+
+
+class Blockchain
+ extend Forwardable
+ def_delegators :@chain, :[], :size, :each, :empty?, :any?, :last
+
+
+ def initialize( chain=[] )
+ @chain = chain
+ end
+
+ def timestamp1637
+ ## change year to 1637 :-)
+ ## note: time (uses signed integer e.g. epoch/unix time starting in 1970 with 0)
+ ## todo: add nano/mili-seconds - why? why not? possible?
+ now = Time.now.utc.to_datetime
+ past = DateTime.new( 1637, now.month, now.mday, now.hour, now.min, now.sec, now.zone )
+ past
+ end
+
+ def <<( txs )
+ ## todo: check if is block or array
+ ## if array (of transactions) - auto-add (build) block
+ ## allow block - why? why not?
+ ## for now just use transactions (keep it simple :-)
+
+ if @chain.size == 0
+ block = Block.first( txs, timestamp: timestamp1637 )
+ else
+ block = Block.next( @chain.last, txs, timestamp: timestamp1637 )
+ end
+ @chain << block
+ end
+
+
+
+ def as_json
+ @chain.map { |block| block.to_h }
+ end
+
+ def transactions
+ ## "accumulate" get all transactions from all blocks "reduced" into a single array
+ @chain.reduce( [] ) { |acc, block| acc + block.transactions }
+ end
+
+
+
+ def self.from_json( data )
+ ## note: assumes data is an array of block records/objects in json
+ chain = data.map { |h| Block.from_h( h ) }
+ self.new( chain )
+ end
+
+
+end # class Blockchain
diff --git a/tulipmania/lib/tulipmania/cache.rb b/tulipmania/lib/tulipmania/cache.rb
new file mode 100644
index 0000000..b7f520f
--- /dev/null
+++ b/tulipmania/lib/tulipmania/cache.rb
@@ -0,0 +1,22 @@
+
+
+class Cache
+ def initialize( name )
+ @name = name
+ end
+
+ def write( data )
+ File.open( @name, 'w:utf-8' ) do |f|
+ f.write JSON.pretty_generate( data )
+ end
+ end
+
+ def read
+ if File.exists?( @name )
+ data = File.open( @name, 'r:bom|utf-8' ).read
+ JSON.parse( data )
+ else
+ nil
+ end
+ end
+end ## class Cache
diff --git a/tulipmania/lib/tulipmania/exchange.rb b/tulipmania/lib/tulipmania/exchange.rb
new file mode 100644
index 0000000..377e5b6
--- /dev/null
+++ b/tulipmania/lib/tulipmania/exchange.rb
@@ -0,0 +1,112 @@
+
+
+
+
+class Exchange
+ attr_reader :pending, :chain, :ledger
+
+
+ def initialize( address )
+ @address = address
+
+ @cache = Cache.new( "data.#{address.downcase}.json" )
+ h = @cache.read
+ if h
+ ## restore blockchain
+ @chain = Blockchain.from_json( h['chain'] )
+ ## restore pending transactions too
+ @pending = Pool.from_json( h['transactions'] )
+ else
+ @chain = Blockchain.new
+ @chain << [Tx.new( Tulipmania.config.rand_coinbase,
+ @address,
+ Tulipmania.config.mining_reward,
+ Tulipmania.config.rand_tulip )] # genesis (big bang!) starter block
+ @pending = Pool.new
+ end
+
+ ## update ledger (balances) with confirmed transactions
+ @ledger = Ledger.new( @chain )
+ end
+
+
+
+ def mine_block!
+ add_transaction( Tx.new( Tulipmania.config.rand_coinbase,
+ @address,
+ Tulipmania.config.mining_reward,
+ Tulipmania.config.rand_tulip ))
+
+ ## add mined (w/ computed/calculated hash) block
+ @chain << @pending.transactions
+ @pending = Pool.new
+
+ ## update ledger (balances) with new confirmed transactions
+ @ledger = Ledger.new( @chain )
+
+ @cache.write as_json
+ end
+
+
+ def sufficient_tulips?( wallet, qty, what )
+ ## (convenience) delegate for ledger
+ ## todo/check: use address instead of wallet - why? why not?
+ ## for now single address wallet (that is, wallet==address)
+ @ledger.sufficient_tulips?( wallet, qty, what )
+ end
+
+
+ def add_transaction( tx )
+ if tx.valid? && transaction_is_new?( tx )
+ @pending << tx
+ @cache.write as_json
+ return true
+ else
+ return false
+ end
+ end
+
+
+ ##
+ # check - how to name incoming chain - chain_new, chain_candidate - why? why not?
+ # what's an intuitive name - what's gets used most often???
+
+ def resolve!( chain_new )
+ # TODO this does not protect against invalid block shapes (bogus COINBASE transactions for example)
+
+ if !chain_new.empty? && chain_new.last.valid? && chain_new.size > @chain.size
+ @chain = chain_new
+ ## update ledger (balances) with new confirmed transactions
+ @ledger = Ledger.new( @chain )
+
+ ## document - keep only pending (unconfirmed) transaction not yet in blockchain ????
+ @pending.update!( @chain.transactions)
+
+ @cache.write as_json
+ return true
+ else
+ return false
+ end
+ end
+
+
+
+ def as_json
+ { chain: @chain.as_json,
+ transactions: @pending.as_json
+ }
+ end
+
+
+
+private
+
+ def transaction_is_new?( tx_new )
+ ## check if tx exists already in blockchain or pending tx pool
+
+ ## todo: use chain.include? to check for include
+ ## avoid loop and create new array for check!!!
+ (@chain.transactions + @pending.transactions).none? { |tx| tx_new.id == tx.id }
+ end
+
+end ## class Exchange
diff --git a/tulipmania/lib/tulipmania/ledger.rb b/tulipmania/lib/tulipmania/ledger.rb
new file mode 100644
index 0000000..143dc6b
--- /dev/null
+++ b/tulipmania/lib/tulipmania/ledger.rb
@@ -0,0 +1,34 @@
+
+class Ledger
+ attr_reader :wallets ## use addresses - why? why not? for now single address wallet (wallet==address)
+
+ def initialize( chain=[] )
+ @wallets = {}
+ chain.each do |block|
+ apply_transactions( block.transactions )
+ end
+ end
+
+ def sufficient_tulips?( wallet, qty, what )
+ return true if Tulipmania.config.coinbase?( wallet )
+
+ @wallets.has_key?( wallet ) &&
+ @wallets[wallet].has_key?( what ) &&
+ @wallets[wallet][what] - qty >= 0
+ end
+
+
+private
+
+ def apply_transactions( transactions )
+ transactions.each do |tx|
+ if sufficient_tulips?(tx.from, tx.qty, tx.what)
+ @wallets[tx.from][tx.what] -= tx.qty unless Tulipmania.config.coinbase?( tx.from )
+ @wallets[tx.to] ||= {} ## make sure wallet exists (e.g. init with empty hash {})
+ @wallets[tx.to][tx.what] ||= 0
+ @wallets[tx.to][tx.what] += tx.qty
+ end
+ end
+ end
+
+end ## class Ledger
diff --git a/tulipmania/lib/tulipmania/node.rb b/tulipmania/lib/tulipmania/node.rb
new file mode 100644
index 0000000..0434d2b
--- /dev/null
+++ b/tulipmania/lib/tulipmania/node.rb
@@ -0,0 +1,82 @@
+
+
+class Node
+ attr_reader :id, :peers, :wallet, :exchange
+
+
+ def initialize( address: )
+ @id = SecureRandom.uuid
+ @peers = []
+ @wallet = Wallet.new( address )
+ @exchange = Exchange.new @wallet.address
+ end
+
+
+ def on_add_peer( host, port )
+ @peers << [host, port]
+ @peers.uniq!
+ # TODO/FIX: no need to send to every peer, just the new one
+ send_chain_to_peers
+ @exchange.pending.each { |tx| send_transaction_to_peers( tx ) }
+ end
+
+ def on_delete_peer( index )
+ @peers.delete_at( index )
+ end
+
+
+ def on_add_transaction( from, to, qty, what, id )
+ ## note: for now must always pass in id - why? why not? possible tx without id???
+ tx = Tx.new( from, to, qty, what, id )
+ if @exchange.sufficient_tulips?( tx.from, tx.qty, tx.what ) && @exchange.add_transaction( tx )
+ send_transaction_to_peers( tx )
+ return true
+ else
+ return false
+ end
+ end
+
+ def on_send( to, qty, what )
+ tx = @wallet.generate_transaction( to, qty, what )
+ if @exchange.sufficient_tulips?( tx.from, tx.qty, tx.what ) && @exchange.add_transaction( tx )
+ send_transaction_to_peers( tx )
+ return true
+ else
+ return false
+ end
+ end
+
+
+ def on_mine!
+ @exchange.mine_block!
+ send_chain_to_peers
+ end
+
+ def on_resolve( data )
+ chain_new = Blockchain.from_json( data )
+ if @exchange.resolve!( chain_new )
+ send_chain_to_peers
+ return true
+ else
+ return false
+ end
+ end
+
+
+
+private
+
+ def send_chain_to_peers
+ data = JSON.pretty_generate( @exchange.as_json ) ## payload in json
+ @peers.each do |(host, port)|
+ Net::HTTP.post(URI::HTTP.build(host: host, port: port, path: '/resolve'), data )
+ end
+ end
+
+ def send_transaction_to_peers( tx )
+ @peers.each do |(host, port)|
+ Net::HTTP.post_form(URI::HTTP.build(host: host, port: port, path: '/transactions'), tx.to_h )
+ end
+ end
+
+end ## class Node
diff --git a/tulipmania/lib/tulipmania/pool.rb b/tulipmania/lib/tulipmania/pool.rb
new file mode 100644
index 0000000..d9002b1
--- /dev/null
+++ b/tulipmania/lib/tulipmania/pool.rb
@@ -0,0 +1,42 @@
+####################################
+# pending (unconfirmed) transactions (mem) pool
+
+class Pool
+ extend Forwardable
+ def_delegators :@transactions, :[], :size, :each, :empty?, :any?
+
+
+ def initialize( transactions=[] )
+ @transactions = transactions
+ end
+
+ def transactions() @transactions; end
+
+ def <<( tx )
+ @transactions << tx
+ end
+
+
+ def update!( txns_confirmed )
+ ## find a better name?
+ ## remove confirmed transactions from pool
+
+ ## document - keep only pending transaction not yet (confirmed) in blockchain ????
+ @transactions = @transactions.select do |tx_unconfirmed|
+ txns_confirmed.none? { |tx_confirmed| tx_confirmed.id == tx_unconfirmed.id }
+ end
+ end
+
+
+
+ def as_json
+ @transactions.map { |tx| tx.to_h }
+ end
+
+ def self.from_json( data )
+ ## note: assumes data is an array of block records/objects in json
+ transactions = data.map { |h| Tx.from_h( h ) }
+ self.new( transactions )
+ end
+
+end # class Pool
diff --git a/tulipmania/lib/tulipmania/service.rb b/tulipmania/lib/tulipmania/service.rb
new file mode 100644
index 0000000..e8bbcb1
--- /dev/null
+++ b/tulipmania/lib/tulipmania/service.rb
@@ -0,0 +1,123 @@
+
+
+module Tulipmania
+
+class Service < Sinatra::Base
+
+ def self.banner
+ "tulipmania/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
+ end
+
+
+PUBLIC_FOLDER = "#{Tulipmania.root}/lib/tulipmania/public"
+VIEWS_FOLDER = "#{Tulipmania.root}/lib/tulipmania/views"
+
+set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
+set :views, VIEWS_FOLDER # set up the views dir
+
+set :static, true # set up static file routing -- check - still needed?
+
+
+set connections: []
+
+
+get '/style.css' do
+ scss :style ## note: converts (pre-processes) style.scss to style.css
+end
+
+
+get '/' do
+ @node = node
+ erb :index
+end
+
+post '/send' do
+ node.on_send( params[:to], params[:qty].to_i, params[:what] )
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
+ redirect '/'
+end
+
+
+post '/transactions' do
+ if node.on_add_transaction(
+ params[:from],
+ params[:to],
+ params[:qty].to_i,
+ params[:what],
+ params[:id]
+ )
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
+ end
+ redirect '/'
+end
+
+post '/mine' do
+ node.on_mine!
+ redirect '/'
+end
+
+post '/peers' do
+ node.on_add_peer( params[:host], params[:port].to_i )
+ redirect '/'
+end
+
+post '/peers/:index/delete' do
+ node.on_delete_peer( params[:index].to_i )
+ redirect '/'
+end
+
+
+
+post '/resolve' do
+ data = JSON.parse(request.body.read)
+ if data['chain'] && node.on_resolve( data['chain'] )
+ status 202 ### 202 Accepted; see httpstatuses.com/202
+ settings.connections.each { |out| out << "data: resolved\n\n" }
+ else
+ status 200 ### 200 OK
+ end
+end
+
+
+get '/events', provides: 'text/event-stream' do
+ stream :keep_open do |out|
+ settings.connections << out
+ out.callback { settings.connections.delete(out) }
+ end
+end
+
+private
+
+#########
+## return network node (built and configured on first use)
+## fix: do NOT use @@ - use a class level method or something
+def node
+ if defined?( @@node )
+ @@node
+ else
+ puts "[debug] tulipmania - build (network) node (address: #{Tulipmania.config.address})"
+ @@node = Node.new( address: Tulipmania.config.address )
+ @@node
+ end
+ ####
+ ## check why this is a syntax error:
+ ## @node ||= do
+ ## puts "[debug] tulipmania - build (network) node (address: #{Tulipmania.config.address})"
+ ## @node = Node.new( address: Tulipmania.config.address )
+ ## end
+end
+
+
+############
+## helpers
+
+def fmt_tulips( hash )
+ lines = []
+ hash.each do |what,qty|
+ lines << "#{what} × #{qty}"
+ end
+ lines.join( ', ' )
+end
+
+end # class Service
+end # module Tulipmania
diff --git a/tulipmania/lib/tulipmania/tool.rb b/tulipmania/lib/tulipmania/tool.rb
new file mode 100644
index 0000000..ee11e6e
--- /dev/null
+++ b/tulipmania/lib/tulipmania/tool.rb
@@ -0,0 +1,66 @@
+# encoding: utf-8
+
+
+module Tulipmania
+
+class Tool
+
+def run( args )
+ opts = {}
+
+ parser = OptionParser.new do |cmd|
+ cmd.banner = "Usage: tulipmania [options]"
+
+ cmd.separator ""
+ cmd.separator " Wallet options:"
+
+ cmd.on("-n", "--name=NAME", "Address name (default: Anne)") do |name|
+ ## use -a or --adr or --address as option flag - why? why not?
+ ## note: default now picks a random address from WALLET_ADDRESSES
+ opts[:address] = name
+ end
+
+
+ cmd.separator ""
+ cmd.separator " Server (node) options:"
+
+ cmd.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |host|
+ opts[:Host] = host ## note: rack server handler expects :Host
+ end
+
+ cmd.on("-p", "--port PORT", "use PORT (default: 4567)") do |port|
+ opts[:Port] = port ## note: rack server handler expects :Post
+ end
+
+ cmd.on("-h", "--help", "Prints this help") do
+ puts cmd
+ exit
+ end
+ end
+
+ parser.parse!( args )
+ pp opts
+
+
+ ###################
+ ## startup server (via rack interface/handler)
+
+ app_class = Service ## use app = Service.new -- why? why not?
+ host = opts[:Host] || '0.0.0.0'
+ port = opts[:Port] || '4567'
+
+ Tulipmania.configure do |config|
+ config.address = opts[:address] || 'Anne'
+ end
+
+ Rack::Handler::WEBrick.run( app_class, Host: host, Port: port ) do |server|
+ ## todo: add traps here - why, why not??
+ end
+
+
+end ## method run
+
+
+end ## class Tool
+
+end ## module Tulipmania
diff --git a/tulipmania/lib/tulipmania/transaction.rb b/tulipmania/lib/tulipmania/transaction.rb
new file mode 100644
index 0000000..80336af
--- /dev/null
+++ b/tulipmania/lib/tulipmania/transaction.rb
@@ -0,0 +1,31 @@
+
+
+class Transaction
+
+ attr_reader :from, :to, :qty, :what, :id
+
+ def initialize( from, to, qty, what, id=SecureRandom.uuid )
+ @from = from
+ @to = to
+ @qty = qty
+ @what = what # tulip name - change to name or title - why? why not?
+ @id = id
+ end
+
+ def self.from_h( hash )
+ self.new *hash.values_at( 'from', 'to', 'qty', 'what', 'id' )
+ end
+
+ def to_h
+ { from: @from, to: @to, qty: @qty, what: @what, id: @id }
+ end
+
+
+ def valid?
+ ## check signature in the future; for now always true
+ true
+ end
+
+end # class Transaction
+
+Tx = Transaction ## add Tx shortcut / alias
diff --git a/tulipmania/lib/tulipmania/version.rb b/tulipmania/lib/tulipmania/version.rb
new file mode 100644
index 0000000..d23a059
--- /dev/null
+++ b/tulipmania/lib/tulipmania/version.rb
@@ -0,0 +1,11 @@
+# encoding: utf-8
+
+module Tulipmania
+
+ VERSION = '0.2.0'
+
+ def self.root
+ "#{File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )}"
+ end
+
+end # module Tulipmania
diff --git a/tulipmania/lib/tulipmania/views/_blockchain.erb b/tulipmania/lib/tulipmania/views/_blockchain.erb
new file mode 100644
index 0000000..85b3ece
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/_blockchain.erb
@@ -0,0 +1,37 @@
+
+
+ Blockchain
+ <%= @node.exchange.chain.size %> blocks
+
+
+
+
+ <% @node.exchange.chain.last(10).reverse.each do |block| %>
+
+
+
+ <% block.transactions.each do |tx| %>
+
+
+ <%= tx.id[0..2] %>
+ |
+
+ <%= tx.from[0..15] %> → <%= tx.to[0..15] %>
+ |
+
+ <%= tx.what %> × <%= tx.qty %>
+ |
+
+ <% end %>
+
+
+ <% end %>
+
+
+ †: Grower Transaction - New Tulips on the Market!
+
+
diff --git a/tulipmania/lib/tulipmania/views/_ledger.erb b/tulipmania/lib/tulipmania/views/_ledger.erb
new file mode 100644
index 0000000..f131ead
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/_ledger.erb
@@ -0,0 +1,15 @@
+
+
Ledger
+
+
+ Address |
+ Tulips |
+
+ <% @node.exchange.ledger.wallets.each do |address, tulips| %>
+
+ <%= address[0..15] %> |
+ <%= fmt_tulips( tulips ) %> |
+
+ <% end %>
+
+
diff --git a/tulipmania/lib/tulipmania/views/_peers.erb b/tulipmania/lib/tulipmania/views/_peers.erb
new file mode 100644
index 0000000..0c1a972
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/_peers.erb
@@ -0,0 +1,24 @@
+
+
Peers
+ <% if @node.peers.any? %>
+
+ <% @node.peers.each_with_index do |(host, port), i| %>
+ -
+ http://<%= host %>:<%= port %>
+
+
+ <% end %>
+
+ <% else %>
+
No peers
+ <% end %>
+
+
diff --git a/tulipmania/lib/tulipmania/views/_pending_transactions.erb b/tulipmania/lib/tulipmania/views/_pending_transactions.erb
new file mode 100644
index 0000000..2a9fc0f
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/_pending_transactions.erb
@@ -0,0 +1,25 @@
+
+
Pending Transactions
+ <% if @node.exchange.pending.any? %>
+
+
+ From |
+ To |
+ What |
+ Qty |
+ Id |
+
+ <% @node.exchange.pending.each do |tx| %>
+
+ <%= tx.from[0..15] %> |
+ <%= tx.to[0..15] %> |
+ <%= tx.what %> |
+ × <%= tx.qty %> |
+ <%= tx.id[0..2] %> |
+
+ <% end %>
+
+ <% else %>
+
No pending transactions
+ <% end %>
+
diff --git a/tulipmania/lib/tulipmania/views/_wallet.erb b/tulipmania/lib/tulipmania/views/_wallet.erb
new file mode 100644
index 0000000..bf1e854
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/_wallet.erb
@@ -0,0 +1,40 @@
+
+
+
+
Address
+
<%= @node.wallet.address %>
+
Tulips
+
<%= fmt_tulips(@node.exchange.ledger.wallets[@node.wallet.address] || {}) %>
+
+
+
diff --git a/tulipmania/lib/tulipmania/views/index.erb b/tulipmania/lib/tulipmania/views/index.erb
new file mode 100644
index 0000000..79bfe0a
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/index.erb
@@ -0,0 +1,30 @@
+
+
+
+ Tulipmania (Anno 1637) Node
+
+
+
+
+ Tulipmania (Anno 1637) Node
+
+
+
+ <%= erb :'_wallet' %>
+ <%= erb :'_pending_transactions' %>
+ <%= erb :'_peers' %>
+ <%= erb :'_ledger' %>
+
+
+
+ <%= erb :'_blockchain' %>
+
+
+
+
+
+
+
diff --git a/tulipmania/lib/tulipmania/views/style.scss b/tulipmania/lib/tulipmania/views/style.scss
new file mode 100644
index 0000000..005aa7a
--- /dev/null
+++ b/tulipmania/lib/tulipmania/views/style.scss
@@ -0,0 +1,172 @@
+
+body {
+ padding: 0;
+ margin: 0;
+ min-width: 960px;
+
+ font-family: 'menlo', monospace;
+ font-size: 14px;
+
+ background: #fff;
+ color: #2B2D2F;
+}
+
+
+.columns {
+ display: flex;
+
+ .left {
+ width: 50%;
+ }
+
+ .right {
+ width: 50%;
+ }
+}
+
+
+h1 {
+ font-size: 24px;
+ font-weight: normal;
+ padding-left: 15px;
+ margin-bottom: 20px;
+}
+
+
+h2 {
+ font-size: 16px;
+}
+
+h2 span {
+ font-size: 14px;
+ color: #597898;
+ font-weight: normal;
+}
+
+label {
+ display: inline-block;
+ // width: 80px;
+ text-align: right;
+ padding-right: 10px;
+}
+
+input[type=text] {
+ display: inline-block;
+ font-size: 14px;
+ padding: 8px;
+ border-radius: 0;
+ border: 0;
+}
+
+table {
+ border-spacing: 0;
+ border-collapse: collapse;
+
+ th {
+ text-align: left;
+ }
+
+ td {
+ vertical-align: top;
+ padding: 5px 15px 5px 0;
+ }
+}
+
+
+ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+input[type=submit] {
+ font-size: 14px;
+ font-family: menlo, monospace;
+ border-radius: 5px;
+ padding: 8px 20px;
+ background: #FFDC00;
+ color: #2B2D2F;
+ border: 0;
+}
+
+input[type=submit].small {
+ font-size: 10px;
+ padding: 4px 10px;
+}
+
+
+
+.wallet {
+ padding: 15px;
+ background: #7FDBFF;
+
+ h2 {
+ margin-bottom: 0;
+ }
+
+ .balance {
+ font-size: 22px;
+ }
+}
+
+
+.pending-transactions {
+ padding: 15px;
+ background: #A3E6FF;
+}
+
+.peers {
+ padding: 15px;
+ background: #C6EFFF;
+
+ li form {
+ display: inline;
+ }
+
+ li {
+ padding: 5px 0px;
+ }
+}
+
+
+.ledger {
+ padding: 15px;
+ background: #E3F7FF;
+}
+
+
+.blockchain {
+ padding: 15px;
+ position: relative;
+ background: #001F3F;
+ color: #fff;
+
+ form {
+ position: absolute;
+ top: 30px;
+ right: 15px;
+ }
+
+ .blocks {
+ border: 1px solid #597898;
+ border-bottom: 0;
+
+ .block {
+ margin: 0;
+ border-bottom: 2px dashed #597898;
+ padding: 10px;
+
+ .header {
+ text-align: center;
+ padding: 0 8px 8px 8px;
+ color: #597898;
+ border-bottom: 1px solid #354c63;
+ margin-bottom: 10px;
+ }
+
+ .id {
+ color: #597898;
+ }
+ }
+ }
+}
diff --git a/tulipmania/lib/tulipmania/wallet.rb b/tulipmania/lib/tulipmania/wallet.rb
new file mode 100644
index 0000000..ceffe17
--- /dev/null
+++ b/tulipmania/lib/tulipmania/wallet.rb
@@ -0,0 +1,15 @@
+###########
+# Single Address Wallet
+
+class Wallet
+ attr_reader :address
+
+ def initialize( address )
+ @address = address
+ end
+
+ def generate_transaction( to, qty, what )
+ Tx.new( address, to, qty, what )
+ end
+
+end # class Wallet
diff --git a/tulipmania/tulipmania.png b/tulipmania/tulipmania.png
new file mode 100644
index 0000000..4f501f7
Binary files /dev/null and b/tulipmania/tulipmania.png differ