merge: lux-graph-subgraphs-update-2026-04-28 (fhe+securities resolver upgrades)

This commit is contained in:
2026-04-28 13:37:18 -07:00
3 changed files with 285 additions and 15 deletions
+142
View File
@@ -0,0 +1,142 @@
# lux/graph — Subgraph State
`lux/graph` is the standalone Go GraphQL indexer (`graph` binary, also runnable
as a Lux VM plugin). It reads any EVM RPC, indexes events into local
SQLite/ZapDB, and serves a Graph-compatible GraphQL API. The "subgraphs" in
this repo are Go resolver packages under `resolvers/<topic>/` plus the central
event-signature table in `indexer/events.go`. There are no `subgraph.yaml`
manifests in this repo — those live in the sibling `uni-v2-subgraph`,
`uni-v3-subgraph`, `uni-v4-subgraph` repos that target this indexer.
## Resolver Subgraphs
| Subgraph | Path | Built-in alias(es) | Status |
| --- | --- | --- | --- |
| ai (A-Chain) | `resolvers/ai/` | `ai`, `achain` | current |
| amm | (handled by `engine.registerAMMResolvers`) | `amm`, `amm-v2`, `uniswap-v2`, `amm-v3`, `uniswap-v3`, `amm-v4`, `uniswap-v4`, `v4` | current |
| bridge (B-Chain) | `resolvers/bridge/` | `bridge`, `bchain` | current |
| dao | `resolvers/dao/` | `dao` | current |
| derivatives | `resolvers/derivatives/` | `derivatives`, `futures`, `options` | current |
| dex (D-Chain CLOB) | `resolvers/dex/` | `dex` | current |
| did | `resolvers/did/` | `did`, `did-registry` | current |
| exchange (X-Chain) | `resolvers/exchange/` | `exchange`, `xchain` | current |
| fhe (T-Chain) | `resolvers/fhe/` | `fhe`, `threshold` | UPDATED 2026-04-28 |
| governance | `resolvers/governance/` | `governance` | current |
| identity (I-Chain) | `resolvers/identity/` | `identity`, `ichain` | current |
| key (K-Chain) | `resolvers/key/` | `key`, `kchain` | current |
| liquid (liquid staking) | `resolvers/liquid/` | `liquid`, `liquid-staking` | current |
| liquidity (omnichain) | `resolvers/liquidity/` | `liquidity`, `liquidity-protocol`, `omnichain` | current |
| liquidprotocol (teleport / liquid-vault) | `resolvers/liquidprotocol/` | `liquid-protocol`, `teleport`, `liquid-vault` | current |
| mpc (M-Chain) | `resolvers/mpc/` | `mpc`, `mchain` | current |
| oracle (O-Chain) | `resolvers/oracle/` | `oracle`, `ochain` | current |
| platform (P-Chain) | `resolvers/platform/` | `platform`, `pchain` | current |
| precompile | `resolvers/precompile/` | `precompile`, `precompiles` | current |
| prediction | `resolvers/prediction/` | `prediction`, `prediction-market` | current |
| privacy | `resolvers/privacy/` | `privacy` | current |
| quantum (Q-Chain) | `resolvers/quantum/` | `quantum`, `qchain` | current |
| relay (R-Chain) | `resolvers/relay/` | `relay`, `rchain` | current |
| securities | `resolvers/securities/` | `securities`, `security-token` | UPDATED 2026-04-28 |
| servicenode (S-Chain) | `resolvers/servicenode/` | `servicenode`, `schain` | current |
| treasury | `resolvers/treasury/` | `treasury` | current |
| utxo | `resolvers/utxo/` | `utxo` | current |
| zk (Z-Chain) | `resolvers/zk/` | `zk`, `zchain` | current |
Empty directories `resolvers/amm/` and `resolvers/threshold/` are intentional:
amm is registered by `engine.registerAMMResolvers`, and `threshold` is an
alias of `fhe` in `engine.LoadBuiltin`.
## Updates landed this sweep (2026-04-28)
### securities (ERC-3643 + ONCHAINID)
Added entity surfaces for ERC-3643 transfer-agent / compliance / identity-
registry workflow plus ONCHAINID claim lifecycle. New resolvers:
- `onchainIdClaim`, `onchainIdClaims` — `ClaimAdded` / `ClaimRemoved` /
`ClaimChanged` from ONCHAINID.
- `transferAgentAction`, `transferAgentActions` — `Recovery`, `AddressFrozen`,
`TokensFrozen`, `TokensUnfrozen` from ERC-3643 token agent.
- `frozenAccount`, `frozenAccounts` — current `AddressFrozen` snapshots.
- `frozenTokens`, `frozenTokensList` — per-holder partial freeze amounts.
- `identityRegistryAction`, `identityRegistryActions` — `IdentityRegistered`,
`IdentityRemoved`, `IdentityStored`, `CountryUpdated` from
IdentityRegistry contract.
Backing entity types for the storage layer: `OnchainIdClaim`,
`TransferAgentAction`, `FrozenAccount`, `FrozenTokens`,
`IdentityRegistryAction`.
### fhe (LP-114 M-Chain × F-Chain policy gating)
Added `FHEPolicy` and `FHEPolicyBinding` entity surfaces so the indexer can
expose encrypted-policy descriptors anchored on M-Chain and their bindings to
on-chain resources. New resolvers: `fhePolicy`, `fhePolicies`,
`fhePolicyBinding`, `fhePolicyBindings`.
## Sibling subgraph repos (deploy via The Graph protocol)
These three sibling repos define `subgraph.yaml` manifests targeting the same
indexer. They live next to `lux/graph` under `~/work/lux/`:
| Repo | Path | Network | Factory address | spec | Last commit |
| --- | --- | --- | --- | --- | --- |
| uni-v2-subgraph | `~/work/lux/uni-v2-subgraph` | `liquidity` | `0xD173926A10A0C4eCd3A51B1422270b65Df0551c1` | `0.0.4` | `9ffaaac feat: update subgraph config and package dependencies` |
| uni-v3-subgraph | `~/work/lux/uni-v3-subgraph` | `liquidity` | `0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84` | `0.0.4` | `5d56a82 feat: update subgraph config and package dependencies` |
| uni-v4-subgraph | `~/work/lux/uni-v4-subgraph` | `liquidity` | `0x0000000000000000000000000000000000009010` (PoolManager) | `0.0.4` | `578adf5 feat: update subgraph config and package dependencies` |
Mainnet factory addresses match `indexer/events.go::LuxMainnet`. The subgraph
manifests use `network: liquidity` for the deploy target name (the Lux DEX
network identifier in the hosted graph node), not the Liquidity company.
## Build status
Build verification deferred this sweep due to a pre-existing `go.sum`
checksum mismatch on `github.com/luxfi/[email protected]` and
`github.com/hanzoai/[email protected]` — the mismatch lives in transitive
deps and is unrelated to the resolver edits in this branch (the securities
and fhe packages only import `context`, `fmt`,
`github.com/luxfi/graph/storage`). `gofmt` is clean on both edited files.
To repair the build locally:
```
GOWORK=off GOSUMDB=off go mod tidy
```
Then `make build` should produce `bin/graph`.
## Deploy state
The `graph` binary deploys via the canonical docker-build reusable workflow on
tag push (`ci/canonical-docker-build` branch on `main` history; image
`ghcr.io/luxfi/graph:<semver>`). No deploy keys for The Graph hosted service
are wired into this repo — the sibling subgraph repos handle hosted-service
deploys via their own `package.json` `deploy` script and require an env-side
`GRAPH_DEPLOY_KEY` to run `graph deploy --product hosted-service luxfi/<name>`.
## Follow-on work
- Wire concrete event-signature constants for ERC-3643 + ONCHAINID into
`indexer/events.go` (mirror the `SigPairCreated` / `SigSwapV2` pattern). The
resolver layer is in place; the indexer still needs the topic-0 hashes:
- `Transfer(address,address,uint256)` — already covered by `SigTransfer`.
- `AddressFrozen(address indexed,bool indexed,address indexed)`,
`TokensFrozen(address indexed,uint256)`,
`TokensUnfrozen(address indexed,uint256)`,
`RecoverySuccess(address,address,address)`.
- `ClaimAdded(bytes32 indexed,uint256 indexed,uint256,address,bytes,bytes,string)`,
`ClaimRemoved(...)`, `ClaimChanged(...)`.
- `IdentityRegistered(address indexed,address indexed)`,
`IdentityRemoved(address indexed,address indexed)`,
`IdentityStored(address indexed,address indexed)`,
`CountryUpdated(address indexed,uint16 indexed)`.
- Add storage-layer entity registrations for the new types so
`s.GetByType` / `s.ListByType` resolve them. The storage layer auto-handles
any `(type, id)` pair, but indexes will speed up list queries.
- Add a built-in alias `"erc3643"` to `engine.LoadBuiltin` that calls
`securities.Register` (currently the alias is `securities` /
`security-token`).
- Resolve `LoadConfig` TODO at `engine/engine.go:258` once the subgraph.yaml
parser lands (separate work item).
- Replace the empty `resolvers/amm/` and `resolvers/threshold/` directories
with package documentation so the layout is self-explanatory.
+51 -8
View File
@@ -1,9 +1,12 @@
// Package fhe provides resolvers for the T-Chain (ThresholdVM / FHE).
//
// Indexes: DKG ceremonies, threshold decryption requests, encrypted compute
// jobs, FHE ciphertext operations, key shares.
// jobs, FHE ciphertext operations, key shares, FHE policy bindings
// (M-Chain × F-Chain policy gating per LP-114).
//
// Entities: DKGCeremony, DecryptionRequest, ComputeJob, Ciphertext, KeyShare
// Entities: DKGCeremony, DecryptionRequest, ComputeJob, Ciphertext, KeyShare,
//
// FHEPolicy, FHEPolicyBinding.
package fhe
import (
@@ -27,38 +30,52 @@ func Register(resolvers map[string]ResolverFunc) {
resolvers["keyShare"] = resolveKeyShare
resolvers["keyShares"] = resolveKeyShares
resolvers["fheStats"] = resolveFHEStats
resolvers["fhePolicy"] = resolveFHEPolicy
resolvers["fhePolicies"] = resolveFHEPolicies
resolvers["fhePolicyBinding"] = resolveFHEPolicyBinding
resolvers["fhePolicyBindings"] = resolveFHEPolicyBindings
}
func resolveDKGCeremony(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("DKGCeremony", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("DKGCeremony", fmt.Sprint(id))
}
return nil, fmt.Errorf("dkgCeremony requires id")
}
func resolveDKGCeremonies(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("DKGCeremony", parseLimit(args))
}
func resolveDecryptionRequest(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("DecryptionRequest", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("DecryptionRequest", fmt.Sprint(id))
}
return nil, fmt.Errorf("decryptionRequest requires id")
}
func resolveDecryptionRequests(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("DecryptionRequest", parseLimit(args))
}
func resolveComputeJob(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("ComputeJob", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("ComputeJob", fmt.Sprint(id))
}
return nil, fmt.Errorf("computeJob requires id")
}
func resolveComputeJobs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("ComputeJob", parseLimit(args))
}
func resolveCiphertext(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("Ciphertext", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("Ciphertext", fmt.Sprint(id))
}
return nil, fmt.Errorf("ciphertext requires id")
}
func resolveCiphertexts(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("Ciphertext", parseLimit(args))
}
func resolveKeyShare(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("KeyShare", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("KeyShare", fmt.Sprint(id))
}
return nil, fmt.Errorf("keyShare requires id")
}
func resolveKeyShares(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
@@ -68,8 +85,34 @@ func resolveFHEStats(_ context.Context, s *storage.Store, _ map[string]interface
return s.GetByType("FHEStats", "1")
}
// FHEPolicy: an encrypted-policy descriptor anchored on M-Chain
// (subjectId, policyHash, threshold, expiresAt).
func resolveFHEPolicy(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FHEPolicy", fmt.Sprint(id))
}
return nil, fmt.Errorf("fhePolicy requires id")
}
func resolveFHEPolicies(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FHEPolicy", parseLimit(args))
}
// FHEPolicyBinding: binding of an FHE policy to an on-chain resource
// (token, vault, identity) — per LP-114 M-Chain × F-Chain integration.
func resolveFHEPolicyBinding(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FHEPolicyBinding", fmt.Sprint(id))
}
return nil, fmt.Errorf("fhePolicyBinding requires id")
}
func resolveFHEPolicyBindings(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FHEPolicyBinding", parseLimit(args))
}
func parseLimit(args map[string]interface{}) int {
limit := 100
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
if l, ok := args["first"]; ok {
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
}
return limit
}
+92 -7
View File
@@ -1,8 +1,15 @@
// Package securities provides resolvers for security tokens.
//
// Indexes: issuances, transfers, dividends, compliance records.
// Indexes ERC-3643 + ONCHAINID surfaces:
// - issuances, transfers, dividends, compliance records (core token)
// - identity registry actions (IdentityRegistry contract)
// - claim lifecycle (ONCHAINID ClaimAdded/Removed/Changed)
// - transfer-agent actions (Recovery, AddressFrozen, TokensFrozen)
//
// Entities: SecurityIssuance, SecurityTransfer, SecurityDividend, SecurityCompliance, SecurityStats
// Entities: SecurityIssuance, SecurityTransfer, SecurityDividend,
//
// SecurityCompliance, SecurityStats, OnchainIdClaim, TransferAgentAction,
// FrozenAccount, FrozenTokens, IdentityRegistryAction.
package securities
import (
@@ -15,6 +22,7 @@ import (
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
func Register(resolvers map[string]ResolverFunc) {
// Core token surfaces.
resolvers["securityIssuance"] = resolveSecurityIssuance
resolvers["securityIssuances"] = resolveSecurityIssuances
resolvers["securityTransfer"] = resolveSecurityTransfer
@@ -24,31 +32,51 @@ func Register(resolvers map[string]ResolverFunc) {
resolvers["securityCompliance"] = resolveSecurityCompliance
resolvers["securityCompliances"] = resolveSecurityCompliances
resolvers["securityStats"] = resolveSecurityStats
// ERC-3643 + ONCHAINID surfaces.
resolvers["onchainIdClaim"] = resolveOnchainIdClaim
resolvers["onchainIdClaims"] = resolveOnchainIdClaims
resolvers["transferAgentAction"] = resolveTransferAgentAction
resolvers["transferAgentActions"] = resolveTransferAgentActions
resolvers["frozenAccount"] = resolveFrozenAccount
resolvers["frozenAccounts"] = resolveFrozenAccounts
resolvers["frozenTokens"] = resolveFrozenTokensRec
resolvers["frozenTokensList"] = resolveFrozenTokensList
resolvers["identityRegistryAction"] = resolveIdentityRegistryAction
resolvers["identityRegistryActions"] = resolveIdentityRegistryActions
}
func resolveSecurityIssuance(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("SecurityIssuance", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("SecurityIssuance", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityIssuance requires id")
}
func resolveSecurityIssuances(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityIssuance", pl(args))
}
func resolveSecurityTransfer(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("SecurityTransfer", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("SecurityTransfer", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityTransfer requires id")
}
func resolveSecurityTransfers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityTransfer", pl(args))
}
func resolveSecurityDividend(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("SecurityDividend", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("SecurityDividend", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityDividend requires id")
}
func resolveSecurityDividends(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityDividend", pl(args))
}
func resolveSecurityCompliance(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok { return s.GetByType("SecurityCompliance", fmt.Sprint(id)) }
if id, ok := args["id"]; ok {
return s.GetByType("SecurityCompliance", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityCompliance requires id")
}
func resolveSecurityCompliances(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
@@ -58,8 +86,65 @@ func resolveSecurityStats(_ context.Context, s *storage.Store, _ map[string]inte
return s.GetByType("SecurityStats", "1")
}
// ONCHAINID claim resolvers (ClaimAdded / ClaimRemoved / ClaimChanged).
func resolveOnchainIdClaim(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("OnchainIdClaim", fmt.Sprint(id))
}
return nil, fmt.Errorf("onchainIdClaim requires id")
}
func resolveOnchainIdClaims(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("OnchainIdClaim", pl(args))
}
// Transfer-agent action resolvers (Recovery / AddressFrozen / TokensFrozen).
func resolveTransferAgentAction(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("TransferAgentAction", fmt.Sprint(id))
}
return nil, fmt.Errorf("transferAgentAction requires id")
}
func resolveTransferAgentActions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("TransferAgentAction", pl(args))
}
// Frozen-account snapshot (current state of AddressFrozen on a holder).
func resolveFrozenAccount(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FrozenAccount", fmt.Sprint(id))
}
return nil, fmt.Errorf("frozenAccount requires id")
}
func resolveFrozenAccounts(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FrozenAccount", pl(args))
}
// Frozen-tokens snapshot (per-holder partial freeze amount).
func resolveFrozenTokensRec(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FrozenTokens", fmt.Sprint(id))
}
return nil, fmt.Errorf("frozenTokens requires id")
}
func resolveFrozenTokensList(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FrozenTokens", pl(args))
}
// IdentityRegistry actions (IdentityRegistered / IdentityRemoved / IdentityStored / CountryUpdated).
func resolveIdentityRegistryAction(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("IdentityRegistryAction", fmt.Sprint(id))
}
return nil, fmt.Errorf("identityRegistryAction requires id")
}
func resolveIdentityRegistryActions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("IdentityRegistryAction", pl(args))
}
func pl(args map[string]interface{}) int {
limit := 100
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
if l, ok := args["first"]; ok {
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
}
return min(limit, 1000)
}