前回はSuperfluidでオンチェーンのサブスクリプション決済を行う方法について記載して、予想以上に反響がありました。今回は通常のクリプト決済を扱いますが、クリプト決済の結果をオフチェーンに反映する方法について書きます。
クリプト決済でオンチェーンのデータを書き換える場合、つまりNFTやERC20トークンの購入・移転などを行う場合、スマートコントラクトで処理が完結するので特に考えることはないです。
一方で、例えば通常のオフチェーンのデータベースで管理しているサービスに暗号資産決済を導入する場合、オンチェーンで決済が完了したことをブロックチェーン側からは通知してくれないので、自社でその情報をブロックチェーン側の情報をデータベースにsyncする必要があります。
スクラッチで開発する場合
具体的な実装ではなく、基本的な概念だけ解説します。
Ethereum上でトークンが転送されたことを検知するためには、イベントの監視が一般的な方法です。ERC20トークンは通常、トークンの転送に関連するイベントを発行します。以下のステップで支払い処理を行うことができます。
1. 各ユーザーのトランザクションの監視
- サーバー側で、各ユーザーが購入を行う際のトークン転送トランザクションを監視します。
- Web3.jsなどのライブラリを使用して、特定のERC20トークン契約の
Transfer
イベントを監視します。
2. Transfer
イベントのフィルタリング
Transfer
イベントには、送信者アドレス、受信者アドレス、転送量などの情報が含まれます。- 運営側のアドレス宛てのトークン転送をフィルタリングして検知します。
3. トランザクションの確認
- イベントがトリガーされた後、トランザクションの詳細を確認して適切な処理を行います。
- トランザクションがまだ確認されていない場合(未承認の場合)は、必要な数の確認を待ちます。
4. サービス内のアイテム付与
- トランザクションが正常に確認されたら、Firestoreなどのデータベースを更新してユーザーにサービス内のアイテムやポイントを付与します。
5. エラーハンドリング
- 何か問題が発生した場合に備え、適切なエラーハンドリングを実装します。
上記の一連の流れを実行するサーバーを定期実行させる必要があります。
Slash Paymentでwebhookを受け取る
上記の部分をいい感じに省略するために、Slash Paymentを使ってみましょう。
Slashは暗号資産決済を簡単に導入できるサービスです。裏側に最適なレートを検索してくれるスマートコントラクトが存在し、顧客は任意のトークンを最適なレートで支払うことができます。
こちらのQuickstartに従って実装していきますが、その前に簡単にNext.js+Firestoreでアプリケーションを作成しておいてください。
https://testnet.slash.fi/admin/dashboard
にウォレットを接続してログインします。
支払いで受け取りたい通貨の種類を選択します。Slashでは1ウォレットアドレスにつき1つのプロジェクトの設定しかできないので注意してください。
先述したようにコントラクトを経由して支払いを受けるので、自分専用のコントラクトをデプロイする必要があります。ネットワークごとに必要です。
デプロイできました。受信アドレスは支払いを受け取るアドレスで初期設定はデプロイしたアドレスになっていますが、他のアドレスに変更することもできます。
支払い設定の画面からQRコードが表示できます。このQRコードが店頭に印刷されて置かれているQRですね。
https://testnet.slash.fi/payment-merchant/5c94c1e5159ca1f3952367961b508ee0
QRを読み込むと支払い画面が出てきます。
実装のほうに戻りましょう。
先ほど用意したNext.jsとFirestoreのプロジェクトにAPIを2つ作る必要があります。一つは決済URLを作成するslash-checkout、もう一つは決済完了時のキックバックを受け取るslash-webhookと名付けます。
公式Docsにnodejsがなかったので自分で書いてみました。
api/slash-checkout/route.ts
import { NextResponse } from "next/server";
import * as crypto from "crypto";
import axios from "axios";
import { SlashCheckoutAPIRequest } from "../../types";
import randomstring from "randomstring";
export async function POST(request: Request) {
const data: SlashCheckoutAPIRequest = await request.json();
const { userId } = data;
const rand = randomstring.generate({
length: 16,
charset: "alphanumeric",
capitalization: "lowercase",
});
const amount = 100;
const orderCode = `${userId}_${rand}`;
const authenticationToken = process.env.SLASH_AUTH_TOKEN;
const hashToken = process.env.SLASH_HASH_TOKEN;
const raw = orderCode + "::" + amount + "::" + hashToken;
const hashHex = crypto.createHash("sha256").update(raw, "utf8").digest("hex");
const amountType = "USD";
const requestObj = {
identification_token: authenticationToken,
order_code: orderCode,
verify_token: hashHex,
amount: amount,
amount_type: amountType,
};
const paymentRequestUrl = "https://testnet.slash.fi/api/v1/payment/receive";
const result = await axios.post(paymentRequestUrl, requestObj);
return NextResponse.json({
payment_id: result.data.token,
checkout_url: result.data.url,
});
}
frontendから実行してcheckout_urlにリダイレクトします。
const slashPayment = async () => {
const response = await fetch(`/api/slash-checkout`, {
method: "POST",
body: JSON.stringify({
userId: user.uid,
}),
}).then((data) => data.json());
console.log(response, "respense");
router.push(response.checkout_url);
};
api/slash-checkout/route.ts
import { NextResponse } from "next/server";
import * as crypto from "crypto";
import { SlashWebhookAPIRequest } from "../../types";
import initializeFirebaseServer from "@/lib/initFirebaseAdmin";
export async function POST(request: Request) {
const data: SlashWebhookAPIRequest = await request.json();
const { order_code, verify_token, amount, result } = data;
const localVerifyToken = crypto
.createHash("sha256")
.update(`${order_code}::${amount}::${process.env.SLAHS_HASH_TOKEN}`)
.digest("hex");
if (localVerifyToken !== verify_token) {
return NextResponse.error();
}
if (!result) {
return NextResponse.error();
}
const splittedCode = order_code.split("_");
const userId = splittedCode[0];
const { db } = initializeFirebaseServer();
const docRef = db.collection(`users`).doc(userId);
await docRef.update({
amount,
});
return NextResponse.json({});
}
Slashからのキックバックが正しいことをverifyして、ユーザーのデータベスを更新しています。
https://testnet.slash.fi/admin/payment/settings/basic
デプロイしたslash-webhookのURLをこちらから設定します。
今回上記コード全部を動かして確認することが間に合わなかったので、少し古いですがこちらのコードを参考にしてみてください
https://github.com/ksuhara/omiyage-nft
まとめ
このように、既存のweb2サービスにクリプト決済を簡単に導入することができました。ちなみにクレカ決済のStripeも非常によく似たプロセスで実装できます。
SlashはPaymentのほかにSlash VaultsというNFTをキーにしてトークンを出し入れするサービスを開発中(現在テストネット)なので、今後それを使ったDapps開発にも挑戦してみたいなと思います。
弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。
また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!