株式会社Pontechのぽんたです。PontechではzkTLSを用いたアプリケーションの開発に着目・注力しています。
zkTLSのプロトコルを使用したアプリケーションの開発方法について記事を書きます。今回はReclaim Protocol編です。
この記事で作るアプリケーション
Xで特定のアカウントをフォローしている人がトークンをClaimできるページを作ります。
https://zk-present.vercel.app/ (こちらのデモでは、SepoliaでUSDCがClaimできます)
主に以下について整理してご紹介します。
- Reclaim ProtocolのダッシュボードでAppと独自のProviderを作成する手順
- ReclaimのProofをVerifyするコントラクトの開発方法
※2024年12月時点での情報をまとめます。一次情報は公式ドキュメントを参照してください。
Reclaim Protocolの設定
https://dev.reclaimprotocol.org/dashboard にログインします。
今回はWeb Appを作成します。Application NameとApp Logoを設定してください。これらはユーザーにProofを要求する場面で表示されます。後から編集可能です。
次にProviderを設定します。Providerとは、ユーザーにどのwebページでどのようなProofを作成してもらうかを規定したものです。のちに自作のProviderに変更するので、一旦適当なProviderを選択して先に進んでください。
Application Secretを保存してください。ここにコードのサンプルがあるので、Reactのフレームワークを実行して試してみてください。私はNext.jsで試しました。
QRコードが表示され、QRコードをスマホで読み込むとApp Clipが立ち上がり、Providerの要求するサイトに移動すると思います。そこでログインすると自動的にzkTLSの計算が走り、Proofがフロントエンドに返されます。
Reclaim ProtocolのProviderの作成
ここまでProviderは既存のものを使ってきましたが、自前のProviderを採用したいので作成していきます。
Providerの作成にはReclaim Devtoolというネイティブアプリのダウンロードが必要です。このアプリはエンドユーザーはダウンロードする必要はない開発者向けのアプリです。
https://dev.reclaimprotocol.org/my-providers から+New Providerボタンを押下します。Session IDが表示されるのでReclaim DevtoolアプリでIDを入力して接続します。
接続が成功すると、アプリ側で実行されたTLS通信がPCのダッシュボード側で検知できるようになります。
Proofを作成したいページにアプリ内ブラウザで遷移します。様々なAPIリクエストが実行されていることが分かります。
この中からユーザーに証明を作成してもらいたいデータが返ってくるエンドポイントを探します。
Xでログインしているユーザーが対象ユーザーをフォローしているかどうかのエンドポイントは以下でした。
https://x.com/i/api/graphql/-0XdHI-mrHWBQd8-oLo1aA/ProfileSpotlightsQuery...
followingとscreen_nameという属性情報を取得したいので、それを選択しましょう。エンドユーザーごとに異なる識別子を求めている場合にはUnique to userにチェック、秘匿化したい情報を求める場合にはPrivate Informationにチェックします。今回はどちらにも当てはまらないのでチェック不要です。
ユーザーに表示されるガイドの文章を設定していきます。
Advance Optionsを開いて、JS Injectionにユーザーのページへ自動で遷移するコードを仕込みます。このJS Injectionを入れた場合、Providerを使うためにReclaim Protocolの運営の承認が必要になります。Reclaimが悪意のあるコードがinjectionされていないか確認しています。
setInterval(function() {
if (window.location.href === "https://x.com/home" || window.location.href === "https://x.com/home/") {
window.location.href = "https://x.com/peaceandwhisky";
}
}, 3000); // 3000 milliseconds = 3 seconds
次のページへ進み、QRコードをスマホで読み込んで実際にProofの生成を試して成功すると、Providerが設定が完了となります。
コントラクトの開発
次に、XでフォローしていることのProofを提出したらUSDCを引き出すことができるコントラクトを作成します。こちらのレポジトリをcloneまたはforkして始めます。
https://github.com/reclaimprotocol/reclaim-solidity-sdk
ちなみに@reclaimprotocol/verifier-solidity-sdkというライブラリがありますが、proofの検証が未完成のままな気がして利用を避けています。
早速コントラクトのコードを見ていただきます。
https://sepolia.etherscan.io/address/0x600a3a23d96c09a65c05071e833e2dd6886cda95#code#F1#L4
function submitProof(IReclaim.Proof memory proof) external {
string memory trgtFollowing = '"following":"';
string memory isFollowing = reclaimContract.extractFieldFromContext(proof.claimInfo.context, trgtFollowing);
require(keccak256(bytes(isFollowing)) == keccak256(bytes("true")), "Not following");
string memory trgtScreenName = '"screen_name":"';
string memory screenName = reclaimContract.extractFieldFromContext(proof.claimInfo.context, trgtScreenName);
require(isValidScreenName(screenName), "Invalid screen_name");
require(!claimed[msg.sender][screenName], "Already claimed for this screen_name");
try reclaimContract.verifyProof(proof) {
claimed[msg.sender][screenName] = true;
uint256 tokenAmount = screenNameToTokenAmount[screenName];
require(token.transfer(msg.sender, tokenAmount), "Token transfer failed");
emit ProofSubmitted(msg.sender, screenName, tokenAmount);
} catch {
// verifyProofが失敗した場合の処理
revert("Proof verification failed");
}
}
まず各チェーンにデプロイされているReclaim Protocolのコントラクトを呼び出せるように、アドレスを設定しておきます。
次にproofの中身を確認したいので、extractFieldFromContext()関数で、proof内の文字列を抽出して、期待する文字列かどうかを確認します。
そしてverifyProof(proof)関数でverifyに成功する場合はUSDCをtransferし、失敗する場合はrevertします。残高となるUSDCはコントラクトに別途手動でデポジットしておきます。本稼働する際にはdepositする関数を用意したり、コントラクトを分割した方が良いでしょう。
フロントエンドの開発
Next.jsのフロントエンドのコードを記載しておきます。ユーザーにウォレット接続を要求し、Reclaim ProtocolのQRコードを表示、Proofを生成できたらコントラクトに接続してトランザクションを発行するコードです。
"use client";
import {
Proof,
ReclaimProofRequest,
transformForOnchain,
} from "@reclaimprotocol/js-sdk";
import QRCode from "react-qr-code";
import { useState } from "react";
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { sepolia } from "viem/chains";
import { disctributionContractAbi } from "./abi";
import Link from "next/link";
/* eslint @typescript-eslint/no-explicit-any: 0 */
declare global {
interface Window {
ethereum: any;
}
}
export default function Home() {
const [walletAddress, setWalletAddress] = useState(null);
const [requestUrl, setRequestUrl] = useState("");
const [proof, setProof] = useState<Proof | null>(null);
const [txStatus, setTxStatus] = useState<string>("");
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert("MetaMaskをインストールしてください。");
return;
}
const [account] = await ethereum.request({
method: "eth_requestAccounts",
});
setWalletAddress(account);
} catch (err) {
console.error(err);
}
};
const getVerificationReq = async () => {
// Your credentials from the Reclaim Developer Portal
// Replace these with your actual credentials
const APP_ID = "your_app_id";
const APP_SECRET = process.env.NEXT_PUBLIC_RECLAIM_APP_SECRET!;
const PROVIDER_ID = "your_provider_id";
// Initialize the Reclaim SDK with your credentials
const reclaimProofRequest = await ReclaimProofRequest.init(
APP_ID,
APP_SECRET,
PROVIDER_ID
);
// Generate the verification request URL
const requestUrl = await reclaimProofRequest.getRequestUrl();
console.log("Request URL:", requestUrl);
setRequestUrl(requestUrl);
// Start listening for proof submissions
await reclaimProofRequest.startSession({
onSuccess: async (proof) => {
console.log("Proof received:", proof);
setProof(proof as Proof);
},
onError: (error: Error) => {
console.error("Error in proof generation:", error);
},
});
};
const claimToken = async () => {
const forOnchain = transformForOnchain(proof as Proof);
console.log("forOnchain", forOnchain);
try {
setTxStatus("トランザクション送信中...");
const client = createWalletClient({
chain: sepolia,
transport: custom(window.ethereum!),
});
const hash = await client.writeContract({
account: walletAddress,
address: "0x8c4eD7cCb92c8892C683De858965647a70eFe45d",
abi: disctributionContractAbi,
functionName: "submitProof",
args: [forOnchain],
});
setTxStatus(`トランザクション送信完了。ハッシュ: ${hash}`);
const publicClient = createPublicClient({
transport: http("https://sepolia.drpc.org"),
chain: sepolia,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status === "success") {
setTxStatus("トランザクションが完了しました!");
} else {
setTxStatus("トランザクションに失敗しました。");
}
} catch (err) {
console.error(err);
setTxStatus("エラーが発生しました。");
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-8">
<div className="bg-white shadow-md rounded-lg p-8 w-full max-w-md">
<p className="my-2 font-semibold text-center">
FujitaさんのフォロワーはUSDCをclaimできるよ!
</p>
<div className="flex justify-center mb-6 ">
<Link href={"https://twitter.com/peaceandwhisky"} target="_blank">
<img
className="dark:invert mx-auto"
src="https://pbs.twimg.com/profile_images/1506991552664866822/nQveomWI_400x400.jpg"
alt="Next.js logo"
width={60}
/>
<p className="mt-2">1. まずFujitaさんをフォローしてね</p>
</Link>
</div>
{/* ウォレット接続 */}
{!walletAddress ? (
<button
onClick={connectWallet}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition-colors mb-4"
>
2. ウォレット接続
</button>
) : (
<p className="text-center text-green-600 mb-4">
接続中のウォレット: {walletAddress}
</p>
)}
{/* 検証要求取得ボタン */}
{!requestUrl && (
<button
onClick={getVerificationReq}
className="w-full bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600 transition-colors mb-4"
>
3. QRコード表示
</button>
)}
{/* QRコード表示 */}
{requestUrl && (
<div className="my-4">
<QRCode className="w-full" value={requestUrl} size={128} />
<p className="mt-2">
4. このQRコードを読んで、立ち上がったアプリでXにログインしてね
</p>
</div>
)}
{/* 検証成功メッセージ */}
{proof && (
<div className="mt-4 p-4 bg-green-100 border border-green-300 text-green-800 rounded text-center">
<h2 className="text-lg font-semibold">
zkProofの作成が完了したよ!
</h2>
</div>
)}
{/* Claimボタン */}
{walletAddress && (
<div className="mt-4">
<button
onClick={claimToken}
className="w-full bg-indigo-500 text-white py-2 px-4 rounded hover:bg-indigo-600 transition-colors"
>
Claim Token
</button>
<p>5. zkProofを提出してトークンを受け取ってね</p>
</div>
)}
{/* トランザクションステータスの表示 */}
{txStatus && (
<div className="mt-4 p-4 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded">
{txStatus}
</div>
)}
</div>
</div>
);
}
以上で開発の流れの説明を終了します。
まとめ
Reclaim Protocolを用いた開発は、ある程度柔軟性が高く、Dapps開発経験がある方であれば比較的容易に取り組めると思います。Reclaim Protocol自体のアップデートが頻繁に行われているので情報が最新でない場合もある点に気をつけてください。
株式会社Pontechでは、Dapps開発の受託を請け負っております。本記事のように採用する技術やサードパーティのサービスの検討からお手伝いすることが可能です。
Dapps開発に関するご相談がございましたら、ぜひ当社までお問い合わせください。