PHPでサイトにBitcoin決済を導入する方法
作成日時 2023/08/21 21:38
最終更新 2023/08/21 23:41
最終更新 2023/08/21 23:41
まず初めに
決済処理の実装
バックエンドの実装
クライアントの実装
PHPでBitcoinを扱えるライブラリは幾つかありますが、どれも最終更新が5年以上前であり、使い物になりません。
本記事では、PHPでサイトにBitcoin決済を導入する方法をご紹介させて頂きます。
決済処理の実装
肝心の決済処理は、予算的にBitcoin Coreノードを立てることができないので、外部のAPIに依存します。
運用するサイトが大規模な場合には、外部のAPIではなく専用のVPSを契約してCoreノードを立てることをお勧めします。
<?php
// 支払いが行われたか確認する関数 by ActiveTK.
// $BTCAddr : 支払先のBitcoinアドレス
// $Amount : 支払うべき金額
function Is_Paid_Success($BTCAddr, $Amount) {
// Bitcoinアドレスに関連するトランザクションを全て取得
$Transactions = json_decode(file_get_contents("https://blockstream.info/api/address/" . $BTCAddr . "/txs"), true);
// それぞれのトランザクションごとに処理
foreach($Transactions as $TxInfo)
// トランザクションが承認されている場合
if ($TxInfo["status"]["confirmed"] == true)
// トランザクションの出力(送金先)をそれぞれ確認
foreach($TxInfo["vout"] as $Output)
// もしも送金先が引数と等しく、かつ支払うべき金額を超えている場合
if ($Output["scriptpubkey_address"] == $BTCAddr && $Amount * 10**8 <= $Output["value"] )
return true;
// それ以外の場合
return false;
}
また、Bitcoin受け取り用のアドレスは、以下のように生成できます。
こちらも、予算に余裕がある場合にはできるだけ外部に依存せず、ローカルで完結するべきです。
<?php
$NewCoin = preg_split("/\r\n|\r|\n/", file_get_contents("https://bitcoin.activetk.jp/gen"));
$PubAddress = trim(explode(": ", $NewCoin[0])[1]);
$PrivAddress = trim(explode(": ", $NewCoin[2])[1]);
echo "公開鍵: {$PubAddress}, 秘密鍵(WIF): {$PrivAddress}";
バックエンドの実装
上記の関数を利用して、以下のようにバックエンド処理を実装します。
<?php
// 販売金額
$value = 0.0005;
// 保存用のデータベース
define( "DSN", "mysql:dbname=データベースの名前;host=127.0.0.1;charset=UTF8" );
define( "DB_USER", "root" );
define( "DB_PASS", "password" );
$dbh = new PDO( DSN, DB_USER, DB_PASS );
/*!
*
* 事前に以下のようなテーブルを作成して下さい
*
CREATE TABLE `PaymentsData` (
`PaymentID` varchar(40) DEFAULT '',
`CreateTime` varchar(16) DEFAULT '',
`UserIPaddr` varchar(400) DEFAULT '',
`UserAgent` varchar(400) DEFAULT '',
`AmountBTC` varchar(20) DEFAULT '',
`PaymentAddrPub` varchar(200) DEFAULT '',
`PaymentAddrWIF` varchar(200) DEFAULT '',
`PaymentStatus` varchar(200) DEFAULT '',
`ProductID` varchar(200) DEFAULT '',
`DownloadCount` varchar(20) DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8
*/
// $_GET["status"] にはユーザーの購入用IDが入る
if (isset($_GET["status"])) {
header("Content-Type: application/json;charset=UTF-8");
// まだ割り当てられていない(新規発行)の場合の処理
if (empty($_GET["status"]) || !is_string($_GET["status"])) {
// 購入用IDをランダムに割り当て
$NewPaymentID = substr(base_convert(sha1(md5(uniqid()).md5(microtime())), 16, 36), 0, 10);
// 受け取り用のBitcoinアドレスを生成
$NewCoin = preg_split("/\r\n|\r|\n/", file_get_contents("https://bitcoin.activetk.jp/gen"));
$PubAddress = trim(explode(": ", $NewCoin[0])[1]);
$PrivAddress = trim(explode(": ", $NewCoin[2])[1]);
$UploaderIPaddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : "";
$UploaderUA = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : "";
// データベースに記録
try {
$stmt = $dbh->prepare(
"insert into PaymentsData(
PaymentID, CreateTime, UserIPaddr, UserAgent, AmountBTC, PaymentAddrPub, PaymentAddrWIF, PaymentStatus, ProductID, DownloadCount
)
value(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)"
);
$stmt->execute( [
$NewPaymentID,
time(),
$UploaderIPaddr,
$UploaderUA,
$value,
$PubAddress,
$PrivAddress,
"Waiting",
$row["UniqueID"],
"0"
] );
} catch (\Throwable $e) { }
// 発行した購入用IDを表示
// newsessidはID、addressは支払い用のアドレス、amountは金額、timeは経過時間
echo json_encode(array("newsessid"=>$NewPaymentID, "address"=>$PubAddress, "amount"=>$value, "time"=>0));
}
// 既に発行している場合の処理
else
{
// データベースから現在の状態を取得
try {
$stmt = $dbh->prepare('select * from PaymentsData where PaymentID = ? limit 1;');
$stmt->execute( [$_GET["status"]] );
$pay = $stmt->fetch( PDO::FETCH_ASSOC );
} catch ( \Throwable $e ) {
die(json_encode(array("error"=>"決済処理中にエラーが発生しました(エラーコード1)。")));
}
// 存在しない購入用IDが指定された場合
if (!isset($pay["PaymentID"]))
die(json_encode(array("error"=>"決済処理中にエラーが発生しました(エラーコード2)。")));
// 既に購入が完了している場合
else if ($pay["PaymentStatus"] == "Done" || Is_Paid_Success($pay["PaymentAddrPub"], $pay["AmountBTC"])) {
try {
// 購入完了を記録して、次回からトランザクションを確認する手間を無くす
if ($pay["PaymentStatus"] != "Done") {
$stmt = $dbh->prepare("update PaymentsData set PaymentStatus = ? where PaymentID = ?;");
$stmt->execute([
"Done",
$pay["PaymentID"]
]);
}
} catch (\Throwable $e) { }
exit(json_encode(array("done"=>$pay["PaymentID"], "address"=>$pay["PaymentAddrPub"], "amount"=>getDec( $pay["AmountBTC"]))));
}
exit(json_encode(array("address"=>$pay["PaymentAddrPub"], "amount"=>getDec( $pay["AmountBTC"]), "time"=>(time() - $pay["CreateTime"]*1))));
}
exit();
}
クライアントの実装
クライアント側では、一定時間ごとにサーバーへfetchを行って、支払いが完了しているか確認します。
<!DOCTYPE html>
<html lang="ja" itemscope="" itemtype="http://schema.org/WebPage" dir="ltr">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<title><?=htmlspecialchars($row["Title"])?> - DataCoinTrade</title>
<meta name="author" content="ActiveTK.">
<meta name="robots" content="All">
<meta name="description" content="Bitcoinでファイルやテキストを販売したり、購入することができます。">
<meta name="copyright" content="Copyright © 2023 ActiveTK. All rights reserved.">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.activetk.jp/ActiveTK.min.js"></script>
<script src="https://code.activetk.jp/jquery-qrcode.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/core.js"></script>
<script>
window.stInt = null;
window.stat = localStorage.getItem("<?=$row["UniqueID"]?>") ? localStorage.getItem("<?=$row["UniqueID"]?>") : "";
document.addEventListener("DOMContentLoaded", function() {
_("main").style.display = "block";
if (localStorage.getItem("<?=$row["UniqueID"]?>")) {
window.stat = localStorage.getItem("<?=$row["UniqueID"]?>");
_("paymentScript").style.display = "block";
_("buy").style.display = "none";
_("buyid").innerText = localStorage.getItem("<?=$row["UniqueID"]?>");
updateStatus();
window.stInt = setInterval('updateStatus()', 5000);
}
_("buy").onclick = function() {
_("paymentScript").style.display = "block";
_("buy").style.display = "none";
updateStatus();
window.stInt = setInterval('updateStatus()', 5000);
}
new Typewriter(_("dot"), {
loop: true,
delay: 75,
autoStart: true,
cursor: '|',
strings: ['']
});
});
function updateStatus() {
fetch('/<?=$row["UniqueID"]?>?status=' + window.stat)
.then((response) => response.json())
.then((data) => ParseStatus(data));
}
function ParseStatus(data) {
if (data["error"])
alert("エラー: " + data["error"]);
if (data["newsessid"]) {
window.stat = data["newsessid"];
localStorage.setItem("<?=$row["UniqueID"]?>", window.stat);
_("buyid").innerText = data["newsessid"];
}
if (data["done"]) {
_("buy").style.display = "none";
_("download").style.display = "block";
_("download").onclick = function() {
window.location.href = "https://datacointrade.com/<?=$row["UniqueID"]?>?download=" + data["done"];
}
_("stat").innerHTML = "トランザクションが承認されました!";
clearInterval(window.stInt);
}
if (data["address"]) {
if (_("btcaddr").innerText != data["address"]) {
_("btcaddr").innerText = data["address"];
$('#qrcode').qrcode({width: 256, height: 256, text: 'bitcoin:' + data["address"]});
}
}
if (data["amount"]) {
_("amount").innerText = data["amount"] + " BTC";
}
if (data["time"]) {
_("timer").innerText = data["time"];
}
}
</script>
</head>
<body style="background-color:#CCFF99;">
<div class="pt-4 sm:pt-10 lg:pt-12" style="display:inline;">
<header class="mx-auto max-w-screen-2xl px-4 md:px-8">
<div class="flex flex-col items-center justify-between gap-4 py-6 md:flex-row">
<nav class="flex flex-wrap justify-center gap-x-4 gap-y-2 md:justify-start md:gap-6">
<a href="/" class="transition duration-100 hover:text-indigo-500 active:text-indigo-600">ホーム</a>
<a href="/license" class="transition duration-100 hover:text-indigo-500 active:text-indigo-600">利用規約</a>
<a href="https://profile.activetk.jp/" class="transition duration-100 hover:text-indigo-500 active:text-indigo-600">開発者</a>
<a href="https://www.activetk.jp/contact" class="transition duration-100 hover:text-indigo-500 active:text-indigo-600">お問い合わせ</a>
</nav>
<div class="flex gap-4">
寄付: 1hackerMy1mcFbMu32ZuiQCvkqQMFnNvX
</div>
</div>
</header>
</div>
<hr style="background-color:#000000;height:2px;">
<br>
<h1 class="text-3xl font-bold" align="center">
<?=htmlspecialchars($row["Title"])?> - DataCoinTrade
</h1>
<br>
<noscript><div align="center"><h1>このページを表示するには、JavaScriptを有効化して下さい。</h1></div></noscript>
<div class="bg-white py-6 sm:py-8 lg:py-12" id="main" style="display:none;">
<div align="center">
<h1 class="text-3xl font-bold">【ファイルの情報】</h1>
<br>
<p>ファイル名: <?= (file_exists("/home/www-data/crypto/" . $row["UniqueID"] . "/filename") ? htmlspecialchars(file_get_contents("/home/www-data/crypto/" . $row["UniqueID"] . "/filename")) : "未指定") ?></p>
<p>ファイルサイズ: <?= (file_exists("/home/www-data/crypto/" . $row["UniqueID"] . "/filesize") ? byte_format(htmlspecialchars(file_get_contents("/home/www-data/crypto/" . $row["UniqueID"] . "/filesize"))*1, 2, true) : "未指定") ?></p>
<p>連絡先: <?=htmlspecialchars($row["ContactEmail"])?></p>
<p>金額: <?=getDec( $value)?> BTC</p>
<br>
<input type="button" id="buy" class="inline-block rounded-lg bg-indigo-500 px-8 py-3 text-center text-sm font-semibold text-white outline-none ring-indigo-300 transition duration-100 hover:bg-indigo-600 focus-visible:ring active:bg-indigo-700 md:text-base" value="データを購入">
<input type="button" id="download" style="display:none;" class="inline-block rounded-lg bg-indigo-500 px-8 py-3 text-center text-sm font-semibold text-white outline-none ring-indigo-300 transition duration-100 hover:bg-indigo-600 focus-visible:ring active:bg-indigo-700 md:text-base" value="データをダウンロード(購入済み)">
<br><br>
<div class="bg-white py-6 sm:py-8 lg:py-12" style="background-color:#e6e6fa;text:#363636;display:none;" id="paymentScript">
<div class="mx-auto max-w-screen-xl px-4 md:px-8">
<div class="grid gap-8 md:grid-cols-2 lg:gap-12">
<div>
<div class="h-64 overflow-hidden rounded-lg bg-gray-100 shadow-lg md:h-auto" id="qrcode"></div>
</div>
<div class="md:pt-8">
<p class="text-center font-bold md:text-left">以下のBitcoinアドレスまで、指定の金額を送金して下さい(決済ID: <span id="buyid"></span>)。</p>
<p class="text-center font-bold md:text-left">ただし、金額にトランザクション手数料(Fee)は含まれず、送金金額に不足がある場合には正常に処理できません。</p>
<br>
<p class="text-center font-bold text-indigo-500 md:text-left" id="amount"></p>
<h1 class="mb-4 text-center text-2xl font-bold text-gray-800 sm:text-3xl md:mb-6 md:text-left" id="btcaddr"></h1>
<br>
<p class="text-center font-bold md:text-left"><span id="stat">トランザクションを待機中..<span id="dot"></span> (<span id="timer">0</span>秒経過)</span></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white pt-4 sm:pt-10 lg:pt-12">
<footer class="mx-auto max-w-screen-2xl px-4 md:px-8">
<div class="py-8 text-center text-sm text-gray-400">(c) 2023 ActiveTK.</div>
</footer>
</div>
</body>
</html>