EIP-681とEIP-3009を使った、eコマースにおけるJPYC決済プラットフォーム
※これはXに書いた記事の丸写しです。
著:TANAKA SENKI
それぞれの規格について
EIP-681
transferトランザクションの可読化を目指した規格。これは、
ethereum:${contractaddress}/transfer?address=${address}&uint256=${amount * decimal}というように書ける。
これを相手に送付して、トランザクションを作ってもらい、署名すれば、トランザクションを行える。ただし、javascriptの場合、uint256のところは、桁数問題からbigintで値渡しをするのが推奨される。
EIP-3009
誰がガス代を支払うかといったトランザクションの変形。一般的にEVMの仕様では、アナウンス者がガス代の支払いを行う。これを用いると、
ユーザーの署名 -> アナウンス -> アナウンスした人がガス代を払い、トランザクションが実行される
というようなこともできる。
一般的に、これらの情報が追加で必要になる。
valiedAfter 署名が有効になる開始時刻(基本的に0)
valiedBefore 署名が有効な終了時刻(有効期限)(10分くらいあればいい)
nonce 固有のランダムな 32 バイト識別子
v (uint8), r (bytes32), s (bytes32) 署名の構成要素
これらの組み合わせで決済を行う
これを行うに際し、プラットホーム内で署名をしなければならず、そのためには、ウォレットをプラットホームと接続する必要がある。これに関しては、PCとwebの両方で使えるようにするなら reownというサービスを使うのがよい。(コードは長いので割愛)。
まず、料金とアドレスが決定したら、サーバー経由でEIP-681規格のURIを送る。その際のカスタムURIはこんな感じでいいだろう。
ethereum:${contractaddress}/transfer?address=${address}&uint256=${amount * decimal}&nonce=${nonce}
nonceは例えば注文番号を用いると、後で共有しやすい。
その後nonceは
nonce = keccak256("ORDER-20260722-001");
というように変換する。その後、店頭のプログラムで、以下のように支払者のウォレットで署名してもらう。
async function signEIP3009Raw(
jpycContractAddress: string,
chainId: number,
fromAddress: string,
toAddress: string,
amountWei: string,
nonce: string // bytes32 hex
) {
const validBefore = Math.floor(Date.now() / 1000) + 900; //ここでは15分とする
const msgParams = JSON.stringify({
domain: {
name: "JPYC",
version: "2",
chainId: chainId,
verifyingContract: jpycContractAddress,
},
message: {
from: fromAddress,
to: toAddress,
value: amountWei,
validAfter: 0,
validBefore: validBefore,
nonce: nonce,
},
primaryType: "TransferWithAuthorization",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
});
// RPCリクエストを直接発行
const signature: string = await window.ethereum.request({
method: "eth_signTypedData_v4",
params: [fromAddress, msgParams], // 第1引数: 署名者のアドレス, 第2引数: JSON文字列
});
// 65バイトのHex文字列(0x...)から v, r, s をスライスして取り出す
const r = "0x" + signature.substring(2, 66);
const s = "0x" + signature.substring(66, 130);
const v = parseInt(signature.substring(130, 132), 16);
return { v, r, s };
}その後出来上がったものを、サーバーに返す。これはこんな感じでカスタムURIを使っていいだろう。(nonceやvaliedAfterやvaliedBefore、to、from、Amountはサーバー側で共有しておくこと)
ethereum3009:orders/${注文番号}?v=${v}&r=${r}&s=${s}サーバーはこの注文番号を照合し、トランザクションを送る。
async function sendJpycPayment(contractAddress, from, to, valueWei, validAfter, validBefore, nonce, v, r, s) {
// 1. スマートコントラクトの関数を実行するための「Data (calldata)」を作成する
// transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)
// の関数シグネチャハッシュ + パラメータエンコード値
const functionInterface = new ethers.Interface([
"function transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)"
]);
const calldata = functionInterface.encodeFunctionData("transferWithAuthorization", [
from,
to,
valueWei,
validAfter,
validBefore,
nonce,
v,
r,
s
]);
// 2. 店舗側ウォレットから eth_sendTransaction を発行
try {
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [{
from: shopRelayerAddress, // 店舗側のウォレット(ガス代を払う人)
to: contractAddress, // JPYCのコントラクトアドレス
data: calldata, // 作成したエンコードデータ
}],
});
console.log("送信成功! Tx Hash:", txHash);
return txHash;
} catch (error) {
console.error("送信失敗:", error);
}
}これで完成だ。途中でTypescriptだったりJavascriptだったりプログラム言語が変わっているのは、許してね
これでeコマースはどう変わる?
プラットフォーム負担になるが、ガスレス決済ができる。
振込手数料やクレジットカードの手数料は、結構無視できない額になっている。ブロックチェーン決済を通して、お得なeコマースを行おう。
あとがき
これを作成するにあたって、コード作成に協力いただいた、Google geminiに感謝の意を表明する。
必ずしも正しいかと言えば、テストは未実施なので、検証は各自でお願いします。
以下、コーヒー代
ここから先は
¥ 330
この記事が気に入ったらチップで応援してみませんか?
