Pay with Bitcoin using Coinbase and Bitpay
Setting up Bitpay
Register for a new account on bitpay , once it is done login to your account and click on My Account which is under My Information section , Now click on API Access keys and generate a new key using Add New API Key button . Keep your api access key safe , If your key is compromised then anyone can access your account .
This key will be used to authenticate with bitpay and access api points . You can look at bitpay api or download api documentation to understand how it works . They also provide official php library which is hosted on github . We wont be using php client we will access bitpay using basic auth and rest api .
Setting up Coinbase
Register for a new account on coinbase , once it is done login to your account and click on Settings from left sidebar , Now click on API Access and generate a new key using New API Key button , select accounts which have access using this api key and select which permissions you need . Keep your api key and api secret safe , If your key is compromised then anyone can access your account . Coinbase have two factor authentication due to which you have to enter token which is sent on mobile as sms to login or change some of the settings . If you even want to see api keys you need to go through two factor authentication which is highly secure .
This key + secret will be used to authenticate with coinbase and access api . You can look at coinbase api to understand how it works . They also provide official php library which is hosted on github . We wont be using php client we will access coinbase using API key and secret . To create invoice using api you need to complete your merchant profile details and have it verified .
Migrations
Setup your database config in app/database.php after that we will create two tables , first one to store billing details and second to store order information , our migrations are below .
php artisan migrate:make billing_table
php artisan migrate:make orders_table
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class BillingTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('billing', function(Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('address');
$table->string('city');
$table->string('state');
$table->string('zip');
$table->string('email');
$table->string('phone');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists("billing");
}
}
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class OrdersTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('orders', function(Blueprint $table) {
$table->increments('id');
$table->integer('billing_id');
$table->string('amount');
$table->string('type');
$table->string('status');
$table->text('response');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('orders');
}
}
php artisan migrate
This will execute our migrations and creates our table schema’s
Views and Routes
We need a basic view where user can enter details like amount and billing information and select checkout type which can be coinbase or bitpay .
Create a new route which will display our index page
Route::get('/','HomeController@index');
Route::post('/process', 'HomeController@processPayment');
Route::post('/notify/{bitcoin_service}', 'HomeController@notify');
In the above code we have created route to index which will display our page , a post route which will handle payment and a notify route which will handle notifications from bitpay and coinbase regarding status change of our invoice .
Our index.blade.php is below
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<style type="text/css">@-ms-viewport{width: device-width;}</style>
<title>KodeInfo - Bitcoin Payment Integration</title>
{{HTML::style('//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css')}}
</head>
<body>
<div class="container">
<div class='row'>
<div class='col-md-4'></div>
<div class='col-md-4'>
{{Form::open(['url'=>'/process','method'=>'POST'])}}
<div class='form-row'>
@include('notify')
<div class='col-xs-12 form-group'>
<label class='control-label'>Amount</label>
<input class='form-control' name="amount" value="{{Input::old('amount')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='col-xs-12 form-group'>
<label class='control-label'>Billing Name</label>
<input class='form-control' name="name" value="{{Input::old('name')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='form-row'>
<div class='col-xs-6 form-group'>
<label class='control-label'>Billing Address</label>
<input class='form-control' name="address" value="{{Input::old('address')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='col-xs-6 form-group'>
<label class='control-label'>Billing City</label>
<input class='form-control' name="city" value="{{Input::old('city')}}" type='text'>
</div>
</div>
</div>
<div class='form-row'>
<div class='form-row'>
<div class='col-xs-6 form-group'>
<label class='control-label'>Billing State</label>
<input class='form-control' name="state" value="{{Input::old('state')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='col-xs-6 form-group'>
<label class='control-label'>Billing Zip</label>
<input class='form-control' name="zip" value="{{Input::old('zip')}}" type='text'>
</div>
</div>
</div>
<div class='form-row'>
<div class='col-xs-12 form-group'>
<label class='control-label'>Billing Email</label>
<input class='form-control' name="email" value="{{Input::old('email')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='col-xs-12 form-group'>
<label class='control-label'>Billing Phone</label>
<input class='form-control' name="phone" value="{{Input::old('phone')}}" type='text'>
</div>
</div>
<div class='form-row'>
<div class='col-xs-12 form-group'>
<label class='control-label'>Pay with</label>
<select class='form-control' name="type" >
<option value="coinbase">Coinbase</option>
<option value="bitpay">Bitpay</option>
</select>
</div>
</div>
<div class='form-row'>
<div class='col-md-12 form-group'>
<button class='form-control btn btn-primary submit-button' type='submit'>Submit »</button>
</div>
</div>
{{Form::close()}}
</div>
<div class='col-md-4'></div>
</div>
</div>
{{HTML::script('//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js')}}
</body>
</html>
Our form action is /process route and method is POST .
We also have notify.blade.php which is included in our index. It checks for errors and display on index page .
Config File
Create a new file bitcoins.php inside app/config , this will store our basic information like url , api key , api secret etc .
<?php
return [
'bitpay_url' => 'https://bitpay.com/api/invoice/',
'bitpay_api_key' => '',
'bitpay_notify_url' => URL::to('/')."/notify/bitpay",
'coinbase_url' => 'https://coinbase.com/api/v1/buttons',
'coinbase_api_key' => '',
'coinbase_api_secret' => '',
'coinbase_notify_url' => URL::to('/')."/notify/coinbase",
];
Insert your bitpay_api_key , coinbase_api_key , coinbase_api_secret in our above config file
Models
Create two eloquent models , one for billing and other for orders .
<?php
class Billing extends Eloquent {
protected $table = "billing";
public $timestamps = true;
}
<?php
class Order extends Eloquent {
protected $table = "orders";
public $timestamps = true;
}
Integrating Bitpay
The /process route will execute processPayment method from HomeController , Our processPayment method will validate inputs , setup billing object , create an order object and then it will check which type of service needs to be executed . If your have selected bitpay then it will execute bitpayRequestCurl and pass order id , amount , billing object .
If there is any exception in bitpayRequestCurl it will go back to index page with inputs and error messages .
public function processPayment()
{
$v = Validator::make(["amount"=>Input::get("amount"),"email"=>Input::get("email")],
["amount"=>"required|integer","email"=>"required|email"]);
if($v->passes()){
$billing = new Billing();
$billing->name = Input::get("name");
$billing->address = Input::get("address");
$billing->city = Input::get("city");
$billing->state = Input::get("state");
$billing->zip = Input::get("zip");
$billing->email = Input::get("email");
$billing->phone = Input::get("phone");
$billing->save();
$order = new Order();
$order->amount = Input::get("amount");
$order->billing_id = $billing->id;
$order->save();
$bitcoinRedirectURL = URL::to("/");
if(Input::get('type')=="bitpay"){
try {
$bitpayResponse = $this->bitpayRequestCurl($order->id,$order->amount,$billing);
} catch (\Exception $e) {
Session::flash("error_msg",$e->getMessage());
return Redirect::back();
}
//Set order status to pending since user didnt paid yet and serialize the response maybe useful later
$order->type = "bitpay";
$order->status = "Pending";
$order->response = serialize($bitpayResponse);
$bitcoinRedirectURL = $bitpayResponse->url;
}
if(Input::get('type')=="coinbase"){
try {
$coinbaseResponse = $this->coinbaseRequestCurl($order->id,$order->amount);
} catch (\Exception $e) {
Session::flash("error_msg",$e->getMessage());
return Redirect::back();
}
$order->type = "coinbase";
$order->status = "Pending";
$order->response = serialize($coinbaseResponse);
$bitcoinRedirectURL = "https://www.coinbase.com/checkouts/".$coinbaseResponse->button->code;
}
return Redirect::to($bitcoinRedirectURL);
}else{
$response = "";
$messages = $v->messages()->all();
foreach($messages as $message){
$response.="<li style='margin-left:10px;'>{$message}</li>";
}
Session::flash("error_msg",$response);
return Redirect::back()->withInput();
}
}
Our bitpayRequestCurl is using curl to access bitpay through basic authentication . We will pass our json encoded options to post fields using CURLOPT_POSTFIELDS . We will set CURLOPT_HTTPAUTH to CURLAUTH_BASIC . If the response is a success we will decode and return it back else throw an exception .
public function bitpayRequestCurl($orderId, $grandTotal,$billing) {
$config = Config::get('bitcoins');
$url = $config['bitpay_url'];
$apiKey = $config['bitpay_api_key'];
$post = array(
'orderID' =>$orderId,
'price' => $grandTotal,
'currency' => 'USD',
'itemDesc' => 'KodeInfo Checkout Invoice',
'notificationEmail'=>'developers@kodeinfo.com',
'notificationURL'=>$config['bitpay_notify_url'],
'physical'=>true,
'fullNotifications'=>true,
'buyerName'=>$billing->name,
'buyerAddress1'=>$billing->address,
'buyerAddress2'=>'',
'buyerCity'=>$billing->city,
'buyerState'=>$billing->state,
'buyerZip'=>$billing->zip,
'buyerEmail'=>$billing->email,
'buyerPhone'=>$billing->phone
);
$post = json_encode($post);
if((isset($url) && trim($url) != '') && (isset($apiKey) && trim($apiKey) != '')) {
try {
$curl = curl_init();
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
$length = strlen($post);
$uname = base64_encode($apiKey);
if($uname) {
$header = array(
'Content-Type: application/json',
'Content-Length: ' . $length,
'Authorization: Basic ' . $uname,
'X-BitPay-Plugin-Info: phplib1.5',
);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_PORT, 443);
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ;
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // verify certificate
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); // check existence of CN and verify that it matches hostname
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
curl_setopt($curl, CURLOPT_FRESH_CONNECT, 1);
$responseString = curl_exec($curl);
if($responseString == false) {
throw new \Exception('Error: ' . curl_error($curl));
} else {
if (!json_decode($responseString, true)) {
throw new \Exception('Error - Invalid JSON: ' . $responseString);
}
}
curl_close($curl);
return json_decode($responseString);
} else {
curl_close($curl);
throw new \Exception('Invalid data found in apiKey value passed to bitpayRequestCurl. (Failed: base64_encode(apikey))');
}
} catch (Exception $e) {
@curl_close($curl);
throw new \Exception('Error: ' . $e->getMessage());
}
} else {
throw new \Exception('Error: You must supply non-empty url and apiKey parameters.');
}
}
If your server do not support curl then you can use bitpayRequest method which does the same thing If the response is a success then decode and return it or else throw an exception .
//You can also use this method which works without Curl
public function bitpayRequest($orderId,$grandTotal,$billing){
$config = Config::get('bitcoins');
$url = $config['bitpay_url'];
$apiKey = $config['bitpay_api_key'];
$post = array(
'orderID' =>$orderId,
'price' => $grandTotal,
'currency' => 'USD',
'itemDesc' => 'KodeInfo Checkout Invoice',
'notificationEmail'=>'developers@kodeinfo.com',
'notificationURL'=>$config['bitpay_notify_url'],
'physical'=>true,
'fullNotifications'=>true,
'buyerName'=>$billing->name,
'buyerAddress1'=>$billing->address,
'buyerAddress2'=>'',
'buyerCity'=>$billing->city,
'buyerState'=>$billing->state,
'buyerZip'=>$billing->zip,
'buyerEmail'=>$billing->email,
'buyerPhone'=>$billing->phone
);
$post = json_encode($post);
$length = strlen($post);
$basic_auth = base64_encode($apiKey);
$context_options = array(
"http" => array(
"method" => "POST",
"header" => "Content-type: application/json\r\n" .
"Content-Length: $length\r\n" .
"Authorization: Basic $basic_auth\r\n"
)
);
$context_options["http"]["content"] = $post;
$context = stream_context_create($context_options);
$response = file_get_contents($url, false, $context);
$decodeResponse = json_decode($response);
if(empty($decodeResponse)){
Log::alert(Request::all());
throw new \Exception("Unable to process payment contact customer support");
}else{
return $decodeResponse;
}
}
Integrating Coinbase
The /process route will execute processPayment method from HomeController , Our processPayment method will validate inputs , setup billing object , create an order object and then it will check which type of service needs to be executed . If your have selected coinbase then it will execute coinbaseRequestCurl and pass order id , price string .
Below is the example request for coinbase
GET /api/v1/account/balance HTTP/1.1
Accept: */*
User-Agent: Ruby
ACCESS_KEY: <YOUR-API-KEY>
ACCESS_SIGNATURE: <YOUR-COMPUTED-SIGNATURE>
ACCESS_NONCE: <YOUR-UPDATED-NONCE>
Connection: close
Host: coinbase.com
Response:
{"amount"=>"50.00000000", "currency"=>"BTC"}
The ACCESS_KEY header is simply your API key.
The ACCESS_SIGNATURE header is a HMAC-SHA256 hash of the nonce concatentated with the full URL and body of the HTTP request, encoded using your API secret.
The nonce is a positive integer number that must increase with every request you make. The nonce parameter can be included in one of three ways:
As a ACCESS_NONCE header in your requests
ACCESS_NONCE: 1234
As a GET parameter in the URL
https://api.coinbase.com/v1/account/balance?nonce=1234
As a root-level parameter in your JSON POST requests
This scheme is generally known as “HMAC authentication” (see wikipedia article).
public function coinbaseRequestCurl($orderId,$priceString){
$config = Config::get('bitcoins');
$apikey = $config["coinbase_api_key"];
$apisecret = $config["coinbase_api_secret"];
$nonce = sprintf('%0.0f',round(microtime(true) * 1000000));
$url = $config['coinbase_url']."?nonce=" . $nonce;
$parameters = [];
$parameters["button"]["name"] = "KodeInfo Checkout Invoice";
$parameters["button"]["custom"] = $orderId;
$parameters["button"]["price_string"] = $priceString;
$parameters["button"]["type"] = "buy_now";
$parameters["button"]["subscription"] = false;
$parameters["button"]["price_currency_iso"] = "USD";
$parameters["button"]["description"] = "KodeInfo Checkout Invoice";
$parameters["button"]["style"] = "custom_large";
$parameters["button"]["include_email"] = true;
$parameters["button"]["callback_url"] = $config['coinbase_notify_url'];
$parameters = http_build_query($parameters, true);
$signature = hash_hmac("sha256", $nonce . $url . $parameters, $apisecret);
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array(
"ACCESS_KEY: " . $apikey,
"ACCESS_NONCE: " . $nonce,
"ACCESS_SIGNATURE: " . $signature
)));
curl_setopt_array($ch, array(
CURLOPT_POSTFIELDS => $parameters,
CURLOPT_POST => true,
));
$response = curl_exec($ch);
curl_close($ch);
$decodeResponse = json_decode($response);
if(sizeof($decodeResponse->errors)>0){
Log::alert(Request::all());
throw new \Exception($decodeResponse->errors[0]);
}else{
return $decodeResponse;
}
}
Response from coinbase will contain code which need to be appended to the below url and redirect so the user can complete the payment.
Thanks
KodeInfo
No Comments
Leave a comment Cancel