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 @@ + +
+ BIP-70 + is here! +
+ + + +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; +}