envio 3.5.1 โ 3.6.1-subgraph
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +33 -10
- package/package.json +6 -6
- package/src/ChainState.res +22 -4
- package/src/ChainState.res.mjs +20 -3
- package/src/ChainState.resi +3 -0
- package/src/Config.res +72 -2
- package/src/Config.res.mjs +57 -15
- package/src/Core.res +15 -0
- package/src/Core.res.mjs +11 -0
- package/src/EffectState.res +2 -2
- package/src/EffectState.res.mjs +2 -2
- package/src/EntityTables.res +32 -0
- package/src/EntityTables.res.mjs +44 -0
- package/src/Envio.res +7 -7
- package/src/Envio.res.mjs +5 -6
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +8 -6
- package/src/HandlerLoader.res +20 -8
- package/src/HandlerLoader.res.mjs +11 -2
- package/src/Hasura.res +37 -3
- package/src/Hasura.res.mjs +23 -5
- package/src/InMemoryStore.res +48 -8
- package/src/InMemoryStore.res.mjs +37 -7
- package/src/IndexerState.res +30 -24
- package/src/IndexerState.res.mjs +20 -35
- package/src/IndexerState.resi +8 -6
- package/src/Internal.res +29 -11
- package/src/Internal.res.mjs +27 -10
- package/src/LoadLayer.res +39 -8
- package/src/LoadLayer.res.mjs +36 -9
- package/src/LoadLayer.resi +2 -0
- package/src/Metrics.res +5 -5
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +183 -37
- package/src/PgStorage.res.mjs +149 -34
- package/src/PruneStaleHistory.res +1 -0
- package/src/PruneStaleHistory.res.mjs +2 -1
- package/src/Sink.res +3 -3
- package/src/Sink.res.mjs +2 -2
- package/src/TestIndexer.res +128 -17
- package/src/TestIndexer.res.mjs +75 -9
- package/src/UserContext.res +375 -53
- package/src/UserContext.res.mjs +281 -39
- package/src/Writing.res +8 -16
- package/src/Writing.res.mjs +10 -10
- package/src/bindings/ClickHouse.res +40 -4
- package/src/bindings/ClickHouse.res.mjs +37 -5
- package/src/db/EntityHistory.res +64 -21
- package/src/db/EntityHistory.res.mjs +43 -22
- package/src/db/InternalTable.res.mjs +31 -31
- package/src/db/Table.res +24 -1
- package/src/db/Table.res.mjs +26 -4
- package/src/sources/SourceManager.res +6 -12
- package/src/sources/SourceManager.res.mjs +8 -5
- package/src/subgraph/blocks.ts +176 -0
- package/src/subgraph/calls.ts +213 -0
- package/src/subgraph/conformance.ts +99 -0
- package/src/subgraph/division.ts +100 -0
- package/src/subgraph/errors.ts +90 -0
- package/src/subgraph/graph-ts-types/VERSION +2 -0
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
- package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
- package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
- package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
- package/src/subgraph/graph-ts-types/index.d.ts +102 -0
- package/src/subgraph/graph-ts.ts +1695 -0
- package/src/subgraph/hosts.ts +148 -0
- package/src/subgraph/runtime.ts +827 -0
- package/src/subgraph/scope.ts +63 -0
- package/svm.schema.json +7 -0
|
@@ -388,18 +388,21 @@ async function getSourceNewHeight(sourceManager, sourceState, knownHeight, stall
|
|
|
388
388
|
if (createSubscription !== undefined && isRealtime) {
|
|
389
389
|
let unsubscribe = createSubscription(newHeight => {
|
|
390
390
|
if (newHeight <= sourceState.knownHeight) {
|
|
391
|
-
return
|
|
391
|
+
return recordRequestStats(sourceState, [{
|
|
392
|
+
method: "heightPushIgnored",
|
|
393
|
+
seconds: 0
|
|
394
|
+
}]);
|
|
392
395
|
}
|
|
396
|
+
recordRequestStats(sourceState, [{
|
|
397
|
+
method: "heightPush",
|
|
398
|
+
seconds: 0
|
|
399
|
+
}]);
|
|
393
400
|
sourceState.knownHeight = newHeight;
|
|
394
401
|
let resolvers = sourceState.pendingHeightResolvers;
|
|
395
402
|
sourceState.pendingHeightResolvers = [];
|
|
396
403
|
resolvers.forEach(resolve => resolve(newHeight));
|
|
397
404
|
});
|
|
398
405
|
sourceState.unsubscribe = unsubscribe;
|
|
399
|
-
recordRequestStats(sourceState, [{
|
|
400
|
-
method: "heightSubscription",
|
|
401
|
-
seconds: 0
|
|
402
|
-
}]);
|
|
403
406
|
} else {
|
|
404
407
|
exit = 1;
|
|
405
408
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block handler's `block.timestamp`.
|
|
3
|
+
*
|
|
4
|
+
* envio hands a block handler only the block number, so the timestamp has to be
|
|
5
|
+
* fetched. Every handler invocation in a batch asks within the same microtask,
|
|
6
|
+
* so the requests are collected and answered by a single HyperSync range query
|
|
7
|
+
* rather than one round trip per block. An RPC endpoint, if one is configured,
|
|
8
|
+
* is the fallback for whatever HyperSync couldn't answer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createPublicClient, fallback, http } from "viem";
|
|
12
|
+
|
|
13
|
+
const API_TOKEN_ENV_VAR = "ENVIO_API_TOKEN";
|
|
14
|
+
|
|
15
|
+
const hypersyncUrl = (chainId: number) => `https://${chainId}.hypersync.xyz/query`;
|
|
16
|
+
|
|
17
|
+
type Waiter = {
|
|
18
|
+
resolve: (timestamp: bigint) => void;
|
|
19
|
+
reject: (error: unknown) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Blocks asked for since the last flush, by chain then block number. */
|
|
23
|
+
let pending = new Map<number, Map<number, Waiter[]>>();
|
|
24
|
+
let flushScheduled = false;
|
|
25
|
+
|
|
26
|
+
let rpcUrls: string[] = [];
|
|
27
|
+
let client: ReturnType<typeof createPublicClient> | null = null;
|
|
28
|
+
|
|
29
|
+
export function configureBlockTimestamps(urls: string[]) {
|
|
30
|
+
rpcUrls = urls;
|
|
31
|
+
client = null;
|
|
32
|
+
pending = new Map();
|
|
33
|
+
flushScheduled = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requestBlockTimestamp(chainId: number, blockNumber: number): Promise<bigint> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let blocks = pending.get(chainId);
|
|
39
|
+
if (!blocks) {
|
|
40
|
+
blocks = new Map();
|
|
41
|
+
pending.set(chainId, blocks);
|
|
42
|
+
}
|
|
43
|
+
const waiters = blocks.get(blockNumber);
|
|
44
|
+
if (waiters) {
|
|
45
|
+
waiters.push({ resolve, reject });
|
|
46
|
+
} else {
|
|
47
|
+
blocks.set(blockNumber, [{ resolve, reject }]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!flushScheduled) {
|
|
51
|
+
flushScheduled = true;
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
flushScheduled = false;
|
|
54
|
+
const collected = pending;
|
|
55
|
+
pending = new Map();
|
|
56
|
+
for (const [chain, blocksForChain] of collected) {
|
|
57
|
+
void answer(chain, blocksForChain);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function answer(chainId: number, blocks: Map<number, Waiter[]>) {
|
|
65
|
+
let timestamps = new Map<number, bigint>();
|
|
66
|
+
|
|
67
|
+
// A batch can hold thousands of blocks, and one argument per block would
|
|
68
|
+
// overflow the call stack inside the try, where it would read as a HyperSync
|
|
69
|
+
// failure and fall back to one round trip per block.
|
|
70
|
+
let lowest = Infinity;
|
|
71
|
+
let highest = -Infinity;
|
|
72
|
+
for (const blockNumber of blocks.keys()) {
|
|
73
|
+
if (blockNumber < lowest) lowest = blockNumber;
|
|
74
|
+
if (blockNumber > highest) highest = blockNumber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
timestamps = await fromHyperSync(chainId, lowest, highest);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (rpcUrls.length === 0) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
for (const waiters of blocks.values()) {
|
|
83
|
+
for (const waiter of waiters) {
|
|
84
|
+
waiter.reject(
|
|
85
|
+
new Error(
|
|
86
|
+
`Envio Subgraph couldn't read a block timestamp from HyperSync: ${message}\n` +
|
|
87
|
+
`Set ${API_TOKEN_ENV_VAR} in .env or the environment โ create one at\n` +
|
|
88
|
+
`https://envio.dev/app/api-tokens โ or set ENVIO_SUBGRAPH_RPC to fall back to RPC.`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [blockNumber, waiters] of blocks) {
|
|
98
|
+
const known = timestamps.get(blockNumber);
|
|
99
|
+
if (known !== undefined) {
|
|
100
|
+
for (const waiter of waiters) waiter.resolve(known);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const timestamp = await fromRpc(blockNumber);
|
|
105
|
+
for (const waiter of waiters) waiter.resolve(timestamp);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type HyperSyncResponse = {
|
|
113
|
+
data: { blocks?: { number: number; timestamp: string }[] }[];
|
|
114
|
+
next_block: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
async function fromHyperSync(
|
|
118
|
+
chainId: number,
|
|
119
|
+
fromBlock: number,
|
|
120
|
+
toBlock: number,
|
|
121
|
+
): Promise<Map<number, bigint>> {
|
|
122
|
+
const token = process.env[API_TOKEN_ENV_VAR];
|
|
123
|
+
const timestamps = new Map<number, bigint>();
|
|
124
|
+
|
|
125
|
+
let cursor = fromBlock;
|
|
126
|
+
// HyperSync answers as much of the range as one response holds and points at
|
|
127
|
+
// where to resume, so a wide range takes more than one round trip.
|
|
128
|
+
while (cursor <= toBlock) {
|
|
129
|
+
const response = await fetch(hypersyncUrl(chainId), {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"content-type": "application/json",
|
|
133
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
from_block: cursor,
|
|
137
|
+
to_block: toBlock + 1,
|
|
138
|
+
include_all_blocks: true,
|
|
139
|
+
field_selection: { block: ["number", "timestamp"] },
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`HyperSync returned ${response.status} ${await response.text()}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = (await response.json()) as HyperSyncResponse;
|
|
148
|
+
for (const page of body.data ?? []) {
|
|
149
|
+
for (const block of page.blocks ?? []) {
|
|
150
|
+
timestamps.set(block.number, BigInt(block.timestamp));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!body.next_block || body.next_block <= cursor) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
cursor = body.next_block;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return timestamps;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fromRpc(blockNumber: number): Promise<bigint> {
|
|
164
|
+
if (rpcUrls.length === 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Envio Subgraph couldn't read the timestamp of block ${blockNumber}: HyperSync didn't\n` +
|
|
167
|
+
`return it, and no RPC fallback is configured. Set ENVIO_SUBGRAPH_RPC in .env or the\n` +
|
|
168
|
+
`environment.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
client ??= createPublicClient({
|
|
172
|
+
transport: fallback(rpcUrls.map((url) => http(url, { retryCount: 0 }))),
|
|
173
|
+
});
|
|
174
|
+
const block = await client.getBlock({ blockNumber: BigInt(blockNumber) });
|
|
175
|
+
return block.timestamp;
|
|
176
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Contract.bind(x).foo()` -> an envio effect over viem.
|
|
3
|
+
*
|
|
4
|
+
* graph-node evaluates a mapping's contract calls against the block the event
|
|
5
|
+
* came from, so the block number is part of the effect's input โ and therefore
|
|
6
|
+
* of its cache key, which is what makes a cached call safe to reuse.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createPublicClient,
|
|
11
|
+
decodeFunctionResult,
|
|
12
|
+
encodeFunctionData,
|
|
13
|
+
fallback,
|
|
14
|
+
http,
|
|
15
|
+
parseAbiParameters,
|
|
16
|
+
type Abi,
|
|
17
|
+
} from "viem";
|
|
18
|
+
import * as Sury from "rescript-schema";
|
|
19
|
+
import { createEffect } from "../Envio.res.mjs";
|
|
20
|
+
import { missingRpcMessage } from "./errors.ts";
|
|
21
|
+
|
|
22
|
+
export type ParsedSignature = {
|
|
23
|
+
name: string;
|
|
24
|
+
inputs: string;
|
|
25
|
+
outputs: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* graph codegen emits `"balanceOf(address):(uint256)"`. Types only, no names โ
|
|
30
|
+
* which is all `parseAbiParameters` needs.
|
|
31
|
+
*/
|
|
32
|
+
export function parseSignature(signature: string): ParsedSignature {
|
|
33
|
+
const open = signature.indexOf("(");
|
|
34
|
+
if (open === -1) {
|
|
35
|
+
throw new Error(`Unreadable contract call signature "${signature}"`);
|
|
36
|
+
}
|
|
37
|
+
const name = signature.slice(0, open);
|
|
38
|
+
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let close = -1;
|
|
41
|
+
for (let index = open; index < signature.length; index++) {
|
|
42
|
+
const char = signature[index];
|
|
43
|
+
if (char === "(") depth++;
|
|
44
|
+
if (char === ")") {
|
|
45
|
+
depth--;
|
|
46
|
+
if (depth === 0) {
|
|
47
|
+
close = index;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (close === -1) {
|
|
53
|
+
throw new Error(`Unreadable contract call signature "${signature}"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inputs = signature.slice(open + 1, close);
|
|
57
|
+
const rest = signature.slice(close + 1).replace(/^:/, "");
|
|
58
|
+
const outputs = rest.startsWith("(") ? rest.slice(1, -1) : rest;
|
|
59
|
+
|
|
60
|
+
return { name, inputs, outputs };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function abiItemFor(signature: string): Abi {
|
|
64
|
+
const { name, inputs, outputs } = parseSignature(signature);
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
type: "function",
|
|
68
|
+
name,
|
|
69
|
+
stateMutability: "view",
|
|
70
|
+
inputs: inputs.trim() === "" ? [] : [...parseAbiParameters(inputs)],
|
|
71
|
+
outputs: outputs.trim() === "" ? [] : [...parseAbiParameters(outputs)],
|
|
72
|
+
},
|
|
73
|
+
] as Abi;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A revert is data: the mapping's `try_` sees `{reverted: true}`. A transport
|
|
78
|
+
* failure is not โ it throws as the handler error so envio retries, and a flaky
|
|
79
|
+
* RPC never fabricates reverted data.
|
|
80
|
+
*/
|
|
81
|
+
export function isRevert(error: unknown): boolean {
|
|
82
|
+
const parts: string[] = [];
|
|
83
|
+
let current: any = error;
|
|
84
|
+
for (let depth = 0; current && depth < 10; depth++) {
|
|
85
|
+
parts.push(current.name, current.shortMessage, current.details, current.message);
|
|
86
|
+
current = current.cause;
|
|
87
|
+
}
|
|
88
|
+
const text = parts.filter(Boolean).join(" | ");
|
|
89
|
+
|
|
90
|
+
if (/revert|ContractFunctionZeroDataError|invalid opcode|out of gas/i.test(text)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type CallInput = {
|
|
97
|
+
chainId: number;
|
|
98
|
+
address: string;
|
|
99
|
+
signature: string;
|
|
100
|
+
args: unknown[];
|
|
101
|
+
blockNumber: number;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type CallOutput = {
|
|
105
|
+
reverted: boolean;
|
|
106
|
+
values: unknown[] | null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let clients: Map<string, ReturnType<typeof createPublicClient>> = new Map();
|
|
110
|
+
|
|
111
|
+
function clientFor(rpcUrls: string[]) {
|
|
112
|
+
const key = rpcUrls.join("|");
|
|
113
|
+
let client = clients.get(key);
|
|
114
|
+
if (!client) {
|
|
115
|
+
client = createPublicClient({
|
|
116
|
+
// Retrying is envio's job: a failed call becomes a handler error and
|
|
117
|
+
// the batch is retried with the effect's dedup still in place.
|
|
118
|
+
transport: fallback(rpcUrls.map((url) => http(url, { retryCount: 0 }))),
|
|
119
|
+
});
|
|
120
|
+
clients.set(key, client);
|
|
121
|
+
}
|
|
122
|
+
return client;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Reset between test indexers, which each bring their own endpoints. */
|
|
126
|
+
export function resetClients() {
|
|
127
|
+
clients = new Map();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* BigInt and Bytes cross the effect boundary through Sury, which needs a
|
|
132
|
+
* serialisable shape โ so the call is described in plain JSON and the graph-ts
|
|
133
|
+
* values are converted on both sides by the shim.
|
|
134
|
+
*/
|
|
135
|
+
export function makeCallEffect(rpcUrls: string[]) {
|
|
136
|
+
return createEffect(
|
|
137
|
+
{
|
|
138
|
+
name: "envio_subgraph_eth_call",
|
|
139
|
+
// Both sides travel as JSON text: the cache key is then the exact call
|
|
140
|
+
// description, and the cached row is readable.
|
|
141
|
+
input: Sury.string,
|
|
142
|
+
output: Sury.string,
|
|
143
|
+
rateLimit: false,
|
|
144
|
+
cache: true,
|
|
145
|
+
crossChain: false,
|
|
146
|
+
},
|
|
147
|
+
async ({ input: encoded }: { input: string }) => {
|
|
148
|
+
const input = JSON.parse(encoded) as CallInput;
|
|
149
|
+
if (rpcUrls.length === 0) {
|
|
150
|
+
throw new Error(missingRpcMessage(input.signature));
|
|
151
|
+
}
|
|
152
|
+
const abi = abiItemFor(input.signature);
|
|
153
|
+
const { name } = parseSignature(input.signature);
|
|
154
|
+
const data = encodeFunctionData({
|
|
155
|
+
abi,
|
|
156
|
+
functionName: name,
|
|
157
|
+
args: input.args.map(decodeArg),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let result;
|
|
161
|
+
try {
|
|
162
|
+
result = await clientFor(rpcUrls).call({
|
|
163
|
+
to: input.address as `0x${string}`,
|
|
164
|
+
data,
|
|
165
|
+
blockNumber: BigInt(input.blockNumber),
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (isRevert(error)) {
|
|
169
|
+
return JSON.stringify({ reverted: true, values: null });
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const decoded = decodeFunctionResult({
|
|
176
|
+
abi,
|
|
177
|
+
functionName: name,
|
|
178
|
+
data: (result.data ?? "0x") as `0x${string}`,
|
|
179
|
+
});
|
|
180
|
+
const values = Array.isArray(decoded) ? decoded : [decoded];
|
|
181
|
+
return JSON.stringify({ reverted: false, values: values.map(encodeArg) });
|
|
182
|
+
} catch {
|
|
183
|
+
// Output the declared signature can't decode counts as a failed call,
|
|
184
|
+
// which is what lets a mapping retry through a different ABI โ the
|
|
185
|
+
// bytes32-vs-string ERC20 name is the case every token subgraph hits.
|
|
186
|
+
return JSON.stringify({ reverted: true, values: null });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Effect inputs/outputs travel as strings, tagged so the type survives. */
|
|
193
|
+
export function encodeArg(value: unknown): string {
|
|
194
|
+
if (typeof value === "bigint") return `i:${value.toString()}`;
|
|
195
|
+
if (typeof value === "boolean") return `b:${value ? "1" : "0"}`;
|
|
196
|
+
if (Array.isArray(value)) return `a:${JSON.stringify(value.map(encodeArg))}`;
|
|
197
|
+
return `s:${String(value)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function decodeArg(value: string): unknown {
|
|
201
|
+
const tag = value.slice(0, 2);
|
|
202
|
+
const body = value.slice(2);
|
|
203
|
+
switch (tag) {
|
|
204
|
+
case "i:":
|
|
205
|
+
return BigInt(body);
|
|
206
|
+
case "b:":
|
|
207
|
+
return body === "1";
|
|
208
|
+
case "a:":
|
|
209
|
+
return (JSON.parse(body) as string[]).map(decodeArg);
|
|
210
|
+
default:
|
|
211
|
+
return body;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shim, checked against the types a subgraph developer codes against.
|
|
3
|
+
*
|
|
4
|
+
* A subgraph's mappings are type-checked by `asc` against
|
|
5
|
+
* `@graphprotocol/graph-ts`, and then run by this shim. Nothing else makes
|
|
6
|
+
* those two agree, so a gap shows up as green types and a runtime error โ the
|
|
7
|
+
* worst feedback loop there is. This file closes it: every export the shim
|
|
8
|
+
* claims to provide has to be assignable to the real one.
|
|
9
|
+
*
|
|
10
|
+
* The declarations come from the real package (see
|
|
11
|
+
* `scripts/generate-graph-ts-types.mjs`). This file is type-checked, never run.
|
|
12
|
+
*
|
|
13
|
+
* A `@ts-expect-error` below is a known gap, and it cleans itself up: closing
|
|
14
|
+
* the gap makes the directive unused, which is itself an error, so the comment
|
|
15
|
+
* has to go with the fix. Adding a new one is a deliberate act.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type * as GraphTs from "./graph-ts-types/index.js";
|
|
19
|
+
import * as Shim from "./graph-ts.ts";
|
|
20
|
+
|
|
21
|
+
// @ts-expect-error graph-ts' BigInt extends Uint8Array and is constructed
|
|
22
|
+
// from a byte length; this one is constructed from the value it holds. No
|
|
23
|
+
// mapping calls `new BigInt(...)` โ they go through `BigInt.from*` โ and
|
|
24
|
+
// every other member matches. The five below are the same difference,
|
|
25
|
+
// reached through a BigInt in their own signatures.
|
|
26
|
+
const _BigInt: typeof GraphTs.BigInt = Shim.BigInt;
|
|
27
|
+
|
|
28
|
+
// @ts-expect-error constructor takes a BigInt (see above).
|
|
29
|
+
const _BigDecimal: typeof GraphTs.BigDecimal = Shim.BigDecimal;
|
|
30
|
+
|
|
31
|
+
const _Bytes: typeof GraphTs.Bytes = Shim.Bytes;
|
|
32
|
+
|
|
33
|
+
const _ByteArray: typeof GraphTs.ByteArray = Shim.ByteArray;
|
|
34
|
+
|
|
35
|
+
const _Address: typeof GraphTs.Address = Shim.Address;
|
|
36
|
+
|
|
37
|
+
const _TypedMap: typeof GraphTs.TypedMap = Shim.TypedMap;
|
|
38
|
+
|
|
39
|
+
const _TypedMapEntry: typeof GraphTs.TypedMapEntry = Shim.TypedMapEntry;
|
|
40
|
+
|
|
41
|
+
const _Entity: typeof GraphTs.Entity = Shim.Entity;
|
|
42
|
+
|
|
43
|
+
const _Value: typeof GraphTs.Value = Shim.Value;
|
|
44
|
+
|
|
45
|
+
const _ValueKind: typeof GraphTs.ValueKind = Shim.ValueKind;
|
|
46
|
+
|
|
47
|
+
const _store: typeof GraphTs.store = Shim.store;
|
|
48
|
+
|
|
49
|
+
// @ts-expect-error `ethereum.Value` carries a BigInt (see above).
|
|
50
|
+
const _ethereum: typeof GraphTs.ethereum = Shim.ethereum;
|
|
51
|
+
|
|
52
|
+
const _dataSource: typeof GraphTs.dataSource = Shim.dataSource;
|
|
53
|
+
|
|
54
|
+
const _DataSourceTemplate: typeof GraphTs.DataSourceTemplate = Shim.DataSourceTemplate;
|
|
55
|
+
|
|
56
|
+
const _DataSourceContext: typeof GraphTs.DataSourceContext = Shim.DataSourceContext;
|
|
57
|
+
|
|
58
|
+
// @ts-expect-error `log` formats a BigInt (see above).
|
|
59
|
+
const _log: typeof GraphTs.log = Shim.log;
|
|
60
|
+
|
|
61
|
+
const _crypto: typeof GraphTs.crypto = Shim.crypto;
|
|
62
|
+
|
|
63
|
+
// @ts-expect-error `json.toBigInt` returns a BigInt (see above).
|
|
64
|
+
const _json: typeof GraphTs.json = Shim.json;
|
|
65
|
+
|
|
66
|
+
// @ts-expect-error `JSONValue.toBigInt` returns a BigInt (see above).
|
|
67
|
+
const _JSONValue: typeof GraphTs.JSONValue = Shim.JSONValue;
|
|
68
|
+
|
|
69
|
+
const _JSONValueKind: typeof GraphTs.JSONValueKind = Shim.JSONValueKind;
|
|
70
|
+
|
|
71
|
+
const _ipfs: typeof GraphTs.ipfs = Shim.ipfs;
|
|
72
|
+
|
|
73
|
+
const _ens: typeof GraphTs.ens = Shim.ens;
|
|
74
|
+
|
|
75
|
+
export type {};
|
|
76
|
+
void [
|
|
77
|
+
_BigInt,
|
|
78
|
+
_BigDecimal,
|
|
79
|
+
_Bytes,
|
|
80
|
+
_ByteArray,
|
|
81
|
+
_Address,
|
|
82
|
+
_TypedMap,
|
|
83
|
+
_TypedMapEntry,
|
|
84
|
+
_Entity,
|
|
85
|
+
_Value,
|
|
86
|
+
_ValueKind,
|
|
87
|
+
_store,
|
|
88
|
+
_ethereum,
|
|
89
|
+
_dataSource,
|
|
90
|
+
_DataSourceTemplate,
|
|
91
|
+
_DataSourceContext,
|
|
92
|
+
_log,
|
|
93
|
+
_crypto,
|
|
94
|
+
_json,
|
|
95
|
+
_JSONValue,
|
|
96
|
+
_JSONValueKind,
|
|
97
|
+
_ipfs,
|
|
98
|
+
_ens,
|
|
99
|
+
];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssemblyScript divides two integers as integers. JavaScript doesn't.
|
|
3
|
+
*
|
|
4
|
+
* `let dayID = timestamp / 86400` is an i32 division in a mapping and a float
|
|
5
|
+
* division here, and the difference travels: Uniswap multiplies the bucket back
|
|
6
|
+
* out and writes it to an `Int` column, where `1588712972.0000002` is a type
|
|
7
|
+
* error rather than a wrong number โ but an entity id built the same way would
|
|
8
|
+
* simply be wrong. Nothing in the shim can intercept `/` between two plain
|
|
9
|
+
* numbers, so the operator is rewritten as the mapping is loaded.
|
|
10
|
+
*
|
|
11
|
+
* The rewrite is textual, driven by the TypeScript parser every subgraph project
|
|
12
|
+
* already has: each `a / b` becomes `__envio_idiv(a, b)`, which truncates only
|
|
13
|
+
* when both operands really are integers and otherwise divides as before โ an
|
|
14
|
+
* `f64` in AssemblyScript divides as a float too.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
type Edit = { at: number; length: number; text: string };
|
|
21
|
+
|
|
22
|
+
let typescript: any | null | undefined;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The project's own TypeScript, which `graph-cli` puts there. Resolving from the
|
|
26
|
+
* project rather than from here keeps the shim free of a parser dependency.
|
|
27
|
+
*/
|
|
28
|
+
export function loadTypeScript(root: string): any | null {
|
|
29
|
+
if (typescript !== undefined) return typescript;
|
|
30
|
+
try {
|
|
31
|
+
const require = createRequire(path.join(path.resolve(root), "package.json"));
|
|
32
|
+
typescript = require("typescript");
|
|
33
|
+
} catch {
|
|
34
|
+
typescript = null;
|
|
35
|
+
}
|
|
36
|
+
return typescript;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function integerDivision(a: unknown, b: unknown): unknown {
|
|
40
|
+
const left = typeof a === "object" && a !== null ? (a as any).valueOf() : a;
|
|
41
|
+
const right = typeof b === "object" && b !== null ? (b as any).valueOf() : b;
|
|
42
|
+
|
|
43
|
+
const integral = (v: unknown) => typeof v === "bigint" || (typeof v === "number" && Number.isInteger(v));
|
|
44
|
+
if (integral(left) && integral(right)) {
|
|
45
|
+
// Only i64 / i64 stays 64-bit; anything narrower is an i32 in AssemblyScript
|
|
46
|
+
// and must come back as a number, or the bigint spreads through every
|
|
47
|
+
// arithmetic that follows.
|
|
48
|
+
if (typeof left === "bigint" && typeof right === "bigint") {
|
|
49
|
+
return left / right;
|
|
50
|
+
}
|
|
51
|
+
return Math.trunc(Number(left) / Number(right));
|
|
52
|
+
}
|
|
53
|
+
return (left as number) / (right as number);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DIVIDE_HELPER = "__envio_idiv";
|
|
57
|
+
|
|
58
|
+
/** Returns the source unchanged when it divides nothing, or can't be parsed. */
|
|
59
|
+
export function rewriteDivision(source: string): string {
|
|
60
|
+
if (!source.includes("/")) return source;
|
|
61
|
+
const ts = typescript;
|
|
62
|
+
if (!ts) return source;
|
|
63
|
+
|
|
64
|
+
let file: any;
|
|
65
|
+
try {
|
|
66
|
+
file = ts.createSourceFile("mapping.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
67
|
+
} catch {
|
|
68
|
+
return source;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const edits: Edit[] = [];
|
|
72
|
+
const visit = (node: any) => {
|
|
73
|
+
if (
|
|
74
|
+
node.kind === ts.SyntaxKind.BinaryExpression &&
|
|
75
|
+
node.operatorToken?.kind === ts.SyntaxKind.SlashToken
|
|
76
|
+
) {
|
|
77
|
+
const left = ts.skipTrivia(source, node.left.pos);
|
|
78
|
+
const operator = ts.skipTrivia(source, node.operatorToken.pos);
|
|
79
|
+
edits.push({ at: left, length: 0, text: `${DIVIDE_HELPER}(` });
|
|
80
|
+
edits.push({ at: operator, length: node.operatorToken.end - operator, text: "," });
|
|
81
|
+
edits.push({ at: node.right.end, length: 0, text: ")" });
|
|
82
|
+
}
|
|
83
|
+
ts.forEachChild(node, visit);
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
ts.forEachChild(file, visit);
|
|
87
|
+
} catch {
|
|
88
|
+
return source;
|
|
89
|
+
}
|
|
90
|
+
if (edits.length === 0) return source;
|
|
91
|
+
|
|
92
|
+
// Applied back to front so an outer division's insertion points stay valid
|
|
93
|
+
// while an inner one is rewritten.
|
|
94
|
+
edits.sort((a, b) => b.at - a.at || b.length - a.length);
|
|
95
|
+
let out = source;
|
|
96
|
+
for (const edit of edits) {
|
|
97
|
+
out = out.slice(0, edit.at) + edit.text + out.slice(edit.at + edit.length);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const ISSUES_URL = "https://github.com/enviodev/hyperindex/issues";
|
|
2
|
+
|
|
3
|
+
/** A feature we recognise and deliberately don't implement. */
|
|
4
|
+
export function unsupported(feature: string, location: string): Error {
|
|
5
|
+
return new Error(
|
|
6
|
+
`Envio Subgraph doesn't support ${feature} yet.\n` +
|
|
7
|
+
` Found in ${location}.\n` +
|
|
8
|
+
`First, make sure you're on the latest envio version โ support may have landed:\n` +
|
|
9
|
+
` pnpm add -D envio@latest\n` +
|
|
10
|
+
`If you're up to date and need this feature, please open an issue (existing\n` +
|
|
11
|
+
`issues welcome a ๐ โ demand drives prioritization):\n ${ISSUES_URL}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Something we don't recognise at all: newer than us, or a typo. */
|
|
16
|
+
export function unknown(thing: string, location: string): Error {
|
|
17
|
+
return new Error(
|
|
18
|
+
`Envio Subgraph doesn't know ${thing}.\n` +
|
|
19
|
+
` Found in ${location}.\n` +
|
|
20
|
+
`This may be a feature newer than this envio version understands, or a typo.\n` +
|
|
21
|
+
`First, make sure you're on the latest envio version:\n` +
|
|
22
|
+
` pnpm add -D envio@latest\n` +
|
|
23
|
+
`If you're up to date and this is a real subgraph feature, please open an\n` +
|
|
24
|
+
`issue so we can add it: ${ISSUES_URL}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const RPC_ENV_VAR = "ENVIO_SUBGRAPH_RPC";
|
|
29
|
+
|
|
30
|
+
/** ยง6b: contract calls need an endpoint HyperSync and HyperRPC don't serve. */
|
|
31
|
+
export function missingRpcMessage(callSite: string): string {
|
|
32
|
+
return (
|
|
33
|
+
`This subgraph performs contract calls (${callSite}), which need an\n` +
|
|
34
|
+
`RPC endpoint โ HyperSync and HyperRPC serve logs and blocks, not eth_call.\n` +
|
|
35
|
+
`Set one in .env or the environment:\n` +
|
|
36
|
+
` ${RPC_ENV_VAR}=https://...\n` +
|
|
37
|
+
`Advanced (matches envio's rpc config; single entry or array):\n` +
|
|
38
|
+
` ${RPC_ENV_VAR}={"url":"https://...","for":"fallback","headers":{...}}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Names a trap must answer for rather than refuse. AS-compiled mappings never
|
|
44
|
+
* feature-detect, so nothing legitimate reaches these โ but `console.log`,
|
|
45
|
+
* `await` and `JSON.stringify` do, and they must not explode.
|
|
46
|
+
*/
|
|
47
|
+
const PASSTHROUGH = new Set([
|
|
48
|
+
"then",
|
|
49
|
+
"toJSON",
|
|
50
|
+
"constructor",
|
|
51
|
+
"valueOf",
|
|
52
|
+
"toString",
|
|
53
|
+
"inspect",
|
|
54
|
+
"nodeType",
|
|
55
|
+
"$$typeof",
|
|
56
|
+
"@@__IMMUTABLE_ITERABLE__@@",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function isPassthrough(prop: PropertyKey): boolean {
|
|
60
|
+
return typeof prop === "symbol" || PASSTHROUGH.has(prop as string);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const PROTOTYPE_PASSTHROUGH = PASSTHROUGH;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Full Proxy for a graph-ts namespace object. Future graph-ts APIs are
|
|
67
|
+
* unenumerable, so only a `get` trap can catch them; these are cold paths, so
|
|
68
|
+
* the trap cost doesn't matter.
|
|
69
|
+
*/
|
|
70
|
+
export function strictNamespace<T extends object>(name: string, target: T): T {
|
|
71
|
+
return new Proxy(target, {
|
|
72
|
+
get(target, prop, receiver) {
|
|
73
|
+
if (prop in target || isPassthrough(prop)) {
|
|
74
|
+
return Reflect.get(target, prop, receiver);
|
|
75
|
+
}
|
|
76
|
+
throw unknown(`the graph-ts API ${name}.${String(prop)}`, `a mapping handler`);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A known-but-refused field: enumerable, so a plain throwing getter is enough. */
|
|
82
|
+
export function refusedGetter(object: object, prop: string, feature: string, location: string) {
|
|
83
|
+
Object.defineProperty(object, prop, {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
get() {
|
|
87
|
+
throw unsupported(feature, location);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|