tapimo 0.10.2 → 0.11.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/dist/App.d.ts +3412 -1968
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +0 -75
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +1 -143
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +74 -74
- package/dist/admin/apps/api-keys.d.ts +4 -4
- package/dist/admin/apps/earn-vaults.d.ts +1 -1
- package/dist/admin/apps/organizations.d.ts +3 -3
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/apps/data/App.d.ts +3618 -2954
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +5 -2
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +1 -1
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +55 -27
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +343 -343
- package/dist/apps/data/routes/balances.d.ts +11 -11
- package/dist/apps/data/routes/blocks.d.ts +5 -5
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts +4 -4
- package/dist/apps/data/routes/exchanges.d.ts +4 -4
- package/dist/apps/data/routes/fee-amm.d.ts +26 -26
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/receipts.d.ts +236 -236
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/tokens.d.ts +3 -3
- package/dist/apps/data/routes/transactions.d.ts +1 -1
- package/dist/apps/data/routes/transfers.d.ts +13 -13
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +1011 -151
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +124 -737
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/funding/routes/deposit-addresses.d.ts +2 -2
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/transfers.d.ts +33 -33
- package/dist/apps/management/App.d.ts +793 -16
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/faucet.d.ts +31 -31
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/members.d.ts +79 -79
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +80 -80
- package/dist/apps/management/routes/webhooks.d.ts +1031 -59
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +27 -4
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +21 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +7 -1
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +9 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +3 -1
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
- package/dist/db/tables/webhookQueueEvents.js +4 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +33 -69
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +63 -100
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +35 -125
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +50 -121
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Schema.d.ts +3 -1
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +4 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +1 -55
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +1 -239
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +83 -44
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +87 -73
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/DepositWebhook.d.ts +25 -0
- package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
- package/dist/internal/funding/DepositWebhook.js +74 -0
- package/dist/internal/funding/DepositWebhook.js.map +1 -0
- package/dist/internal/funding/Reconciliation.d.ts +6 -0
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +42 -10
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
- package/dist/internal/webhookDestinations/slack.js +16 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -1
- package/package.json +1 -1
- package/src/App.ts +3 -0
- package/src/Client.test-d.ts +22 -1
- package/src/Webhooks.ts +1 -220
- package/src/apps/data/App.ts +8 -2
- package/src/apps/data/routes/activities.test.ts +107 -11
- package/src/apps/data/routes/activities.ts +64 -32
- package/src/apps/data/routes/webhooks.test.ts +179 -1497
- package/src/apps/data/routes/webhooks.ts +150 -974
- package/src/apps/management/routes/webhooks.test.ts +79 -1
- package/src/apps/management/routes/webhooks.ts +40 -4
- package/src/db/Db.test.ts +1 -5
- package/src/db/Db.ts +23 -0
- package/src/db/Schema.ts +13 -0
- package/src/db/tables/organizations.test.ts +5 -2
- package/src/db/tables/webhookQueueEvents.ts +7 -1
- package/src/db/tables/webhookSubscriptions.ts +89 -138
- package/src/internal/MetricSink.test.ts +41 -373
- package/src/internal/MetricSink.ts +92 -273
- package/src/internal/Schema.test.ts +18 -0
- package/src/internal/Schema.ts +13 -2
- package/src/internal/WebhookTransfer.ts +1 -351
- package/src/internal/Webhooks.test.ts +102 -139
- package/src/internal/Webhooks.ts +175 -106
- package/src/internal/funding/DepositWebhook.test.ts +131 -0
- package/src/internal/funding/DepositWebhook.ts +101 -0
- package/src/internal/funding/Reconciliation.test.ts +14 -0
- package/src/internal/funding/Reconciliation.ts +161 -86
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/webhookDestinations/slack.ts +15 -0
- package/src/Webhooks.bench.ts +0 -714
- package/src/Webhooks.memory.bench.ts +0 -250
- package/src/Webhooks.test.ts +0 -146
package/src/Webhooks.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
import * as AppWebhooks from './apps/data/routes/webhooks.js'
|
|
2
|
-
import * as Db from './db/Db.js'
|
|
3
|
-
import * as core_Webhooks from './internal/Webhooks.js'
|
|
4
|
-
import * as MetricSink from './internal/MetricSink.js'
|
|
5
|
-
import * as Store from './internal/Store.js'
|
|
6
|
-
import type * as Metrics from './Metrics.js'
|
|
7
|
-
import * as Viem from './internal/Viem.js'
|
|
8
|
-
|
|
9
|
-
/** How often a poller reclaims expired rows, independent of tick cadence. */
|
|
10
|
-
const pruneIntervalMs = 60_000
|
|
11
|
-
|
|
12
1
|
// Re-export the runtime-agnostic primitives (subscriptions, signing, delivery,
|
|
13
2
|
// `deliverAndRecord`, cursors, …) so consumers reach everything through one
|
|
14
|
-
// `Webhooks` namespace.
|
|
15
|
-
// `createPoller` below.
|
|
3
|
+
// `Webhooks` namespace.
|
|
16
4
|
export * from './internal/Webhooks.js'
|
|
17
5
|
|
|
18
6
|
// Re-export the webhook resource's zod schemas so consumers can validate and
|
|
@@ -20,210 +8,3 @@ export * from './internal/Webhooks.js'
|
|
|
20
8
|
// `type`), plus the per-event `TransferEvent` / `TransactionEvent` / `PingEvent`
|
|
21
9
|
// payloads. Infer a type with `z.infer<typeof Webhooks.schema.Envelope>`.
|
|
22
10
|
export { enrichEventData, eventTypes, scanBlock, schema } from './apps/data/routes/webhooks.js'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Builds a webhook poller bound to a state store and chain config.
|
|
26
|
-
*
|
|
27
|
-
* This is the entry point a host runtime drives from chain-head notifications
|
|
28
|
-
* or a timer. Each {@link Poller.tick} scans active subscriptions from their
|
|
29
|
-
* cursors up to `head - confirmations` and dispatches matching events.
|
|
30
|
-
*
|
|
31
|
-
* Every onchain event type scans a confirmed window directly over JSON-RPC, so
|
|
32
|
-
* the poller builds memoized viem clients via {@link Viem.createGetClient}. The
|
|
33
|
-
* poller keeps `App.create` a pure Hono app: it constructs its own clients
|
|
34
|
-
* instead of reaching into app internals, and owns no request state.
|
|
35
|
-
*/
|
|
36
|
-
export function createPoller(options: createPoller.Options): createPoller.Poller {
|
|
37
|
-
// Persistent memoize cache for token metadata, shared across ticks (the
|
|
38
|
-
// webhook state store used to double-duty as this cache).
|
|
39
|
-
const cache = options.cache ?? Store.memory()
|
|
40
|
-
const getClient = Viem.createGetClient({
|
|
41
|
-
defaultChainId: options.defaultChainId,
|
|
42
|
-
rpc: options.rpc,
|
|
43
|
-
})
|
|
44
|
-
// Chain-head ticks run about once per block, so the janitor is throttled here
|
|
45
|
-
// rather than issuing deletes per block. Reads filter expiry at query time, so
|
|
46
|
-
// reclaiming late only costs space.
|
|
47
|
-
let prunedAt: number | undefined
|
|
48
|
-
|
|
49
|
-
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
async tick(input = {}) {
|
|
53
|
-
const chainId = input.chainId ?? options.defaultChainId ?? Viem.defaultChainId
|
|
54
|
-
|
|
55
|
-
// Wrap the transport so each enqueue (or inline delivery) emits a timed
|
|
56
|
-
// `webhook:enqueue` metric. The wrapper preserves the dispatch contract: it
|
|
57
|
-
// returns the underlying result unchanged and is a no-op when no metrics
|
|
58
|
-
// backend is configured.
|
|
59
|
-
const inner = input.dispatch ?? options.dispatch
|
|
60
|
-
const innerBatch =
|
|
61
|
-
input.dispatchBatch ?? (input.dispatch === undefined ? options.dispatchBatch : undefined)
|
|
62
|
-
const dispatch =
|
|
63
|
-
metrics && inner
|
|
64
|
-
? async (dispatchable: AppWebhooks.poll.Dispatchable) => {
|
|
65
|
-
const start = Date.now()
|
|
66
|
-
const result = await inner(dispatchable)
|
|
67
|
-
metrics.record({
|
|
68
|
-
chainId: dispatchable.envelope.chainId,
|
|
69
|
-
durationMs: Date.now() - start,
|
|
70
|
-
eventType: dispatchable.envelope.type,
|
|
71
|
-
ok: result.ok,
|
|
72
|
-
type: 'webhook:enqueue',
|
|
73
|
-
})
|
|
74
|
-
return result
|
|
75
|
-
}
|
|
76
|
-
: inner
|
|
77
|
-
const dispatchBatch = innerBatch
|
|
78
|
-
? async (
|
|
79
|
-
dispatchables: readonly AppWebhooks.poll.Dispatchable[],
|
|
80
|
-
context: AppWebhooks.poll.DispatchContext,
|
|
81
|
-
) => {
|
|
82
|
-
const start = Date.now()
|
|
83
|
-
const result = await innerBatch(dispatchables, context)
|
|
84
|
-
const durationMs = Date.now() - start
|
|
85
|
-
for (const dispatchable of dispatchables)
|
|
86
|
-
metrics?.record({
|
|
87
|
-
chainId: dispatchable.envelope.chainId,
|
|
88
|
-
durationMs,
|
|
89
|
-
eventType: dispatchable.envelope.type,
|
|
90
|
-
ok: result.ok,
|
|
91
|
-
type: 'webhook:enqueue',
|
|
92
|
-
})
|
|
93
|
-
return result
|
|
94
|
-
}
|
|
95
|
-
: undefined
|
|
96
|
-
|
|
97
|
-
// Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
|
|
98
|
-
// connection each run.
|
|
99
|
-
const db = Db.get(options.db)
|
|
100
|
-
const now = input.now ?? options.now
|
|
101
|
-
if (prunedAt === undefined || Date.now() - prunedAt >= pruneIntervalMs) {
|
|
102
|
-
prunedAt = Date.now()
|
|
103
|
-
await core_Webhooks.pruneExpired(db, { now }).catch(() => {})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const start = Date.now()
|
|
107
|
-
let cursorLagBlocks: number | undefined
|
|
108
|
-
try {
|
|
109
|
-
const result = await AppWebhooks.poll({
|
|
110
|
-
batchSize: input.batchSize ?? options.batchSize,
|
|
111
|
-
cache,
|
|
112
|
-
chainId,
|
|
113
|
-
client: getClient(chainId),
|
|
114
|
-
confirmations: input.confirmations ?? options.confirmations,
|
|
115
|
-
db,
|
|
116
|
-
dispatch,
|
|
117
|
-
dispatchBatch,
|
|
118
|
-
eventTypes: input.eventTypes ?? options.eventTypes,
|
|
119
|
-
...(input.headBlockNumber === undefined
|
|
120
|
-
? {}
|
|
121
|
-
: { headBlockNumber: input.headBlockNumber }),
|
|
122
|
-
maxBlocks: input.maxBlocks ?? options.maxBlocks,
|
|
123
|
-
now,
|
|
124
|
-
signal: input.signal,
|
|
125
|
-
...(metrics
|
|
126
|
-
? {
|
|
127
|
-
onCursorLag(blocks: number) {
|
|
128
|
-
cursorLagBlocks = blocks
|
|
129
|
-
},
|
|
130
|
-
onScanCheckpoint(checkpoint: AppWebhooks.poll.ScanCheckpoint) {
|
|
131
|
-
metrics.record({ chainId, type: 'webhook:poll-scan', ...checkpoint })
|
|
132
|
-
// Ship the start boundary before the RPC allocation can terminate the isolate.
|
|
133
|
-
metrics.flush()
|
|
134
|
-
},
|
|
135
|
-
onStage(stage: AppWebhooks.poll.Stage) {
|
|
136
|
-
metrics.record({ chainId, type: 'webhook:poll', ...stage })
|
|
137
|
-
},
|
|
138
|
-
onSubscriptions(count: number) {
|
|
139
|
-
metrics.record({ chainId, count, type: 'webhook:poll-subscriptions' })
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
: {}),
|
|
143
|
-
})
|
|
144
|
-
metrics?.record({
|
|
145
|
-
cursorLagBlocks,
|
|
146
|
-
meta: { durationMs: Date.now() - start, outcome: 'ok' },
|
|
147
|
-
result,
|
|
148
|
-
type: 'webhook:tick',
|
|
149
|
-
})
|
|
150
|
-
return result
|
|
151
|
-
} catch (error) {
|
|
152
|
-
metrics?.record({
|
|
153
|
-
cursorLagBlocks,
|
|
154
|
-
meta: { chainId, durationMs: Date.now() - start, outcome: 'error' },
|
|
155
|
-
result: undefined,
|
|
156
|
-
type: 'webhook:tick',
|
|
157
|
-
})
|
|
158
|
-
throw error
|
|
159
|
-
} finally {
|
|
160
|
-
metrics?.flush()
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export declare namespace createPoller {
|
|
167
|
-
/** A row to dispatch, paired with its owning subscription. */
|
|
168
|
-
type Dispatchable = AppWebhooks.poll.Dispatchable
|
|
169
|
-
|
|
170
|
-
/** Delivery transport: hands a matched event to a queue or delivers inline. */
|
|
171
|
-
type Dispatch = NonNullable<AppWebhooks.poll.Options['dispatch']>
|
|
172
|
-
|
|
173
|
-
/** Batch delivery transport: confirms all rows together or leaves their cursor unchanged. */
|
|
174
|
-
type DispatchBatch = NonNullable<AppWebhooks.poll.Options['dispatchBatch']>
|
|
175
|
-
|
|
176
|
-
/** Summary counters returned by {@link Poller.tick}. */
|
|
177
|
-
type Result = AppWebhooks.poll.Result
|
|
178
|
-
|
|
179
|
-
/** Knobs shared by every {@link Poller.tick}; each is overridable per tick. */
|
|
180
|
-
type Defaults = {
|
|
181
|
-
/** Max matching rows scanned per subscription per tick (default 200). */
|
|
182
|
-
batchSize?: number | undefined
|
|
183
|
-
/** Reorg-safety window: only emit blocks this far behind head (default 1). */
|
|
184
|
-
confirmations?: number | undefined
|
|
185
|
-
/**
|
|
186
|
-
* Delivery transport. Defaults to inline `deliverAndRecord` (self-host); on
|
|
187
|
-
* Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
|
|
188
|
-
*/
|
|
189
|
-
dispatch?: Dispatch | undefined
|
|
190
|
-
/** Batch delivery transport used when the runtime can enqueue multiple rows atomically. */
|
|
191
|
-
dispatchBatch?: DispatchBatch | undefined
|
|
192
|
-
/** Event types to poll (default all). */
|
|
193
|
-
eventTypes?: AppWebhooks.poll.Options['eventTypes']
|
|
194
|
-
/** Backfill cap: most blocks an RPC scanner inspects per tick (default 1000). */
|
|
195
|
-
maxBlocks?: number | undefined
|
|
196
|
-
/** Clock for envelope timestamps; injectable for tests. */
|
|
197
|
-
now?: (() => Date) | undefined
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Options for {@link createPoller}. */
|
|
201
|
-
type Options = Defaults & {
|
|
202
|
-
/** Cache for token metadata shared across ticks. */
|
|
203
|
-
cache?: Store.Store | undefined
|
|
204
|
-
/** Database holding subscriptions + cursors, or a factory resolved per tick (Workers/Hyperdrive). */
|
|
205
|
-
db: Db.Source
|
|
206
|
-
/** Tempo chain id polled when a tick omits one. */
|
|
207
|
-
defaultChainId?: Viem.ChainId | undefined
|
|
208
|
-
/** Metrics backend; emits `webhook:tick` per tick and `webhook:enqueue` per dispatch. */
|
|
209
|
-
metrics?: Metrics.Metrics | undefined
|
|
210
|
-
/** RPC options for the viem clients that scan chain events. */
|
|
211
|
-
rpc?: Viem.getClient.Rpc | undefined
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Per-tick overrides; falls back to the {@link Options} defaults. */
|
|
215
|
-
type TickOptions = Defaults & {
|
|
216
|
-
/** Chain to poll this tick (default `options.defaultChainId`). */
|
|
217
|
-
chainId?: Viem.ChainId | undefined
|
|
218
|
-
/** Chain head to work back from; resolved from RPC when omitted. */
|
|
219
|
-
headBlockNumber?: bigint | number | undefined
|
|
220
|
-
/** Stops scanning and dispatch when the caller loses its execution lease. */
|
|
221
|
-
signal?: AbortSignal | undefined
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** A chain-head-driven webhook poller. */
|
|
225
|
-
type Poller = {
|
|
226
|
-
/** Runs one poll tick, resolving the RPC head when not supplied. */
|
|
227
|
-
tick(options?: TickOptions): Promise<Result>
|
|
228
|
-
}
|
|
229
|
-
}
|
package/src/apps/data/App.ts
CHANGED
|
@@ -224,7 +224,13 @@ export function data(options: data.Options = {}) {
|
|
|
224
224
|
.route('/', transactionActivities({ fx: options.fx }))
|
|
225
225
|
.route('/', transfers({ fx: options.fx }))
|
|
226
226
|
.route('/', verifiedTokens())
|
|
227
|
-
.route(
|
|
227
|
+
.route(
|
|
228
|
+
'/',
|
|
229
|
+
webhooks({
|
|
230
|
+
applicationEventTypes: webhook?.applicationEventTypes ?? [],
|
|
231
|
+
enabled: options.webhook !== false,
|
|
232
|
+
}),
|
|
233
|
+
)
|
|
228
234
|
.route('/', zones())
|
|
229
235
|
.route('/', rpc())
|
|
230
236
|
.route('/', coingecko())
|
|
@@ -445,7 +451,7 @@ function webhooksBlock() {
|
|
|
445
451
|
'tempo-event-id',
|
|
446
452
|
'A unique, stable id for this event (`evt_…`). The same event may be delivered more than once, so use this id to skip duplicates.',
|
|
447
453
|
),
|
|
448
|
-
header('tempo-event-type', 'What happened: `token:transfer`, `transaction:included`, `log:emitted`, `block:created`, or `ping`.'), // prettier-ignore
|
|
454
|
+
header('tempo-event-type', 'What happened: `token:transfer`, `transaction:included`, `log:emitted`, `block:created`, `funding:deposit.updated`, or `ping`.'), // prettier-ignore
|
|
449
455
|
],
|
|
450
456
|
requestBody: {
|
|
451
457
|
content: { 'application/json': { schema: envelope } },
|
|
@@ -35,8 +35,6 @@ const liveAccounts = {
|
|
|
35
35
|
channel: '0x5217c502adf3c96a9551770af4caa5f5144a6984',
|
|
36
36
|
// TokenCreated + Mint + Transfer.
|
|
37
37
|
creator: '0x253fb3bf1e3d75ad6ee1554f68f4ea0568f7d24e',
|
|
38
|
-
// Token burns.
|
|
39
|
-
burner: '0x69519497b59bd64e109d993157d45f14455cc7e6',
|
|
40
38
|
// KeyAuthorized + KeyRevoked + Approval (+ transfer/mint/exchange).
|
|
41
39
|
keychain: '0xfe776fd28e06ecc71a157398b2c9991318f1ea2e',
|
|
42
40
|
// Swaps through the stablecoin DEX (output kept, so it stays a plain `swap`).
|
|
@@ -50,27 +48,29 @@ const accounts =
|
|
|
50
48
|
? {
|
|
51
49
|
channel: localAccounts.keychain,
|
|
52
50
|
creator: localAccounts.creator,
|
|
53
|
-
burner: localAccounts.burner,
|
|
54
51
|
keychain: localAccounts.keychain,
|
|
55
52
|
swapper: localAccounts.swapper,
|
|
56
53
|
}
|
|
57
54
|
: liveAccounts
|
|
58
55
|
|
|
59
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* The account expected to surface each activity type. Burns are seeded per run
|
|
58
|
+
* instead: a live burner keeps accruing activity until its last burn falls off
|
|
59
|
+
* the first page.
|
|
60
|
+
*/
|
|
60
61
|
const coverage =
|
|
61
|
-
runtime.mode === 'localnet'
|
|
62
|
+
runtime.mode === 'localnet' && localAccounts
|
|
62
63
|
? {
|
|
63
|
-
approval:
|
|
64
|
-
burn:
|
|
65
|
-
mint:
|
|
66
|
-
'token-created':
|
|
67
|
-
transfer:
|
|
64
|
+
approval: localAccounts.keychain,
|
|
65
|
+
burn: localAccounts.burner,
|
|
66
|
+
mint: localAccounts.creator,
|
|
67
|
+
'token-created': localAccounts.creator,
|
|
68
|
+
transfer: localAccounts.creator,
|
|
68
69
|
}
|
|
69
70
|
: {
|
|
70
71
|
'access-key-created': accounts.keychain,
|
|
71
72
|
'access-key-revoked': accounts.keychain,
|
|
72
73
|
approval: accounts.keychain,
|
|
73
|
-
burn: accounts.burner,
|
|
74
74
|
mint: accounts.creator,
|
|
75
75
|
'session-closed': accounts.channel,
|
|
76
76
|
swap: accounts.swapper,
|
|
@@ -165,6 +165,54 @@ describe('GET /addresses/:address/activities', () => {
|
|
|
165
165
|
expect(find(body.data, type)).toBeDefined()
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
+
// Localnet seeds its burner through the shared fixtures, so this covers the
|
|
169
|
+
// hosted testnet, where no address stays burn-topped for long.
|
|
170
|
+
test.skipIf(runtime.mode === 'localnet')(
|
|
171
|
+
'classifies a seeded burn',
|
|
172
|
+
async () => {
|
|
173
|
+
const account = Tempo.accounts[11]
|
|
174
|
+
const wallet = Tempo.getClient({ account })
|
|
175
|
+
await Actions.faucet.fundSync(Tempo.client, { account, timeout: 60_000 })
|
|
176
|
+
|
|
177
|
+
// Burning needs the `issuer` role, so the account burns a token it owns.
|
|
178
|
+
const { token } = await Actions.token.createSync(wallet, {
|
|
179
|
+
currency: 'USD',
|
|
180
|
+
name: 'Seeded Burn USD',
|
|
181
|
+
symbol: 'SBUSD',
|
|
182
|
+
})
|
|
183
|
+
await sendTransactionSync(wallet, {
|
|
184
|
+
account,
|
|
185
|
+
calls: [
|
|
186
|
+
Actions.token.grantRoles.call(wallet, { role: 'issuer', to: account.address, token }),
|
|
187
|
+
Actions.token.mint.call(wallet, { amount: 1_000_000n, to: account.address, token }),
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
const { receipt } = await Actions.token.burnSync(wallet, { amount: 100_000n, token })
|
|
191
|
+
|
|
192
|
+
const deadline = Date.now() + 90_000
|
|
193
|
+
let burn: Activities.Entry | undefined
|
|
194
|
+
while (Date.now() < deadline) {
|
|
195
|
+
const response = await client.v1.addresses[':address'].activities.$get(
|
|
196
|
+
{ param: { address: account.address }, query: { limit: '50' } },
|
|
197
|
+
{ headers: { 'cache-control': 'no-store', ...TestApp.auth.headers } },
|
|
198
|
+
)
|
|
199
|
+
if (response.status === 200) {
|
|
200
|
+
const body = await TestApp.json(response, Activities.schema.getAddressActivities.Response)
|
|
201
|
+
burn = find(body.data, 'burn')
|
|
202
|
+
if (burn?.type === 'burn' && burn.transactionHash === receipt.transactionHash) break
|
|
203
|
+
}
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
205
|
+
}
|
|
206
|
+
expect(burn?.type).toBe('burn')
|
|
207
|
+
if (burn?.type !== 'burn') return
|
|
208
|
+
expect(burn.transactionHash).toBe(receipt.transactionHash)
|
|
209
|
+
expect(burn.data.sender.toLowerCase()).toBe(account.address.toLowerCase())
|
|
210
|
+
expect(burn.data.sourceToken.address.toLowerCase()).toBe(token.toLowerCase())
|
|
211
|
+
expect(burn.data.sourceAmount.baseUnits).toBe('100000')
|
|
212
|
+
},
|
|
213
|
+
180_000,
|
|
214
|
+
)
|
|
215
|
+
|
|
168
216
|
test('transfer data mirrors the `Transfer` resource', async () => {
|
|
169
217
|
const transfer = find((await activities(accounts.creator)).data, 'transfer')
|
|
170
218
|
expect(transfer?.type).toBe('transfer')
|
|
@@ -666,6 +714,54 @@ describe('Activities.list classification', () => {
|
|
|
666
714
|
expect(log.data).toBe('0x1234')
|
|
667
715
|
expect(log.topics).toEqual([unknownSelector, pad(viewer)])
|
|
668
716
|
})
|
|
717
|
+
|
|
718
|
+
test('degrades a topic-encoded approval with a fee transfer to `unknown`', async () => {
|
|
719
|
+
const approvalToken = '0x4120c7ec7e9ead9dcf44b35b5134ebdbfc6e3de9' as Address.Address
|
|
720
|
+
const feeToken = '0x20c0000000000000000000000000000000000000' as Address.Address
|
|
721
|
+
const feeManager = '0xfeec000000000000000000000000000000000000'
|
|
722
|
+
const approvalSelector = '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
|
|
723
|
+
const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
|
|
724
|
+
const pad = (value: string) => `0x${value.slice(2).padStart(64, '0')}`
|
|
725
|
+
const txHash = '0xd2439ad8016d4ffd6aebe835734ee4d118a688714a4f9aebcc11f87a2b22e896' as Hex.Hex
|
|
726
|
+
const common = {
|
|
727
|
+
blockNum: 15_781_805,
|
|
728
|
+
blockTimestamp: 1_776_703_284,
|
|
729
|
+
feePayer: viewer,
|
|
730
|
+
source: 'q1' as const,
|
|
731
|
+
txHash,
|
|
732
|
+
txIdx: 0,
|
|
733
|
+
txSender: viewer,
|
|
734
|
+
}
|
|
735
|
+
const { items } = await Activities.list(
|
|
736
|
+
provider([
|
|
737
|
+
{
|
|
738
|
+
...common,
|
|
739
|
+
data: '0x',
|
|
740
|
+
logAddress: approvalToken,
|
|
741
|
+
logIdx: 0,
|
|
742
|
+
selector: approvalSelector,
|
|
743
|
+
topic1: pad(viewer),
|
|
744
|
+
topic2: pad('0x6063259f720ee125e5bff693779ab8118b0118a7'),
|
|
745
|
+
topic3: '0x2a9063ed52b7d417e441f378325359c9ce274f5d6c3ecf11b7d42f24b9c90b7f',
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
...common,
|
|
749
|
+
data: `0x${(5_815).toString(16).padStart(64, '0')}`,
|
|
750
|
+
logAddress: feeToken,
|
|
751
|
+
logIdx: 1,
|
|
752
|
+
selector: transferSelector,
|
|
753
|
+
topic1: pad(viewer),
|
|
754
|
+
topic2: pad(feeManager),
|
|
755
|
+
topic3: null,
|
|
756
|
+
},
|
|
757
|
+
]),
|
|
758
|
+
{ address: viewer, store: store() },
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
expect(items).toHaveLength(1)
|
|
762
|
+
expect(items[0]?.type).toBe('unknown')
|
|
763
|
+
expect(items[0]?.events).toHaveLength(2)
|
|
764
|
+
})
|
|
669
765
|
})
|
|
670
766
|
|
|
671
767
|
describe('Activities.list precompile classification', () => {
|
|
@@ -2727,15 +2727,46 @@ function classifyFromLogs(
|
|
|
2727
2727
|
viewer: Address.Address
|
|
2728
2728
|
},
|
|
2729
2729
|
): RawItem[] {
|
|
2730
|
-
const
|
|
2730
|
+
const decoded = decodeRows(rows)
|
|
2731
|
+
const items = (() => {
|
|
2732
|
+
try {
|
|
2733
|
+
return classifyFromLogsCore(rows, ctx, decoded)
|
|
2734
|
+
} catch {
|
|
2735
|
+
// Indexed logs can match a known signature without satisfying that event's expected shape.
|
|
2736
|
+
return [
|
|
2737
|
+
unknown(ctx.itemBase(Math.min(...rows.map((row) => row.logIdx))), {
|
|
2738
|
+
events: buildTxEvents(rows, decoded),
|
|
2739
|
+
signer: signerForRows(rows, { fallback: ctx.signer, viewer: ctx.viewer }),
|
|
2740
|
+
}),
|
|
2741
|
+
]
|
|
2742
|
+
}
|
|
2743
|
+
})()
|
|
2731
2744
|
// When the caller requested raw logs, attach the full per-transaction log set
|
|
2732
2745
|
// (decoded when possible) to every classified item so no co-located log is
|
|
2733
2746
|
// hidden behind the headline. `unknown` items already carry it.
|
|
2734
2747
|
if (!ctx.includeLogs) return items
|
|
2735
|
-
const events = buildTxEvents(rows,
|
|
2748
|
+
const events = buildTxEvents(rows, decoded)
|
|
2736
2749
|
return items.map((item) => (item.type === 'unknown' ? item : { ...item, events }))
|
|
2737
2750
|
}
|
|
2738
2751
|
|
|
2752
|
+
/** Resolves an access-key signer from raw rows, falling back to the transaction signer. */
|
|
2753
|
+
function signerForRows(
|
|
2754
|
+
rows: readonly Row[],
|
|
2755
|
+
options: { fallback: 'self' | Address.Address; viewer: Address.Address },
|
|
2756
|
+
) {
|
|
2757
|
+
const accessKeySpendRow = rows.find(
|
|
2758
|
+
(row) =>
|
|
2759
|
+
row.selector === accessKeySpendSelector &&
|
|
2760
|
+
Address.isEqual(row.logAddress, Addresses.accountKeychain),
|
|
2761
|
+
)
|
|
2762
|
+
const accessKeySigner = accessKeySpendRow?.topic2
|
|
2763
|
+
? (`0x${accessKeySpendRow.topic2.slice(-40)}`.toLowerCase() as Address.Address)
|
|
2764
|
+
: null
|
|
2765
|
+
return accessKeySigner && !Address.isEqual(accessKeySigner, options.viewer)
|
|
2766
|
+
? accessKeySigner
|
|
2767
|
+
: options.fallback
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2739
2770
|
/** Classifies a Q1 (log-bearing) group via `parseEventLogs`, picking one headline item. */
|
|
2740
2771
|
function classifyFromLogsCore(
|
|
2741
2772
|
rows: Row[],
|
|
@@ -2746,24 +2777,11 @@ function classifyFromLogsCore(
|
|
|
2746
2777
|
signer: 'self' | Address.Address
|
|
2747
2778
|
viewer: Address.Address
|
|
2748
2779
|
},
|
|
2780
|
+
events = decodeRows(rows),
|
|
2749
2781
|
): RawItem[] {
|
|
2750
2782
|
const { blockNum, fingerprintMap, itemBase, viewer } = ctx
|
|
2751
2783
|
|
|
2752
|
-
|
|
2753
|
-
// is the access-key publicKey. Use it as the signer so successive access-key
|
|
2754
|
-
// actions group together.
|
|
2755
|
-
const accessKeySpendRow = rows.find(
|
|
2756
|
-
(r) =>
|
|
2757
|
-
r.selector === accessKeySpendSelector &&
|
|
2758
|
-
Address.isEqual(r.logAddress, Addresses.accountKeychain),
|
|
2759
|
-
)
|
|
2760
|
-
const accessKeySigner = accessKeySpendRow?.topic2
|
|
2761
|
-
? (`0x${accessKeySpendRow.topic2.slice(-40)}`.toLowerCase() as Address.Address)
|
|
2762
|
-
: null
|
|
2763
|
-
const signer: 'self' | Address.Address =
|
|
2764
|
-
accessKeySigner && !Address.isEqual(accessKeySigner, viewer) ? accessKeySigner : ctx.signer
|
|
2765
|
-
|
|
2766
|
-
const events = decodeRows(rows)
|
|
2784
|
+
const signer = signerForRows(rows, { fallback: ctx.signer, viewer })
|
|
2767
2785
|
|
|
2768
2786
|
const minLogIndex = Math.min(...rows.map((r) => r.logIdx))
|
|
2769
2787
|
|
|
@@ -3251,6 +3269,14 @@ function classifyFromLogsCore(
|
|
|
3251
3269
|
const approvalEvent = finalEvents.find(
|
|
3252
3270
|
(e) =>
|
|
3253
3271
|
e.eventName === 'Approval' &&
|
|
3272
|
+
// ERC-721 uses the same selector but indexes its token ID as topic3. In
|
|
3273
|
+
// non-strict mode viem otherwise decodes the missing ERC-20 amount as zero.
|
|
3274
|
+
rows.some(
|
|
3275
|
+
(row) =>
|
|
3276
|
+
row.logIdx === e.logIndex &&
|
|
3277
|
+
row.topic3 === null &&
|
|
3278
|
+
/^0x[0-9a-fA-F]{64}$/.test(row.data ?? ''),
|
|
3279
|
+
) &&
|
|
3254
3280
|
!Address.isEqual((e.args as { spender: Address.Address }).spender, Addresses.zoneOutbox),
|
|
3255
3281
|
)
|
|
3256
3282
|
if (approvalEvent) {
|
|
@@ -4338,21 +4364,27 @@ function unknown(
|
|
|
4338
4364
|
|
|
4339
4365
|
/** Decodes a group's raw rows into `viem/tempo` events via `parseEventLogs` (non-strict). */
|
|
4340
4366
|
function decodeRows(rows: readonly Row[]) {
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4367
|
+
return rows.flatMap((r) => {
|
|
4368
|
+
const log: Log = {
|
|
4369
|
+
address: r.logAddress as `0x${string}`,
|
|
4370
|
+
blockHash: '0x0' as Hex.Hex,
|
|
4371
|
+
blockNumber: BigInt(r.blockNum),
|
|
4372
|
+
data: (r.data ?? '0x') as Hex.Hex,
|
|
4373
|
+
logIndex: r.logIdx,
|
|
4374
|
+
removed: false,
|
|
4375
|
+
topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean) as [Hex.Hex, ...Hex.Hex[]],
|
|
4376
|
+
transactionHash: r.txHash as `0x${string}`,
|
|
4377
|
+
transactionIndex: r.txIdx,
|
|
4378
|
+
}
|
|
4379
|
+
try {
|
|
4380
|
+
return parseEventLogs({
|
|
4381
|
+
abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault],
|
|
4382
|
+
logs: [log],
|
|
4383
|
+
strict: false,
|
|
4384
|
+
})
|
|
4385
|
+
} catch {
|
|
4386
|
+
return []
|
|
4387
|
+
}
|
|
4356
4388
|
})
|
|
4357
4389
|
}
|
|
4358
4390
|
|