paypro: add payment protocol example.

This commit is contained in:
Christopher Jeffrey 2014-07-24 23:07:31 -07:00
parent 078d85ea19
commit 8c74b94791
7 changed files with 987 additions and 0 deletions

1
examples/PayPro/bitcore.js Symbolic link
View File

@ -0,0 +1 @@
../../browser/bundle.js

493
examples/PayPro/customer.js Normal file
View File

@ -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 = '<span style="color:red;">' + msg + '</span>';
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 = '<span style="color:red;">' + msg + '</span>';
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);
}());

View File

@ -0,0 +1,28 @@
<!doctype html>
<title>Payment Protocol</title>
<link rel="stylesheet" href="/style.css">
<h1>Payment Protocol</h1>
<p>
<a href="https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki"><strong>BIP-70</strong></a>
is here!
</p>
<form method="POST" action="/-/request">
<ul>
<li>BitPay T-Shirt: <strong>0.00002000 BTC</strong></li>
<li>BitPay Mug: <strong>0.00001000 BTC</strong></li>
</ul>
<p>These items will cost you a total of <strong>0.00003000 BTC</strong>.</p>
<p>Would you like to checkout?</p>
<input type="text" name="memo" placeholder="Message to merchant..." value="">
<input type="submit" value="Checkout">
</form>
<p id="load">Loading...</p>
<pre id="log"></pre>
<script src="./bitcore.js"></script>
<script src="./customer.js"></script>

3
examples/PayPro/index.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
module.exports = require('./server');

View File

@ -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" }
}

376
examples/PayPro/server.js Executable file
View File

@ -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;
}

66
examples/PayPro/style.css Normal file
View File

@ -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;
}