KodeInfo | Learning resources for web and mobile development

Pay with Bitcoin using Coinbase and Bitpay

December 10th, 2014 15:02:59 by Imran Iqbal Comments(0) - Views(10444)

Hi ,
Today we will learn how to integrate payment with bitcoins , we will integrate coinbase and bitpay to pay with bitcoins .

 
 

 

 

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 .

Bitpay API Page

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');
	}

}

Now run 

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

<?php

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 response is a success then decode and return it or else throw 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 user can complete the payment 
I will update this tutorial to show you how to get notifications and update our order details .


Note : This tutorial is only limited to creating order / invoice and redirecting users so they can pay . If you want to do anything more than that use the respective php libraries .

 

Thanks
KodeInfo

 

Author

  • Imran Iqbal
    Imran Iqbal

    Imran is a web developer and consultant from India. He is the founder of KodeInfo, the PHP and Laravel Community . In the meantime he follows other projects, works as a freelance backend consultant for PHP applications and studies IT Engineering . He loves to learn new things, not only about PHP or development but everything.

Related

WHY USE A FRAMEWORK OVER PLAIN PHP

WHY USE A FRAMEWORK OVER PLAIN PHP
read more

GETTING STARTED WITH LARAVEL

GETTING STARTED WITH LARAVEL
read more

UNDERSTANDING LARAVEL STRUCTURE

UNDERSTANDING LARAVEL STRUCTURE
read more

UNDERSTANDING LARAVEL ROUTES

UNDERSTANDING LARAVEL ROUTES
read more

comments powered by Disqus