Book

 · 大約1個月 ago

x402 payment js - Nov 4, 10:00

import { Controller } from "@hotwired/stimulus"

export default class extends Controller { static values = { url: String }

async pay() { if (!window.ethereum) { alert("⚠️ 请先安装钱包") return }

try {
// 1️⃣ 请求资源
const res = await fetch(this.urlValue)
const payInfo = await res.json()

if (res.status !== 402) {
alert("✅ 已解锁或无需支付")
return
}

// 2️⃣ 获取支付要求
const accept = payInfo.accepts[0]
const { payTo, asset, maxAmountRequired, network, scheme, extra } = accept

console.log("🧾 支付要求:", accept)
console.log("📦 Extra 信息:", extra)

// 3️⃣ 连接钱包
const accounts = await window.ethereum.request({
method: "eth_requestAccounts"
})
const from = accounts[0]

// 4️⃣ 生成 nonce 和时间戳
const nonce = this.generateNonce()
const timestamp = Math.floor(Date.now() / 1000)
const validAfter = timestamp - 300
const validBefore = timestamp + 600

// 5️⃣ 获取 Chain ID
const chainId = await this.getChainId()
console.log("🔗 Chain ID:", chainId)

// 6️⃣ 读取合约信息(调试用)
await this.debugContractInfo(asset)

// 7️⃣ 构造 EIP-712 签名数据(尝试多个配置)
const domainConfigs = [
// 配置 1: 使用 extra 信息
{
name: extra?.name || "USD Coin",
version: extra?.version?.toString() || "2",
chainId: chainId,
verifyingContract: asset
},
// 配置 2: 标准 USDC
{
name: "USD Coin",
version: "2",
chainId: chainId,
verifyingContract: asset
},
// 配置 3: FiatToken
{
name: "FiatToken",
version: "2",
chainId: chainId,
verifyingContract: asset
}
]

// 尝试第一个配置
const domain = domainConfigs[0]

console.log("🏛️ Domain 配置:")
console.log(JSON.stringify(domain, null, 2))

const types = {
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" }
]
}

const message = {
from: from,
to: payTo,
value: maxAmountRequired,
validAfter: validAfter,
validBefore: validBefore,
nonce: nonce
}

console.log("📝 签名消息:")
console.log(JSON.stringify(message, null, 2))

// 8️⃣ 请求用户签名
const signature = await window.ethereum.request({
method: "eth_signTypedData_v4",
params: [
from,
JSON.stringify({
domain,
types,
primaryType: "TransferWithAuthorization",
message
})
]
})

console.log("✍️ 签名完成:", signature)

// 9️⃣ 构造 Payment Payload
const paymentPayload = {
x402Version: 1,
scheme: scheme || "exact",
network: network,
payload: {
authorization: {
from: from,
to: payTo,
value: maxAmountRequired.toString(),
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
nonce: nonce
},
signature: signature
}
}

console.log("=" .repeat(60))
console.log("🔐 Payment Payload:")
console.log(JSON.stringify(paymentPayload, null, 2))
console.log("=" .repeat(60))

// 🔟 编码并发送
const encoded = btoa(JSON.stringify(paymentPayload))

const verifyRes = await fetch(this.urlValue, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-PAYMENT": encoded
},
body: JSON.stringify({})
})

console.log("📬 响应状态:", verifyRes.status)

if (verifyRes.ok) {
const data = await verifyRes.json()
alert("✅ 支付成功!内容已解锁")
console.log("📄 内容:", data)
window.location.reload()
} else {
const err = await verifyRes.text()
console.error("❌ 验证失败:", err)
alert(`❌ 验证失败: ${err}`)
}

} catch (e) {
console.error("💥 出错:", e)

if (e.code === 4001) {
alert("❌ 您拒绝了签名")
} else {
alert("💥 支付失败: " + e.message)
}
}


}

// 调试:读取合约信息 async debugContractInfo(asset) { try { console.log("🔍 读取合约信息...")

// name()
const nameData = "0x06fdde03"
const nameResult = await window.ethereum.request({
method: "eth_call",
params: [{ to: asset, data: nameData }, "latest"]
})
console.log(" name() 原始结果:", nameResult)

// version()
const versionData = "0x54fd4d50"
const versionResult = await window.ethereum.request({
method: "eth_call",
params: [{ to: asset, data: versionData }, "latest"]
})
console.log(" version() 原始结果:", versionResult)

// DOMAIN_SEPARATOR()
const domainData = "0x3644e515"
const domainResult = await window.ethereum.request({
method: "eth_call",
params: [{ to: asset, data: domainData }, "latest"]
})
console.log(" DOMAIN_SEPARATOR():", domainResult)

} catch (e) {
console.warn("读取合约信息失败(可忽略):", e.message)
}


}

generateNonce() { const array = new Uint8Array(32) crypto.getRandomValues(array) return "0x" + Array.from(array) .map(b => b.toString(16).padStart(2, "0")) .join("") }

async getChainId() { const chainIdHex = await window.ethereum.request({ method: "eth_chainId" }) return parseInt(chainIdHex, 16) } }