Repository navigation
Expand file tree
/
Copy pathadapter.ts
More file actions
130 lines (124 loc) · 7.05 KB
/
Copy pathadapter.ts
File metadata and controls
130 lines (124 loc) · 7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { OperationalState } from './operational-state';
import { ProtocolCategory, ProtocolMetadata, ScoreResult } from './types';
import type { FactorMapFor } from './weights';
/**
* Version of the Adapter contract itself (not of any protocol). Bumped only
* when the shape of the interface below changes in a way adapters must react
* to — a required method, a changed signature, a new error model.
*
* 1 — fetchRawData / computeRiskFactors / score.
* 2 — adds the required `operationalState(raw)` method. Required,
* not optional, deliberately: an optional method is one every future adapter
* can quietly skip, which is precisely the retrofit debt that decision was
* made to stop accumulating. This constant exists so that forcing every
* implementor to update is a labelled event rather than a silent break, and
* this is the first time it has been used for one.
* 3 — adds the required `metadata.category` field and a `TCategory` parameter
* that scopes `operationalState`'s vocabulary to it. Required
* for the same reason `operationalState` was: a protocol with no category is
* not a protocol we know how to score, and an optional field would default
* the first adapter of every future category into lending's rulebook. Every
* implementor must name its category; nothing infers one.
*
* 4 — the factor map is DERIVED from `TCategory` rather than named by the
* adapter. The version before it had a third parameter, `TFactors`,
* defaulted to `RiskFactorMap`, to make the first `dex` adapter compile,
* and it was left explicitly unreviewed. The review revised rather than
* affirmed it: nothing tied `TFactors` to `TCategory`, so
* `Adapter<Raw, 'dex', RiskFactorMap>` compiled (a dex adapter scored on
* lending's five) and so did a map of invented keys. Both were checked, not
* supposed. `computeRiskFactors`/`score` now speak `FactorMapFor<TCategory>`
* — the key set `CATEGORY_FACTORS` declares for that category — so an
* adapter cannot publish factors its own rulebook does not name.
*
* BUMPED, where the defaulted `TFactors` parameter deliberately was not,
* because the bar is "a change adapters must react to" and this is one: an
* adapter that spelled three parameters must drop the third. A defaulted
* parameter nobody had to name was not; a removed one that someone did name
* is.
*/
export const ADAPTER_INTERFACE_VERSION = 4 as const;
/**
* The contract every protocol adapter implements.
*
* Lifecycle, driven by the indexer on an interval:
* 1. fetchRawData() — pull raw on-chain state via Soroban RPC
* 2. computeRiskFactors() — reduce raw state into the shared factor taxonomy
* 3. score() — reduce factors into a single comparable number
* 4. operationalState() — classify the market's live restrictions, unscored
*
* These are separate methods rather than one run() call so the
* indexer can persist/inspect intermediate output, and so scoring logic
* can be unit-tested against fixed factor inputs without touching RPC.
*
* Errors: adapters throw on failure (RPC unreachable, malformed response,
* missing contract data, etc). The indexer is responsible for catching
* per-adapter failures and recording a failed/stale run rather than
* crashing the whole cycle — adapters themselves should not swallow errors.
*
* TRawData is intentionally adapter-specific: Blend's raw shape has
* nothing in common with, say, YieldBlox's.
*
* TCategory is the rulebook this adapter is scored under. It is threaded through
* `metadata` and `operationalState` so those two cannot disagree: an adapter
* declaring `category: 'lending'` is checked against lending's operation
* vocabulary, not against the union of every category's. Defaulted to the whole
* union so a heterogeneous list (`Adapter<unknown>[]`, the indexer's run loop)
* still types, exactly as it did before this parameter existed.
*
* THE FACTOR MAP IS DERIVED FROM `TCategory`, NOT A THIRD PARAMETER. Which
* factors a category scores is declared once, in `CATEGORY_FACTORS`
* (`./weights.ts`), and `FactorMapFor<TCategory>` is that declaration read as a
* type. So an adapter cannot name its own key set: declaring `'dex'` and
* returning lending's five is a compile error, and so is inventing a key no
* rulebook has.
*
* That is the revision of the earlier `TFactors` parameter, and the reason for
* it is that the parameter did not connect to anything. `RiskFactorMap` is
* **lending's** map — `Record<RiskFactorType, …>`, its five keys fixed — and
* `dex` scores `adminKeySafety` and `assetControlSafety`, so the first
* non-lending adapter could not implement this interface at all; `TFactors`
* opened it up with a defaulted parameter to get that adapter compiling, and the
* decision was flagged as unreviewed. The review found what the parameter allowed:
* `Adapter<Raw, 'dex', RiskFactorMap>` and `Adapter<Raw, 'dex', { madeUpSafety }>`
* both compiled, because nothing related the two parameters. Deriving relates
* them.
*
* `FactorMapFor` DISTRIBUTES over the category union, which is what keeps the
* default working: `TCategory` defaults to all of `ProtocolCategory` so that a
* heterogeneous `Adapter<unknown>[]` still types, and the map then resolves to
* the union of every category's — "some category's factors" — rather than to
* their intersection, which is the latent bug `OperationFor` in
* `operational-state.ts` records being fixed. Checked rather than assumed; see
* that type's note in `./weights.ts`.
*/
export interface Adapter<
TRawData = unknown,
TCategory extends ProtocolCategory = ProtocolCategory,
> {
readonly metadata: ProtocolMetadata<TCategory>;
fetchRawData(): Promise<TRawData>;
computeRiskFactors(rawData: TRawData): Promise<FactorMapFor<TCategory>>;
score(factors: FactorMapFor<TCategory>): ScoreResult<FactorMapFor<TCategory>>;
/**
* The market's live operational state — which user operations its own gating
* logic currently refuses. Published beside the score and **never scored**;
* see `operational-state.ts` and METHODOLOGY.md for why.
*
* Takes the same raw data `computeRiskFactors` does, so no extra RPC round
* trip is spent on it and the state published alongside a score is the state
* that was true when that score's inputs were read. Synchronous for the same
* reason: everything it needs is already in `rawData`, and an implementation
* that has to await something is reaching for the chain a second time.
*
* Classify with `toOperationalState` (and `mostRestrictive` where a protocol
* gates per reserve) rather than constructing the object by hand — that
* function is the shared rule, and hand-rolling it is how two adapters come to
* disagree about what "frozen" means.
*
* The operations this may report are `TCategory`'s, not a global set — see
* `CATEGORY_OPERATIONS`. `OperationalLevel` is shared across categories and is
* the same ladder for all of them.
*/
operationalState(rawData: TRawData): OperationalState<TCategory>;
}