Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
94c215e
GraphQL stub
radrow Sep 29, 2025
236b341
More GraphQL resolvers and tests
radrow Sep 29, 2025
efda4b3
Fix stupid test dep
radrow Sep 29, 2025
f3f9ab8
Code formatting
ghallak Oct 28, 2025
8eb69d8
Add stats queries
ghallak Oct 29, 2025
33def4e
Refactor graphql schema
ghallak Oct 30, 2025
b5db8e3
Add more queries
ghallak Nov 3, 2025
f65b4a0
Update blocks resolvers and queries
ghallak Nov 5, 2025
f86d0d2
Run mix format
ghallak Nov 5, 2025
83f5589
Micro blocks queries
ghallak Nov 6, 2025
fb9838f
Remove scope from micro blocks query
ghallak Nov 6, 2025
9328cdb
Remove duplicated transactions query
ghallak Nov 6, 2025
402dc90
Fix single transaction query
ghallak Nov 6, 2025
2f1b49a
Fix pending txns queries
ghallak Nov 6, 2025
f8473c6
Remove block query from txns queries
ghallak Nov 6, 2025
78ba564
Comment txns and txns count resolvers
ghallak Nov 6, 2025
041aa34
Fix oracles types, comment extends
ghallak Nov 6, 2025
365fb33
Remove duplicated funs
ghallak Nov 6, 2025
fae4161
Fix all oracle queries
ghallak Nov 6, 2025
3705687
Enable oracle extensions
ghallak Nov 6, 2025
d837ec4
Fix all channel queries
ghallak Nov 6, 2025
a7d9e26
Disable aex9 queries
ghallak Nov 6, 2025
f54dec5
Enable aex9 contract queries
ghallak Nov 6, 2025
670f2e0
Enable aex9 contract balances
ghallak Nov 6, 2025
e4bee66
Enable balance history query
ghallak Nov 6, 2025
513de12
Comment unused aex141 queries and types
ghallak Nov 7, 2025
7ef29eb
Complete the stats queries
ghallak Nov 7, 2025
901c147
Remove duplicated stats types
ghallak Nov 7, 2025
9042c40
Convert map keys from string to atom before return
ghallak Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import Config

# Ensure consistent node name for tests to match persisted DB owner (avoids :wrong_db_owner_node)
config :kernel, :distributed, [:'aeternity@localhost']

# Do not start embedded node services for GraphQL/data correctness tests to avoid DB owner mismatch
# Use full node services in tests; ensure you invoke tests with:
# elixir --name aeternity@localhost -S mix test
config :ae_mdw, :start_node_services, true
config :ae_mdw, :sync, true

Comment on lines +3 to +11
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed

# Sync
config :ae_mdw,
sync: false,
Expand Down
149 changes: 149 additions & 0 deletions docs/graphql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# GraphQL Integration (Experimental)

Status: INITIAL SKELETON (alpha). Only a single field (`contract(id)`) is exposed so far.

This document explains how to use, test, and extend the new GraphQL endpoint.

---
## Endpoints

| Path | Method | Description |
|------|--------|-------------|
| `/graphql` | POST | Execute GraphQL operations (queries & mutations — only queries exist now). |
| `/graphiql` | GET | Interactive GraphiQL / Playground UI (available only in non-prod Mix envs). |

Both are mounted ahead of the REST versioned routes. CORS follows the existing `:api` pipeline configuration.

### Example Query
```graphql
{
contract(id: "ct_invalid") { id aexn_type meta_name meta_symbol }
}
```
Expected error response:
```json
{
"errors": [ { "message": "invalid_contract_id" } ]
}
```

### Current Fields
| Field | Args | Returns | Notes |
|-------|------|---------|-------|
| `contract` | `id: ID!` | `Contract` | Fetches contract metadata; only partially populated today. |

`Contract` type fields:
- `id: ID!` (echo of provided contract pubkey)
- `aexn_type: String` (e.g. `"aex9"`, `"aex141"`, or null)
- `meta_name: String`
- `meta_symbol: String`

> Decimals, version, holders, total supply, and on-chain state are not yet exposed.
---
## Resolver Behavior & Limitations
- The resolver decodes `ct_`-prefixed contract public keys using the node encoder.
- If the ID does not look like a contract pubkey, it returns an Absinthe error with message `invalid_contract_id`.
- It tries to derive AEXN token meta info (name & symbol) when the contract type is recognized.
- It currently expects a `:state` entry in the Absinthe context (to be provided by future context plug). If absent, it may fall back to returning `missing_state` errors when more logic is added.

Planned improvements:
1. Add context bridge to inject the same chain state used by REST (so queries reflect latest sync snapshot).
2. Add batching (Dataloader) if multiple contracts are queried in one request.
3. Provide structured error extensions (e.g. `{ code, detail }`).

---
## Running Queries (Development)
1. Start the application normally (REST server + GraphQL):
```bash
mix phx.server
```
2. Navigate to: `http://localhost:4000/graphiql`
3. Run the sample query.

If you run into node / DB ownership errors while running tests or starting the node, set a consistent Erlang node name:
```bash
elixir --name aeternity@localhost -S mix phx.server
```
Or for tests:
```bash
elixir --name aeternity@localhost -S mix test
```

