diff --git a/package.json b/package.json index 82a52aca..723da842 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.13", + "version": "0.1.14", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/user/verify-ows-binding/route.ts b/src/app/api/user/verify-ows-binding/route.ts new file mode 100644 index 00000000..377a4053 --- /dev/null +++ b/src/app/api/user/verify-ows-binding/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from "next/server"; +import { verifyMessage } from "viem"; + +/** + * POST /api/user/verify-ows-binding + * Verifies that an OWS wallet binding signature is valid. + * + * Body: { owsWallet, humanWallet, signature } + * Message format: "I authorize {humanWallet} as my PlotLink owner. Wallet: {owsWallet}" + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { owsWallet, humanWallet, signature } = body; + + if (!owsWallet || !humanWallet || !signature) { + return NextResponse.json( + { valid: false, error: "owsWallet, humanWallet, and signature are required" }, + { status: 400 }, + ); + } + + if (!/^0x[a-fA-F0-9]{40}$/.test(owsWallet) || !/^0x[a-fA-F0-9]{40}$/.test(humanWallet)) { + return NextResponse.json( + { valid: false, error: "Invalid wallet address format" }, + { status: 400 }, + ); + } + + const message = `I authorize ${humanWallet} as my PlotLink owner. Wallet: ${owsWallet}`; + + const valid = await verifyMessage({ + address: owsWallet as `0x${string}`, + message, + signature: signature as `0x${string}`, + }); + + if (!valid) { + return NextResponse.json( + { valid: false, error: "Signature does not match the OWS wallet address" }, + { status: 400 }, + ); + } + + return NextResponse.json({ valid: true }); + } catch (err) { + return NextResponse.json( + { valid: false, error: err instanceof Error ? err.message : "Verification failed" }, + { status: 500 }, + ); + } +} diff --git a/src/components/AgentRegister.tsx b/src/components/AgentRegister.tsx index 835773c3..5d1d55d2 100644 --- a/src/components/AgentRegister.tsx +++ b/src/components/AgentRegister.tsx @@ -37,7 +37,253 @@ const SET_WALLET_TYPES = { ], } as const; -export function AgentRegister() { +/* ───────────────────────────────────────────────────────────────────────────── + * Link AI Writer — OWS binding verification + registration + * ───────────────────────────────────────────────────────────────────────────── */ + +function LinkAIWriter() { + const { address } = useAccount(); + const { writeContractAsync } = useWriteContract(); + + const [owsWallet, setOwsWallet] = useState(""); + const [bindingSignature, setBindingSignature] = useState(""); + const [verifying, setVerifying] = useState(false); + const [verified, setVerified] = useState(false); + const [linking, setLinking] = useState(false); + const [linkTxHash, setLinkTxHash] = useState(); + const [linkedAgentId, setLinkedAgentId] = useState(); + const [done, setDone] = useState(false); + const [error, setError] = useState(null); + + // Wallet bind step (after registration) + const [walletBindSig, setWalletBindSig] = useState(""); + const [walletBindDeadline, setWalletBindDeadline] = useState(""); + const [bindingWallet, setBindingWallet] = useState(false); + const [bindTxHash, setBindTxHash] = useState(); + const [walletBound, setWalletBound] = useState(false); + + const validInputs = /^0x[a-fA-F0-9]{40}$/.test(owsWallet) && bindingSignature.startsWith("0x") && bindingSignature.length > 10; + + async function handleVerifyAndLink() { + if (!address) return; + try { + setError(null); + setVerifying(true); + + // Step 1: Verify binding signature + const verifyRes = await fetch("/api/user/verify-ows-binding", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ owsWallet, humanWallet: address, signature: bindingSignature }), + }); + const verifyData = await verifyRes.json(); + if (!verifyRes.ok || !verifyData.valid) { + throw new Error(verifyData.error || "Invalid binding signature"); + } + setVerified(true); + setVerifying(false); + + // Step 2: Register on-chain (human wallet signs as owner) + setLinking(true); + const agentURI = JSON.stringify({ + name: `AI Writer`, + description: "AI fiction writer linked via PlotLink OWS", + type: "ows-writer", + owsWallet, + linkedBy: address, + registeredAt: new Date().toISOString(), + }); + + const hash = await writeContractAsync({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "register", + args: [agentURI], + }); + setLinkTxHash(hash); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + + let newAgentId: bigint | undefined; + const registeredLog = receipt.logs.find((log) => { + try { + const decoded = decodeEventLog({ abi: erc8004Abi, data: log.data, topics: log.topics }); + return decoded.eventName === "Registered"; + } catch { return false; } + }); + if (registeredLog) { + const decoded = decodeEventLog({ abi: erc8004Abi, data: registeredLog.data, topics: registeredLog.topics }); + if (decoded.eventName === "Registered") { + newAgentId = decoded.args.agentId; + setLinkedAgentId(newAgentId); + } + } + + // Step 3: Persist to DB (agent_wallet deferred until setAgentWallet succeeds on-chain) + const cacheRes = await fetch("/api/user/agent-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + agentId: newAgentId?.toString(), + name: "AI Writer", + description: "AI fiction writer linked via PlotLink OWS", + agentOwner: address, + }), + }); + if (!cacheRes.ok) { + setError("On-chain registration succeeded, but DB cache failed — will sync on next visit"); + } + + setDone(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Linking failed"); + } finally { + setVerifying(false); + setLinking(false); + } + } + + async function handleWalletBind() { + if (!linkedAgentId || !walletBindSig || !walletBindDeadline || !address) return; + try { + setError(null); + setBindingWallet(true); + const hash = await writeContractAsync({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "setAgentWallet", + args: [linkedAgentId, owsWallet as `0x${string}`, BigInt(walletBindDeadline), walletBindSig as Hex], + }); + setBindTxHash(hash); + await publicClient.waitForTransactionReceipt({ hash }); + // Persist wallet binding to DB + fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { agent_wallet: owsWallet.toLowerCase() }, + }), + }).catch(() => {}); + setWalletBound(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Wallet binding failed"); + } finally { + setBindingWallet(false); + } + } + + if (done) { + return ( +
+
+

+ {walletBound ? "Linked! AI writer registered and wallet bound on-chain." : "Registered! Now bind your OWS wallet."} +

+ {linkedAgentId !== undefined &&

Agent ID: {linkedAgentId.toString()}

} +

OWS wallet: {owsWallet.slice(0, 6)}...{owsWallet.slice(-4)}

+
+ {linkTxHash && ( + + )} + + {/* Wallet bind step */} + {!walletBound && linkedAgentId !== undefined && ( +
+

Complete wallet binding

+

+ Go to your OWS app → Settings → enter Agent ID {linkedAgentId.toString()} → click "Generate Wallet Bind Code". Paste the signature and deadline below. +

+
+ + setWalletBindSig(e.target.value)} placeholder="0x..." + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+
+ + setWalletBindDeadline(e.target.value)} placeholder="e.g. 1712345678" + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+ +
+ )} + + {bindTxHash && ( + + )} + + {error && ( +
{error}
+ )} +
+ ); + } + + return ( +
+

+ Connect your local PlotLink OWS Writer app to your PlotLink account. + Your AI writer will appear as "{address ? `${address.slice(0, 6)}...` : "your"}'s AI Writer" on PlotLink. +

+
+

1. Open PlotLink OWS app → Settings → "Link to PlotLink"

+

2. Enter your PlotLink wallet address → app generates a binding code

+

3. Paste the binding code below

+
+ +
+ + setOwsWallet(e.target.value)} placeholder="0x..." + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+
+ + setBindingSignature(e.target.value)} placeholder="0x..." + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+ + {error && ( +
{error}
+ )} + + {verified && !linking && ( +
+ Signature verified. Registering on-chain... +
+ )} + + {linkTxHash && !done && ( + + )} + + +
+ ); +} + +/* ───────────────────────────────────────────────────────────────────────────── + * Direct Registration — existing flow (owner wallet = agent wallet) + * ───────────────────────────────────────────────────────────────────────────── */ + +function DirectRegister() { const { address } = useAccount(); const { writeContractAsync } = useWriteContract(); const { signTypedDataAsync } = useSignTypedData(); @@ -192,7 +438,7 @@ export function AgentRegister() { const stepNum = typeof step === "number" ? step : 3; return ( -
+
{/* Step indicator */}
{([1, 2] as const).map((s) => ( @@ -386,3 +632,34 @@ export function AgentRegister() {
); } + +/* ───────────────────────────────────────────────────────────────────────────── + * AgentRegister — combines both sections + * ───────────────────────────────────────────────────────────────────────────── */ + +export function AgentRegister() { + return ( +
+ {/* Section 1: Link AI Writer */} +
+

Link AI Writer (PlotLink OWS App)

+

No coding required — paste the binding code from your OWS app

+ +
+ + {/* Separator */} +
+
+ or +
+
+ + {/* Section 2: Direct Registration */} +
+

Register New AI Agent

+

For developers building custom agent integrations

+ +
+
+ ); +}