feat: Align grant registration with new gateway struct#150
Conversation
Replace the legacy `grant: string` JSON blob + `fileIds: uint256[]` with first-class EIP-712 fields `scopes: string[]`, `grantVersion: uint256`, and `expiresAt: uint256`, and add `grantVersion` to GrantRevocation so the monotonic nonce defends both events against replay. Update the gateway client wire format and GET response shape (now carries `expiresAt`/`expired`/`paymentStatus`/`paidAt`/`paidBy`/`grantVersion`/ `fee`) to match data-gateway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
|
Findings
Verification
|
Add the GenericPayment EIP-712 typed-data and gateway-client wrappers for /v1/escrow/balance, /v1/escrow/deposit, and /v1/escrow/pay so a builder can settle a grant's registration + data-access fees before calling the Personal Server. Adds `dataPortabilityEscrow` to the contracts config (so existing fixtures gain the field) and a NATIVE_VANA_ASSET sentinel for the native-asset case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add primitives for funding the escrow balance from a wallet —
buildDepositNativeRequest / buildDepositTokenRequest return the
raw {to, data, value?} request object for any tx-submission stack
(viem, ethers, wallet-rpc, MPC, Safe). The credited `account` is
encoded in calldata, so a third party can fund someone else's escrow.
Add an e2e test that wires the deposit + payment helpers through a
real viem WalletClient against an in-memory L1 (custom RPC transport)
and a fetch-mocked gateway that decodes the same calldata via
ESCROW_DEPOSIT_ABI and recovers the GenericPayment signer with
recoverTypedDataAddress — the same cryptographic checks the real
gateway runs. Covers happy path, 402 insufficient-balance, and 409
nonce-replay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /v1/escrow/pay endpoint returns `breakdown.registrationPaid` (true when this call flipped the grant from pending→paid), but the SDK type was reading `registrationSettled` — a name nothing on the gateway side emits. Renamed throughout, including the e2e mock so a future rename on either side surfaces as a test failure rather than `undefined`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… balance GET /v1/escrow/balance returns four numbers per asset — balance, pendingAmount, authorizedAmount, availableAmount — but the SDK type exposed only the first two. Builders couldn't read the soft-lock headroom (availableAmount) before calling /v1/escrow/pay without bypassing the SDK type. Surface both fields; reshape the e2e mock so balance reflects gross deposits and authorized accumulates per pay, matching the gateway's pre-settle steady state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two typed-data sets land alongside dataRegistryDomain — both verified against the DataRegistryV2 contract: - ADD_DATA_TYPES — owner signs to register (scope, dataHash, metadataHash, expectedVersion). expectedVersion is CAS — the contract rejects if the on-chain version has moved. - RECORD_DATA_ACCESS_TYPES — a trusted personal server signs to attest that (scope, version) was delivered to `accessor`. Used as the accessRecord on /v1/escrow/pay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /v1/escrow/pay accepts an optional access record — a server-signed delivery receipt that the next /v1/settle pass replays on-chain via DataRegistryV2.recordDataAccess. Without it, the SDK couldn't drive per-call data-access payments through to the on-chain totalAccesses counter; callers had to bypass the SDK to attach the receipt. Extends the e2e mock to validate the accessRecord signature via RECORD_DATA_ACCESS_TYPES + dataRegistryDomain and check the recovered signer is one of the grant owner's trusted servers — the same three checks the real gateway runs. New test exercises the second-payment + accessRecord path against a freshly-signed receipt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /v1/builders was the one register endpoint without an SDK wrapper. Builder-side registration now goes through the SDK end-to-end — ownerAddress signs BUILDER_REGISTRATION_TYPES, the gateway recovers and deterministically derives builderId. Returned id is reused as the granteeId on subsequent grants. e2e mock implements the matching recovery + computeBuilderId; happy path no longer relies on seedBuilder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /v1/data was the entire data-point flow with zero SDK surface. ownerAddress signs ADD_DATA_TYPES (commits to scope, dataHash, metadataHash, expectedVersion); the SDK posts and surfaces the gateway-computed dataPointId. The 409 stale-CAS path throws with the gateway's exact error message so callers can read currentExpectedVersion and re-sign — distinct from the idempotent-409 semantics of grants / builders / servers. e2e mock implements the matching AddData recovery + dataPointId derivation; new tests cover both 201 and 409 paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /v1/settle drains pending on-chain rows (grants, servers, data points, access records) to the relayer, promotes 'submitting' → 'confirmed' for previously-submitted rows, and reconciles 'confirmed' → 'finalized' once Moksha's finalized tip catches up. Returns a single envelope with all three phases' outcomes. The SDK now exposes settle() + the full SettleItem / SettlePromoteResult / SettleReconcileItem discriminated unions, so callers can drive the end-to-end lifecycle without bypassing the SDK. e2e mock applies a synthetic settle pass that flips paid+pending grants to 'confirmed' with a stamped settleTxHash; happy-path test now exercises the drain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fields GatewayGrantResponse.status was typed as 'pending' | 'confirmed', but the schema's settle lifecycle adds 'submitting', 'finalized', and 'reorged' — without these, TypeScript narrowed incorrectly on any post-settle code path. Also adds settleTxHash, settleSubmittedAt, revocationTxHash, revocationSubmittedAt to the response shape so callers can read the chain-side metadata the gateway already returns. e2e mock surfaces the new fields in handleGetGrant; happy-path test now asserts the post-settle grant reads back as 'confirmed' with a populated settleTxHash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/e2e-escrow-deposit.ts walks the full deposit + builder + server + data point + grant + payments + settle + reconcile flow against a live gateway + Moksha L1, driving every gateway endpoint through the SDK's GatewayClient + EIP-712 helpers instead of raw fetch / signTypedData. Mirrors data-gateway/scripts/e2e-escrow-deposit.ts step-for-step so future drift on either side surfaces here. Smoke-tested against a local gateway on Moksha: real depositNative tx mined, submitEscrowDeposit reconciled, getEscrowBalance returned the new authorizedAmount + availableAmount fields, registerBuilder succeeded. Run with `npm run e2e:deposit` after populating .env.local with the deployed contract addresses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… e2e The server, data point, and access record asserts hard-required status='confirmed' from /v1/settle, but the gateway returns 'submitting' whenever the relayer broadcasts without waiting for the receipt — a valid lifecycle state that the reconcile pass in the poll-for-finalized loop promotes to 'confirmed' → 'finalized'. Mirror the grant check (already tolerant of both) via a shared requireSettleProgress helper so the four op types are treated consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the branch to the prerelease workflow's push-trigger list so every push publishes @opendatalabs/vana-sdk@<ver>-canary.<sha> to npm under the `canary` dist-tag. Downstreams that want to try the grant-struct + escrow changes early can pin with `npm i @opendatalabs/vana-sdk@canary`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r e2e Adds a standalone e2e-canary/ outside the workspace that pins @opendatalabs/vana-sdk@3.2.0-canary.88d802d (the published canary matching this branch's HEAD when added). Because it lives outside packages/*, npm resolves the SDK from the registry rather than the workspace — exactly what a downstream consumer experiences after `npm i @opendatalabs/vana-sdk@canary`. The script is a verbatim copy of packages/vana-sdk/scripts/e2e-escrow-deposit.ts with the one import changed to "@opendatalabs/vana-sdk". The in-package script stays as the source-of-truth dev-loop test (relative imports of unbuilt source); this canary-pinned variant verifies the publishing pipeline produced an artifact consumers can actually use. Usage: cd e2e-canary && npm install && npm run e2e:deposit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nputs - grants.test.ts: replace the 0x000...001 "private key = 1" placeholder (preexisting on main; this commit removes it from this branch's tip) with generatePrivateKey() so future code stays clean. - e2e scripts: log ownerAddress / scope / dataHash / metadataHash / expectedVersion / verifyingContract at the data-point registration step so signing-related mismatches surface in the run output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a8087c5 to
e3ef84a
Compare
The gateway's /v1/escrow/pay re-resolves the canonical fee on every
call by reading the on-chain FeeRegistry contract (lib/fee-registry.ts).
SDK consumers had no way to read the same source of truth — they had
to hardcode fee constants and hope they matched. Against the local
gateway with default fixtures it usually worked; against any other
deployment it would 400 with "Payment amount does not match canonical
fee total."
Adds protocol/fee-registry.ts mirroring the gateway adapter:
- FEE_REGISTRY_ABI (parseAbi of the deployed shape)
- getFee(client, config, kind, opts?) — reads one fee record
- getOpFee(client, config, opts?) — combined view for the pay handler,
rejects asset-mismatch between the two kinds
- FeeEntry / OpFee / FeeKind / FeeRegistryOptions types
DataPortabilityContracts gains `feeRegistry: string`; all six existing
test fixtures + the validator updated accordingly. e2e-escrow-deposit.ts
now resolves REGISTRATION_FEE / DATA_ACCESS_FEE / FEE_ASSET /
PROTOCOL_FEE_RECIPIENT from getOpFee() at startup instead of from
hardcoded env defaults — matches the gateway's e2e initFeeSchedule()
pattern. Only FEE_REGISTRY_CONTRACT env var is required now.
No-cache by design: long-running consumers should wrap with their own
TTL. Pre-commit sweep confirmed no private keys leaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flight Three related fixes after running the e2e against the Vercel preview gateway revealed they were independent failure modes stacked on top of each other: 1. FeeRegistry drift. The SDK's getOpFee() reads from whatever FEE_REGISTRY_CONTRACT the local .env.local pins; the deployed gateway reads from its own Vercel env. When they disagree, the SDK-computed `amount` won't match the gateway's expected total and /v1/escrow/pay returns 400. Trust grant.fee.totalDue (the gateway is the authority that validates payments anyway) and warn loudly if our local read differs. 2. Hardcoded 0.1 VANA deposit was 10^15× oversized on a Moksha FeeRegistry with sub-gwei fees. Auto-size DEPOSIT_AMOUNT to exactly the bundled total (1 registration + (1+EXTRA_ACCESS_COUNT) data-access) at startup, after the fee schedule is resolved. Env override still wins for callers who want headroom. 3. Funder pre-flight checked balance >= DEPOSIT_AMOUNT but ignored gas. With deposits sub-wei small, the actual blocker is gas (~5e13 wei on Moksha) and viem reported it as a cryptic "gas required exceeds allowance" from the RPC sim. Now estimates gas via getGasPrice() × 120k × 2× safety and rolls it into the pre-flight, with both components surfaced in the error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the canary-pinned consumer test from 3.2.0-canary.88d802d to 3.2.0-canary.2b8e377, which is the first published canary that contains FeeRegistry support (getOpFee) and the pre-flight + auto-size fixes. Re-syncs the script verbatim from packages/vana-sdk/scripts (with the one import line swapped to "@opendatalabs/vana-sdk") so the canary-pinned variant tracks the latest fixes. Verified end-to-end against the Vercel preview gateway at dp-rpc-git-dpv2-fee-and-payment-opendatalabs.vercel.app: all 21 steps green including the on-chain reconcile loop (123s to finalized) and the bundled Settled events sum check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adapts the SDK to two breaking gateway changes that landed together
(data-gateway commits d4b965d + f00008c):
1. FeeKind rename + per-op-type fees. The old binary registration /
data_access split becomes five kinds:
- grant_registration (was 'registration')
- data_access (unchanged)
- data_registration (new)
- server_registration (new)
- builder_registration (new)
Each op type (grant, data, server, builder) now has its own
one-time registration fee, gated independently in the on-chain
FeeRegistry. New `REGISTRATION_KIND_FOR_OP` mapping resolves op
type → FeeKind; getOpFee takes an `opType` parameter and routes
accordingly. data_access remains a per-call surcharge on grants
only — other op types get dataAccessFee = 0n / dataAccessEnabled
= false.
2. Disabled fees no longer throw. Operators can toggle a fee off
without redeploying; the gateway treats `enabled: false` as
"no payment required". getFee() now returns disabled entries
verbatim instead of throwing — only the zero-payee check still
fires (gated on enabled). OpFee gains `registrationEnabled` /
`dataAccessEnabled` flags so callers see why amounts are zero.
getOpFee's asset-mismatch check only fires when both kinds
enabled.
Also: add `revokedAt: string | null` to ServerInfo. The gateway's
GET /v1/servers/:addr handler has been returning this field; the SDK
type just never surfaced it.
Updates: extended FeeRegistryOptions with the four new op-name escape
hatches, expanded fee-registry.test.ts to 11 cases covering disabled
fees + per-op-type semantics + asset-mismatch tolerance, fixed the
in-package e2e script to pass 'grant' to getOpFee and pick the
correct payee when one kind is disabled. 691/691 tests pass.
Breaking: callers using the old 'registration' FeeKind name or the
two-arg getOpFee signature must update. No hardcoded keys leaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… grant
The gateway's new per-kind fee gating (data-gateway commits d4b965d,
6f435d4, f00008c) lets operators toggle grant_registration and
data_access independently. When both are disabled the entire payment
flow short-circuits, which the previous e2e couldn't represent — it
auto-sized DEPOSIT_AMOUNT=0, called depositNative(value=0) and reverted
on-chain.
Per-kind branching now matches the gateway's own e2e
(scripts/e2e-escrow-deposit.ts):
REGISTRATION_ENABLED || DATA_ACCESS_ENABLED:
deposit + balance polling + payment ↦ as before
both disabled:
skip deposit (no money to flow), skip POST /v1/escrow/pay
(gateway would 409 'Payment not required'), accept grant born
paymentStatus='paid' on creation, accept paidBy=null after
The access-record loop now always runs — when data_access is
disabled, amount=0 + accessRecord still posts and the gateway
replays the server-signed RecordDataAccess on-chain via
recordDataAccess (gateway commit 6f435d4).
bundledTotal + expectedSettledCount account for disabled kinds. The
on-chain Settled-events validation block is skipped when bundledTotal=0
(no events fire). Balance-compare is skipped when no deposit was made.
The reconcile-pass output for the grant op stuck at 'unchanged' on the
disabled-fee path even after the chain tip caught up (other ops
finalized cleanly — a gateway-side reconcile gap). The script now
queries gateway.getGrant() directly after the polling loop to read
the canonical current status, rather than trusting the misleading
reconcile-pass output. Also: resilient settle() loop tolerates
transient 500s, finalize timeout bumped to 5min.
Verified end-to-end against
dp-rpc-git-dpv2-fee-and-payment-opendatalabs.vercel.app
with both fees currently disabled — all 19 steps green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old default auto-sized to `REGISTRATION_FEE + (1 + EXTRA_ACCESS_COUNT) × DATA_ACCESS_FEE` from the on-chain FeeRegistry. On the current preview deployments both kinds are disabled, so that resolved to 0 and the deposit step was skipped entirely. On larger-fee deployments it'd over-deposit for no good reason. Switch to a flat 1 gwei default (= 0.000000001 VANA) — plenty of headroom against any sub-gwei fee schedule, negligible cost on testnet, predictable across runs. Callers running against deployments with larger fees can still set DEPOSIT_AMOUNT in .env.local; the funder pre-flight + the gateway's 402 'insufficient balance' surface "needs more" cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /v1/settle reconcile-pass output is keyed to the run that PROMOTED the row, not to the row's current state. On the gateway's disabled-fee path the grant's reconcile output stays at 'unchanged' indefinitely even after the row's actual `grants.status` column moves to 'finalized' — meaning the e2e was wasting the full 5min timeout waiting for a signal that never arrives. Switch the grant slot in the polling loop to call gateway.getGrant() each iteration and use the canonical DB state. Other ops still use their reconcile output (which works correctly for them). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the canary-pinned consumer test from 3.2.0-canary.2b8e377 to 3.2.0-canary.2817fa3, which is the first canary that contains per-kind fee enable/disable handling and the direct-grant-query polling fix. Verified end-to-end against the Vercel preview gateway at dp-rpc-git-dpv2-fee-and-payment-opendatalabs.vercel.app with both fees currently disabled: 19 steps green in 124s (was waiting the full 5min on the reconcile-pass output before the direct-query fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the legacy
grant: stringJSON blob +fileIds: uint256[]with first-class EIP-712 fieldsscopes: string[],grantVersion: uint256, andexpiresAt: uint256, and addgrantVersionto GrantRevocation so the monotonic nonce defends both events against replay. Update the gateway client wire format and GET response shape (now carriesexpiresAt/expired/paymentStatus/paidAt/paidBy/grantVersion/fee) to match data-gateway.