Blog Post
Read our latest blog posts and stay updated with the latest trends and insights in the blockchain industry.
Explore a wide range of topics and discover valuable information that can help you enhance your knowledge and skills.
By Chaingateway
Jan 13, 2025
This tutorial will guide you through building a payment gateway in Laravel that supports Tron (TRC) and JST (TRC20) payments. It includes features like generating wallets for payment sessions, handling webhooks for transaction notifications, and verifying transaction status before processing.
By the end of this tutorial, you will have a functional payment gateway that uses Chaingateway’s API for blockchain interactions.
This tutorial should only describe the basic process of how the implementation works. This will also work for Bitcoin, Ethereum, Binance Smart Chain, and Polygon with some small adaptations.
Before diving into the implementation, let’s understand the tools we’ll use:
Chaingateway is a blockchain API service that simplifies interaction with Blockchain networks like Tron. It allows you to:
Visit the official documentation for more details:
To interact with the Chaingateway API, you’ll need an API key. Follow these steps:
Before proceeding, ensure you have:
.env
file with your database credentials.This tutorial builds a payment gateway with the following features:
First, we’ll configure Laravel to use the Chaingateway API.
To interact with Chaingateway, you need to authenticate every request using an API key and specify the blockchain network you’re working with (e.g., testnet or mainnet). This step ensures your application can communicate with Chaingateway seamlessly.
Add Chaingateway configuration to config/app.php
:
'Chaingateway' => [
'api_url' => env('Chaingateway_API_URL', 'https://beta.Chaingateway.io/api/v2'),
'api_key' => env('Chaingateway_API_KEY'),
'network' => env('Chaingateway_NETWORK', 'testnet'), // Use 'mainnet' for production
'cold_wallet' => env('COLD_WALLET'),
],
Next, open your .env
file and add the following:
Chaingateway_API_URL=https://api.Chaingateway.io/api/v2
Chaingateway_API_KEY=your_api_key_here
Chaingateway_NETWORK=testnet
COLD_WALLET=your_cold_wallet_address
api_url
: The base URL for the Chaingateway API.api_key
: Your personal API key for authenticating requests.network
: Specify whether you’re using the testnet (for development) or mainnet (for production).cold_wallet
: The secure wallet where funds will be forwarded after verification.Routes define how users interact with your application. We’ll set up routes for:
routes/web.php
use App\Http\Controllers\PaymentController;
Route::get('/payment', [PaymentController::class, 'showPaymentPage']);
Route::post('/start-payment-session', [PaymentController::class, 'startPaymentSession']);
Route::get('/payment-session/{id}', [PaymentController::class, 'showPaymentSession'])->name('showPaymentSession');
Route::post('/webhook', [PaymentController::class, 'handleWebhook']);
/payment
: Displays a page with a button to start a new payment session./start-payment-session
: Creates a new session and generates a wallet address./payment-session/{id}
: Displays the wallet address and session status./webhook
: Receives notifications about incoming transactions from Chaingateway.To disable csrf protection on the webhooks endpoint, we need to exclude it in bootstrap/app.php. If you use older versions of Laravel, head over to https://laravel.com/docs/11.x/csrf#csrf-excluding-uris to see how it works for your version
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// Exclude webhook route from scrf protection
$middleware->validateCsrfTokens(except: [
'webhook',
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
We need two database tables:
Run the following commands:
php artisan make:model Wallet -m
php artisan make:model PaymentSession -m
Wallet Migration
In database/migrations/<timestamp>_create_wallets_table.php
:
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->string('address')->unique(); // Wallet address
$table->string('private_key'); // Private key for transactions
$table->timestamps();
});
PaymentSession Migration
In database/migrations/<timestamp>_create_payment_sessions_table.php
:
Schema::create('payment_sessions', function (Blueprint $table) {
$table->id();
$table->string('status')->default('Pending'); // Pending, Completed, or Failed
$table->foreignId('wallet_id')->constrained()->onDelete('cascade'); // Links to Wallet
$table->decimal('amount', 18, 8)->nullable(); // Amount sent to this session
$table->string('currency')->default('TRX'); // Currency of the amount
$table->decimal('received_amount', 18, 8)->nullable(); // Amount sent to this session
$table->string('webhook_id')->nullable();
$table->timestamps();
});
Run the migrations:
php artisan migrate
We should also ensure that the fields are fillable and relations are built correctly. To do so, we will adapt the models.
Wallet Model
in app\Models\Wallet.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Wallet extends Model
{
protected $fillable = ['address', 'private_key'];
public function paymentSessions()
{
return $this->hasMany(PaymentSession::class);
}
}
PaymentSession
in app\Models\PaymentSession.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PaymentSession extends Model
{
protected $fillable = ['wallet_id', 'status', 'amount', 'currency', 'received_amount', 'webhook_id'];
public function wallet()
{
return $this->belongsTo(Wallet::class);
}
}
The PaymentController
handles all the logic for our application:
Generate the controller:
php artisan make:controller PaymentController
Here’s the complete implementation of PaymentController
:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Models\PaymentSession;
use App\Models\Transaction;
use App\Models\Wallet;
class PaymentController extends Controller
{
private $apiUrl;
private $apiKey;
private $network;
private $coldWallet;
public function __construct()
{
$this->apiUrl = config('app.Chaingateway.api_url');
$this->apiKey = config('app.Chaingateway.api_key');
$this->network = config('app.Chaingateway.network');
$this->coldWallet = config('app.Chaingateway.cold_wallet');
}
public function showPaymentPage()
{
return view('payment');
}
/**
* Start payment session
*
* This Function will create a new wallet address and webhook in Chaingateway.
*
*/
public function startPaymentSession(Request $request)
{
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
'X-Network' => $this->network,
])->post("{$this->apiUrl}/tron/addresses");
if ($response->successful()) {
$walletData = $response->json()['data'];
$wallet = Wallet::create([
'address' => $walletData['address'],
'private_key' => $walletData['privateKey'],
]);
$wbhookUrl = route('handleWebhook');
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
'X-Network' => $this->network,
])->post("{$this->apiUrl}/tron/webhooks", [
'to' => $wallet->address,
'url' => $wbhookUrl,
]);
$webhookData = $response->json()['data'];
$paymentSession = PaymentSession::create([
'wallet_id' => $wallet->id,
'webhook_id' => $webhookData['id'],
'status' => 'Pending',
]);
return redirect()->route('showPaymentSession', ['id' => $paymentSession->id]);
}
return back()->withErrors(['error' => 'Failed to create payment session.']);
}
public function showPaymentSession($id)
{
$paymentSession = PaymentSession::with('wallet')->findOrFail($id);
return view('payment-session', compact('paymentSession'));
}
public function handleWebhook(Request $request)
{
$transactionData = $request->all();
$wallet = Wallet::where('address', $transactionData['to'])->first();
if (!$wallet) {
return response()->json(['error' => 'Wallet not found'], 404);
}
$paymentSession = PaymentSession::where('wallet_id', $wallet->id)->first();
if (!$paymentSession) {
return response()->json(['error' => 'Payment session not found'], 404);
}
/**
* We always should check the transaction receipt if the transaction really was successful
*/
$receiptResponse = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
'X-Network' => $this->network,
])->get("{$this->apiUrl}/tron/transactions/{$transactionData['txid']}/receipt/decoded");
if ($receiptResponse->successful() && $receiptResponse->json()['data']['status'] == 'SUCCESS') {
$paymentSession->status = 'Completed';
$paymentSession->received_amount = $transactionData['amount'];
/**
* you should check if the amount received is the same as the amount requested
* if not, you should refund the user or do something else.
* Amounts could vary, for example, due to the transaction fee. You should consider that by adding a margin.
* You should also check if the transaction is a TRC20 token transaction and the contract is the same as the one you are expecting.
*
*/
$amountDifference = abs($paymentSession->amount - $transactionData['amount']);
$allowedDifference = $paymentSession->amount * 0.10; // 10% of the payment session amount
if ($amountDifference >= $allowedDifference) {
// Allow a difference of up to 10%, update the contract address
// If the amount is overpaid or underpaid by more than 10%
if ($transactionData['amount'] > $paymentSession->amount) {
$paymentSession->status = 'overpaid';
} else {
$paymentSession->status = 'underpaid';
}
}
if($paymentSession->currency == 'JST' && $transactionData['contractaddress'] != 'TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3'){
$paymentSession->status = 'Wrong currency received';
}
/**
* Do this if you want to move your funds to an cold wallet only.
* You can also send the funds to another wallet or do nothing.
* In case of TRC20 tokens, you need to ensure you have enough TRX to pay for the transaction fee.
* You can also use the Chaingateway Tron Paymaster feature so you dont need to handle the fees.
$endpoint = $transactionData['contractaddress']
? "{$this->apiUrl}/tron/transactions/trc20"
: "{$this->apiUrl}/tron/transactions";
Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
'X-Network' => $this->network,
])->post($endpoint, [
'amount' => $transactionData['amount'],
'privatekey' => $wallet->private_key,
'to' => $this->coldWallet,
'from' => $transactionData['to'],
'contractaddress' => $transactionData['contractaddress'],
]);
*/
} else {
$paymentSession->status = 'Failed';
}
/**
* Delete Webhook in Chaingateway
* Only do that if you will not use the address again
*/
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
'X-Network' => $this->network,
])->delete("{$this->apiUrl}/tron/webhooks/{$paymentSession->webhook_id}");
$paymentSession->save();
return response()->json(['status' => 'success']);
}
}
showPaymentPage
: Displays the main payment page with a form to start a new session.startPaymentSession
: Generates a wallet address, creates a new payment session, and redirects the user to the session page.showPaymentSession
: Displays the wallet address and the session status.handleWebhook
: Processes notifications from Chaingateway, verifies transaction success, updates the session’s status, and forwards funds to the cold wallet (optional).To create a new payment session, this tutorial uses a basic form where you type in amount and currency. This should normally be done by your checkout process.
In resources/views/payment.blade.php
:
<!DOCTYPE html>
<html>
<head>
<title>Start Payment Session</title>
</head>
<body>
<h1>Start a New Payment Session</h1>
<form action="/start-payment-session" method="POST">
@csrf
<label for="amount">Amount:</label>
<input type="number" step="0.01" name="amount" id="amount" required>
<br>
<label for="currency">Currency:</label>
<select name="currency" id="currency" required>
<option value="TRX">TRX</option>
<option value="JST">JST (TRC20)</option>
</select>
<br>
<button type="submit">Start Payment Session</button>
</form>
</body>
</html>
On the Payment Session page, users can check their payment status. This is also a very basic example. In a real-life scenario, you would use more interactive polling or websockets to refresh the payment status.
In resources/views/payment-session.blade.php
:
<!DOCTYPE html>
<html>
<head>
<title>Payment Session</title>
</head>
<body>
<h1>Payment Session</h1>
<p>Send <strong> </strong> to the address below:</p>
<p><strong></strong></p>
<p>Status: <strong></strong></p>
<p>Received: <strong> </strong></p>
</body>
</html>
Run the Laravel development server:
php artisan serve
/payment
to start a new payment session./webhook
.We hope this tutorial shows you how easy it is to implement our API to receive crypto payments. If you have any further questions or need help during implementation, we are always here to help! You can reach out to our very supportive community or write us an email. See here how to stay in touch: https://Chaingateway.io/support
Enter your email to receive our latest newsletter.
Don't worry, we don't spam
chaingateway
Solana vs Tron: Which blockchain stands out for DeFi, gaming, or content creation? Explore use cases, pros, and cons to decide for yourself.
Importance of Blockchain APIs in the digital landscape, their benefits, and how they can be used to enhance the overall user experience.
Calculate Tron fees easily with our free TRX Calculator. Optimize costs for transactions and smart contracts, and save up to 60% with our Tron Paymaster API.
We use cookies to enhance your experience. By continuing to visit this site, you agree to our use of cookies.