diff --git a/examples/PayPro/bitcore.js b/examples/PayPro/bitcore.js new file mode 120000 index 0000000..af56eb8 --- /dev/null +++ b/examples/PayPro/bitcore.js @@ -0,0 +1 @@ +../../browser/bundle.js \ No newline at end of file diff --git a/examples/PayPro/customer.js b/examples/PayPro/customer.js new file mode 100644 index 0000000..3fa8711 --- /dev/null +++ b/examples/PayPro/customer.js @@ -0,0 +1,493 @@ +/** + * Payment-Customer - A Payment Protocol demonstration. + * This file will run in node or the browser. + * Copyright (c) 2014, BitPay + * https://github.com/bitpay/bitcore + */ + +;(function() { + +/** + * Global + */ + +var window = this; +var global = this; + +/** + * Platform + */ + +var isNode = !!(typeof process === 'object' && process && process.versions.node); + +// Disable strictSSL +if (isNode) { + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; +} + +/** + * Dependencies + */ + +var bitcore = require('bitcore'); +var PayPro = bitcore.PayPro; +var Transaction = bitcore.Transaction; +var TransactionBuilder = bitcore.TransactionBuilder; + +/** + * Variables + */ + +var port = 8080; + +if (isNode) { + var argv = require('optimist').argv; + if (argv.p || argv.port) { + port = +argv.p || +argv.port; + } +} else { + port = +window.location.port || 443; +} + +var merchant = isNode + ? parseMerchantURI(argv.m || argv.u || argv._[0]) + : parseMerchantURI(window.merchantURI); + +/** + * Send Payment + */ + +if (isNode) { + var Buffer = global.Buffer; +} else { + var Buffer = function Buffer(data) { + var ab = new ArrayBuffer(data.length); + var view = new Uint8Array(ab); + data._size = data.length; + for (var i = 0; i < data._size; i++) { + view[i] = data[i]; + } + if (!view.slice) { + // view.slice = ab.slice.bind(ab); + view.slice = function(start, end) { + if (end < 0) { + end = data._size + end; + } + data._size = end - start; + var ab = new ArrayBuffer(data._size); + var view = new Uint8Array(ab); + for (var i = 0, j = start; j < end; i++, j++) { + view[i] = data[j]; + } + return view; + }; + } + return view; + }; + Buffer.byteLength = function(buf) { + var bytes = 0 + , ch; + + for (var i = 0; i < buf.length; i++) { + ch = buf.charCodeAt(i); + if (ch > 0xff) { + bytes += 2; + } else { + bytes++; + } + } + + return bytes; + }; +} + +function request(options, callback) { + if (typeof options === 'string') { + options = { uri: options }; + } + + options.method = options.method || 'GET'; + options.headers = options.headers || {}; + + if (!isNode) { + var xhr = new XMLHttpRequest(); + xhr.open(options.method, options.uri, true); + + Object.keys(options.headers).forEach(function(key) { + var val = options.headers[key]; + if (key === 'Content-Length') return; + if (key === 'Content-Transfer-Encoding') return; + xhr.setRequestHeader(key, val); + }); + + // For older browsers: + // xhr.overrideMimeType('text/plain; charset=x-user-defined'); + + // Newer browsers: + xhr.responseType = 'arraybuffer'; + + xhr.onload = function(event) { + var response = xhr.response; + var buf = new Uint8Array(response); + return callback(null, xhr, buf); + }; + + if (options.body) { + xhr.send(options.body); + } else { + xhr.send(null); + } + + return; + } + + return require('request')(options, callback); +} + +function sendPayment(msg, callback) { + if (arguments.length === 1) { + callback = msg; + msg = null; + } + + return request({ + method: 'POST', + uri: 'https://localhost:' + port + '/-/request', + headers: { + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, + 'Content-Type': 'application/octet-stream', + 'Content-Length': 0 + }, + encoding: null + }, function(err, res, body) { + if (err) return callback(err); + + body = PayPro.PaymentRequest.decode(body); + + var pr = new PayPro(); + pr = pr.makePaymentRequest(body); + + var ver = pr.get('payment_details_version'); + var pki_type = pr.get('pki_type'); + var pki_data = pr.get('pki_data'); + var details = pr.get('serialized_payment_details'); + var sig = pr.get('signature'); + + // Verify Signature + var verified = pr.verify(); + + if (!verified) { + return callback(new Error('Server sent a bad signature.')); + } + + details = PayPro.PaymentDetails.decode(details); + var pd = new PayPro(); + pd = pd.makePaymentDetails(details); + var network = pd.get('network'); + var outputs = pd.get('outputs'); + var time = pd.get('time'); + var expires = pd.get('expires'); + var memo = pd.get('memo'); + var payment_url = pd.get('payment_url'); + var merchant_data = pd.get('merchant_data'); + + print('You are currently on this BTC network:'); + print(network); + print('The server sent you a message:'); + print(memo); + + var refund_outputs = []; + + var rpo = new PayPro(); + rpo = rpo.makeOutput(); + rpo.set('amount', 0); + rpo.set('script', new Buffer([ + 118, // OP_DUP + 169, // OP_HASH160 + 76, // OP_PUSHDATA1 + 20, // number of bytes + 0xcf, + 0xbe, + 0x41, + 0xf4, + 0xa5, + 0x18, + 0xed, + 0xc2, + 0x5a, + 0xf7, + 0x1b, + 0xaf, + 0xc7, + 0x2f, + 0xb6, + 0x1b, + 0xfc, + 0xfc, + 0x4f, + 0xcd, + 136, // OP_EQUALVERIFY + 172 // OP_CHECKSIG + ])); + + refund_outputs.push(rpo.message); + + // We send this to the serve after receiving a PaymentRequest + var pay = new PayPro(); + pay = pay.makePayment(); + pay.set('merchant_data', merchant_data); + pay.set('transactions', [createTX()]); + pay.set('refund_to', refund_outputs); + + msg = msg || 'Hi server, I would like to give you some money.'; + + if (isNode && argv.memo) { + msg = argv.memo; + } + + pay.set('memo', msg); + pay = pay.serialize(); + + return request({ + method: 'POST', + uri: payment_url, + headers: { + // BIP-71 + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, + 'Content-Type': 'application/bitcoin-payment', + 'Content-Length': pay.length + '', + 'Content-Transfer-Encoding': 'binary' + }, + body: pay, + encoding: null + }, function(err, res, body) { + if (err) return callback(err); + body = PayPro.PaymentACK.decode(body); + var ack = new PayPro(); + ack = ack.makePaymentACK(body); + var payment = ack.get('payment'); + var memo = ack.get('memo'); + print('Our payment was acknowledged!'); + print('Message from Merchant: %s', memo); + return callback(); + }); + }); +} + +/** + * Helpers + */ + +// URI Spec +// A backwards-compatible request: +// bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3D2a8628fc2fbe +// Non-backwards-compatible equivalent: +// bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe +function parseMerchantURI(uri) { + uri = uri || 'bitcoin:?r=https://localhost:' + port + '/-/request'; + var query, id; + if (uri.indexOf('bitcoin:') !== 0) { + throw new Error('Not a Bitcoin URI.'); + } + if (~uri.indexOf(':?')) { + query = uri.split(':?')[1]; + } else { + // Legacy URI + uri = uri.substring('bitcoin:'.length); + uri = uri.split('?'); + id = uri[0]; + query = uri[1]; + } + query = parseQS(query); + if (!query.r) { + throw new Error('No uri.'); + } + if (id) { + query.id = id; + } + return query; +} + +function parseQS(query) { + var out = {}; + var parts = query.split('&'); + parts.forEach(function(part) { + var parts = part.split('='); + var key = parts[0]; + var value = parts[1]; + out[key] = value; + }); + return out; +} + +function createTX() { + // Addresses + var addrs = [ + 'mzTQ66VKcybz9BD1LAqEwMFp9NrBGS82sY', + 'mmu9k3KzsDMEm9JxmJmZaLhovAoRKW3zr4', + 'myqss64GNZuWuFyg5LTaoTCyWEpKH56Fgz' + ]; + + // Private keys in WIF format (see TransactionBuilder.js for other options) + var keys = [ + 'cVvr5YmWVAkVeZWAawd2djwXM4QvNuwMdCw1vFQZBM1SPFrtE8W8', + 'cPyx1hXbe3cGQcHZbW3GNSshCYZCriidQ7afR2EBsV6ReiYhSkNF' + // 'cUB9quDzq1Bj7pocenmofzNQnb1wJNZ5V3cua6pWKzNL1eQtaDqQ' + ]; + + var unspent = [{ + // http://blockexplorer.com/testnet/rawtx/1fcfe898cc2612f8b222bd3b4ac8d68bf95d43df8367b71978c184dea35bde22 + 'txid': '1fcfe898cc2612f8b222bd3b4ac8d68bf95d43df8367b71978c184dea35bde22', + 'vout': 1, + 'address': addrs[0], + 'scriptPubKey': '76a94c14cfbe41f4a518edc25af71bafc72fb61bfcfc4fcd88ac', + 'amount': 1.60000000, + 'confirmations': 9 + }, + + { + // http://blockexplorer.com/testnet/rawtx/0624c0c794447b0d2343ae3d20382983f41b915bb115a834419e679b2b13b804 + 'txid': '0624c0c794447b0d2343ae3d20382983f41b915bb115a834419e679b2b13b804', + 'vout': 1, + 'address': addrs[1], + 'scriptPubKey': '76a94c14460376539c219c5e3274d86f16b40e806b37817688ac', + 'amount': 1.60000000, + 'confirmations': 9 + } + ]; + + // define transaction output + var outs = [{ + address: addrs[2], + amount: 0.00003000 + }]; + + // set change address + var opts = { + remainderOut: { + address: addrs[0] + } + }; + + var tx = new TransactionBuilder(opts) + .setUnspent(unspent) + .setOutputs(outs) + .sign(keys) + .build(); + + print(''); + print('Customer created transaction:'); + print(tx.getStandardizedObject()); + print(''); + + return tx.serialize(); +} + +/** + * Helpers + */ + +function clientLog(args, isError) { + var log = document.getElementById('log'); + var msg = args[0]; + if (typeof msg !== 'string') { + msg = JSON.stringify(msg, null, 2); + if (isError) msg = '' + msg + ''; + log.innerHTML += msg + '\n'; + return; + } + var i = 0; + msg = msg.replace(/%[sdji]/g, function(ch) { + i++; + if (ch === 'j' || typeof args[i] !== 'string') { + return JSON.stringify(args[i]); + } + return args[i]; + }); + if (isError) msg = '' + msg + ''; + log.innerHTML += msg + '\n'; +} + +function print() { + var args = Array.prototype.slice.call(arguments); + if (!isNode) { + return clientLog(args, false); + } + var util = require('util'); + if (typeof args[0] !== 'string') { + args[0] = util.inspect(args[0], null, 20, true); + console.log('\x1b[32mCustomer:\x1b[m'); + console.log(args[0]); + return; + } + if (!args[0]) return process.stdout.write('\n'); + var msg = '\x1b[32mCustomer:\x1b[m ' + + util.format.apply(util.format, args); + return process.stdout.write(msg + '\n'); +} + +function error() { + var args = Array.prototype.slice.call(arguments); + if (!isNode) { + return clientLog(args, true); + } + var util = require('util'); + if (typeof args[0] !== 'string') { + args[0] = util.inspect(args[0], null, 20, true); + console.log('\x1b[32mCustomer:\x1b[m'); + console.log(args[0]); + return; + } + if (!args[0]) return process.stderr.write('\n'); + var msg = '\x1b[32mCustomer:\x1b[m \x1b[31m' + + util.format.apply(util.format, args) + '\x1b[m'; + return process.stderr.write(msg + '\n'); +} + +/** + * Execute + */ + +if (isNode) { + if (!module.parent) { + sendPayment(function(err) { + if (err) return error(err.message); + print('Payment sent successfully.'); + }); + } else { + var customer = sendPayment; + customer.sendPayment = sendPayment; + customer.print = print; + customer.error = error; + module.exports = customer; + } +} else { + var customer = sendPayment; + customer.sendPayment = sendPayment; + customer.print = print; + customer.error = error; + window.customer = window.sendPayment = customer; + window.onload = function() { + var form = document.getElementsByTagName('form')[0]; + var memo = document.querySelector('input[name="memo"]'); + var loader = document.getElementById('load'); + loader.style.display = 'none'; + form.onsubmit = function() { + form.style.display = 'none'; + loader.style.display = 'block'; + form.onsubmit = function() { return false; }; + customer.sendPayment(memo.value || null, function(err) { + loader.style.display = 'none'; + if (err) return error(err.message); + print('Payment sent successfully.'); + }); + return false; + }; + }; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); diff --git a/examples/PayPro/index.html b/examples/PayPro/index.html new file mode 100644 index 0000000..4905292 --- /dev/null +++ b/examples/PayPro/index.html @@ -0,0 +1,28 @@ + +Payment Protocol + + +

Payment Protocol

+ +

+ BIP-70 + is here! +

+ +
+ +

These items will cost you a total of 0.00003000 BTC.

+

Would you like to checkout?

+ + +
+ +

Loading...

+ +

+
+
+
diff --git a/examples/PayPro/index.js b/examples/PayPro/index.js
new file mode 100755
index 0000000..bfbc85d
--- /dev/null
+++ b/examples/PayPro/index.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+module.exports = require('./server');
diff --git a/examples/PayPro/package.json b/examples/PayPro/package.json
new file mode 100644
index 0000000..12afe21
--- /dev/null
+++ b/examples/PayPro/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "payment-server",
+  "description": "Payment Protocol (BIP-70) for Bitcoin",
+  "author": "Christopher Jeffrey",
+  "version": "0.0.0",
+  "main": "./index.js",
+  "bin": "./index.js",
+  "preferGlobal": false,
+  "repository": "git://github.com/bitpay/bitcore.git",
+  "homepage": "https://github.com/bitpay/bitcore",
+  "bugs": { "url": "https://github.com/bitpay/bitcore/issues" },
+  "keywords": ["bitcoin", "bip-70", "payment", "protocol"],
+  "tags": ["bitcoin", "bip-70", "payment", "protocol"],
+  "dependencies": {
+    "express": "4.6.1",
+    "request": "2.39.0",
+    "optimist": "0.6.1"
+  },
+  "engines": { "node": ">= 0.10.0" }
+}
diff --git a/examples/PayPro/server.js b/examples/PayPro/server.js
new file mode 100755
index 0000000..165faf8
--- /dev/null
+++ b/examples/PayPro/server.js
@@ -0,0 +1,376 @@
+#!/bin/bash
+
+/**
+ * Payment-Server - A Payment Protocol demonstration.
+ * Copyright (c) 2014, BitPay
+ * https://github.com/bitpay/bitcore
+ */
+
+/**
+ * Modules
+ */
+
+var https = require('https');
+var fs = require('fs');
+var path = require('path');
+var qs = require('querystring');
+var crypto = require('crypto');
+
+// Disable strictSSL
+process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
+
+/**
+ * Dependencies
+ */
+
+var argv = require('optimist').argv;
+var express = require('express');
+var bitcore = require('bitcore');
+
+var PayPro = bitcore.PayPro;
+var Transaction = bitcore.Transaction;
+var TransactionBuilder = bitcore.TransactionBuilder;
+
+/**
+ * Variables
+ */
+
+var isNode = !argv.b && !argv.browser;
+
+var app = express();
+
+var x509 = {
+  priv: fs.readFileSync(__dirname + '/../../test/data/x509.key'),
+  pub: fs.readFileSync(__dirname + '/../../test/data/x509.pub'),
+  der: fs.readFileSync(__dirname + '/../../test/data/x509.der'),
+  pem: fs.readFileSync(__dirname + '/../../test/data/x509.crt')
+};
+
+var server = https.createServer({
+  key: fs.readFileSync(__dirname + '/../../test/data/x509.key'),
+  cert: fs.readFileSync(__dirname + '/../../test/data/x509.crt')
+});
+
+/**
+ * Ignore Cache Headers
+ * Allow CORS
+ * Accept Payments
+ */
+
+app.use(function(req, res, next) {
+  var setHeader = res.setHeader;
+
+  res.setHeader = function(name) {
+    switch (name) {
+      case 'Cache-Control':
+      case 'Last-Modified':
+      case 'ETag':
+        return;
+    }
+    return setHeader.apply(res, arguments);
+  };
+
+  res.setHeader('Access-Control-Allow-Origin', '*');
+
+  res.setHeader('Accept', PayPro.PAYMENT_CONTENT_TYPE);
+
+  return next();
+});
+
+/**
+ * Body Parser
+ */
+
+app.use('/-/pay', function(req, res, next) {
+  var buf = [];
+
+  req.on('error', function(err) {
+    error('Request Error: %s', err.message);
+    try {
+      req.socket.destroy();
+    } catch (e) {
+      ;
+    }
+  });
+
+  req.on('data', function(data) {
+    buf.push(data);
+  });
+
+  req.on('end', function(data) {
+    if (data) buf.push(data);
+    buf = Buffer.concat(buf, buf.length);
+    req.paymentData = buf;
+    return next();
+  })
+});
+
+/**
+ * Router
+ */
+
+// Not used in express 4.x
+// app.use(app.router);
+
+/**
+ * Receive "I want to pay"
+ */
+
+app.uid = 0;
+
+app.post('/-/request', function(req, res, next) {
+  print('Received payment "request" from %s.', req.socket.remoteAddress);
+
+  var outputs = [];
+
+  var po = new PayPro();
+  po = po.makeOutput();
+  // number of satoshis to be paid
+  po.set('amount', 0);
+  // a TxOut script where the payment should be sent. similar to OP_CHECKSIG
+  po.set('script', new Buffer([
+    118, // OP_DUP
+    169, // OP_HASH160
+    76,  // OP_PUSHDATA1
+    20,  // number of bytes
+    0xcf,
+    0xbe,
+    0x41,
+    0xf4,
+    0xa5,
+    0x18,
+    0xed,
+    0xc2,
+    0x5a,
+    0xf7,
+    0x1b,
+    0xaf,
+    0xc7,
+    0x2f,
+    0xb6,
+    0x1b,
+    0xfc,
+    0xfc,
+    0x4f,
+    0xcd,
+    136, // OP_EQUALVERIFY
+    172  // OP_CHECKSIG
+  ]));
+
+  outputs.push(po.message);
+
+  /**
+   * Payment Details
+   */
+
+  var mdata = new Buffer([0]);
+  app.uid++;
+  if (app.uid > 0xffff) {
+    throw new Error('UIDs bigger than 0xffff not supported.');
+  } else if (app.uid > 0xff) {
+    mdata = new Buffer([(app.uid >> 8) & 0xff, (app.uid >> 0) & 0xff])
+  } else {
+    mdata = new Buffer([0, app.uid])
+  }
+  var now = Date.now() / 1000 | 0;
+  var pd = new PayPro();
+  pd = pd.makePaymentDetails();
+  pd.set('network', 'test');
+  pd.set('outputs', outputs);
+  pd.set('time', now);
+  pd.set('expires', now * 60 * 60 * 24);
+  pd.set('memo', 'Hello, this is the server, we would like some money.');
+  var port = +req.headers.host.split(':')[1] || server.port;
+  pd.set('payment_url', 'https://localhost:' + port + '/-/pay');
+  pd.set('merchant_data', mdata);
+
+  /*
+   * PaymentRequest
+   */
+
+  var cr = new PayPro();
+  cr = cr.makeX509Certificates();
+  cr.set('certificate', [x509.der]);
+
+  // We send the PaymentRequest to the customer
+  var pr = new PayPro();
+  pr = pr.makePaymentRequest();
+  pr.set('payment_details_version', 1);
+  pr.set('pki_type', 'x509+sha256');
+  pr.set('pki_data', cr.serialize());
+  pr.set('serialized_payment_details', pd.serialize());
+  pr.sign(x509.priv);
+
+  pr = pr.serialize();
+
+  // BIP-71 - set the content-type
+  res.setHeader('Content-Type', PayPro.PAYMENT_REQUEST_CONTENT_TYPE);
+  res.setHeader('Content-Length', pr.length + '');
+  res.setHeader('Content-Transfer-Encoding', 'binary');
+
+  res.send(pr);
+});
+
+/**
+ * Receive Payment
+ */
+
+app.post('/-/pay', function(req, res, next) {
+  var body = req.paymentData;
+
+  body = PayPro.Payment.decode(body);
+
+  var pay = new PayPro();
+  pay = pay.makePayment(body);
+  var merchant_data = pay.get('merchant_data');
+  var transactions = pay.get('transactions');
+  var refund_to = pay.get('refund_to');
+  var memo = pay.get('memo');
+
+  print('Received payment from %s.', req.socket.remoteAddress);
+  print('Customer Message: %s', memo);
+  print('Payment Message:');
+  print(pay);
+
+  // We send this to the customer after receiving a Payment
+  // Then we propogate the transaction through bitcoin network
+  var ack = new PayPro();
+  ack = ack.makePaymentACK();
+  ack.set('payment', pay.message);
+  ack.set('memo', 'Thank you for your payment!');
+
+  ack = ack.serialize();
+
+  // BIP-71 - set the content-type
+  res.setHeader('Content-Type', PayPro.PAYMENT_ACK_CONTENT_TYPE);
+  res.setHeader('Content-Length', ack.length + '');
+  res.setHeader('Content-Transfer-Encoding', 'binary');
+
+  transactions = transactions.map(function(tx) {
+    tx.buffer = tx.buffer.slice(tx.offset, tx.limit);
+    var ptx = new bitcore.Transaction();
+    ptx.parse(tx.buffer);
+    return ptx;
+  });
+
+  (function retry() {
+    var timeout = setTimeout(function() {
+      if (conn) {
+        transactions.forEach(function(tx) {
+          var id = tx.getHash().toString('hex');
+          print('');
+          print('Sending transaction with txid: %s', id);
+          print(tx.getStandardizedObject());
+
+          var pending = 1;
+          peerman.on('ack', function listener() {
+            if (!--pending) {
+              peerman.removeListener('ack', listener);
+              clearTimeout(timeout);
+              print('Transaction sent to peer successfully.');
+            }
+          });
+
+          print('Broadcasting transaction...');
+          conn.sendTx(tx);
+        });
+      } else {
+        print('No BTC network connection. Retrying...');
+        conn = peerman.getActiveConnection();
+        retry();
+      }
+    }, 1000);
+  })();
+
+  res.send(ack);
+});
+
+/**
+ * Bitcoin
+ */
+
+var conn;
+
+var peerman = new bitcore.PeerManager({
+  network: 'testnet'
+});
+
+peerman.peerDiscovery = true;
+
+peerman.addPeer(new bitcore.Peer('testnet-seed.bitcoin.petertodd.org', 18333));
+peerman.addPeer(new bitcore.Peer('testnet-seed.bluematt.me', 18333));
+
+peerman.on('connect', function() {
+  conn = peerman.getActiveConnection();
+});
+
+peerman.start();
+
+/**
+ * File Access
+ */
+
+app.use(express.static(__dirname));
+
+/**
+ * Helpers
+ */
+
+var bitcorePath = path.dirname(require.resolve('bitcore/package.json'));
+var log = require(bitcorePath + '/util/log');
+
+log.err = error;
+log.debug = error;
+log.info = print;
+
+var util = require('util');
+
+function print() {
+  var args = Array.prototype.slice.call(arguments);
+  if (typeof args[0] !== 'string') {
+    args[0] = util.inspect(args[0], null, 20, true);
+    console.log('\x1b[34mServer:\x1b[m');
+    console.log(args[0]);
+    return;
+  }
+  if (!args[0]) return process.stdout.write('\n');
+  var msg = '\x1b[34mServer:\x1b[m '
+    + util.format.apply(util.format, args);
+  return process.stdout.write(msg + '\n');
+}
+
+function error() {
+  var args = Array.prototype.slice.call(arguments);
+  if (typeof args[0] !== 'string') {
+    args[0] = util.inspect(args[0], null, 20, true);
+    console.log('\x1b[34mServer:\x1b[m');
+    console.log(args[0]);
+    return;
+  }
+  if (!args[0]) return process.stderr.write('\n');
+  var msg = '\x1b[34mServer:\x1b[m \x1b[31m'
+    + util.format.apply(util.format, args)
+    + '\x1b[m';
+  return process.stderr.write(msg + '\n');
+}
+
+/**
+ * Start Server
+ */
+
+server.on('request', app);
+server.app = app;
+server.port = +argv.p || +argv.port || 8080;
+
+if (!module.parent || path.basename(module.parent.filename) === 'index.js') {
+  server.listen(server.port, function(addr) {
+    if (!isNode) return;
+    var customer = require('./customer');
+    customer.sendPayment(function(err) {
+      if (err) return error(err.message);
+      customer.print('Payment sent successfully.');
+    });
+  });
+} else {
+  module.exports = server;
+}
diff --git a/examples/PayPro/style.css b/examples/PayPro/style.css
new file mode 100644
index 0000000..0d5a080
--- /dev/null
+++ b/examples/PayPro/style.css
@@ -0,0 +1,66 @@
+/**
+ * Stylesheet for Payment Protocol
+ */
+
+/**
+ * Raleway
+ */
+
+@font-face {
+  font-family: 'Raleway';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Raleway'), url(http://themes.googleusercontent.com/static/fonts/raleway/v7/cIFypx4yrWPDz3zOxk7hIQLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
+}
+
+/**
+ * Ubuntu
+ */
+
+@font-face {
+  font-family: 'Ubuntu';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Ubuntu'), url(https://themes.googleusercontent.com/static/fonts/ubuntu/v5/lhhB5ZCwEkBRbHMSnYuKyA.ttf) format('truetype');
+}
+
+article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary {
+	display: block
+}
+
+html {
+  width: 840px;
+  font-family: "Raleway", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", "Helvetica", "Verdana", sans-serif;
+  font-size: 22px;
+  line-height: 30px;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  color: #000;
+  background-image: -webkit-gradient( linear, 0 0, 0 100%, color-stop(0, rgba(0, 0, 0, 0.15)), color-stop(0.2, transparent), color-stop(0.8, transparent), color-stop(1, rgba(0, 0, 0, 0.15)));
+  background-image: -moz-linear-gradient( -90deg, rgba(0, 0, 0, 0.15) 0%, transparent 20%, transparent 80%, rgba(0, 0, 0, 0.15) 100%);
+  background-attachment: fixed;
+  background-color: #c1d3e3;
+}
+
+input {
+  display: block;
+  margin: 0 0 20px 0;
+  font-family: "Raleway", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", "Helvetica", "Verdana", sans-serif;
+  font-size: 22px;
+  line-height: 30px;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  color: #000;
+}
+
+body {
+  padding: 20px;
+  text-shadow: rgba(0, 0, 0, 0.025) 0 -1px 0, rgba(255, 255, 255, 0.2) 0 1px 0;
+}
+
+h1 {
+  width: 350px;
+  color: #000;
+  font: 60px/1.0 "Ubuntu", "Helvetica", "Verdana", "Arial", sans-serif;
+  margin-left: 20px;
+}