-
Notifications
You must be signed in to change notification settings - Fork 1
Add first-class ERC-8004 claim registration tracking #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| const db = require('../../lib/db'); | ||
| const { stableStringify, sha256Hex, pinJsonToPinata } = require('../../lib/ipfsPinning'); | ||
| const { trackPinnedCardRegistration } = require('../../lib/claims/agent-registrations'); | ||
|
|
||
| function firstObjectValue(obj) { | ||
| if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null; | ||
|
|
@@ -114,7 +115,10 @@ module.exports = async function handler(req, res) { | |
| } | ||
|
|
||
| const allPinned = cardsResult.rows.every((r) => r.card_cid && r.card_ipfs_uri && r.card_sha256); | ||
| if (allPinned) return res.status(200).json({ ok: true, status: 'ALREADY_PINNED', claimId, cards: cardsResult.rows }); | ||
| if (allPinned) { | ||
| await Promise.all(cardsResult.rows.map(trackPinnedCardRegistration)); | ||
| return res.status(200).json({ ok: true, status: 'ALREADY_PINNED', claimId, cards: cardsResult.rows }); | ||
| } | ||
|
|
||
| const gatewayBase = (process.env.IPFS_GATEWAY_BASE_URL || 'https://gateway.pinata.cloud/ipfs').replace(/\/$/, ''); | ||
|
|
||
|
|
@@ -130,6 +134,7 @@ module.exports = async function handler(req, res) { | |
| const hash = sha256Hex(canonical); | ||
|
|
||
| if (row.card_cid && row.card_ipfs_uri && row.card_sha256) { | ||
| await trackPinnedCardRegistration(row); | ||
| pinned.push({ ens: row.ens, card_cid: row.card_cid, card_sha256: row.card_sha256, card_gateway_url: row.card_gateway_url }); | ||
| continue; | ||
| } | ||
|
|
@@ -145,6 +150,7 @@ module.exports = async function handler(req, res) { | |
| where id = $1`, | ||
| [row.id, cid, ipfsUri, gatewayUrl, hash, provider] | ||
| ); | ||
| await trackPinnedCardRegistration({ ...row, card_cid: cid, card_ipfs_uri: ipfsUri }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When ERC-8004 configuration is enabled and this database upsert fails—for example, during a transient database error or before migration 012 is applied—the exception is handled as an IPFS failure even though Pinata and the preceding Useful? React with 👍 / 👎. |
||
| pinned.push({ ens: row.ens, card_cid: cid, card_sha256: hash, card_gateway_url: gatewayUrl }); | ||
| } catch (error) { | ||
| await db.query("update agent_cards set pin_status = 'error', pin_error = $2 where id = $1", [row.id, String(error && error.message ? error.message : 'pinning_failed')]); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ module.exports = async function handler(req, res) { | |
| const auth = getClaimAuth(req, claim); | ||
| if (!auth.ok) return unauthorizedClaimResponse(res); | ||
| let cards = []; | ||
| let registrations = []; | ||
| try { | ||
| const cardsResult = await db.query( | ||
| `select ens, card_cid, card_ipfs_uri, card_gateway_url, card_sha256, pin_status from agent_cards where claim_id = $1 order by ens asc`, | ||
|
|
@@ -44,6 +45,17 @@ module.exports = async function handler(req, res) { | |
| } catch (_error) { | ||
| cards = []; | ||
| } | ||
| try { | ||
| const registrationsResult = await db.query( | ||
| `select standard, ens, chain_id, registry_address, agent_id, agent_uri, registration_status, | ||
| ensip25_status, verified_at | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an ENSIP-25 verifier populates the new Useful? React with 👍 / 👎. |
||
| from agent_registrations where claim_id = $1 order by ens asc, standard asc`, | ||
| [claimId] | ||
| ); | ||
| registrations = registrationsResult.rows; | ||
| } catch (_error) { | ||
| registrations = []; | ||
| } | ||
| const recordsVerified = claim.tenant_signer_record_status === 'records_verified' || claim.tenant_signer_record_status === 'verified'; | ||
| const paymentConfirmed = claim.status === 'paid' || claim.status === 'cards_pinned' || claim.status === 'active' || claim.payment_status === 'paid' || Boolean(claim.paid_at); | ||
| const pipeline = { | ||
|
|
@@ -58,7 +70,7 @@ module.exports = async function handler(req, res) { | |
| first_action_receipt: claim.first_action_receipt_status || 'not_generated', | ||
| agent_live: paymentConfirmed && recordsVerified && cardsStatus(cards) === 'cards_pinned' && claim.genesis_receipt_id && claim.tenant_proof_status === 'verified' && (claim.first_action_receipt_status === 'verified' || typeof claim.first_action_receipt_status === 'undefined') ? 'live' : 'not_live', | ||
| }; | ||
| return res.status(200).json({ ok: true, read_only: true, claim: { ...stripClaimSecrets(claim), cardsStatus: cardsStatus(cards), managed_ens_publication: { status: claim.managed_ens_publication_status || 'not_started', signer_ens: claim.tenant_signer_ens || null, parent_namespace: claim.managed_ens_parent_namespace || null, record_names: Object.keys(claim.managed_ens_required_txt_records || claim.tenant_signer_txt_records || {}), helper_copy: 'The operator must publish the generated TXT records to ENS before signer verification can pass.', verified_at: claim.managed_ens_verified_at || null, error: claim.managed_ens_publication_error || null } }, pipeline, cards }); | ||
| return res.status(200).json({ ok: true, read_only: true, claim: { ...stripClaimSecrets(claim), cardsStatus: cardsStatus(cards), managed_ens_publication: { status: claim.managed_ens_publication_status || 'not_started', signer_ens: claim.tenant_signer_ens || null, parent_namespace: claim.managed_ens_parent_namespace || null, record_names: Object.keys(claim.managed_ens_required_txt_records || claim.tenant_signer_txt_records || {}), helper_copy: 'The operator must publish the generated TXT records to ENS before signer verification can pass.', verified_at: claim.managed_ens_verified_at || null, error: claim.managed_ens_publication_error || null } }, pipeline, cards, registrations }); | ||
| } catch (_error) { | ||
| return res.status(500).json({ ok: false, status: 'CLAIM_STATUS_UNAVAILABLE' }); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| create table if not exists public.agent_registrations ( | ||
| id uuid primary key default gen_random_uuid(), | ||
| claim_id text not null, | ||
| ens text not null, | ||
| standard text not null default 'erc8004', | ||
| chain_id text not null, | ||
| registry_address text not null, | ||
| agent_id text, | ||
| agent_uri text, | ||
| agent_card_cid text, | ||
| registration_tx_hash text, | ||
| registration_status text not null default 'pending', | ||
| ensip25_status text not null default 'not_checked', | ||
| ensip25_claim_key text, | ||
| ensip25_verified_at timestamptz, | ||
| registered_at timestamptz, | ||
| verified_at timestamptz, | ||
| metadata_json jsonb, | ||
| last_error text, | ||
| last_error_at timestamptz, | ||
| created_at timestamptz not null default now(), | ||
| updated_at timestamptz not null default now(), | ||
| constraint agent_registrations_claim_id_fkey foreign key (claim_id) | ||
| references public.claim_requests(claim_id) on delete cascade | ||
| ); | ||
|
|
||
| create index if not exists idx_agent_registrations_claim_id on public.agent_registrations(claim_id); | ||
| create index if not exists idx_agent_registrations_ens on public.agent_registrations(ens); | ||
| create index if not exists idx_agent_registrations_standard on public.agent_registrations(standard); | ||
| create index if not exists idx_agent_registrations_registration_status on public.agent_registrations(registration_status); | ||
| create index if not exists idx_agent_registrations_agent_id on public.agent_registrations(agent_id); | ||
| create unique index if not exists idx_agent_registrations_registry_identity | ||
| on public.agent_registrations(standard, ens, chain_id, registry_address); | ||
|
|
||
| create or replace function public.set_agent_registrations_updated_at() | ||
| returns trigger | ||
| language plpgsql | ||
| as $$ | ||
| begin | ||
| new.updated_at = now(); | ||
| return new; | ||
| end; | ||
| $$; | ||
|
|
||
| drop trigger if exists set_agent_registrations_updated_at on public.agent_registrations; | ||
| create trigger set_agent_registrations_updated_at | ||
| before update on public.agent_registrations | ||
| for each row execute function public.set_agent_registrations_updated_at(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 'use strict'; | ||
|
|
||
| const db = require('../db'); | ||
|
|
||
| /** Idempotently records card discovery metadata; it does not perform registration or verification. */ | ||
| async function upsertErc8004Registration({ claimId, ens, chainId, registryAddress, agentUri = null, agentCardCid = null, metadata = null }) { | ||
| if (!claimId || !ens || !chainId || !registryAddress) { | ||
| throw new Error('claimId, ens, chainId, and registryAddress are required'); | ||
| } | ||
|
|
||
| const result = await db.query( | ||
| `insert into agent_registrations | ||
| (claim_id, ens, standard, chain_id, registry_address, agent_uri, agent_card_cid, metadata_json) | ||
| values ($1, $2, 'erc8004', $3, $4, $5, $6, $7::jsonb) | ||
| on conflict (standard, ens, chain_id, registry_address) do update | ||
| set claim_id = excluded.claim_id, | ||
|
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a second claim contains the same ENS name, chain, and registry as an earlier claim—which the claim intake permits because ENS is not unique across claims—this conflict handler reassigns the existing registration row to the new claim while intentionally preserving its Useful? React with 👍 / 👎. |
||
| agent_uri = coalesce(excluded.agent_uri, agent_registrations.agent_uri), | ||
| agent_card_cid = coalesce(excluded.agent_card_cid, agent_registrations.agent_card_cid), | ||
| metadata_json = coalesce(excluded.metadata_json, agent_registrations.metadata_json) | ||
| returning *`, | ||
| [claimId, ens, chainId, registryAddress, agentUri, agentCardCid, metadata == null ? null : JSON.stringify(metadata)] | ||
| ); | ||
| return result.rows[0] || null; | ||
| } | ||
|
|
||
| async function trackPinnedCardRegistration(card) { | ||
| const chainId = process.env.ERC8004_CHAIN_ID; | ||
| const registryAddress = process.env.ERC8004_REGISTRY_ADDRESS; | ||
| if (!chainId || !registryAddress) return null; | ||
| return upsertErc8004Registration({ | ||
| claimId: card.claim_id, | ||
| ens: card.ens, | ||
| chainId, | ||
| registryAddress, | ||
| agentUri: card.card_ipfs_uri || null, | ||
| agentCardCid: card.card_cid || null, | ||
| }); | ||
| } | ||
|
|
||
| module.exports = { upsertErc8004Registration, trackPinnedCardRegistration }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an existing claim whose cards were pinned before this deployment, the main admin “Run activation pipeline” path checks
cardsPinnedinapi/admin/run-activation-pipeline.js:79-83and deliberately skipspinAgentCards, so this newly added all-pinned backfill is never reached. Such claims continue to show no registration metadata unless an operator knows to invoke the separate pin endpoint manually; perform the tracking from the orchestration path or provide an explicit migration/backfill so the normal workflow populates these rows.Useful? React with 👍 / 👎.