Merge pull request #456 from chjj/paypro_example
Add payment protocol example
This commit is contained in:
commit
b43b93c002
|
@ -0,0 +1 @@
|
||||||
|
../../browser/bundle.js
|
|
@ -0,0 +1,495 @@
|
||||||
|
/**
|
||||||
|
* 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 = isNode
|
||||||
|
? require('../../')
|
||||||
|
: 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);
|
||||||
|
}());
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
module.exports = require('./server');
|
|
@ -0,0 +1,375 @@
|
||||||
|
#!/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('../../');
|
||||||
|
|
||||||
|
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 log = require('../../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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -114,6 +114,9 @@
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sinon": "^1.10.3"
|
"sinon": "^1.10.3",
|
||||||
|
"express": "4.6.1",
|
||||||
|
"request": "2.39.0",
|
||||||
|
"optimist": "0.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue