Skip to content

Commit

Permalink
PHP command-line util for creating payment requests
Browse files Browse the repository at this point in the history
  • Loading branch information
gavinandresen committed Mar 28, 2013
1 parent 6ce98a0 commit 5f4a9b7
Show file tree
Hide file tree
Showing 2 changed files with 240 additions and 0 deletions.
81 changes: 81 additions & 0 deletions php/base58.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

//
// Decode a "base58check" address into its parts: one-byte version, 20-byte hash, 4-byte checksum.
// Based on code from Jeff Garziks picocoin project.
//
// Returns either false or an array with (version, hash, checksum)
// Relies on bcmath to do the heavy lifting.
//
function decode_base58($btcaddress)
{
// Compute big base58 number:
$chars="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
$n = "0";
for ($i = 0; $i < strlen($btcaddress); $i++) {
$p1 = strpos($chars, $btcaddress[$i]);
if ($p1 === false) return false;
$n = bcmul($n, "58");
$n = bcadd($n, (string)$p1);
}
// Peel off bytes to get checksum / hash / version:
$checksum = "";
for ($i = 0; $i < 4; $i++) {
$byte = bcmod($n, "256");
$checksum = chr((int)$byte) . $checksum;
$n = bcdiv($n, "256");
}
$hash = "";
for ($i = 0; $i < 20; $i++) {
$byte = bcmod($n, "256");
$hash = chr((int)$byte) . $hash;
$n = bcdiv($n, "256");
}
$version = (int)$n;

// Make sure checksum is correct:
$check = hash('sha256', hash('sha256', chr($version).$hash, true), true);
if (substr($check,0,4) != $checksum) return false;

return array($version, $hash, $checksum);
}

//
// Convert a Bitcoin address to a raw-bytes Script
//
// Returns false if passed an invalid Bitcoin address,
// otherwise returns an array
// containing (boolean fTestnet, string Script)
//
function address_to_script($btcaddress)
{
$vhc = decode_base58($btcaddress);
if ($vhc === False) return False;

$version = $vhc[0];
$hash = $vhc[1];

$testnet = false;
$script = "";
switch ($version) {
case 111:
$testnet = true; // ... fall through
case 0:
// Pay to public key:
// DUP HASH160 push-0x14-bytes ...hash... EQUALVERIFY CHECKSIG
$script = "\x76\xa9\x14".$hash."\x88\xac";
break;
case 196:
$testnet = true; // ... fall through
case 5:
// Pay to script hash:
// HASH160 push-0x14-bytes ... hash ... EQUAL
$script = "\xa9\x14".$hash."\x87";
break;
default:
return false;
}
return array($testnet, $script);
}

?>
159 changes: 159 additions & 0 deletions php/paymentrequest-create.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env php
<?php

// Command-line utility to create a payment request and
// send it to standard output, to demonstrate how you'd use
// the code in paymentrequest.php / certificates.php in web server
// code
//

$usage = <<<USAGE
Required arguments:
paytoaddress= : one of your bitcoin addresses (ideally, a unique-per-customer address)
certificate= : PEM-encoded file containing certificate
privatekey= : PEM file containing private key for certificate
Optional:
amount= : amount (in BTC) that needs to be paid
memo= : message to user
expires= : unix timestamp (integer) when this Request expires
receipt_url= : URL where a Payment message should be sent
out= : file to write to (default: standard output)
single_use= : 1 is single-use, 0 is multi-use
USAGE;

// Protocol buffer stuff:
require_once 'DrSlump/Protobuf.php';
\DrSlump\Protobuf::autoload();
require_once 'paymentrequest.php';

// Certificate handling stuff:
require_once 'certificates.php';

// Bitcoin address stuff:
require_once 'base58.php';

$details = new \payments\PaymentDetails();
$details->setTime(time());

$paymentRequest = new \payments\PaymentRequest();

$payto = NULL;
$amount = NULL;
$certificate = NULL;
$privatekey = NULL;
$outfile = NULL;

for ($i = 1; $i < $argc; $i++) {
$keyval = explode("=", $argv[$i]);
if (count($keyval) != 2) {
echo "Unrecognized argument: ".$argv[$i]."\n";
echo $usage;
exit(1);
}
$key = trim($keyval[0], "-");
$val = $keyval[1];
switch ($key) {
case "paytoaddress":
$payto = $val;
break;
case "certificate":
$certificate = $val;
break;
case "privatekey":
$privatekey = $val;
break;
case "amount":
$amount = $val;
break;
case "memo":
$details->setMemo($val);
break;
case "expires":
$details->setExpires($val);
break;
case "receipt_url":
$details->setReceiptUrl($val);
break;
case "out":
$outfile = $val;
break;
case "single_use":
$details->setSingleUse($val ? true : false);
break;
default:
echo "Unrecognized argument: ".$argv[$i]."\n";
echo $usage;
exit(1);
}
}

if ($payto === NULL || $certificate === NULL || $privatekey === NULL) {
echo "You must specify paytoaddress= certificate= and privatekey=\n";
exit(1);
}

$paymentRequest->setSerializedPaymentDetails($details->serialize());

$certChain = new \payments\X509Certificates();
$leafCert = file_get_contents($certificate);

// http-fetch parent certificates. In a real web application, you should avoid
// constantly re-fetching the certificate chain, and should, instead, fetch it once
// and then store it in your database or in a file on disk and only re-fetch it when
// your certificate expires or one of the intermediate certificates is revoked/replaced.
$certs = fetch_chain($leafCert);
foreach ($certs as $cert) {
$certChain->addCertificate($cert);
}

//
// Create signature
//
$paymentRequest->setPkiType("x509+sha1");
$paymentRequest->setPkiData($certChain->serialize());

$priv_key = file_get_contents($privatekey);
$pkeyid = openssl_get_privatekey($priv_key);

$paymentRequest->setSignature("");
$dataToSign = $paymentRequest->serialize();

$signature = "";
$result = openssl_sign($dataToSign, $signature, $pkeyid, OPENSSL_ALGO_SHA1);
if ($signature === FALSE) {
echo "ERROR: signing failed.\n";
exit(1);
}
$paymentRequest->setSignature($signature);

$data = $paymentRequest->serialize();

//
// Done; output:
//

if ($outfile) {
file_put_contents($outfile, $data);
}
else {
echo $data;
}

exit(0);

// A web application serving up a payment request would do something like this:

header('Content-Type: application/x-bitcoin-payment-request');
$filename = "r".(string)time().".bitcoinpaymentrequest"; // ... or any unique filename
header('Content-Disposition: inline; filename='.$filename);
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Length: ' . (string)strlen($data));
echo $data;

exit(0);

?>

0 comments on commit 5f4a9b7

Please sign in to comment.