tapimo 0.5.0 → 0.5.2
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 +4854 -4829
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +20 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.js +3 -3
- package/dist/Assets.js.map +1 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +1 -2
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -10
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +6 -34
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +3 -3
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +4 -1
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +6 -34
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +2 -2
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +2 -2
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +33 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +1398 -1405
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +25 -38
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +108 -3
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +409 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +245 -245
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/funding.d.ts +236 -146
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +23 -12
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +9 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +66 -73
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +21 -27
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/data/routes/webhooks.d.ts +35 -35
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +176 -42
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/management/App.d.ts +520 -504
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +5 -4
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +120 -120
- package/dist/apps/management/routes/billing.d.ts +211 -211
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +72 -72
- package/dist/apps/management/routes/invite-links.d.ts +159 -159
- package/dist/apps/management/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/members.d.ts +80 -80
- package/dist/apps/management/routes/orgs.d.ts +59 -43
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +18 -1
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +109 -109
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/usage.d.ts +79 -79
- package/dist/apps/mcp.js +2 -2
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +2 -2
- package/dist/apps/relay/App.js +2 -2
- package/dist/apps/relay/Sponsorships.d.ts +3 -0
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +15 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +4 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.js +1 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +26 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +2 -11
- package/dist/db/tables/requestUsage.d.ts.map +1 -1
- package/dist/db/tables/requestUsage.js +1 -20
- package/dist/db/tables/requestUsage.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +52 -10
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/internal/Auth.d.ts +4 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +15 -8
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +9 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +12 -3
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +176 -56
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +100 -5
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -12
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +2 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +4 -4
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Tidx.d.ts +9 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -7
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +14 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +19 -1
- package/dist/internal/Viem.js.map +1 -1
- package/package.json +7 -2
- package/src/ApiKeys.test.ts +2 -0
- package/src/App.test.ts +15 -4
- package/src/App.ts +31 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +30 -3
- package/src/Scope.ts +4 -2
- package/src/Webhooks.ts +5 -10
- package/src/admin/apps/api-keys.test.ts +27 -0
- package/src/admin/apps/api-keys.ts +8 -1
- package/src/admin/apps/scopes.test.ts +7 -2
- package/src/admin/apps/scopes.ts +2 -2
- package/src/admin/ui/src/components/Dialog.tsx +8 -2
- package/src/admin/ui/src/lib/scopes.test.ts +19 -0
- package/src/admin/ui/src/lib/scopes.ts +12 -0
- package/src/admin/ui/src/routes/api-keys.tsx +128 -27
- package/src/admin/ui.generated.ts +33 -33
- package/src/apps/composition.test.ts +77 -0
- package/src/apps/data/App.ts +25 -45
- package/src/apps/data/FundingQuoteProviders.test.ts +615 -5
- package/src/apps/data/FundingQuoteProviders.ts +570 -4
- package/src/apps/data/routes/funding.test.ts +181 -7
- package/src/apps/data/routes/funding.ts +27 -12
- package/src/apps/data/routes/indexer.test.ts +82 -20
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/data/routes/tokens.test.ts +54 -0
- package/src/apps/data/routes/transfers.test.ts +73 -1
- package/src/apps/data/routes/transfers.ts +24 -30
- package/src/apps/data/routes/webhooks.test.ts +203 -0
- package/src/apps/data/routes/webhooks.ts +191 -54
- package/src/apps/management/Billing.test.ts +52 -3
- package/src/apps/management/Billing.ts +5 -4
- package/src/apps/management/routes/billing.ts +6 -1
- package/src/apps/management/routes/orgs.ts +19 -1
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +6 -5
- package/src/apps/relay/App.ts +2 -2
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/apps/relay/Sponsorships.ts +24 -1
- package/src/db/Db.test.ts +5 -5
- package/src/db/Db.ts +4 -0
- package/src/db/Schema.ts +1 -1
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/organizations.test.ts +32 -2
- package/src/db/tables/organizations.ts +28 -0
- package/src/db/tables/requestUsage.ts +3 -23
- package/src/db/tables/sponsoredTransactions.test.ts +60 -2
- package/src/db/tables/sponsoredTransactions.ts +67 -21
- package/src/db/tables/stripeCustomers.test.ts +2 -2
- package/src/internal/Auth.test.ts +63 -10
- package/src/internal/Auth.ts +21 -9
- package/src/internal/Cache.test.ts +19 -0
- package/src/internal/Cache.ts +13 -1
- package/src/internal/EdgeCache.test.ts +40 -0
- package/src/internal/EdgeCache.ts +11 -3
- package/src/internal/FundingQuotes.test.ts +130 -3
- package/src/internal/FundingQuotes.ts +113 -6
- package/src/internal/Log.ts +21 -14
- package/src/internal/RateLimit.test.ts +12 -0
- package/src/internal/RateLimit.ts +4 -4
- package/src/internal/Tidx.test.ts +48 -0
- package/src/internal/Tidx.ts +21 -7
- package/src/internal/Viem.test.ts +46 -0
- package/src/internal/Viem.ts +34 -1
|
@@ -271,7 +271,24 @@ describe('GET /transfers', () => {
|
|
|
271
271
|
},
|
|
272
272
|
)
|
|
273
273
|
|
|
274
|
-
test('embeds a capped total count on demand
|
|
274
|
+
test('embeds a capped unscoped total count on demand', async () => {
|
|
275
|
+
const client = TestApp.client()
|
|
276
|
+
const response = await client.v1.transfers.$get(
|
|
277
|
+
{ query: { include: 'totalCount', limit: '5' } },
|
|
278
|
+
TestApp.auth,
|
|
279
|
+
)
|
|
280
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
281
|
+
|
|
282
|
+
expect(response.status).toBe(200)
|
|
283
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
284
|
+
expect(body.meta).toBeDefined()
|
|
285
|
+
expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
|
|
286
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
287
|
+
expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
|
|
288
|
+
expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount >= Schema.countCap)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('embeds a capped token-scoped total count on demand', async () => {
|
|
275
292
|
const client = TestApp.client()
|
|
276
293
|
|
|
277
294
|
if (Runtime.get().mode === 'localnet') expect(TestApp.token).toBeDefined()
|
|
@@ -350,6 +367,61 @@ describe('GET /transfers', () => {
|
|
|
350
367
|
},
|
|
351
368
|
)
|
|
352
369
|
|
|
370
|
+
test.skipIf(Runtime.get().mode !== 'localnet')(
|
|
371
|
+
'counts unscoped cross-token transfers',
|
|
372
|
+
async () => {
|
|
373
|
+
const crossToken = Runtime.get().fixtures?.crossTokenTransfer
|
|
374
|
+
expect(crossToken).toBeDefined()
|
|
375
|
+
if (!crossToken) return
|
|
376
|
+
|
|
377
|
+
const client = TestApp.client()
|
|
378
|
+
const response = await client.v1.transfers.$get(
|
|
379
|
+
{
|
|
380
|
+
query: {
|
|
381
|
+
address: crossToken.sender,
|
|
382
|
+
include: 'crossToken,totalCount',
|
|
383
|
+
limit: '50',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
TestApp.auth,
|
|
387
|
+
)
|
|
388
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
389
|
+
|
|
390
|
+
expect(response.status).toBe(200)
|
|
391
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
392
|
+
expect(body.data.some((entry) => entry.transactionHash === crossToken.hash)).toBe(true)
|
|
393
|
+
expect(body.meta).toBeDefined()
|
|
394
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
test.skipIf(Runtime.get().mode !== 'localnet')(
|
|
399
|
+
'counts token-scoped cross-token transfers',
|
|
400
|
+
async () => {
|
|
401
|
+
const crossToken = Runtime.get().fixtures?.crossTokenTransfer
|
|
402
|
+
expect(crossToken).toBeDefined()
|
|
403
|
+
if (!crossToken) return
|
|
404
|
+
|
|
405
|
+
const client = TestApp.client()
|
|
406
|
+
const response = await client.v1.transfers.$get(
|
|
407
|
+
{
|
|
408
|
+
query: {
|
|
409
|
+
include: 'crossToken,totalCount',
|
|
410
|
+
limit: '50',
|
|
411
|
+
token: crossToken.destinationToken,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
TestApp.auth,
|
|
415
|
+
)
|
|
416
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
417
|
+
|
|
418
|
+
expect(response.status).toBe(200)
|
|
419
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
420
|
+
expect(body.meta).toBeDefined()
|
|
421
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
|
|
353
425
|
test('surfaces a 502 when transfer data is unavailable', async () => {
|
|
354
426
|
// Point at an unreachable indexer so the upstream query fails deterministically.
|
|
355
427
|
const client = TestApp.client({ tidx: { baseUrl: 'http://127.0.0.1:1' } })
|
|
@@ -565,15 +565,15 @@ export declare namespace query {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
/**
|
|
568
|
-
* Context-free core of {@link query}:
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
* memoization) and by the webhook poller (uncached, ascending, per-row cursors).
|
|
568
|
+
* Context-free core of {@link query}: reads token-scoped rows from
|
|
569
|
+
* `token_transfers`, otherwise the decoded `Transfer` CTE, then paginates and
|
|
570
|
+
* decodes them. Also used by the webhook poller.
|
|
572
571
|
*/
|
|
573
572
|
export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan.Page> {
|
|
574
573
|
const { chainId, limit, order } = options
|
|
575
574
|
const { tidx } = deps
|
|
576
575
|
const direction = order === 'asc' ? 'ASC' : 'DESC'
|
|
576
|
+
const tokenScoped = options.token !== undefined
|
|
577
577
|
// Keyset pagination: anchor the page below the previous row's `(block, log_idx)`
|
|
578
578
|
// position instead of a numeric offset, so rows arriving at the head can't
|
|
579
579
|
// shift items across pages. A malformed cursor falls back to the head page.
|
|
@@ -598,19 +598,17 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
|
|
|
598
598
|
)
|
|
599
599
|
const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
|
600
600
|
|
|
601
|
-
// Fetch one extra row to detect `hasMore` without a separate count query.
|
|
602
|
-
// global minimum page size (`Schema.minLimit`) keeps this at or above the
|
|
603
|
-
// indexer's floor for signature-decoded `Transfer` queries.
|
|
601
|
+
// Fetch one extra row to detect `hasMore` without a separate count query.
|
|
604
602
|
const result = await tidx.fetch({
|
|
605
603
|
chainId,
|
|
604
|
+
...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
|
|
606
605
|
query: `
|
|
607
|
-
SELECT "from", "to", address, value, tx_hash, block_num, log_idx, block_timestamp
|
|
608
|
-
FROM Transfer
|
|
606
|
+
SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
|
|
607
|
+
FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
|
|
609
608
|
${where}
|
|
610
609
|
ORDER BY block_num ${direction}, log_idx ${direction}
|
|
611
610
|
LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
|
|
612
|
-
|
|
613
|
-
signatures: [signature],
|
|
611
|
+
` as string,
|
|
614
612
|
})
|
|
615
613
|
|
|
616
614
|
// The next page anchors below the last fetched row's `(block, log_idx)`.
|
|
@@ -687,17 +685,14 @@ export declare namespace scan {
|
|
|
687
685
|
}
|
|
688
686
|
|
|
689
687
|
/**
|
|
690
|
-
* Builds
|
|
691
|
-
*
|
|
692
|
-
* by the page query ({@link scan}) and the capped count ({@link countScan}) so a
|
|
693
|
-
* `totalCount` always matches the page it annotates; the keyset cursor clause is
|
|
694
|
-
* page-only and added by {@link scan} on top of these.
|
|
688
|
+
* Builds shared transfer filters. A token predicate targets `token_transfers`'s
|
|
689
|
+
* physical `token` column; unscoped queries omit it and use the `Transfer` CTE.
|
|
695
690
|
*/
|
|
696
691
|
function filterClauses(options: filterClauses.Options): string[] {
|
|
697
692
|
const { eitherSide, fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } =
|
|
698
693
|
options
|
|
699
694
|
const filters: string[] = []
|
|
700
|
-
if (token !== undefined) filters.push(`
|
|
695
|
+
if (token !== undefined) filters.push(`token = '${token}'`)
|
|
701
696
|
if (eitherSide !== undefined) filters.push(`("from" = '${eitherSide}' OR "to" = '${eitherSide}')`)
|
|
702
697
|
if (sender !== undefined) filters.push(`"from" = '${sender}'`)
|
|
703
698
|
if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
|
|
@@ -766,12 +761,9 @@ export declare namespace count {
|
|
|
766
761
|
}
|
|
767
762
|
|
|
768
763
|
/**
|
|
769
|
-
* Context-free core of {@link count}:
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* the indexer's execution budget on the 10⁹-plus row table (an unbounded count
|
|
773
|
-
* there blows the 5s `max_execution_time`); a result equal to the cap is a
|
|
774
|
-
* lower bound, surfaced as `totalCountCapped: true`.
|
|
764
|
+
* Context-free core of {@link count}: counts token-scoped rows from
|
|
765
|
+
* `token_transfers`, otherwise the decoded `Transfer` CTE. The capped inner
|
|
766
|
+
* scan bounds work; the SQL aggregate runs on ClickHouse.
|
|
775
767
|
*/
|
|
776
768
|
export async function countScan(
|
|
777
769
|
deps: scan.Deps,
|
|
@@ -780,6 +772,7 @@ export async function countScan(
|
|
|
780
772
|
const { chainId } = options
|
|
781
773
|
const { tidx } = deps
|
|
782
774
|
const filters = filterClauses(options)
|
|
775
|
+
const tokenScoped = options.token !== undefined
|
|
783
776
|
const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
|
784
777
|
|
|
785
778
|
// Folded count: cross-token transfers collapse several `Transfer` legs into
|
|
@@ -789,14 +782,14 @@ export async function countScan(
|
|
|
789
782
|
if (options.crossToken) {
|
|
790
783
|
const result = await tidx.fetch({
|
|
791
784
|
chainId,
|
|
785
|
+
...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
|
|
792
786
|
query: `
|
|
793
|
-
SELECT tx_hash, log_idx, "from", "to", address, value
|
|
794
|
-
FROM Transfer
|
|
787
|
+
SELECT tx_hash, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
|
|
788
|
+
FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
|
|
795
789
|
${where}
|
|
796
790
|
ORDER BY block_num, log_idx
|
|
797
791
|
LIMIT ${Schema.countCap}
|
|
798
|
-
|
|
799
|
-
signatures: [signature],
|
|
792
|
+
` as string,
|
|
800
793
|
})
|
|
801
794
|
const rawCount = result.rows.length
|
|
802
795
|
let suppressed = 0
|
|
@@ -809,16 +802,17 @@ export async function countScan(
|
|
|
809
802
|
|
|
810
803
|
const result = await tidx.fetch({
|
|
811
804
|
chainId,
|
|
805
|
+
engine: 'clickhouse',
|
|
806
|
+
...(tokenScoped ? {} : { signatures: [signature] }),
|
|
812
807
|
query: `
|
|
813
808
|
SELECT count(*) AS total
|
|
814
809
|
FROM (
|
|
815
810
|
SELECT 1
|
|
816
|
-
FROM Transfer
|
|
811
|
+
FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
|
|
817
812
|
${where}
|
|
818
813
|
LIMIT ${Schema.countCap}
|
|
819
814
|
) AS capped
|
|
820
|
-
|
|
821
|
-
signatures: [signature],
|
|
815
|
+
` as string,
|
|
822
816
|
})
|
|
823
817
|
|
|
824
818
|
const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
|
|
@@ -50,6 +50,49 @@ const validBody = {
|
|
|
50
50
|
eventType: 'token:transfer',
|
|
51
51
|
} as const
|
|
52
52
|
|
|
53
|
+
describe('log filter selectivity', () => {
|
|
54
|
+
const address = '0x0000000000000000000000000000000000000001'
|
|
55
|
+
const signature = 'event Transfer(address indexed from, address indexed to, uint256 value)'
|
|
56
|
+
const topic0 = `0x${'11'.repeat(32)}`
|
|
57
|
+
const topic1 = `0x${'22'.repeat(32)}`
|
|
58
|
+
|
|
59
|
+
test('rejects a negated address as the only firehose guard', () => {
|
|
60
|
+
const parsed = Webhooks.schema.LogFilters.safeParse({
|
|
61
|
+
address: { not: address },
|
|
62
|
+
})
|
|
63
|
+
expect(parsed.success).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('accepts pushdown-capable address filters', () => {
|
|
67
|
+
expect(Webhooks.schema.LogFilters.safeParse({ address }).success).toBe(true)
|
|
68
|
+
expect(Webhooks.schema.LogFilters.safeParse({ address: { eq: address } }).success).toBe(true)
|
|
69
|
+
expect(Webhooks.schema.LogFilters.safeParse({ address: { in: [address] } }).success).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('requires pushdown-capable raw topics', () => {
|
|
73
|
+
expect(Webhooks.schema.LogFilters.safeParse({ topic0, topic1 }).success).toBe(true)
|
|
74
|
+
expect(Webhooks.schema.LogFilters.safeParse({ topic0, topic1: { not: topic1 } }).success).toBe(
|
|
75
|
+
false,
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('requires a pushdown-capable indexed argument', () => {
|
|
80
|
+
expect(
|
|
81
|
+
Webhooks.schema.LogFilters.safeParse({ args: { from: address }, signature }).success,
|
|
82
|
+
).toBe(true)
|
|
83
|
+
expect(Webhooks.schema.LogFilters.safeParse({ args: {}, signature }).success).toBe(false)
|
|
84
|
+
expect(
|
|
85
|
+
Webhooks.schema.LogFilters.safeParse({ args: { value: '0x1' }, signature }).success,
|
|
86
|
+
).toBe(false)
|
|
87
|
+
expect(
|
|
88
|
+
Webhooks.schema.LogFilters.safeParse({
|
|
89
|
+
args: { from: { eq: { and: [{ eq: address }] } } },
|
|
90
|
+
signature,
|
|
91
|
+
}).success,
|
|
92
|
+
).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
53
96
|
describe('feature gate', () => {
|
|
54
97
|
test('returns 404 when webhooks are disabled', async () => {
|
|
55
98
|
// Routes stay mounted for a stable schema, but the resource is disabled.
|
|
@@ -631,6 +674,26 @@ describe('slack destination', () => {
|
|
|
631
674
|
})
|
|
632
675
|
|
|
633
676
|
describe('betterstack destination', () => {
|
|
677
|
+
test('separates redacted read destinations from write destinations', () => {
|
|
678
|
+
const slack = { type: 'slack', url: 'https://hooks.slack.com/…' } as const
|
|
679
|
+
const betterstack = {
|
|
680
|
+
token: '[redacted]',
|
|
681
|
+
type: 'betterstack',
|
|
682
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
683
|
+
} as const
|
|
684
|
+
|
|
685
|
+
expect(Webhooks.schema.getWebhook.Response.shape.destination.safeParse(slack).success).toBe(
|
|
686
|
+
true,
|
|
687
|
+
)
|
|
688
|
+
expect(
|
|
689
|
+
Webhooks.schema.getWebhook.Response.shape.destination.safeParse(betterstack).success,
|
|
690
|
+
).toBe(true)
|
|
691
|
+
expect(Webhooks.schema.updateWebhook.Body.safeParse({ destination: slack }).success).toBe(false)
|
|
692
|
+
expect(Webhooks.schema.updateWebhook.Body.safeParse({ destination: betterstack }).success).toBe(
|
|
693
|
+
false,
|
|
694
|
+
)
|
|
695
|
+
})
|
|
696
|
+
|
|
634
697
|
test('creates a subscription with a Better Stack source', async () => {
|
|
635
698
|
const app = TestApp.client({
|
|
636
699
|
auth: { keys: [writer] },
|
|
@@ -659,6 +722,109 @@ describe('betterstack destination', () => {
|
|
|
659
722
|
`)
|
|
660
723
|
})
|
|
661
724
|
|
|
725
|
+
test('redacts destination bearer credentials from read responses', async () => {
|
|
726
|
+
const app = TestApp.client({ auth: { keys: [writer, reader] } })
|
|
727
|
+
const slack = await app.v1.webhooks.$post(
|
|
728
|
+
{
|
|
729
|
+
json: {
|
|
730
|
+
destination: {
|
|
731
|
+
type: 'slack',
|
|
732
|
+
url: 'https://hooks.slack.com/services/T000/B000/secret',
|
|
733
|
+
},
|
|
734
|
+
eventType: 'token:transfer',
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
as(writer),
|
|
738
|
+
)
|
|
739
|
+
const betterstack = await app.v1.webhooks.$post(
|
|
740
|
+
{
|
|
741
|
+
json: {
|
|
742
|
+
destination: {
|
|
743
|
+
token: 'src_secret',
|
|
744
|
+
type: 'betterstack',
|
|
745
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
746
|
+
},
|
|
747
|
+
eventType: 'token:transfer',
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
as(writer),
|
|
751
|
+
)
|
|
752
|
+
const slackCreated = await TestApp.json(slack, Webhooks.schema.createWebhook.Response)
|
|
753
|
+
const betterstackCreated = await TestApp.json(
|
|
754
|
+
betterstack,
|
|
755
|
+
Webhooks.schema.createWebhook.Response,
|
|
756
|
+
)
|
|
757
|
+
expect(slackCreated.destination).toStrictEqual({
|
|
758
|
+
type: 'slack',
|
|
759
|
+
url: 'https://hooks.slack.com/services/T000/B000/secret',
|
|
760
|
+
})
|
|
761
|
+
expect(betterstackCreated.destination).toStrictEqual({
|
|
762
|
+
token: 'src_secret',
|
|
763
|
+
type: 'betterstack',
|
|
764
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
const slackRead = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
768
|
+
{ param: { id: slackCreated.id } },
|
|
769
|
+
as(reader),
|
|
770
|
+
)
|
|
771
|
+
const betterstackRead = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
|
|
772
|
+
{ param: { id: betterstackCreated.id } },
|
|
773
|
+
as(reader),
|
|
774
|
+
)
|
|
775
|
+
const slackBody = await TestApp.json(slackRead, Webhooks.schema.getWebhook.Response)
|
|
776
|
+
const betterstackBody = await TestApp.json(betterstackRead, Webhooks.schema.getWebhook.Response)
|
|
777
|
+
|
|
778
|
+
expect(slackBody.destination).toStrictEqual({
|
|
779
|
+
type: 'slack',
|
|
780
|
+
url: 'https://hooks.slack.com/…',
|
|
781
|
+
})
|
|
782
|
+
expect(betterstackBody.destination).toStrictEqual({
|
|
783
|
+
token: '[redacted]',
|
|
784
|
+
type: 'betterstack',
|
|
785
|
+
url: 'https://s1234567.eu-nbg-2.betterstackdata.com',
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
const listed = await app.v1.webhooks.$get({ query: {} }, as(reader))
|
|
789
|
+
const listedBody = await TestApp.json(listed, Webhooks.schema.listWebhooks.Response)
|
|
790
|
+
expect(
|
|
791
|
+
listedBody.data.find((subscription) => subscription.id === slackCreated.id)?.destination,
|
|
792
|
+
).toStrictEqual(slackBody.destination)
|
|
793
|
+
expect(
|
|
794
|
+
listedBody.data.find((subscription) => subscription.id === betterstackCreated.id)
|
|
795
|
+
?.destination,
|
|
796
|
+
).toStrictEqual(betterstackBody.destination)
|
|
797
|
+
|
|
798
|
+
const slackUpdated = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
|
|
799
|
+
{ json: { status: 'paused' }, param: { id: slackCreated.id } },
|
|
800
|
+
as(writer),
|
|
801
|
+
)
|
|
802
|
+
const betterstackUpdated = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
|
|
803
|
+
{ json: { status: 'paused' }, param: { id: betterstackCreated.id } },
|
|
804
|
+
as(writer),
|
|
805
|
+
)
|
|
806
|
+
expect(
|
|
807
|
+
(await TestApp.json(slackUpdated, Webhooks.schema.updateWebhook.Response)).destination,
|
|
808
|
+
).toStrictEqual(slackBody.destination)
|
|
809
|
+
expect(
|
|
810
|
+
(await TestApp.json(betterstackUpdated, Webhooks.schema.updateWebhook.Response)).destination,
|
|
811
|
+
).toStrictEqual(betterstackBody.destination)
|
|
812
|
+
|
|
813
|
+
const slackRoundTrip = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
|
|
814
|
+
{ json: { destination: slackBody.destination }, param: { id: slackCreated.id } },
|
|
815
|
+
as(writer),
|
|
816
|
+
)
|
|
817
|
+
const betterstackRoundTrip = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
|
|
818
|
+
{
|
|
819
|
+
json: { destination: betterstackBody.destination },
|
|
820
|
+
param: { id: betterstackCreated.id },
|
|
821
|
+
},
|
|
822
|
+
as(writer),
|
|
823
|
+
)
|
|
824
|
+
expect(slackRoundTrip.status).toBe(400)
|
|
825
|
+
expect(betterstackRoundTrip.status).toBe(400)
|
|
826
|
+
})
|
|
827
|
+
|
|
662
828
|
test('rejects a Better Stack destination whose host is not betterstackdata.com', async () => {
|
|
663
829
|
const app = TestApp.client({
|
|
664
830
|
auth: { keys: [writer] },
|
|
@@ -1570,6 +1736,38 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
|
1570
1736
|
expect(tx?.sender).toBe(windowSender)
|
|
1571
1737
|
})
|
|
1572
1738
|
|
|
1739
|
+
test('transaction:included matches exact empty calldata through the poller', async () => {
|
|
1740
|
+
const db = TestApp.database()
|
|
1741
|
+
const subscription = await Core.createSubscription(db, {
|
|
1742
|
+
chainId: 42431,
|
|
1743
|
+
eventType: 'transaction:included',
|
|
1744
|
+
filters: { input: { eq: '0x' } },
|
|
1745
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1746
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1747
|
+
})
|
|
1748
|
+
await Core.setCursor(db, subscription.id, windowCursor)
|
|
1749
|
+
|
|
1750
|
+
const dispatched: Webhooks.poll.Dispatchable[] = []
|
|
1751
|
+
const result = await Webhooks.poll({
|
|
1752
|
+
chainId: 42431,
|
|
1753
|
+
dispatch: async (input) => {
|
|
1754
|
+
dispatched.push(input)
|
|
1755
|
+
return { ok: true }
|
|
1756
|
+
},
|
|
1757
|
+
eventTypes: ['transaction:included'],
|
|
1758
|
+
headBlockNumber: windowHead,
|
|
1759
|
+
client,
|
|
1760
|
+
db,
|
|
1761
|
+
tidx: realTidx,
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
expect(result.scanned).toMatchInlineSnapshot(`1`)
|
|
1765
|
+
expect(dispatched[0]?.envelope.data).toMatchObject({
|
|
1766
|
+
hash: '0x359a47d5a7a582ae412e8ba8bb729b326464f2e97d00bcabf28694e919327f33',
|
|
1767
|
+
input: '0x',
|
|
1768
|
+
})
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1573
1771
|
test('transaction:included routes a status filter through receipts', async () => {
|
|
1574
1772
|
const db = TestApp.database()
|
|
1575
1773
|
const subscription = await Core.createSubscription(db, {
|
|
@@ -1882,6 +2080,11 @@ describe('matchesOperator', () => {
|
|
|
1882
2080
|
expect(Webhooks.matchesOperator(addr, { eq: addr })).toBe(true)
|
|
1883
2081
|
})
|
|
1884
2082
|
|
|
2083
|
+
test('bytes equality handles empty calldata without numeric conversion', () => {
|
|
2084
|
+
expect(Webhooks.matchesOperator('0x', { eq: '0x' })).toBe(true)
|
|
2085
|
+
expect(Webhooks.matchesOperator('0x', { eq: '0x00' })).toBe(false)
|
|
2086
|
+
})
|
|
2087
|
+
|
|
1885
2088
|
test('{ in } matches membership', () => {
|
|
1886
2089
|
expect(Webhooks.matchesOperator(addr, { in: [addr2, addr] })).toBe(true)
|
|
1887
2090
|
expect(Webhooks.matchesOperator(addr, { in: [addr2] })).toBe(false)
|