@o1-labs/mina-archive-node-graphql 0.0.6 → 2.0.0
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/LICENSE +201 -0
- package/README.md +92 -268
- package/build/src/blockchain/utils.js.map +1 -1
- package/build/src/config.js +86 -0
- package/build/src/config.js.map +1 -0
- package/build/src/db/archive-node-adapter/archive-node-adapter.js +29 -2
- package/build/src/db/archive-node-adapter/archive-node-adapter.js.map +1 -1
- package/build/src/db/archive-node-adapter/postgres-options.js +35 -0
- package/build/src/db/archive-node-adapter/postgres-options.js.map +1 -0
- package/build/src/db/sql/best-chain.js +5 -0
- package/build/src/db/sql/best-chain.js.map +1 -0
- package/build/src/db/sql/events-actions/queries.js +102 -21
- package/build/src/db/sql/events-actions/queries.js.map +1 -1
- package/build/src/db/sql/verification-key-updates/queries.js +100 -0
- package/build/src/db/sql/verification-key-updates/queries.js.map +1 -0
- package/build/src/db/sql/zkapp-commands/queries.js +363 -0
- package/build/src/db/sql/zkapp-commands/queries.js.map +1 -0
- package/build/src/db/sql/zkapp-commands/types.js +2 -0
- package/build/src/db/sql/zkapp-commands/types.js.map +1 -0
- package/build/src/errors/error.js +4 -1
- package/build/src/errors/error.js.map +1 -1
- package/build/src/index.js +44 -7
- package/build/src/index.js.map +1 -1
- package/build/src/resolvers.js +42 -23
- package/build/src/resolvers.js.map +1 -1
- package/build/src/schema-version.js +2 -0
- package/build/src/schema-version.js.map +1 -0
- package/build/src/server/cors.js +41 -0
- package/build/src/server/cors.js.map +1 -0
- package/build/src/server/graceful-shutdown.js +42 -0
- package/build/src/server/graceful-shutdown.js.map +1 -0
- package/build/src/server/graphql-armor.js +39 -0
- package/build/src/server/graphql-armor.js.map +1 -0
- package/build/src/server/logger.js +45 -0
- package/build/src/server/logger.js.map +1 -0
- package/build/src/server/metrics.js +72 -0
- package/build/src/server/metrics.js.map +1 -0
- package/build/src/server/plugins.js +31 -15
- package/build/src/server/plugins.js.map +1 -1
- package/build/src/server/rate-limit.js +143 -0
- package/build/src/server/rate-limit.js.map +1 -0
- package/build/src/server/readiness.js +45 -0
- package/build/src/server/readiness.js.map +1 -0
- package/build/src/server/request-logging.js +58 -0
- package/build/src/server/request-logging.js.map +1 -0
- package/build/src/server/server.js +38 -13
- package/build/src/server/server.js.map +1 -1
- package/build/src/services/actions-service/actions-service.js +26 -15
- package/build/src/services/actions-service/actions-service.js.map +1 -1
- package/build/src/services/blocks-service/blocks-service.js +10 -9
- package/build/src/services/blocks-service/blocks-service.js.map +1 -1
- package/build/src/services/network-service/network-service.js +17 -9
- package/build/src/services/network-service/network-service.js.map +1 -1
- package/build/src/services/verification-key-updates-service/verification-key-updates-service.interface.js +2 -0
- package/build/src/services/verification-key-updates-service/verification-key-updates-service.interface.js.map +1 -0
- package/build/src/services/verification-key-updates-service/verification-key-updates-service.js +41 -0
- package/build/src/services/verification-key-updates-service/verification-key-updates-service.js.map +1 -0
- package/build/src/services/zkapp-commands-service/zkapp-commands-service.interface.js +2 -0
- package/build/src/services/zkapp-commands-service/zkapp-commands-service.interface.js.map +1 -0
- package/build/src/services/zkapp-commands-service/zkapp-commands-service.js +115 -0
- package/build/src/services/zkapp-commands-service/zkapp-commands-service.js.map +1 -0
- package/build/src/tracing/jaeger-tracing.js +2 -1
- package/build/src/tracing/jaeger-tracing.js.map +1 -1
- package/build/tests/devnet-dump/action-state-invariants.js +165 -0
- package/build/tests/devnet-dump/action-state-invariants.js.map +1 -0
- package/build/tests/devnet-dump/devnet-dump.test.js +24 -10
- package/build/tests/devnet-dump/devnet-dump.test.js.map +1 -1
- package/build/tests/devnet-dump/setup.js +63 -17
- package/build/tests/devnet-dump/setup.js.map +1 -1
- package/build/tests/integration/action-state-ordering.test.js +139 -0
- package/build/tests/integration/action-state-ordering.test.js.map +1 -0
- package/build/tests/integration/action-state-setup.js +48 -0
- package/build/tests/integration/action-state-setup.js.map +1 -0
- package/build/tests/integration/consensus-vs-chain.test.js +81 -0
- package/build/tests/integration/consensus-vs-chain.test.js.map +1 -0
- package/build/tests/integration/integration.test.js +166 -2
- package/build/tests/integration/integration.test.js.map +1 -1
- package/build/tests/integration/postgres-timeout.test.js +92 -0
- package/build/tests/integration/postgres-timeout.test.js.map +1 -0
- package/build/tests/integration/setup.js +58 -1
- package/build/tests/integration/setup.js.map +1 -1
- package/build/tests/integration/verification-key-setup.js +49 -0
- package/build/tests/integration/verification-key-setup.js.map +1 -0
- package/build/tests/integration/verification-key-updates.test.js +159 -0
- package/build/tests/integration/verification-key-updates.test.js.map +1 -0
- package/build/tests/live-api/events.test.js +11 -7
- package/build/tests/live-api/events.test.js.map +1 -1
- package/build/tests/live-api/networkState.test.js +1 -0
- package/build/tests/live-api/networkState.test.js.map +1 -1
- package/build/tests/live-network/live-network.test.js +1 -0
- package/build/tests/live-network/live-network.test.js.map +1 -1
- package/build/tests/resolvers.test.js +65 -37
- package/build/tests/resolvers.test.js.map +1 -1
- package/build/tests/services/verification-key-updates-service/verification-key-updates-service.test.js +35 -0
- package/build/tests/services/verification-key-updates-service/verification-key-updates-service.test.js.map +1 -0
- package/build/tests/services/zkapp-commands-service/zkapp-commands-service.test.js +159 -0
- package/build/tests/services/zkapp-commands-service/zkapp-commands-service.test.js.map +1 -0
- package/build/tests/test-helpers.js +15 -0
- package/build/tests/test-helpers.js.map +1 -1
- package/build/tests/unit/best-chain-anchor.test.js +70 -0
- package/build/tests/unit/best-chain-anchor.test.js.map +1 -0
- package/build/tests/unit/config.test.js +106 -0
- package/build/tests/unit/config.test.js.map +1 -0
- package/build/tests/unit/cors.test.js +95 -0
- package/build/tests/unit/cors.test.js.map +1 -0
- package/build/tests/unit/error-masking.test.js +119 -0
- package/build/tests/unit/error-masking.test.js.map +1 -0
- package/build/tests/unit/graceful-shutdown.test.js +99 -0
- package/build/tests/unit/graceful-shutdown.test.js.map +1 -0
- package/build/tests/unit/graphql-armor.test.js +121 -0
- package/build/tests/unit/graphql-armor.test.js.map +1 -0
- package/build/tests/unit/logging.test.js +120 -0
- package/build/tests/unit/logging.test.js.map +1 -0
- package/build/tests/unit/metrics.test.js +114 -0
- package/build/tests/unit/metrics.test.js.map +1 -0
- package/build/tests/unit/network-service.test.js +66 -0
- package/build/tests/unit/network-service.test.js.map +1 -0
- package/build/tests/unit/postgres-options.test.js +56 -0
- package/build/tests/unit/postgres-options.test.js.map +1 -0
- package/build/tests/unit/rate-limit.test.js +271 -0
- package/build/tests/unit/rate-limit.test.js.map +1 -0
- package/build/tests/unit/readiness.test.js +75 -0
- package/build/tests/unit/readiness.test.js.map +1 -0
- package/build/tests/unit/schema-nullability.test.js +40 -0
- package/build/tests/unit/schema-nullability.test.js.map +1 -0
- package/build/tests/unit/schema-version.test.js +48 -0
- package/build/tests/unit/schema-version.test.js.map +1 -0
- package/build/tests/unit/yoga-http-contract.test.js +30 -0
- package/build/tests/unit/yoga-http-contract.test.js.map +1 -0
- package/build/tests/unit/zkapp-commands-gating.test.js +38 -0
- package/build/tests/unit/zkapp-commands-gating.test.js.map +1 -0
- package/build/zkapp/utils.js +2 -0
- package/build/zkapp/utils.js.map +1 -1
- package/package.json +33 -14
- package/schema.graphql +126 -8
- package/src/blockchain/types.ts +65 -1
- package/src/blockchain/utils.ts +26 -2
- package/src/config.ts +133 -0
- package/src/db/archive-node-adapter/archive-node-adapter.interface.ts +26 -3
- package/src/db/archive-node-adapter/archive-node-adapter.ts +65 -2
- package/src/db/archive-node-adapter/postgres-options.ts +90 -0
- package/src/db/sql/best-chain.ts +34 -0
- package/src/db/sql/events-actions/queries.ts +168 -24
- package/src/db/sql/verification-key-updates/queries.ts +132 -0
- package/src/db/sql/zkapp-commands/queries.ts +406 -0
- package/src/db/sql/zkapp-commands/types.ts +43 -0
- package/src/envionment.d.ts +20 -0
- package/src/errors/error.ts +17 -1
- package/src/index.ts +64 -7
- package/src/resolvers-types.ts +311 -24
- package/src/resolvers.ts +70 -34
- package/src/schema-version.ts +24 -0
- package/src/server/cors.ts +97 -0
- package/src/server/graceful-shutdown.ts +76 -0
- package/src/server/graphql-armor.ts +78 -0
- package/src/server/logger.ts +72 -0
- package/src/server/metrics.ts +104 -0
- package/src/server/plugins.ts +53 -19
- package/src/server/rate-limit.ts +274 -0
- package/src/server/readiness.ts +78 -0
- package/src/server/request-logging.ts +83 -0
- package/src/server/server.ts +70 -15
- package/src/services/actions-service/actions-service.ts +87 -24
- package/src/services/blocks-service/blocks-service.ts +16 -14
- package/src/services/network-service/network-service.ts +30 -12
- package/src/services/verification-key-updates-service/verification-key-updates-service.interface.ts +9 -0
- package/src/services/verification-key-updates-service/verification-key-updates-service.ts +72 -0
- package/src/services/zkapp-commands-service/zkapp-commands-service.interface.ts +9 -0
- package/src/services/zkapp-commands-service/zkapp-commands-service.ts +181 -0
- package/src/tracing/jaeger-tracing.ts +2 -1
- package/build/tests/consensus/run-compare.js +0 -40
- package/build/tests/consensus/run-compare.js.map +0 -1
- package/build/tests/consensus/select-consensus-precomputed.js +0 -80
- package/build/tests/consensus/select-consensus-precomputed.js.map +0 -1
- package/build/tests/consensus/types.js +0 -15
- package/build/tests/consensus/types.js.map +0 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { Plugin } from 'graphql-yoga';
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
useRateLimit,
|
|
5
|
+
createRateLimiter,
|
|
6
|
+
resolveRateLimitConfig,
|
|
7
|
+
RATE_LIMIT_DEFAULTS,
|
|
8
|
+
};
|
|
9
|
+
export type { RateLimitConfig };
|
|
10
|
+
|
|
11
|
+
/** Probes must never be throttled, so orchestrators keep working. */
|
|
12
|
+
const EXEMPT_PATHS = new Set(['/healthcheck', '/readiness']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Global, per-client-IP request rate limiting. A public GraphQL endpoint with no
|
|
16
|
+
* throttle lets one client monopolise the server and the backing Postgres. This
|
|
17
|
+
* is a coarse first line of defence applied before any GraphQL work happens.
|
|
18
|
+
*
|
|
19
|
+
* The counter is in-memory and therefore per-instance: behind multiple replicas
|
|
20
|
+
* each enforces its own share of the limit. A shared store (e.g. Redis) is the
|
|
21
|
+
* follow-up for exact cross-replica limits — see the deployment hardening issue.
|
|
22
|
+
*/
|
|
23
|
+
interface RateLimitConfig {
|
|
24
|
+
/** Max requests allowed per client per window. `0` disables rate limiting. */
|
|
25
|
+
max: number;
|
|
26
|
+
/** Length of the fixed window in milliseconds. */
|
|
27
|
+
windowMs: number;
|
|
28
|
+
/**
|
|
29
|
+
* Number of trusted proxy hops in front of this server. `0` ignores
|
|
30
|
+
* forwarding headers entirely and keys on the socket address, which is the
|
|
31
|
+
* only safe reading when the server is directly exposed.
|
|
32
|
+
*/
|
|
33
|
+
trustProxy: number;
|
|
34
|
+
/**
|
|
35
|
+
* Whether TRUST_PROXY was explicitly and validly set. No default is correct
|
|
36
|
+
* for both topologies: socket-keying behind a load balancer puts every client
|
|
37
|
+
* in one bucket, while trusting X-Forwarded-For without a hop count lets any
|
|
38
|
+
* caller mint a fresh bucket.
|
|
39
|
+
*/
|
|
40
|
+
trustProxyConfigured: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RATE_LIMIT_DEFAULTS: RateLimitConfig = {
|
|
44
|
+
max: 600,
|
|
45
|
+
windowMs: 60_000,
|
|
46
|
+
trustProxy: 0,
|
|
47
|
+
trustProxyConfigured: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type EnvSource = Record<string, string | undefined>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a non-negative integer from an env value, falling back to `fallback` when
|
|
54
|
+
* it is missing or malformed. We never throw, so a typo can't disable the limiter
|
|
55
|
+
* by accident — it reverts to the safe default.
|
|
56
|
+
*/
|
|
57
|
+
function intFromEnv(value: string | undefined, fallback: number): number {
|
|
58
|
+
if (value === undefined || value.trim() === '') return fallback;
|
|
59
|
+
const parsed = Number(value);
|
|
60
|
+
if (!Number.isInteger(parsed) || parsed < 0) return fallback;
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse TRUST_PROXY strictly. Missing or malformed means "not configured", not
|
|
66
|
+
* "directly exposed", so a typo cannot turn into a global shared bucket behind
|
|
67
|
+
* a load balancer.
|
|
68
|
+
*/
|
|
69
|
+
function hopsFromEnv(value: string | undefined): number | undefined {
|
|
70
|
+
if (value === undefined || value.trim() === '') return undefined;
|
|
71
|
+
const parsed = Number(value);
|
|
72
|
+
if (!Number.isInteger(parsed) || parsed < 0) return undefined;
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveRateLimitConfig(env: EnvSource = process.env): RateLimitConfig {
|
|
77
|
+
const hops = hopsFromEnv(env.TRUST_PROXY);
|
|
78
|
+
return {
|
|
79
|
+
max: intFromEnv(env.RATE_LIMIT_MAX, RATE_LIMIT_DEFAULTS.max),
|
|
80
|
+
windowMs: Math.max(
|
|
81
|
+
1,
|
|
82
|
+
intFromEnv(env.RATE_LIMIT_WINDOW_MS, RATE_LIMIT_DEFAULTS.windowMs)
|
|
83
|
+
),
|
|
84
|
+
trustProxy: hops ?? RATE_LIMIT_DEFAULTS.trustProxy,
|
|
85
|
+
trustProxyConfigured: hops !== undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface RateLimitResult {
|
|
90
|
+
allowed: boolean;
|
|
91
|
+
limit: number;
|
|
92
|
+
remaining: number;
|
|
93
|
+
resetAt: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fixed-window counter keyed by client id. `now` is injectable so the windowing
|
|
98
|
+
* logic can be tested deterministically. Counting is independent of how the id
|
|
99
|
+
* is derived, so this takes only the window options.
|
|
100
|
+
*/
|
|
101
|
+
function createRateLimiter(
|
|
102
|
+
config: Pick<RateLimitConfig, 'max' | 'windowMs'>,
|
|
103
|
+
now: () => number = Date.now
|
|
104
|
+
) {
|
|
105
|
+
const buckets = new Map<string, { count: number; resetAt: number }>();
|
|
106
|
+
|
|
107
|
+
function check(id: string): RateLimitResult {
|
|
108
|
+
const time = now();
|
|
109
|
+
let bucket = buckets.get(id);
|
|
110
|
+
if (!bucket || time >= bucket.resetAt) {
|
|
111
|
+
bucket = { count: 0, resetAt: time + config.windowMs };
|
|
112
|
+
buckets.set(id, bucket);
|
|
113
|
+
}
|
|
114
|
+
bucket.count += 1;
|
|
115
|
+
return {
|
|
116
|
+
allowed: bucket.count <= config.max,
|
|
117
|
+
limit: config.max,
|
|
118
|
+
remaining: Math.max(0, config.max - bucket.count),
|
|
119
|
+
resetAt: bucket.resetAt,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Drop expired buckets so memory doesn't grow with unique client count. */
|
|
124
|
+
function prune(): void {
|
|
125
|
+
const time = now();
|
|
126
|
+
for (const [id, bucket] of buckets) {
|
|
127
|
+
if (time >= bucket.resetAt) buckets.delete(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { check, prune, size: () => buckets.size };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type MaybeNodeSocket = {
|
|
135
|
+
req?: { socket?: { remoteAddress?: string } };
|
|
136
|
+
socket?: { remoteAddress?: string };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function socketAddress(serverContext: unknown): string {
|
|
140
|
+
const ctx = serverContext as MaybeNodeSocket;
|
|
141
|
+
return ctx?.req?.socket?.remoteAddress ?? ctx?.socket?.remoteAddress ?? 'unknown';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Identify the client, trusting forwarding headers only as far as `trustProxy`
|
|
146
|
+
* hops allow.
|
|
147
|
+
*
|
|
148
|
+
* `X-Forwarded-For` is client-supplied and every proxy *appends* to it, so the
|
|
149
|
+
* left-hand entries are whatever the caller sent and cannot be trusted: reading
|
|
150
|
+
* the first hop lets one source rotate the header to mint a fresh bucket per
|
|
151
|
+
* request and evade the limit entirely (while growing the bucket Map). Only the
|
|
152
|
+
* rightmost entries — appended by proxies we actually control — are meaningful,
|
|
153
|
+
* so with N trusted hops the real client is the Nth entry from the right.
|
|
154
|
+
*
|
|
155
|
+
* Honouring the header at all still matters: it is what keeps NAT'd and
|
|
156
|
+
* LB-fronted clients in their own buckets instead of collapsing onto one shared
|
|
157
|
+
* address. Hence a hop count rather than a blanket on/off.
|
|
158
|
+
*/
|
|
159
|
+
function clientId(
|
|
160
|
+
request: Request,
|
|
161
|
+
serverContext: unknown,
|
|
162
|
+
trustProxy: number
|
|
163
|
+
): string {
|
|
164
|
+
if (trustProxy <= 0) return socketAddress(serverContext);
|
|
165
|
+
|
|
166
|
+
const forwarded = request.headers
|
|
167
|
+
.get('x-forwarded-for')
|
|
168
|
+
?.split(',')
|
|
169
|
+
.map((part) => part.trim())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
if (forwarded?.length) {
|
|
172
|
+
// Undefined when the chain is shorter than the configured hop count (a
|
|
173
|
+
// misconfiguration, or a request injected inside the trust boundary) —
|
|
174
|
+
// fall through to the socket rather than trust an attacker-chosen entry.
|
|
175
|
+
const client = forwarded[forwarded.length - trustProxy];
|
|
176
|
+
if (client) return client;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Single-valued and overwritten by the adjacent proxy, so it carries no hop
|
|
180
|
+
// structure; only meaningful once we've established there is a proxy at all.
|
|
181
|
+
const realIp = request.headers.get('x-real-ip')?.trim();
|
|
182
|
+
if (realIp) return realIp;
|
|
183
|
+
|
|
184
|
+
return socketAddress(serverContext);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Yoga plugin that rejects over-limit requests with HTTP 429 before they reach
|
|
189
|
+
* GraphQL parsing/execution. Returns a no-op plugin when `max` is 0 (disabled).
|
|
190
|
+
*/
|
|
191
|
+
function useRateLimit(
|
|
192
|
+
env: EnvSource = process.env,
|
|
193
|
+
warn: (message: string) => void = console.warn
|
|
194
|
+
): Plugin {
|
|
195
|
+
const config = resolveRateLimitConfig(env);
|
|
196
|
+
if (config.max <= 0) return {};
|
|
197
|
+
|
|
198
|
+
if (!config.trustProxyConfigured) {
|
|
199
|
+
warn(
|
|
200
|
+
'[rate-limit] TRUST_PROXY is not set — rate limiting is DISABLED. No ' +
|
|
201
|
+
'default is safe: keying on the socket address behind a load balancer ' +
|
|
202
|
+
'puts every client in one bucket, and trusting X-Forwarded-For without ' +
|
|
203
|
+
'a hop count lets any caller bypass the limit. Set TRUST_PROXY=0 for ' +
|
|
204
|
+
'a directly-exposed server, or to the number of proxy hops in front of ' +
|
|
205
|
+
'this API. NOTE: a GCP external Application Load Balancer appends TWO ' +
|
|
206
|
+
'entries (client IP, then forwarding-rule IP), so a bare GCP LB is 2, ' +
|
|
207
|
+
'plus 1 per extra in-cluster proxy. Set RATE_LIMIT_MAX=0 to silence this.'
|
|
208
|
+
);
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const limiter = createRateLimiter(config);
|
|
213
|
+
|
|
214
|
+
// TRUST_PROXY=0 is the safe default, but it's wrong for the expected
|
|
215
|
+
// production topology, and the symptom — every client sharing one bucket
|
|
216
|
+
// behind the LB — otherwise appears only as unexplained throttling. Warn
|
|
217
|
+
// once on the first forwarded request rather than per request.
|
|
218
|
+
let warnedAboutProxy = config.trustProxy > 0;
|
|
219
|
+
const warnIfProxied = (request: Request) => {
|
|
220
|
+
const chain = request.headers.get('x-forwarded-for');
|
|
221
|
+
if (warnedAboutProxy || chain === null) return;
|
|
222
|
+
warnedAboutProxy = true;
|
|
223
|
+
const hops = chain.split(',').filter((part) => part.trim()).length;
|
|
224
|
+
warn(
|
|
225
|
+
'[rate-limit] TRUST_PROXY=0 but requests carry X-Forwarded-For — every ' +
|
|
226
|
+
'client behind the proxy shares one rate-limit bucket. Set TRUST_PROXY ' +
|
|
227
|
+
`to the number of proxy hops in front of this API (observed chain ` +
|
|
228
|
+
`length: ${hops}) to bucket clients individually.`
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
// Periodically reclaim memory from expired buckets; unref'd so it never keeps
|
|
232
|
+
// the process alive on shutdown.
|
|
233
|
+
const sweep = setInterval(() => limiter.prune(), config.windowMs);
|
|
234
|
+
if (typeof sweep.unref === 'function') sweep.unref();
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
onRequest({ request, serverContext, url, endResponse, fetchAPI }) {
|
|
238
|
+
if (EXEMPT_PATHS.has(url.pathname)) return;
|
|
239
|
+
// Yoga's CORS plugin answers OPTIONS before user-land plugins today, but
|
|
240
|
+
// keep the exemption local too: a throttled preflight is an opaque browser
|
|
241
|
+
// CORS error rather than a useful 429.
|
|
242
|
+
if (request.method === 'OPTIONS') return;
|
|
243
|
+
warnIfProxied(request);
|
|
244
|
+
|
|
245
|
+
const result = limiter.check(
|
|
246
|
+
clientId(request, serverContext, config.trustProxy)
|
|
247
|
+
);
|
|
248
|
+
if (result.allowed) return;
|
|
249
|
+
|
|
250
|
+
const retryAfter = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
|
|
251
|
+
endResponse(
|
|
252
|
+
new fetchAPI.Response(
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
errors: [
|
|
255
|
+
{
|
|
256
|
+
message: 'Too many requests. Please retry later.',
|
|
257
|
+
extensions: { code: 'RATE_LIMITED' },
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
}),
|
|
261
|
+
{
|
|
262
|
+
status: 429,
|
|
263
|
+
headers: {
|
|
264
|
+
'content-type': 'application/json',
|
|
265
|
+
'retry-after': String(retryAfter),
|
|
266
|
+
'x-ratelimit-limit': String(result.limit),
|
|
267
|
+
'x-ratelimit-remaining': String(result.remaining),
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Plugin } from 'graphql-yoga';
|
|
2
|
+
import type { DatabaseAdapter } from '../db/archive-node-adapter/archive-node-adapter.interface.js';
|
|
3
|
+
|
|
4
|
+
export { useReadiness, READINESS_PATH };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Readiness endpoint path. Distinct from Yoga's built-in `/healthcheck`, which is
|
|
8
|
+
* a *liveness* probe (the process is up). Readiness additionally checks that the
|
|
9
|
+
* database is reachable, so an orchestrator can stop routing traffic to an
|
|
10
|
+
* instance whose Postgres is down without killing the pod outright.
|
|
11
|
+
*/
|
|
12
|
+
const READINESS_PATH = '/readiness';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upper bound on one readiness ping. Keep it below the orchestrator probe
|
|
16
|
+
* timeout so the server answers 503 itself instead of letting the probe hang.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_PING_TIMEOUT_MS = 2000;
|
|
19
|
+
|
|
20
|
+
function pingTimeoutFromEnv(value: string | undefined): number {
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
23
|
+
? parsed
|
|
24
|
+
: DEFAULT_PING_TIMEOUT_MS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolves, never rejects: the probe endpoint must not return 500. */
|
|
28
|
+
async function pingWithin(
|
|
29
|
+
db: Pick<DatabaseAdapter, 'ping'>,
|
|
30
|
+
timeoutMs: number
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
33
|
+
const timedOut = new Promise<boolean>((resolve) => {
|
|
34
|
+
timer = setTimeout(() => {
|
|
35
|
+
console.warn(`[readiness] database ping exceeded ${timeoutMs}ms`);
|
|
36
|
+
resolve(false);
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
});
|
|
39
|
+
const answered = Promise.resolve()
|
|
40
|
+
.then(() => db.ping())
|
|
41
|
+
.catch(() => false);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return await Promise.race([answered, timedOut]);
|
|
45
|
+
} finally {
|
|
46
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Yoga plugin serving a readiness probe at `READINESS_PATH`. Returns 200 when the
|
|
52
|
+
* database answers a trivial query, 503 otherwise. Registered ahead of other
|
|
53
|
+
* request hooks so probes are never throttled or otherwise interfered with.
|
|
54
|
+
*/
|
|
55
|
+
function useReadiness(
|
|
56
|
+
db: Pick<DatabaseAdapter, 'ping'>,
|
|
57
|
+
timeoutMs = pingTimeoutFromEnv(process.env.READINESS_PING_TIMEOUT_MS)
|
|
58
|
+
): Plugin {
|
|
59
|
+
return {
|
|
60
|
+
async onRequest({ url, endResponse, fetchAPI }) {
|
|
61
|
+
if (url.pathname.replace(/\/$/, '') !== READINESS_PATH) return;
|
|
62
|
+
|
|
63
|
+
const ready = await pingWithin(db, timeoutMs);
|
|
64
|
+
endResponse(
|
|
65
|
+
new fetchAPI.Response(
|
|
66
|
+
JSON.stringify({ status: ready ? 'ready' : 'not ready' }),
|
|
67
|
+
{
|
|
68
|
+
status: ready ? 200 : 503,
|
|
69
|
+
headers: {
|
|
70
|
+
'content-type': 'application/json',
|
|
71
|
+
'cache-control': 'no-store',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Plugin } from 'graphql-yoga';
|
|
3
|
+
import { logger as defaultLogger } from './logger.js';
|
|
4
|
+
import type { Logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export { useRequestLogging, requestIdFor };
|
|
7
|
+
|
|
8
|
+
/** Paths whose requests are not access-logged (frequent orchestrator probes). */
|
|
9
|
+
const QUIET_PATHS = new Set(['/healthcheck', '/readiness']);
|
|
10
|
+
/** Max characters kept from an inbound X-Request-Id. */
|
|
11
|
+
const MAX_REQUEST_ID_LENGTH = 128;
|
|
12
|
+
/** Printable ASCII only, minus quote and backslash. */
|
|
13
|
+
const UNSAFE_REQUEST_ID_CHARS = /[^\x20-\x7e]|["\\]/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Correlation id per in-flight request. Stored in a WeakMap so it is dropped
|
|
17
|
+
* automatically once the request is garbage-collected. Other log sites (e.g. the
|
|
18
|
+
* GraphQL error logger) can look it up to tie their lines to the access log.
|
|
19
|
+
*/
|
|
20
|
+
const requestIds = new WeakMap<Request, string>();
|
|
21
|
+
|
|
22
|
+
function requestIdFor(request: Request): string | undefined {
|
|
23
|
+
return requestIds.get(request);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitizeRequestId(raw: string | null): string {
|
|
27
|
+
if (!raw) return randomUUID();
|
|
28
|
+
const cleaned = raw
|
|
29
|
+
.replace(UNSAFE_REQUEST_ID_CHARS, '')
|
|
30
|
+
.slice(0, MAX_REQUEST_ID_LENGTH)
|
|
31
|
+
.trim();
|
|
32
|
+
return cleaned.length > 0 ? cleaned : randomUUID();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathOf(rawUrl: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(rawUrl).pathname;
|
|
38
|
+
} catch {
|
|
39
|
+
return rawUrl;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Yoga plugin that assigns each request a correlation id (honouring an inbound
|
|
45
|
+
* bounded `X-Request-Id`) and emits one structured access-log line per request
|
|
46
|
+
* with the id, method, path, status, and duration. Probe endpoints are skipped
|
|
47
|
+
* to avoid log spam. `log` is injectable for tests.
|
|
48
|
+
*/
|
|
49
|
+
function useRequestLogging(log: Logger = defaultLogger): Plugin {
|
|
50
|
+
const startTimes = new WeakMap<Request, number>();
|
|
51
|
+
return {
|
|
52
|
+
onRequest({ request }) {
|
|
53
|
+
const id = sanitizeRequestId(request.headers.get('x-request-id'));
|
|
54
|
+
requestIds.set(request, id);
|
|
55
|
+
startTimes.set(request, Date.now());
|
|
56
|
+
},
|
|
57
|
+
onResponse({ request, response }) {
|
|
58
|
+
const path = pathOf(request.url);
|
|
59
|
+
if (QUIET_PATHS.has(path)) return;
|
|
60
|
+
|
|
61
|
+
let id = requestIds.get(request);
|
|
62
|
+
let start = startTimes.get(request);
|
|
63
|
+
if (id === undefined || start === undefined) {
|
|
64
|
+
// CORS and health-check plugins can short-circuit before onRequest, but
|
|
65
|
+
// onResponse still runs. Backfill so access lines remain correlated.
|
|
66
|
+
id = sanitizeRequestId(request.headers.get('x-request-id'));
|
|
67
|
+
start = Date.now();
|
|
68
|
+
requestIds.set(request, id);
|
|
69
|
+
startTimes.set(request, start);
|
|
70
|
+
}
|
|
71
|
+
log.info(
|
|
72
|
+
{
|
|
73
|
+
requestId: id,
|
|
74
|
+
method: request.method,
|
|
75
|
+
path,
|
|
76
|
+
status: response.status,
|
|
77
|
+
durationMs: Date.now() - start,
|
|
78
|
+
},
|
|
79
|
+
'request completed'
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -1,30 +1,85 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { createYoga } from 'graphql-yoga';
|
|
2
3
|
import { createServer } from 'http';
|
|
3
4
|
import { Plugin } from '@envelop/core';
|
|
4
5
|
import { schema } from '../resolvers.js';
|
|
5
6
|
import type { GraphQLContext } from '../context.js';
|
|
7
|
+
import { parseBoolean } from '../config.js';
|
|
8
|
+
import { useReadiness } from './readiness.js';
|
|
9
|
+
import { resolveCorsOptions, warnIfCorsDisabled } from './cors.js';
|
|
10
|
+
import { logger, resolveYogaLogLevel } from './logger.js';
|
|
6
11
|
|
|
7
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
BLOCK_RANGE_SIZE,
|
|
14
|
+
ZKAPP_COMMAND_RANGE_SIZE,
|
|
15
|
+
ZKAPP_COMMAND_ACCOUNT_UPDATE_LIMIT,
|
|
16
|
+
ENABLE_BLOCK_TRANSACTION_DETAILS,
|
|
17
|
+
ENABLE_ZKAPP_COMMANDS_QUERY,
|
|
18
|
+
buildYoga,
|
|
19
|
+
buildServer,
|
|
20
|
+
};
|
|
8
21
|
|
|
9
|
-
const LOG_LEVEL = (process.env.LOG_LEVEL as LogLevel) || 'info';
|
|
10
22
|
const BLOCK_RANGE_SIZE = Number(process.env.BLOCK_RANGE_SIZE) || 10000;
|
|
11
|
-
const ENABLE_BLOCK_TRANSACTION_DETAILS =
|
|
12
|
-
process.env.ENABLE_BLOCK_TRANSACTION_DETAILS
|
|
23
|
+
const ENABLE_BLOCK_TRANSACTION_DETAILS = parseBoolean(
|
|
24
|
+
process.env.ENABLE_BLOCK_TRANSACTION_DETAILS
|
|
25
|
+
);
|
|
26
|
+
// The zkappCommands range query is far heavier than block transaction detail —
|
|
27
|
+
// it expands account updates — so it gets its own switch rather than riding on
|
|
28
|
+
// that flag. Default off: an operator opts in deliberately.
|
|
29
|
+
const ENABLE_ZKAPP_COMMANDS_QUERY = parseBoolean(
|
|
30
|
+
process.env.ENABLE_ZKAPP_COMMANDS_QUERY
|
|
31
|
+
);
|
|
32
|
+
const ZKAPP_COMMAND_RANGE_SIZE =
|
|
33
|
+
Number(process.env.ZKAPP_COMMAND_RANGE_SIZE) || 1000;
|
|
34
|
+
const ZKAPP_COMMAND_ACCOUNT_UPDATE_LIMIT =
|
|
35
|
+
Number(process.env.ZKAPP_COMMAND_ACCOUNT_UPDATE_LIMIT) || 5000;
|
|
36
|
+
const YOGA_LOG_LEVEL = resolveYogaLogLevel();
|
|
13
37
|
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
const yogaLog =
|
|
39
|
+
(level: 'debug' | 'info' | 'warn' | 'error') =>
|
|
40
|
+
(...args: unknown[]) => {
|
|
41
|
+
const err = args.find((arg): arg is Error => arg instanceof Error);
|
|
42
|
+
const msg = args
|
|
43
|
+
.filter((arg) => !(arg instanceof Error))
|
|
44
|
+
.map((arg) =>
|
|
45
|
+
typeof arg === 'string' ? arg : inspect(arg, { depth: 3 })
|
|
46
|
+
)
|
|
47
|
+
.join(' ');
|
|
48
|
+
logger[level](err ? { err } : {}, msg || 'yoga');
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function buildYoga(context: GraphQLContext, plugins: Plugin[]) {
|
|
52
|
+
const cors = resolveCorsOptions();
|
|
53
|
+
|
|
54
|
+
return createYoga<GraphQLContext>({
|
|
16
55
|
schema,
|
|
17
|
-
logging:
|
|
56
|
+
logging:
|
|
57
|
+
YOGA_LOG_LEVEL === false
|
|
58
|
+
? false
|
|
59
|
+
: {
|
|
60
|
+
debug: yogaLog('debug'),
|
|
61
|
+
info: yogaLog('info'),
|
|
62
|
+
warn: yogaLog('warn'),
|
|
63
|
+
error: yogaLog('error'),
|
|
64
|
+
},
|
|
18
65
|
graphqlEndpoint: '/',
|
|
19
66
|
landingPage: false,
|
|
67
|
+
// Liveness — the process is up and serving HTTP.
|
|
20
68
|
healthCheckEndpoint: '/healthcheck',
|
|
21
|
-
graphiql: process.env.ENABLE_GRAPHIQL
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
69
|
+
graphiql: parseBoolean(process.env.ENABLE_GRAPHIQL),
|
|
70
|
+
// Mask unexpected (non-GraphQLError) errors so internal details — SQL,
|
|
71
|
+
// connection strings, stack traces — never reach clients. `isDev: false`
|
|
72
|
+
// keeps Envelop from attaching original errors when NODE_ENV=development.
|
|
73
|
+
maskedErrors: { isDev: false },
|
|
74
|
+
// Readiness (DB reachable) is prepended so probes short-circuit before any
|
|
75
|
+
// other request hook (e.g. rate limiting) can interfere with them.
|
|
76
|
+
plugins: [useReadiness(context.db_client), ...plugins],
|
|
77
|
+
cors,
|
|
27
78
|
context,
|
|
28
79
|
});
|
|
29
|
-
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildServer(context: GraphQLContext, plugins: Plugin[]) {
|
|
83
|
+
warnIfCorsDisabled(resolveCorsOptions());
|
|
84
|
+
return createServer(buildYoga(context, plugins));
|
|
30
85
|
}
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_TOKEN_ID } from '../../blockchain/constants.js';
|
|
|
12
12
|
import { createBlockInfo } from '../../blockchain/utils.js';
|
|
13
13
|
import {
|
|
14
14
|
getActionsQuery,
|
|
15
|
-
|
|
15
|
+
resolveActionStateBoundary,
|
|
16
16
|
} from '../../db/sql/events-actions/queries.js';
|
|
17
17
|
import {
|
|
18
18
|
partitionBlocks,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../../tracing/tracer.js';
|
|
29
29
|
import {
|
|
30
30
|
throwActionStateError,
|
|
31
|
+
throwActionStateOutOfRangeError,
|
|
31
32
|
throwBlockRangeError,
|
|
32
33
|
} from '../../errors/error.js';
|
|
33
34
|
import { BLOCK_RANGE_SIZE } from '../../server/server.js';
|
|
@@ -70,32 +71,73 @@ class ActionsService implements IActionsService {
|
|
|
70
71
|
return actionsData;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Resolves an action state to the block height at which this account's action
|
|
76
|
+
* state first became that value, and raises a typed error when the request
|
|
77
|
+
* cannot be answered completely.
|
|
78
|
+
*
|
|
79
|
+
* The filter has to use a position on the chain. It used to compare
|
|
80
|
+
* `zkapp_field.id`, the interning key of the value, which records when a value
|
|
81
|
+
* was first written to the archive and not where it sits on the chain. Those
|
|
82
|
+
* two orders disagree on any archive that was filled out of chain order, and
|
|
83
|
+
* the filter then dropped real actions.
|
|
84
|
+
*/
|
|
85
|
+
async resolveBoundaryHeight(
|
|
86
|
+
argumentName: 'fromActionState' | 'endActionState',
|
|
87
|
+
actionState: string,
|
|
88
|
+
address: string,
|
|
89
|
+
tokenId: string,
|
|
90
|
+
from?: number,
|
|
91
|
+
to?: number
|
|
92
|
+
): Promise<string | undefined> {
|
|
93
|
+
const [boundary] = await resolveActionStateBoundary(
|
|
94
|
+
this.client,
|
|
95
|
+
address,
|
|
96
|
+
tokenId,
|
|
97
|
+
actionState,
|
|
98
|
+
from?.toString(),
|
|
99
|
+
to?.toString()
|
|
100
|
+
);
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.client,
|
|
80
|
-
fromActionState
|
|
102
|
+
if (!boundary || boundary.boundary_height === null) {
|
|
103
|
+
throwActionStateError(
|
|
104
|
+
`${argumentName} ${actionState} is not an action state of account ${address}`
|
|
81
105
|
);
|
|
82
|
-
|
|
83
|
-
throwActionStateError(
|
|
84
|
-
`fromActionState ${fromActionState} does not exist`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
106
|
+
return undefined;
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
|
|
109
|
+
const boundaryHeight = Number(boundary.boundary_height);
|
|
110
|
+
const windowStart = Number(boundary.window_start);
|
|
111
|
+
|
|
112
|
+
// The checkpoint is older than the block range this query covers. For
|
|
113
|
+
// `fromActionState` that alone does not make the answer wrong: if the account
|
|
114
|
+
// emitted no action between the checkpoint and the start of the window, every
|
|
115
|
+
// action in the window is still the complete answer, and
|
|
116
|
+
// `state_entering_window` says so. Rejecting in that case would break the
|
|
117
|
+
// common case of a quiet zkApp folding from its genesis action state, which
|
|
118
|
+
// is the default o1js path because o1js sends no `from`/`to`.
|
|
119
|
+
//
|
|
120
|
+
// `endActionState` gets no such rescue. It bounds the request from above, so
|
|
121
|
+
// a checkpoint below the window means the whole requested span is below the
|
|
122
|
+
// window and every action in it is missing. An empty answer would be silently
|
|
123
|
+
// wrong, which is the failure this change exists to remove.
|
|
124
|
+
const answerIsStillComplete =
|
|
125
|
+
argumentName === 'fromActionState' && boundary.state_entering_window === true;
|
|
126
|
+
if (boundaryHeight < windowStart && !answerIsStillComplete) {
|
|
127
|
+
throwActionStateOutOfRangeError(
|
|
128
|
+
`${argumentName} ${actionState} is at block height ${boundaryHeight}, which is below the ` +
|
|
129
|
+
`queried block range starting at ${windowStart}, and this account emitted actions in ` +
|
|
130
|
+
`between. The answer would be missing them. Retry with from: ${boundaryHeight} ` +
|
|
131
|
+
`(at most ${BLOCK_RANGE_SIZE} blocks per query, so paginate if needed).`
|
|
92
132
|
);
|
|
93
|
-
if (!endActionStateExists || !endActionStateExists.length) {
|
|
94
|
-
throwActionStateError(
|
|
95
|
-
`endActionState ${endActionState} does not exist`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
133
|
}
|
|
134
|
+
|
|
135
|
+
return boundary.boundary_height;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async executeActionsQuery(input: ActionFilterOptionsInput) {
|
|
139
|
+
const { address, to, from, endActionState, fromActionState } = input;
|
|
140
|
+
|
|
99
141
|
let { tokenId, status } = input;
|
|
100
142
|
|
|
101
143
|
tokenId ||= DEFAULT_TOKEN_ID;
|
|
@@ -109,6 +151,27 @@ class ActionsService implements IActionsService {
|
|
|
109
151
|
);
|
|
110
152
|
}
|
|
111
153
|
|
|
154
|
+
const fromActionStateHeight = fromActionState
|
|
155
|
+
? await this.resolveBoundaryHeight(
|
|
156
|
+
'fromActionState',
|
|
157
|
+
fromActionState.toString(),
|
|
158
|
+
address,
|
|
159
|
+
tokenId,
|
|
160
|
+
from ?? undefined,
|
|
161
|
+
to ?? undefined
|
|
162
|
+
)
|
|
163
|
+
: undefined;
|
|
164
|
+
const endActionStateHeight = endActionState
|
|
165
|
+
? await this.resolveBoundaryHeight(
|
|
166
|
+
'endActionState',
|
|
167
|
+
endActionState.toString(),
|
|
168
|
+
address,
|
|
169
|
+
tokenId,
|
|
170
|
+
from ?? undefined,
|
|
171
|
+
to ?? undefined
|
|
172
|
+
)
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
112
175
|
return getActionsQuery(
|
|
113
176
|
this.client,
|
|
114
177
|
address,
|
|
@@ -116,8 +179,8 @@ class ActionsService implements IActionsService {
|
|
|
116
179
|
status,
|
|
117
180
|
to?.toString(),
|
|
118
181
|
from?.toString(),
|
|
119
|
-
|
|
120
|
-
|
|
182
|
+
fromActionStateHeight,
|
|
183
|
+
endActionStateHeight
|
|
121
184
|
);
|
|
122
185
|
}
|
|
123
186
|
|