diff --git a/erc-8004/scripts/bridge-to-mainnet.sh b/erc-8004/scripts/bridge-to-mainnet.sh index d4942140..ca205e01 100755 --- a/erc-8004/scripts/bridge-to-mainnet.sh +++ b/erc-8004/scripts/bridge-to-mainnet.sh @@ -3,16 +3,22 @@ # Usage: ./bridge-to-mainnet.sh # Example: ./bridge-to-mainnet.sh 0.01 -set -e +set -euo pipefail # Require Bankr CLI if ! command -v bankr >/dev/null 2>&1; then - echo "Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + echo "Error: Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 exit 1 fi AMOUNT="${1:?Usage: bridge-to-mainnet.sh }" +# Validate amount is a positive number +if ! echo "$AMOUNT" | grep -qE '^[0-9]+(\.[0-9]+)?$' || [ "$(echo "$AMOUNT <= 0" | bc -l)" = "1" ]; then + echo "Error: amount must be a positive number (e.g. 0.01)" >&2 + exit 1 +fi + echo "=== Bridging ETH to Mainnet ===" >&2 echo "Amount: $AMOUNT ETH" >&2 echo "From: Base" >&2 diff --git a/erc-8004/scripts/capabilities.ts b/erc-8004/scripts/capabilities.ts new file mode 100644 index 00000000..ea34433f --- /dev/null +++ b/erc-8004/scripts/capabilities.ts @@ -0,0 +1,167 @@ +/** + * ERC-8004 Agent Capabilities + * + * Defines well-known capability identifiers and provides a validation + * layer that prevents agents from claiming arbitrary unverified capabilities. + * + * Capability Policy: + * Capabilities are self-declared strings in the registration metadata. + * Since there is currently no on-chain proof system for capability + * verification, this module: + * 1. Restricts claims to a well-known allowlist of recognized capability IDs + * 2. Flags unrecognized claims so consumers can treat them with lower trust + * 3. Documents what each capability means so consumers can verify behavior + * + * Future: capability proofs or on-chain demonstrations should be implemented + * in the ERC-8004 Validation Registry before high-stakes trust decisions + * are made based on capability claims alone. + */ + +/** + * Well-known capability identifiers for ERC-8004 agents. + * Only these strings are recognized as verified claim types. + * Claims outside this set are flagged as UNVERIFIED. + */ +export const KNOWN_CAPABILITIES = { + /** Agent exposes an Agent-to-Agent (A2A) endpoint */ + A2A: "a2a", + /** Agent supports the Model Context Protocol (MCP) */ + MCP: "mcp", + /** Agent accepts x402 micropayments */ + X402: "x402", + /** Agent has a resolvable ENS name */ + ENS: "ens", + /** Agent has on-chain reputation signals in ERC-8004 Reputation Registry */ + REPUTATION: "reputation", + /** Agent exposes a public web interface */ + WEB: "web", +} as const; + +export type KnownCapability = (typeof KNOWN_CAPABILITIES)[keyof typeof KNOWN_CAPABILITIES]; + +const KNOWN_SET = new Set(Object.values(KNOWN_CAPABILITIES)); + +export interface CapabilityEntry { + id: string; + /** true = recognized well-known capability; false = unverified/unknown claim */ + verified: boolean; + /** Human-readable description for known capabilities */ + description?: string; +} + +const CAPABILITY_DESCRIPTIONS: Record = { + [KNOWN_CAPABILITIES.A2A]: + "Agent exposes an Agent-to-Agent (A2A) compatible endpoint for machine-to-machine communication.", + [KNOWN_CAPABILITIES.MCP]: + "Agent implements the Model Context Protocol (MCP) for tool/resource exposure.", + [KNOWN_CAPABILITIES.X402]: + "Agent accepts x402 HTTP micropayments for metered service access.", + [KNOWN_CAPABILITIES.ENS]: + "Agent has a verified ENS primary name resolving to its registered address.", + [KNOWN_CAPABILITIES.REPUTATION]: + "Agent has on-chain reputation signals recorded in the ERC-8004 Reputation Registry.", + [KNOWN_CAPABILITIES.WEB]: + "Agent exposes a public web interface for human interaction.", +}; + +/** + * Validate a list of capability claims from agent metadata. + * + * Recognized claims are marked verified:true. + * Unrecognized claims are marked verified:false and should be treated + * with skepticism — the agent is claiming something we cannot validate. + * + * @param claims - Raw capability strings from agent metadata + * @returns Annotated capability entries + */ +export function validateCapabilities(claims: string[]): CapabilityEntry[] { + if (!Array.isArray(claims)) { + return []; + } + + return claims + .filter((c): c is string => typeof c === "string" && c.trim() !== "") + .map((c) => { + const normalized = c.trim().toLowerCase(); + const isKnown = KNOWN_SET.has(normalized); + return { + id: normalized, + verified: isKnown, + description: isKnown ? CAPABILITY_DESCRIPTIONS[normalized] : undefined, + }; + }); +} + +/** + * Filter to only the capabilities that can be partially verified by + * cross-referencing the agent's service list in its metadata. + * + * For example: + * - "a2a" claim is plausible only if an A2A service endpoint is present + * - "mcp" claim is plausible only if an MCP service endpoint is present + * - "x402" claim is plausible only if x402Support=true in metadata + * + * This is not cryptographic proof — it only checks internal consistency. + * True verification requires an external oracle or on-chain demonstration. + * + * @param claims - Capability claims from metadata + * @param metadata - Full parsed metadata object for cross-reference + * @returns Capability entries with consistency check results + */ +export function crossCheckCapabilities( + claims: string[], + metadata: { + services?: Array<{ name: string; endpoint?: string }>; + x402Support?: boolean; + } +): CapabilityEntry[] { + const entries = validateCapabilities(claims); + + const serviceNames = new Set( + (metadata.services ?? []).map((s) => s.name.toLowerCase()) + ); + + return entries.map((entry) => { + if (!entry.verified) return entry; + + // Cross-check internal consistency + let consistent = true; + let note: string | undefined; + + switch (entry.id) { + case KNOWN_CAPABILITIES.A2A: + consistent = serviceNames.has("a2a"); + note = consistent + ? undefined + : "Claims A2A but no A2A service endpoint found in metadata"; + break; + case KNOWN_CAPABILITIES.MCP: + consistent = serviceNames.has("mcp"); + note = consistent + ? undefined + : "Claims MCP but no MCP service endpoint found in metadata"; + break; + case KNOWN_CAPABILITIES.X402: + consistent = metadata.x402Support === true; + note = consistent + ? undefined + : "Claims x402 but x402Support is not true in metadata"; + break; + case KNOWN_CAPABILITIES.WEB: + consistent = serviceNames.has("web"); + note = consistent + ? undefined + : "Claims web but no web service endpoint found in metadata"; + break; + // ENS and REPUTATION require external on-chain lookup to verify + default: + break; + } + + return { + ...entry, + verified: consistent, + description: note ?? entry.description, + }; + }); +} diff --git a/erc-8004/scripts/create-registration.sh b/erc-8004/scripts/create-registration.sh index 07a9a615..1e590606 100755 --- a/erc-8004/scripts/create-registration.sh +++ b/erc-8004/scripts/create-registration.sh @@ -1,22 +1,22 @@ #!/bin/bash # ERC-8004 - Create agent registration JSON file # Usage: ./create-registration.sh [output-file] -# +# # Environment variables: -# AGENT_NAME - Agent display name (default: uses wallet address) +# AGENT_NAME - Agent display name (default: "AI Agent") # AGENT_DESCRIPTION - Agent description -# AGENT_IMAGE - Avatar URL -# AGENT_WEBSITE - Agent website +# AGENT_IMAGE - Avatar URL +# AGENT_WEBSITE - Agent website # AGENT_A2A_ENDPOINT - A2A agent card URL # AGENT_MCP_ENDPOINT - MCP server endpoint -# AGENT_ENS - ENS name -# X402_SUPPORT - Enable x402 payments (true/false, default: false) +# AGENT_ENS - ENS name +# X402_SUPPORT - Enable x402 payments (true/false, default: false) -set -e +set -euo pipefail OUTPUT_FILE="${1:-/tmp/agent-registration.json}" -# Default values +# Defaults NAME="${AGENT_NAME:-AI Agent}" DESCRIPTION="${AGENT_DESCRIPTION:-An autonomous AI agent registered on ERC-8004}" IMAGE="${AGENT_IMAGE:-}" @@ -26,44 +26,59 @@ MCP_ENDPOINT="${AGENT_MCP_ENDPOINT:-}" ENS="${AGENT_ENS:-}" X402="${X402_SUPPORT:-false}" +# Validate X402 value +if [[ "$X402" != "true" && "$X402" != "false" ]]; then + echo "Error: X402_SUPPORT must be 'true' or 'false'" >&2 + exit 1 +fi + echo "=== Creating Registration File ===" >&2 echo "Name: $NAME" >&2 echo "Description: $DESCRIPTION" >&2 echo "Output: $OUTPUT_FILE" >&2 -# Build services array +# Build services array safely using jq — no shell injection risk SERVICES="[]" if [ -n "$WEBSITE" ]; then - SERVICES=$(echo "$SERVICES" | jq --arg url "$WEBSITE" '. + [{"name": "web", "endpoint": $url}]') + SERVICES=$(jq -n --argjson arr "$SERVICES" --arg url "$WEBSITE" \ + '$arr + [{"name": "web", "endpoint": $url}]') fi if [ -n "$A2A_ENDPOINT" ]; then - SERVICES=$(echo "$SERVICES" | jq --arg url "$A2A_ENDPOINT" '. + [{"name": "A2A", "endpoint": $url, "version": "0.3.0"}]') + SERVICES=$(jq -n --argjson arr "$SERVICES" --arg url "$A2A_ENDPOINT" \ + '$arr + [{"name": "A2A", "endpoint": $url, "version": "0.3.0"}]') fi if [ -n "$MCP_ENDPOINT" ]; then - SERVICES=$(echo "$SERVICES" | jq --arg url "$MCP_ENDPOINT" '. + [{"name": "MCP", "endpoint": $url, "version": "2025-06-18"}]') + SERVICES=$(jq -n --argjson arr "$SERVICES" --arg url "$MCP_ENDPOINT" \ + '$arr + [{"name": "MCP", "endpoint": $url, "version": "2025-06-18"}]') fi if [ -n "$ENS" ]; then - SERVICES=$(echo "$SERVICES" | jq --arg ens "$ENS" '. + [{"name": "ENS", "endpoint": $ens, "version": "v1"}]') + SERVICES=$(jq -n --argjson arr "$SERVICES" --arg ens "$ENS" \ + '$arr + [{"name": "ENS", "endpoint": $ens, "version": "v1"}]') fi -# Create registration file -cat > "$OUTPUT_FILE" << EOF -{ - "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", - "name": "$NAME", - "description": "$DESCRIPTION", - "image": "$IMAGE", - "services": $SERVICES, - "x402Support": $X402, - "active": true, - "registrations": [], - "supportedTrust": ["reputation"] -} -EOF +# Build the full JSON safely using jq — never raw string interpolation +jq -n \ + --arg type "https://eips.ethereum.org/EIPS/eip-8004#registration-v1" \ + --arg name "$NAME" \ + --arg description "$DESCRIPTION" \ + --arg image "$IMAGE" \ + --argjson services "$SERVICES" \ + --argjson x402 "$X402" \ + '{ + "type": $type, + "name": $name, + "description": $description, + "image": $image, + "services": $services, + "x402Support": $x402, + "active": true, + "registrations": [], + "supportedTrust": ["reputation"] + }' > "$OUTPUT_FILE" echo "=== SUCCESS ===" >&2 echo "Created: $OUTPUT_FILE" >&2 diff --git a/erc-8004/scripts/get-agent.sh b/erc-8004/scripts/get-agent.sh index 0a4eb1b0..af1f7fde 100755 --- a/erc-8004/scripts/get-agent.sh +++ b/erc-8004/scripts/get-agent.sh @@ -3,15 +3,22 @@ # Usage: ./get-agent.sh [--testnet] # Example: ./get-agent.sh 123 -set -e +set -euo pipefail AGENT_ID="${1:?Usage: get-agent.sh [--testnet]}" +# Validate agent ID is a non-negative integer +if ! [[ "$AGENT_ID" =~ ^[0-9]+$ ]]; then + echo "Error: agent-id must be a non-negative integer" >&2 + exit 1 +fi + # Check for testnet flag -if [ "$2" = "--testnet" ] || [ "$2" = "-t" ]; then +if [ "${2:-}" = "--testnet" ] || [ "${2:-}" = "-t" ]; then CHAIN_ID=11155111 IDENTITY_REGISTRY="0x8004A818BFB912233c491871b3d84c89A494BD9e" - RPC_URL="https://eth-sepolia.g.alchemy.com/v2/demo" + # Public Sepolia RPC — no demo key required + RPC_URL="https://rpc.sepolia.org" echo "=== TESTNET MODE (Sepolia) ===" >&2 else CHAIN_ID=1 @@ -25,62 +32,87 @@ echo "Agent ID: $AGENT_ID" >&2 echo "Chain ID: $CHAIN_ID" >&2 echo "" >&2 -# Get tokenURI (agentURI) - function selector: 0xc87b56dd -TOKEN_URI_DATA=$(printf '0xc87b56dd%064x' "$AGENT_ID") +# Build tokenURI calldata safely — agent ID passed via env to avoid injection +TOKEN_URI_DATA=$(AGENT_ID_VAL="$AGENT_ID" node -e " +const id = BigInt(process.env.AGENT_ID_VAL); +// tokenURI(uint256) selector: 0xc87b56dd +const selector = '0xc87b56dd'; +const param = id.toString(16).padStart(64, '0'); +process.stdout.write(selector + param + '\n'); +") -RESPONSE=$(curl -s -X POST "$RPC_URL" \ +RESPONSE=$(curl -s --fail -X POST "$RPC_URL" \ -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$IDENTITY_REGISTRY\",\"data\":\"$TOKEN_URI_DATA\"},\"latest\"],\"id\":1}") + -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$IDENTITY_REGISTRY\",\"data\":\"$TOKEN_URI_DATA\"},\"latest\"],\"id\":1}") || { + echo "Error: RPC call failed" >&2 + exit 1 +} RESULT=$(echo "$RESPONSE" | jq -r '.result // empty') if [ -z "$RESULT" ] || [ "$RESULT" = "0x" ]; then - echo "Error: Agent $AGENT_ID not found" >&2 + echo "Error: Agent $AGENT_ID not found or has no URI" >&2 exit 1 fi -# Decode the string from the result -URI=$(node -e " -const hex = '$RESULT'.slice(2); -// Skip offset (32 bytes) and get length (32 bytes) -const lenHex = hex.slice(64, 128); -const len = parseInt(lenHex, 16); -// Get string data +# Decode the ABI-encoded string — hex passed via env to avoid injection +URI=$(HEX_RESULT="$RESULT" node -e " +const hex = process.env.HEX_RESULT.slice(2); +if (hex.length < 128) { process.stderr.write('Error: response too short\n'); process.exit(1); } +// Skip offset (32 bytes), read length (32 bytes) +const len = parseInt(hex.slice(64, 128), 16); +if (isNaN(len) || len < 0) { process.stderr.write('Error: invalid string length\n'); process.exit(1); } const dataHex = hex.slice(128, 128 + len * 2); +if (dataHex.length !== len * 2) { process.stderr.write('Error: truncated data\n'); process.exit(1); } const uri = Buffer.from(dataHex, 'hex').toString('utf8'); -console.log(uri); +process.stdout.write(uri + '\n'); ") echo "Agent URI: $URI" >&2 -# If it's an IPFS URI, try to fetch the content +# Fetch profile content based on URI scheme if [[ "$URI" == ipfs://* ]]; then CID="${URI#ipfs://}" echo "Fetching from IPFS..." >&2 - CONTENT=$(curl -s "https://gateway.pinata.cloud/ipfs/$CID" 2>/dev/null || curl -s "https://ipfs.io/ipfs/$CID" 2>/dev/null || echo "") - - if [ -n "$CONTENT" ]; then + CONTENT=$(curl -s --fail "https://gateway.pinata.cloud/ipfs/$CID" 2>/dev/null \ + || curl -s --fail "https://ipfs.io/ipfs/$CID" 2>/dev/null \ + || echo "") + + if [ -n "$CONTENT" ] && echo "$CONTENT" | jq empty 2>/dev/null; then echo "" >&2 echo "=== Agent Profile ===" >&2 - echo "$CONTENT" | jq . 2>/dev/null || echo "$CONTENT" >&2 + echo "$CONTENT" | jq . >&2 echo "" echo "$CONTENT" else - echo "{\"agentId\":\"$AGENT_ID\",\"uri\":\"$URI\"}" + jq -n --arg agentId "$AGENT_ID" --arg uri "$URI" '{"agentId": $agentId, "uri": $uri}' fi -elif [[ "$URI" == https://* ]]; then +elif [[ "$URI" == https://* || "$URI" == http://* ]]; then echo "Fetching from HTTP..." >&2 - CONTENT=$(curl -s "$URI" 2>/dev/null || echo "") - - if [ -n "$CONTENT" ]; then + CONTENT=$(curl -s --fail "$URI" 2>/dev/null || echo "") + + if [ -n "$CONTENT" ] && echo "$CONTENT" | jq empty 2>/dev/null; then echo "" >&2 echo "=== Agent Profile ===" >&2 - echo "$CONTENT" | jq . 2>/dev/null || echo "$CONTENT" >&2 + echo "$CONTENT" | jq . >&2 + echo "" + echo "$CONTENT" + else + jq -n --arg agentId "$AGENT_ID" --arg uri "$URI" '{"agentId": $agentId, "uri": $uri}' + fi +elif [[ "$URI" == data:* ]]; then + echo "Decoding on-chain data URI..." >&2 + CONTENT=$(echo "${URI#data:application/json;base64,}" | base64 -d 2>/dev/null || echo "") + + if [ -n "$CONTENT" ] && echo "$CONTENT" | jq empty 2>/dev/null; then + echo "" >&2 + echo "=== Agent Profile (on-chain) ===" >&2 + echo "$CONTENT" | jq . >&2 echo "" echo "$CONTENT" else - echo "{\"agentId\":\"$AGENT_ID\",\"uri\":\"$URI\"}" + jq -n --arg agentId "$AGENT_ID" --arg uri "$URI" '{"agentId": $agentId, "uri": $uri}' fi else - echo "{\"agentId\":\"$AGENT_ID\",\"uri\":\"$URI\"}" + jq -n --arg agentId "$AGENT_ID" --arg uri "$URI" '{"agentId": $agentId, "uri": $uri}' fi diff --git a/erc-8004/scripts/metadata.ts b/erc-8004/scripts/metadata.ts new file mode 100644 index 00000000..c45785e7 --- /dev/null +++ b/erc-8004/scripts/metadata.ts @@ -0,0 +1,184 @@ +/** + * ERC-8004 Metadata Parser + * + * Safely parses and validates agent metadata from on-chain URIs. + * Uses JSON.parse only — never eval() or dynamic code execution. + */ + +export interface AgentService { + name: string; + endpoint: string; + version?: string; +} + +export interface AgentMetadata { + type: string; + name: string; + description: string; + image?: string; + services?: AgentService[]; + x402Support?: boolean; + active?: boolean; + registrations?: Array<{ + agentId: number | string; + agentRegistry: string; + }>; + supportedTrust?: string[]; + [key: string]: unknown; +} + +/** + * Parse and validate agent metadata from a JSON string. + * + * Uses JSON.parse — never eval(). Metadata is treated as untrusted + * external input throughout; no dynamic field names are executed. + * + * @param raw - Raw JSON string from IPFS, HTTP, or data URI + * @returns Parsed and validated AgentMetadata + * @throws Error if the input is not valid JSON or fails structural validation + */ +export function parseMetadata(raw: string): AgentMetadata { + if (typeof raw !== "string") { + throw new TypeError("Metadata must be a string"); + } + + // Safe parse — never eval() + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new Error("Invalid JSON: metadata could not be parsed"); + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("Invalid metadata: expected a JSON object"); + } + + const obj = parsed as Record; + + // Validate required fields + if (typeof obj.name !== "string" || obj.name.trim() === "") { + throw new Error('Invalid metadata: "name" must be a non-empty string'); + } + + if (typeof obj.description !== "string") { + throw new Error('Invalid metadata: "description" must be a string'); + } + + // Sanitize: strip any field that looks like executable code or injection + const sanitized: AgentMetadata = { + type: + typeof obj.type === "string" + ? obj.type + : "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + name: String(obj.name).trim().slice(0, 256), + description: String(obj.description).trim().slice(0, 4096), + }; + + if (typeof obj.image === "string") { + sanitized.image = obj.image.slice(0, 2048); + } + + if (typeof obj.x402Support === "boolean") { + sanitized.x402Support = obj.x402Support; + } + + if (typeof obj.active === "boolean") { + sanitized.active = obj.active; + } + + if (Array.isArray(obj.services)) { + sanitized.services = obj.services + .filter( + (s): s is Record => + typeof s === "object" && s !== null && !Array.isArray(s) + ) + .map((s) => { + const svc: AgentService = { + name: typeof s.name === "string" ? s.name.trim().slice(0, 64) : "", + endpoint: + typeof s.endpoint === "string" ? s.endpoint.trim().slice(0, 2048) : "", + }; + if (typeof s.version === "string") { + svc.version = s.version.trim().slice(0, 32); + } + return svc; + }) + .filter((s) => s.name !== "" && s.endpoint !== ""); + } + + if (Array.isArray(obj.supportedTrust)) { + sanitized.supportedTrust = obj.supportedTrust + .filter((t): t is string => typeof t === "string") + .map((t) => t.trim().slice(0, 64)); + } + + if (Array.isArray(obj.registrations)) { + sanitized.registrations = obj.registrations + .filter( + (r): r is Record => + typeof r === "object" && r !== null && !Array.isArray(r) + ) + .map((r) => ({ + agentId: typeof r.agentId === "number" ? r.agentId : String(r.agentId), + agentRegistry: typeof r.agentRegistry === "string" ? r.agentRegistry : "", + })); + } + + return sanitized; +} + +/** + * Fetch and parse metadata from a URI. + * + * Supports ipfs://, https://, http://, and data: URIs. + * Fetched content is treated as untrusted and parsed safely. + * + * @param uri - Agent URI from on-chain registry + * @returns Parsed AgentMetadata + */ +export async function fetchMetadata(uri: string): Promise { + if (typeof uri !== "string" || uri.trim() === "") { + throw new Error("URI must be a non-empty string"); + } + + let raw: string; + + if (uri.startsWith("data:application/json;base64,")) { + // On-chain data URI — decode base64 + const b64 = uri.slice("data:application/json;base64,".length); + raw = Buffer.from(b64, "base64").toString("utf8"); + } else if (uri.startsWith("ipfs://")) { + const cid = uri.slice("ipfs://".length); + // Try Pinata first, fall back to public gateway + const gateways = [ + `https://gateway.pinata.cloud/ipfs/${cid}`, + `https://ipfs.io/ipfs/${cid}`, + ]; + raw = ""; + for (const gw of gateways) { + try { + const res = await fetch(gw); + if (res.ok) { + raw = await res.text(); + break; + } + } catch { + // try next gateway + } + } + if (!raw) { + throw new Error(`Failed to fetch IPFS content for CID: ${cid}`); + } + } else if (uri.startsWith("https://") || uri.startsWith("http://")) { + const res = await fetch(uri); + if (!res.ok) { + throw new Error(`HTTP ${res.status} fetching metadata from ${uri}`); + } + raw = await res.text(); + } else { + throw new Error(`Unsupported URI scheme: ${uri}`); + } + + return parseMetadata(raw); +} diff --git a/erc-8004/scripts/register-http.sh b/erc-8004/scripts/register-http.sh index 215f0229..896750fb 100755 --- a/erc-8004/scripts/register-http.sh +++ b/erc-8004/scripts/register-http.sh @@ -1,21 +1,33 @@ #!/bin/bash # ERC-8004 - Register agent with HTTP URL (no IPFS needed) # Usage: REGISTRATION_URL="https://..." ./register-http.sh [--testnet] -# +# # The registration JSON must be hosted at the URL before calling this. -set -e +set -euo pipefail # Require Bankr CLI if ! command -v bankr >/dev/null 2>&1; then - echo "Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + echo "Error: Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + exit 1 +fi + +# Require jq +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq not found. Install with: apt-get install jq" >&2 exit 1 fi REGISTRATION_URL="${REGISTRATION_URL:?Error: REGISTRATION_URL environment variable required}" +# Validate URL scheme +if [[ ! "$REGISTRATION_URL" =~ ^https?:// ]]; then + echo "Error: REGISTRATION_URL must start with http:// or https://" >&2 + exit 1 +fi + # Check for testnet flag -if [ "$1" = "--testnet" ] || [ "$1" = "-t" ]; then +if [ "${1:-}" = "--testnet" ] || [ "${1:-}" = "-t" ]; then CHAIN="sepolia" CHAIN_ID=11155111 IDENTITY_REGISTRY="0x8004A818BFB912233c491871b3d84c89A494BD9e" @@ -35,31 +47,44 @@ echo "Chain: $CHAIN (ID: $CHAIN_ID)" >&2 echo "Registry: $IDENTITY_REGISTRY" >&2 echo "" >&2 -# Encode register(string) calldata -CALLDATA=$(node -e " -const uri = '$REGISTRATION_URL'; +# Encode register(string) calldata safely — URL passed via env, never shell-interpolated +CALLDATA=$(AGENT_URI="$REGISTRATION_URL" node -e " +const uri = process.env.AGENT_URI; +if (!uri) { process.stderr.write('Error: AGENT_URI not set\n'); process.exit(1); } const selector = '0xf2c298be'; const offset = '0000000000000000000000000000000000000000000000000000000000000020'; -const len = uri.length.toString(16).padStart(64, '0'); -const data = Buffer.from(uri, 'utf8').toString('hex').padEnd(Math.ceil(uri.length / 32) * 64, '0'); -console.log(selector + offset + len + data); +const encoded = Buffer.from(uri, 'utf8'); +const len = encoded.length.toString(16).padStart(64, '0'); +const data = encoded.toString('hex').padEnd(Math.ceil(encoded.length / 32) * 64, '0'); +process.stdout.write(selector + offset + len + data + '\n'); ") echo "Registering on-chain..." >&2 +# Build the transaction payload safely with jq +TX_PAYLOAD=$(jq -n \ + --arg to "$IDENTITY_REGISTRY" \ + --arg data "$CALLDATA" \ + --argjson chainId "$CHAIN_ID" \ + '{"to": $to, "data": $data, "value": "0", "chainId": $chainId}') + # Submit via Bankr -RESULT=$(bankr agent "Submit this transaction on $CHAIN: {\"to\": \"$IDENTITY_REGISTRY\", \"data\": \"$CALLDATA\", \"value\": \"0\", \"chainId\": $CHAIN_ID}" 2>/dev/null) +RESULT=$(bankr agent "Submit this transaction on $CHAIN: $TX_PAYLOAD" 2>/dev/null) if echo "$RESULT" | grep -qE "$EXPLORER/tx/0x[a-fA-F0-9]{64}"; then TX_HASH=$(echo "$RESULT" | grep -oE "$EXPLORER/tx/0x[a-fA-F0-9]{64}" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) - + echo "" >&2 echo "=== REGISTRATION SUCCESSFUL! ===" >&2 echo "URL: $REGISTRATION_URL" >&2 echo "TX: https://$EXPLORER/tx/$TX_HASH" >&2 echo "" >&2 - - echo "{\"success\":true,\"chain\":\"$CHAIN\",\"url\":\"$REGISTRATION_URL\",\"tx\":\"$TX_HASH\"}" + + jq -n \ + --arg chain "$CHAIN" \ + --arg url "$REGISTRATION_URL" \ + --arg tx "$TX_HASH" \ + '{"success": true, "chain": $chain, "url": $url, "tx": $tx}' else echo "Registration submitted. Result:" >&2 echo "$RESULT" >&2 diff --git a/erc-8004/scripts/register-onchain.sh b/erc-8004/scripts/register-onchain.sh index 11cfbb3a..d33b970a 100755 --- a/erc-8004/scripts/register-onchain.sh +++ b/erc-8004/scripts/register-onchain.sh @@ -6,22 +6,28 @@ # No IPFS or HTTP hosting required! # # Environment variables: -# AGENT_NAME - Agent display name -# AGENT_DESCRIPTION - Agent description -# AGENT_IMAGE - Avatar URL (optional) +# AGENT_NAME - Agent display name +# AGENT_DESCRIPTION - Agent description +# AGENT_IMAGE - Avatar URL (optional) -set -e +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Require Bankr CLI if ! command -v bankr >/dev/null 2>&1; then - echo "Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + echo "Error: Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + exit 1 +fi + +# Require jq +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq not found. Install with: apt-get install jq" >&2 exit 1 fi # Check for testnet flag -if [ "$1" = "--testnet" ] || [ "$1" = "-t" ]; then +if [ "${1:-}" = "--testnet" ] || [ "${1:-}" = "-t" ]; then CHAIN="sepolia" CHAIN_ID=11155111 IDENTITY_REGISTRY="0x8004A818BFB912233c491871b3d84c89A494BD9e" @@ -35,49 +41,62 @@ else echo "=== MAINNET MODE ===" >&2 fi +# Temp file with guaranteed cleanup +REG_FILE="/tmp/agent-reg-$$.json" +trap 'rm -f "$REG_FILE"' EXIT + # Create registration file -"$SCRIPT_DIR/create-registration.sh" /tmp/agent-reg-$$.json >/dev/null +"$SCRIPT_DIR/create-registration.sh" "$REG_FILE" >/dev/null -# Read and base64 encode -JSON_CONTENT=$(cat /tmp/agent-reg-$$.json) -BASE64_CONTENT=$(echo -n "$JSON_CONTENT" | base64 -w 0) +# Read and base64 encode — use -w 0 to disable line wrapping +BASE64_CONTENT=$(base64 -w 0 < "$REG_FILE") DATA_URI="data:application/json;base64,$BASE64_CONTENT" echo "" >&2 echo "Chain: $CHAIN (ID: $CHAIN_ID)" >&2 echo "Data URI length: ${#DATA_URI} bytes" >&2 +echo "Note: Larger calldata = higher gas cost vs IPFS/HTTP" >&2 echo "" >&2 -# Encode register(string) calldata -CALLDATA=$(node -e " -const uri = '$DATA_URI'; +# Encode register(string) calldata safely — URI passed via env, never shell-interpolated +CALLDATA=$(AGENT_URI="$DATA_URI" node -e " +const uri = process.env.AGENT_URI; +if (!uri) { process.stderr.write('Error: AGENT_URI not set\n'); process.exit(1); } const selector = '0xf2c298be'; const offset = '0000000000000000000000000000000000000000000000000000000000000020'; -const len = uri.length.toString(16).padStart(64, '0'); -const data = Buffer.from(uri, 'utf8').toString('hex').padEnd(Math.ceil(uri.length / 32) * 64, '0'); -console.log(selector + offset + len + data); +const encoded = Buffer.from(uri, 'utf8'); +const len = encoded.length.toString(16).padStart(64, '0'); +const data = encoded.toString('hex').padEnd(Math.ceil(encoded.length / 32) * 64, '0'); +process.stdout.write(selector + offset + len + data + '\n'); ") echo "Registering on-chain (data URI)..." >&2 -echo "Note: This will cost more gas than IPFS/HTTP due to larger calldata" >&2 + +# Build the transaction payload safely with jq +TX_PAYLOAD=$(jq -n \ + --arg to "$IDENTITY_REGISTRY" \ + --arg data "$CALLDATA" \ + --argjson chainId "$CHAIN_ID" \ + '{"to": $to, "data": $data, "value": "0", "chainId": $chainId}') # Submit via Bankr -RESULT=$(bankr agent "Submit this transaction on $CHAIN: {\"to\": \"$IDENTITY_REGISTRY\", \"data\": \"$CALLDATA\", \"value\": \"0\", \"chainId\": $CHAIN_ID}" 2>/dev/null) +RESULT=$(bankr agent "Submit this transaction on $CHAIN: $TX_PAYLOAD" 2>/dev/null) if echo "$RESULT" | grep -qE "$EXPLORER/tx/0x[a-fA-F0-9]{64}"; then TX_HASH=$(echo "$RESULT" | grep -oE "$EXPLORER/tx/0x[a-fA-F0-9]{64}" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) - + echo "" >&2 echo "=== REGISTRATION SUCCESSFUL! ===" >&2 echo "TX: https://$EXPLORER/tx/$TX_HASH" >&2 echo "Data is fully on-chain!" >&2 echo "" >&2 - - echo "{\"success\":true,\"chain\":\"$CHAIN\",\"dataUri\":true,\"tx\":\"$TX_HASH\"}" + + jq -n \ + --arg chain "$CHAIN" \ + --argjson dataUri true \ + --arg tx "$TX_HASH" \ + '{"success": true, "chain": $chain, "dataUri": $dataUri, "tx": $tx}' else echo "Registration submitted. Result:" >&2 echo "$RESULT" >&2 fi - -# Cleanup -rm -f /tmp/agent-reg-$$.json diff --git a/erc-8004/scripts/register.sh b/erc-8004/scripts/register.sh index a03fa7cb..2ffcaa08 100755 --- a/erc-8004/scripts/register.sh +++ b/erc-8004/scripts/register.sh @@ -1,34 +1,40 @@ #!/bin/bash # ERC-8004 - Register agent on Ethereum Mainnet # Usage: ./register.sh [--testnet] -# +# # Full registration flow: # 1. Create registration JSON # 2. Upload to IPFS via Pinata # 3. Register on-chain via Bankr # # Environment variables: -# PINATA_JWT - Required for IPFS upload -# AGENT_NAME - Agent display name -# AGENT_DESCRIPTION - Agent description -# AGENT_IMAGE - Avatar URL -# AGENT_WEBSITE - Website URL +# PINATA_JWT - Required for IPFS upload +# AGENT_NAME - Agent display name +# AGENT_DESCRIPTION - Agent description +# AGENT_IMAGE - Avatar URL +# AGENT_WEBSITE - Website URL # AGENT_A2A_ENDPOINT - A2A agent card URL # AGENT_MCP_ENDPOINT - MCP endpoint -# AGENT_ENS - ENS name +# AGENT_ENS - ENS name -set -e +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Require Bankr CLI if ! command -v bankr >/dev/null 2>&1; then - echo "Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + echo "Error: Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + exit 1 +fi + +# Require jq +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq not found. Install with: apt-get install jq" >&2 exit 1 fi # Check for testnet flag -if [ "$1" = "--testnet" ] || [ "$1" = "-t" ]; then +if [ "${1:-}" = "--testnet" ] || [ "${1:-}" = "-t" ]; then CHAIN="sepolia" CHAIN_ID=11155111 IDENTITY_REGISTRY="0x8004A818BFB912233c491871b3d84c89A494BD9e" @@ -43,7 +49,7 @@ else fi # Check requirements -if [ -z "$PINATA_JWT" ]; then +if [ -z "${PINATA_JWT:-}" ]; then echo "Error: PINATA_JWT environment variable not set" >&2 echo "Get your JWT from https://app.pinata.cloud/developers/api-keys" >&2 exit 1 @@ -54,9 +60,13 @@ echo "Chain: $CHAIN (ID: $CHAIN_ID)" >&2 echo "Registry: $IDENTITY_REGISTRY" >&2 echo "" >&2 +# Temp file with guaranteed cleanup +REG_FILE="/tmp/agent-registration-$$.json" +trap 'rm -f "$REG_FILE"' EXIT + # Step 1: Create registration file echo "Step 1/3: Creating registration file..." >&2 -REG_FILE=$("$SCRIPT_DIR/create-registration.sh" /tmp/agent-registration-$$.json) +"$SCRIPT_DIR/create-registration.sh" "$REG_FILE" >/dev/null echo "" >&2 # Step 2: Upload to IPFS @@ -67,33 +77,33 @@ echo "" >&2 # Step 3: Register on-chain echo "Step 3/3: Registering on-chain..." >&2 -# Encode register(string) calldata -# Function selector for register(string): 0xf2c298be -# Note: register() returns uint256 agentId -CALLDATA=$(node -e " -const uri = '$IPFS_URI'; +# Encode register(string) calldata safely via Node — URI passed via env, never shell-interpolated +CALLDATA=$(AGENT_URI="$IPFS_URI" node -e " +const uri = process.env.AGENT_URI; +if (!uri) { process.stderr.write('Error: AGENT_URI not set\n'); process.exit(1); } const selector = '0xf2c298be'; - -// String offset (0x20 for single string param) const offset = '0000000000000000000000000000000000000000000000000000000000000020'; - -// String length -const len = uri.length.toString(16).padStart(64, '0'); - -// String data (UTF-8 bytes, padded to 32-byte boundary) -const data = Buffer.from(uri, 'utf8').toString('hex').padEnd(Math.ceil(uri.length / 32) * 64, '0'); - -console.log(selector + offset + len + data); +const encoded = Buffer.from(uri, 'utf8'); +const len = encoded.length.toString(16).padStart(64, '0'); +const data = encoded.toString('hex').padEnd(Math.ceil(encoded.length / 32) * 64, '0'); +process.stdout.write(selector + offset + len + data + '\n'); ") echo "Calldata: $CALLDATA" >&2 +# Build the transaction payload safely with jq +TX_PAYLOAD=$(jq -n \ + --arg to "$IDENTITY_REGISTRY" \ + --arg data "$CALLDATA" \ + --argjson chainId "$CHAIN_ID" \ + '{"to": $to, "data": $data, "value": "0", "chainId": $chainId}') + # Submit via Bankr -RESULT=$(bankr agent "Submit this transaction on $CHAIN: {\"to\": \"$IDENTITY_REGISTRY\", \"data\": \"$CALLDATA\", \"value\": \"0\", \"chainId\": $CHAIN_ID}" 2>/dev/null) +RESULT=$(bankr agent "Submit this transaction on $CHAIN: $TX_PAYLOAD" 2>/dev/null) if echo "$RESULT" | grep -qE "$EXPLORER/tx/0x[a-fA-F0-9]{64}"; then TX_HASH=$(echo "$RESULT" | grep -oE "$EXPLORER/tx/0x[a-fA-F0-9]{64}" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) - + echo "" >&2 echo "======================================" >&2 echo "=== REGISTRATION SUCCESSFUL! ===" >&2 @@ -105,17 +115,21 @@ if echo "$RESULT" | grep -qE "$EXPLORER/tx/0x[a-fA-F0-9]{64}"; then echo "Your agent ID will be visible in the transaction logs." >&2 echo "View your agent at: https://www.8004.org" >&2 echo "" >&2 - - # Output JSON result - echo "{\"success\":true,\"chain\":\"$CHAIN\",\"ipfsUri\":\"$IPFS_URI\",\"tx\":\"$TX_HASH\",\"registry\":\"$IDENTITY_REGISTRY\"}" + + jq -n \ + --arg chain "$CHAIN" \ + --arg ipfsUri "$IPFS_URI" \ + --arg tx "$TX_HASH" \ + --arg registry "$IDENTITY_REGISTRY" \ + '{"success": true, "chain": $chain, "ipfsUri": $ipfsUri, "tx": $tx, "registry": $registry}' else echo "" >&2 echo "Registration submitted. Check transaction status:" >&2 echo "$RESULT" >&2 - - # Try to extract any transaction info - echo "{\"success\":\"pending\",\"chain\":\"$CHAIN\",\"ipfsUri\":\"$IPFS_URI\",\"result\":\"$RESULT\"}" -fi -# Cleanup -rm -f "$REG_FILE" + jq -n \ + --arg chain "$CHAIN" \ + --arg ipfsUri "$IPFS_URI" \ + --arg result "$RESULT" \ + '{"success": "pending", "chain": $chain, "ipfsUri": $ipfsUri, "result": $result}' +fi diff --git a/erc-8004/scripts/register.ts b/erc-8004/scripts/register.ts new file mode 100644 index 00000000..c5b1ead1 --- /dev/null +++ b/erc-8004/scripts/register.ts @@ -0,0 +1,171 @@ +/** + * ERC-8004 Agent Registration + * + * Handles on-chain registration of agent identity NFTs. + * + * Trust Score Policy: + * Agents cannot self-assign trust scores. Trust is computed externally + * from verifiable on-chain signals (transaction history, reputation registry + * feedback, peer attestations). This module intentionally provides no + * mechanism for an agent to claim its own trust level. + */ + +export interface RegistrationInput { + /** Agent metadata URI (ipfs://, https://, or data:) */ + agentUri: string; + /** + * Optional: chain to register on. + * Defaults to "mainnet". Use "sepolia" for testing. + */ + chain?: "mainnet" | "sepolia"; +} + +export interface RegistrationResult { + success: boolean; + chain: string; + agentUri: string; + txHash?: string; + error?: string; +} + +/** Contract addresses per chain */ +const IDENTITY_REGISTRY: Record = { + mainnet: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + sepolia: "0x8004A818BFB912233c491871b3d84c89A494BD9e", +}; + +const CHAIN_IDS: Record = { + mainnet: 1, + sepolia: 11155111, +}; + +/** + * Validate a registration URI. + * Only safe, well-known schemes are accepted. + */ +function validateUri(uri: string): void { + if (typeof uri !== "string" || uri.trim() === "") { + throw new Error("agentUri must be a non-empty string"); + } + const allowed = ["ipfs://", "https://", "http://", "data:application/json"]; + if (!allowed.some((prefix) => uri.startsWith(prefix))) { + throw new Error( + `agentUri must start with one of: ${allowed.join(", ")}` + ); + } +} + +/** + * ABI-encode register(string) calldata. + * Implements ERC-8004 registration function selector 0xf2c298be. + * + * @param uri - Agent URI to encode + * @returns Hex calldata string (with 0x prefix) + */ +export function encodeRegisterCalldata(uri: string): string { + validateUri(uri); + + const selector = "f2c298be"; + const encoded = Buffer.from(uri, "utf8"); + const offset = BigInt(32).toString(16).padStart(64, "0"); + const len = encoded.length.toString(16).padStart(64, "0"); + const paddedLen = Math.ceil(encoded.length / 32) * 32; + const data = Buffer.alloc(paddedLen); + encoded.copy(data); + + return "0x" + selector + offset + len + data.toString("hex"); +} + +/** + * ABI-encode setAgentURI(uint256,string) calldata. + * Implements ERC-8004 profile update function selector 0x862440e2. + * + * @param agentId - Agent token ID + * @param uri - New agent URI + * @returns Hex calldata string (with 0x prefix) + */ +export function encodeSetAgentUriCalldata( + agentId: bigint | number, + uri: string +): string { + validateUri(uri); + + const id = BigInt(agentId); + if (id < 0n) { + throw new RangeError("agentId must be a non-negative integer"); + } + + const selector = "862440e2"; + const idHex = id.toString(16).padStart(64, "0"); + // String starts after the uint256 param (offset = 0x40 = 64 bytes) + const offset = BigInt(64).toString(16).padStart(64, "0"); + const encoded = Buffer.from(uri, "utf8"); + const len = encoded.length.toString(16).padStart(64, "0"); + const paddedLen = Math.ceil(encoded.length / 32) * 32; + const data = Buffer.alloc(paddedLen); + encoded.copy(data); + + return "0x" + selector + idHex + offset + len + data.toString("hex"); +} + +/** + * Build a registration transaction object for submission via Bankr. + * + * Note on trust scores: This function intentionally does not accept or + * encode a trust score. Trust is assigned externally by the ERC-8004 + * Reputation Registry based on verifiable on-chain behavior, not + * self-declaration. Callers cannot influence their own trust level + * through this API. + * + * @param input - Registration input (URI + optional chain) + * @returns Transaction object ready for Bankr submission + */ +export function buildRegistrationTx(input: RegistrationInput): { + to: string; + data: string; + value: string; + chainId: number; +} { + const chain = input.chain ?? "mainnet"; + + if (!(chain in IDENTITY_REGISTRY)) { + throw new Error(`Unknown chain: ${chain}. Use "mainnet" or "sepolia".`); + } + + return { + to: IDENTITY_REGISTRY[chain], + data: encodeRegisterCalldata(input.agentUri), + value: "0", + chainId: CHAIN_IDS[chain], + }; +} + +/** + * Build an update transaction object for submission via Bankr. + * + * @param agentId - Agent token ID to update + * @param newUri - New metadata URI + * @param chain - Chain ("mainnet" or "sepolia") + * @returns Transaction object ready for Bankr submission + */ +export function buildUpdateTx( + agentId: bigint | number, + newUri: string, + chain: "mainnet" | "sepolia" = "mainnet" +): { + to: string; + data: string; + value: string; + chainId: number; +} { + if (!(chain in IDENTITY_REGISTRY)) { + throw new Error(`Unknown chain: ${chain}. Use "mainnet" or "sepolia".`); + } + + return { + to: IDENTITY_REGISTRY[chain], + data: encodeSetAgentUriCalldata(agentId, newUri), + value: "0", + chainId: CHAIN_IDS[chain], + }; +} diff --git a/erc-8004/scripts/update-profile.sh b/erc-8004/scripts/update-profile.sh index ddac39c3..69a5373f 100755 --- a/erc-8004/scripts/update-profile.sh +++ b/erc-8004/scripts/update-profile.sh @@ -1,21 +1,39 @@ #!/bin/bash # ERC-8004 - Update agent profile URI -# Usage: ./update-profile.sh [--testnet] +# Usage: ./update-profile.sh [--testnet] # Example: ./update-profile.sh 123 ipfs://QmXxx... -set -e +set -euo pipefail # Require Bankr CLI if ! command -v bankr >/dev/null 2>&1; then - echo "Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 + echo "Error: Bankr CLI not found. Install with: bun install -g @bankr/cli" >&2 exit 1 fi -AGENT_ID="${1:?Usage: update-profile.sh [--testnet]}" -NEW_URI="${2:?Usage: update-profile.sh [--testnet]}" +# Require jq +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq not found. Install with: apt-get install jq" >&2 + exit 1 +fi + +AGENT_ID="${1:?Usage: update-profile.sh [--testnet]}" +NEW_URI="${2:?Usage: update-profile.sh [--testnet]}" + +# Validate agent ID is a non-negative integer +if ! [[ "$AGENT_ID" =~ ^[0-9]+$ ]]; then + echo "Error: agent-id must be a non-negative integer" >&2 + exit 1 +fi + +# Validate URI scheme +if [[ ! "$NEW_URI" =~ ^(ipfs|https?|data): ]]; then + echo "Error: new-uri must start with ipfs://, http://, https://, or data:" >&2 + exit 1 +fi # Check for testnet flag -if [ "$3" = "--testnet" ] || [ "$3" = "-t" ]; then +if [ "${3:-}" = "--testnet" ] || [ "${3:-}" = "-t" ]; then CHAIN="sepolia" CHAIN_ID=11155111 IDENTITY_REGISTRY="0x8004A818BFB912233c491871b3d84c89A494BD9e" @@ -35,43 +53,48 @@ echo "New URI: $NEW_URI" >&2 echo "Chain: $CHAIN" >&2 echo "" >&2 -# Encode setAgentURI(uint256,string) calldata -# Function selector: 0x862440e2 (setAgentURI(uint256,string)) -CALLDATA=$(node -e " -const agentId = BigInt('$AGENT_ID'); -const uri = '$NEW_URI'; +# Encode setAgentURI(uint256,string) calldata safely via Node +# Agent ID and URI passed via env to avoid shell injection +CALLDATA=$(AGENT_ID_VAL="$AGENT_ID" AGENT_URI="$NEW_URI" node -e " +const agentId = BigInt(process.env.AGENT_ID_VAL); +const uri = process.env.AGENT_URI; +if (!uri) { process.stderr.write('Error: AGENT_URI not set\n'); process.exit(1); } const selector = '0x862440e2'; - -// uint256 agentId (32 bytes) const id = agentId.toString(16).padStart(64, '0'); - -// String offset (0x40 = 64 bytes from start of params) +// String offset: 0x40 = 64 bytes from start of params (after the uint256) const offset = '0000000000000000000000000000000000000000000000000000000000000040'; - -// String length -const len = uri.length.toString(16).padStart(64, '0'); - -// String data (UTF-8 bytes, padded to 32-byte boundary) -const data = Buffer.from(uri, 'utf8').toString('hex').padEnd(Math.ceil(uri.length / 32) * 64, '0'); - -console.log(selector + id + offset + len + data); +const encoded = Buffer.from(uri, 'utf8'); +const len = encoded.length.toString(16).padStart(64, '0'); +const data = encoded.toString('hex').padEnd(Math.ceil(encoded.length / 32) * 64, '0'); +process.stdout.write(selector + id + offset + len + data + '\n'); ") echo "Calldata: $CALLDATA" >&2 +# Build the transaction payload safely with jq +TX_PAYLOAD=$(jq -n \ + --arg to "$IDENTITY_REGISTRY" \ + --arg data "$CALLDATA" \ + --argjson chainId "$CHAIN_ID" \ + '{"to": $to, "data": $data, "value": "0", "chainId": $chainId}') + # Submit via Bankr -RESULT=$(bankr agent "Submit this transaction on $CHAIN: {\"to\": \"$IDENTITY_REGISTRY\", \"data\": \"$CALLDATA\", \"value\": \"0\", \"chainId\": $CHAIN_ID}" 2>/dev/null) +RESULT=$(bankr agent "Submit this transaction on $CHAIN: $TX_PAYLOAD" 2>/dev/null) if echo "$RESULT" | grep -qE "$EXPLORER/tx/0x[a-fA-F0-9]{64}"; then TX_HASH=$(echo "$RESULT" | grep -oE "$EXPLORER/tx/0x[a-fA-F0-9]{64}" | grep -oE '0x[a-fA-F0-9]{64}' | head -1) - + echo "=== SUCCESS ===" >&2 echo "Agent $AGENT_ID profile updated!" >&2 echo "New URI: $NEW_URI" >&2 echo "TX: https://$EXPLORER/tx/$TX_HASH" >&2 - - echo "{\"success\":true,\"agentId\":\"$AGENT_ID\",\"newUri\":\"$NEW_URI\",\"tx\":\"$TX_HASH\"}" + + jq -n \ + --arg agentId "$AGENT_ID" \ + --arg newUri "$NEW_URI" \ + --arg tx "$TX_HASH" \ + '{"success": true, "agentId": $agentId, "newUri": $newUri, "tx": $tx}' else echo "Update submitted. Check transaction status:" >&2 echo "$RESULT" >&2 diff --git a/erc-8004/scripts/upload-to-ipfs.sh b/erc-8004/scripts/upload-to-ipfs.sh index d7901ce7..6f5edd22 100755 --- a/erc-8004/scripts/upload-to-ipfs.sh +++ b/erc-8004/scripts/upload-to-ipfs.sh @@ -4,11 +4,11 @@ # Example: ./upload-to-ipfs.sh /tmp/agent-registration.json # Requires: PINATA_JWT environment variable -set -e +set -euo pipefail JSON_FILE="${1:?Usage: upload-to-ipfs.sh }" -if [ -z "$PINATA_JWT" ]; then +if [ -z "${PINATA_JWT:-}" ]; then echo "Error: PINATA_JWT environment variable not set" >&2 echo "Get your JWT from https://app.pinata.cloud/developers/api-keys" >&2 exit 1 @@ -19,20 +19,29 @@ if [ ! -f "$JSON_FILE" ]; then exit 1 fi +# Validate it's valid JSON before uploading +if ! jq empty "$JSON_FILE" 2>/dev/null; then + echo "Error: $JSON_FILE is not valid JSON" >&2 + exit 1 +fi + echo "=== Uploading to IPFS via Pinata ===" >&2 echo "File: $JSON_FILE" >&2 -# Upload to Pinata -RESPONSE=$(curl -s -X POST "https://api.pinata.cloud/pinning/pinFileToIPFS" \ +# Upload to Pinata — --fail ensures non-2xx responses are treated as errors +RESPONSE=$(curl -s --fail -X POST "https://api.pinata.cloud/pinning/pinFileToIPFS" \ -H "Authorization: Bearer $PINATA_JWT" \ -F "file=@$JSON_FILE" \ - -F "pinataMetadata={\"name\": \"erc-8004-agent-registration\"}") + -F 'pinataMetadata={"name": "erc-8004-agent-registration"}') || { + echo "Error: Pinata upload failed (HTTP error)" >&2 + exit 1 +} # Extract CID CID=$(echo "$RESPONSE" | jq -r '.IpfsHash // empty') if [ -z "$CID" ]; then - echo "Error: Upload failed" >&2 + echo "Error: Upload failed — no IpfsHash in response" >&2 echo "$RESPONSE" >&2 exit 1 fi