feat(api): index swaps settled by NEAR Intents, off by default

The price of a token on NEAR Intents is the ratio inside a trade that
`intents.near` actually executed; there is no pool, oracle or stored price,
and a 1Click quote is an offer. This reads `execute_intents` transactions on
the contract's shard, keeps the `token_diff` legs whose receipt succeeded and
whose intent is in `intents_executed`, and records one trade per asset/counter
pair per outcome, counting each unit once whichever side the taker was on.

- core: event parsing, trade building, shard lookup, price and VWAP, tested
  against the logs of mainnet tx 85uy9uCF (USDT on Tron for ZEC).
- data: a NEAR client over the existing RpcClient, live then archival;
  migration 0009 (intents_trade, intents_scan, near_token).
- api: a contiguous-cursor scan and a supply/listing poll per chain, and
  GET /v1/chains/{chain}/intents/{swaps,price}. Prices are in counter-token
  units; no USD until a stablecoin is named by contract.

Gated on `[chains.intents]`, which the config template leaves commented out:
FastNEAR's free RPC sustains about two requests a second against ~1.7 needed
to follow the tip, and neardata allows 30 blocks a minute. It waits for our
own node. A test renders the template and fails if the block is enabled.

Verified end to end against mainnet with ZEC configured: the trade at NEAR
218,261,895 was recorded and served by both routes.

Refs #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
This commit is contained in:
2026-10-03 18:35:32 +03:00
co-authored by Claude Opus 5.5
parent dc68fe3be3
commit 8128201ad9
32 changed files with 2670 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BigUintDec } from "./BigUintDec";
/**
* The asset's price against one counter token, over its latest trades.
*/
export type CounterPrice = {
/**
* The counter token, as a multi-token id.
*/
counter_token: string,
/**
* Its self-reported symbol. Display only.
*/
symbol: string | null,
/**
* Its decimals, when it is a NEP-141 token that said.
*/
decimals: number | null,
/**
* How many trades the figures below cover. Every figure is only as good
* as this count, and one trade is one trade however it is labelled.
*/
trades: number,
/**
* The latest trade's price.
*/
last_price: number | null,
/**
* When it settled.
*/
last_at: string,
/**
* Where it settled.
*/
last_near_height: number,
/**
* Volume-weighted over the trades counted — the ratio of the summed
* amounts, so a dust trade at a silly price moves it by its size.
*/
vwap: number | null,
/**
* Asset traded over those trades, base units.
*/
asset_volume: BigUintDec,
/**
* Counter token traded over those trades, base units.
*/
counter_volume: BigUintDec,
/**
* When the oldest of them settled.
*/
first_at: string, };
+27
View File
@@ -0,0 +1,27 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BigUintDec } from "./BigUintDec";
/**
* Whether there is anything to trade yet, read rather than assumed.
*
* Until the asset exists on NEAR there can be no swap, and saying so with
* the evidence beats an empty panel — or a price from somewhere else.
*/
export type IntentsListing = {
/**
* When these were read.
*/
checked_at: string,
/**
* The token contract's `ft_total_supply`: everything bridged to NEAR.
*/
total_supply: BigUintDec | null,
/**
* What `intents.near` holds of it: what can be traded at all.
*/
in_intents: BigUintDec | null,
/**
* Whether 1Click's public token list names the asset. Off-chain, and only
* a listing — it says swaps can be *requested*, not that any settled.
*/
oneclick_listed: boolean | null, };
+46
View File
@@ -0,0 +1,46 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ChainId } from "./ChainId";
import type { CounterPrice } from "./CounterPrice";
import type { IntentsListing } from "./IntentsListing";
/**
* The asset's price on NEAR Intents, per counter token.
*/
export type IntentsPrice = {
/**
* Which chain's token.
*/
chain: ChainId,
/**
* The tracked asset, as a multi-token id.
*/
asset: string,
/**
* Its decimals, from its own `ft_metadata`.
*/
asset_decimals: number | null,
/**
* Trades per counter token the figures are taken over, at most.
*/
window_trades: number,
/**
* One entry per counter token ever traded in the indexed range, most
* recently traded first.
*/
counters: Array<CounterPrice>,
/**
* Supply and listing, when they have been read.
*/
listing: IntentsListing | null,
/**
* What the indexer reads, in words — and so what it cannot see.
*/
reads: string,
/**
* Lowest NEAR height read.
*/
indexed_from: number,
/**
* Highest NEAR height read.
*/
indexed_to: number, };
+71
View File
@@ -0,0 +1,71 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BigUintDec } from "./BigUintDec";
/**
* One settled trade of the asset against one counter token.
*/
export type IntentsSwap = {
/**
* NEAR block it settled in.
*/
near_height: number,
/**
* Position among the trades in that block. With the height, the cursor.
*/
trade_index: number,
/**
* That block's own timestamp.
*/
at: string,
/**
* The `execute_intents` transaction.
*/
tx_hash: string,
/**
* The receipt whose outcome logged it.
*/
receipt_id: string,
/**
* The other token, as a multi-token id (`nep141:…`).
*/
counter_token: string,
/**
* That token's own `ft_metadata.symbol`. A label it chose for itself —
* never an identity, because anyone can deploy a token called USDC.
*/
counter_symbol: string | null,
/**
* The asset that changed hands, in base units.
*/
asset_amount: BigUintDec,
/**
* The counter token that changed hands, in base units.
*/
counter_amount: BigUintDec,
/**
* Counter tokens per one whole unit of the asset. Absent when either
* token's decimals are unknown.
*/
price: number | null,
/**
* `buy` or `sell`, from the taker's side. **Inferred** from the one leg
* that carried a `referral`; absent when that could not be told.
*/
side: string | null,
/**
* The account inferred to be the taker.
*/
taker: string | null,
/**
* The front end the taker came through, as it named itself.
*/
referral: string | null,
/**
* Every account with a leg in the trade — solvers by their named
* accounts, nothing more is claimed about who they are.
*/
accounts: Array<string>,
/**
* Every intent in the trade.
*/
intent_hashes: Array<string>, };
+34
View File
@@ -0,0 +1,34 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ChainId } from "./ChainId";
import type { IntentsSwap } from "./IntentsSwap";
/**
* A page of swaps, oldest first.
*/
export type IntentsSwaps = {
/**
* Which chain's token.
*/
chain: ChainId,
/**
* The tracked asset, as a multi-token id.
*/
asset: string,
/**
* The swaps.
*/
swaps: Array<IntentsSwap>,
/**
* Cursor for the next page, `near_height:trade_index`, when this one was
* full. Absent when there is nothing more *in the indexed range*.
*/
next: string | null,
/**
* Lowest NEAR height read. A swap below it is missing from this answer,
* not from the chain.
*/
indexed_from: number,
/**
* Highest NEAR height read.
*/
indexed_to: number, };