> Avoid using `--sname` with an `@host` suffix; use `--name` for long names or `--sname aeternity` for short names.
---
## Testing
A minimal ExUnit test exists at:
`test/ae_mdw_web/graphql/contract_query_test.exs`

It currently checks error handling for invalid IDs. The test spins up the full application tree which includes a heavy node startup phase.

### Making Tests Faster (Planned)
A future refactor will:
- Introduce a config flag (e.g. `config :ae_mdw, :start_node_services, false` in test) to skip aecore-related apps in unit tests.
- Provide a mock or lightweight in-memory state for resolvers.

---
## Roadmap
| Phase | Goal | Notes |
|-------|------|------|
| 1 | Basic contract query (DONE) | Skeleton online. |
| 2 | Context bridge & graceful missing state handling | Use existing StatePlug output. |
| 3 | Add account & name queries | Mirror frequently used REST endpoints. |
| 4 | Pagination & connections | Cursor-based pattern for lists (contracts, transfers). |
| 5 | Complexity & depth limits | Mitigate resource exhaustion; e.g. `max_depth: 12`. |
| 6 | Token (AEX9 / AEX141) richer fields | Supply, holders, balances (with pagination). |
| 7 | Subscriptions (optional) | Uses existing PubSub for real-time events. |
| 8 | Telemetry + structured errors | Unified metrics and improved DX. |

---
## Design Principles
- Parity-first: GraphQL fields will align with existing REST semantics before introducing novel aggregations.
- Explicit pagination: No unbounded list fields.
- Streaming / large scans avoided; use indexed / cached paths exposed by the DB layer.
- Deterministic errors: Each validation failure maps to a stable error message code.

---
## Extending the Schema
1. Add a new resolver module under `lib/ae_mdw_web/graphql/resolvers/`.
2. Define object / field additions in `AeMdwWeb.GraphQL.Schema` (or split into type modules later with `import_types`).
3. Ensure arguments are validated early; return domain errors via `{:error, code}`.
4. Add tests that do NOT rely on the full chain when possible (inject or mock state).

Example (future) field stub:
```elixir
field :account, :account do
arg :id, non_null(:id)
resolve &AccountResolver.account/3
end
```

---
## Security Considerations
- Depth & complexity limits are not yet enforced (add them before exposing publicly).
- Rate limiting is inherited from existing stack (none specific to GraphQL yet).
- User input is limited to IDs now; later additions must validate pagination cursors and filters.

---
## Contributing
Open a PR adding new fields and include:
- Schema changes
- Resolver(s)
- Unit tests (success + failure)
- Brief addition to this doc (Roadmap or new section)

---
## FAQ
**Why Absinthe instead of generating GraphQL from REST automatically?**
Absinthe offers strong flexibility, custom middleware, and Elixir-native patterns for batching and instrumentation.

**Will REST be deprecated?**
Not in the short term. GraphQL is additive and will target aggregate and selective data retrieval patterns first.

**How do I enable GraphiQL in prod for debugging?**
You should not. If absolutely necessary, guard it behind an env flag and temporary branch only.

---
## Support / Contact
File an issue in the repository with the `graphql` label for feature requests or bugs.
Comment on lines +122 to +149
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

9 changes: 7 additions & 2 deletions lib/ae_mdw/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ defmodule AeMdw.Application do

init_public(:contract_cache)
init(:app_ctrl_server)
init(:aecore_services)
init(:aesync)

if Application.get_env(:ae_mdw, :start_node_services, true) do
init(:aecore_services)
init(:aesync)
else
Logger.info("[AeMdw] Skipping aecore/aesync services in test mode")
end
init(:tables)
init(:formatters)

Expand Down
45 changes: 39 additions & 6 deletions lib/ae_mdw/validate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,45 @@ defmodule AeMdw.Validate do
do: (type in AE.tx_types() && {:ok, type}) || {:error, ErrInput.TxType.exception(value: type)}

def tx_type(type) when is_binary(type) do
try do
tx_type(String.to_existing_atom(type <> "_tx"))
rescue
ArgumentError ->
{:error, ErrInput.TxType.exception(value: type)}
end
# Accept different user facing variants:
# * "SpendTx" (CamelCase + Tx suffix)
# * "spend_tx" (snake case with _tx)
# * "spend" (base name without _tx)
# * "Spend" (CamelCase without Tx)
# We try a sequence of normalized candidates until one maps to an existing atom
# present in AE.tx_types().
import Macro, only: [underscore: 1]

base = type
underscored = underscore(type)

candidates =
[
base,
underscored,
(underscored |> String.replace_suffix("_tx", "")),
(base |> String.replace_suffix("Tx", "")) |> underscore()
]
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.flat_map(fn cand ->
cond do
String.ends_with?(cand, "_tx") -> [cand]
true -> [cand <> "_tx", cand]
end
end)
|> Enum.uniq()

Enum.reduce_while(candidates, {:error, ErrInput.TxType.exception(value: type)}, fn cand, _acc ->
try do
atom = String.to_existing_atom(cand)
case tx_type(atom) do
{:ok, _} = ok -> {:halt, ok}
_ -> {:cont, {:error, ErrInput.TxType.exception(value: type)}}
end
rescue
ArgumentError -> {:cont, {:error, ErrInput.TxType.exception(value: type)}}
end
end)
end

@spec tx_group(tx_group() | binary()) :: {:ok, tx_group()} | {:error, ErrInput.t()}
Expand Down
Loading
Loading