Skip to content

feat: Align grant registration with new gateway struct#150

Open
letonchanh wants to merge 24 commits into
mainfrom
feat/grant-registration-struct
Open

feat: Align grant registration with new gateway struct#150
letonchanh wants to merge 24 commits into
mainfrom
feat/grant-registration-struct

Conversation

@letonchanh
Copy link
Copy Markdown
Member

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.

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
vana-console Ignored Ignored Jun 2, 2026 9:22pm
vana-rbac-auditor Ignored Ignored Jun 2, 2026 9:22pm
vana-vibes-demo Ignored Ignored Jun 2, 2026 9:22pm

Request Review

@github-actions
Copy link
Copy Markdown

Findings

  • packages/vana-sdk/src/protocol/eip712.ts:100: The EIP-712 grant shape was updated to scopes/grantVersion/expiresAt, but the exported DataPortabilityPermissionsABI still exposes legacy grant/fileIds and revocation nonce/permissionId structs. If SDK consumers use getAbi("DataPortabilityPermissions"), they can now sign one shape and submit another. Regenerate/update the ABI if the contract/gateway contract surface changed.

  • packages/vana-sdk/src/protocol/grants.ts:68: toUint256 accepts number and converts with BigInt(value), which silently preserves already-rounded unsafe integers. For uint256 fields, reject non-safe integers with Number.isSafeInteger(value) or require string/bigint for grantVersion and expiresAt.

Verification

  • Could not run tests or typecheck: dependencies are not installed in this checkout (vitest missing, @types/node missing).

letonchanh and others added 15 commits May 20, 2026 22:05
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>
@letonchanh letonchanh force-pushed the feat/grant-registration-struct branch from a8087c5 to e3ef84a Compare June 2, 2026 03:54
letonchanh and others added 8 commits June 2, 2026 12:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant