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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Context, Hono } from 'hono'
|
|
2
|
-
import { type AbiEvent, parseAbiItem } from 'viem'
|
|
2
|
+
import { type AbiEvent, decodeEventLog, encodeEventTopics, parseAbiItem, type RpcLog } from 'viem'
|
|
3
3
|
import * as z from 'zod/mini'
|
|
4
4
|
|
|
5
5
|
import type * as App from '../../../App.js'
|
|
@@ -15,6 +15,7 @@ import type * as Tidx from '../../../internal/Tidx.js'
|
|
|
15
15
|
import * as Ttl from '../../../internal/Ttl.js'
|
|
16
16
|
import * as Value from '../../../internal/Value.js'
|
|
17
17
|
import type * as Viem from '../../../internal/Viem.js'
|
|
18
|
+
import type * as WebhookDestination from '../../../internal/WebhookDestination.js'
|
|
18
19
|
import * as Webhooks from '../../../internal/Webhooks.js'
|
|
19
20
|
import * as Receipts from './receipts.js'
|
|
20
21
|
import * as Transactions from './transactions.js'
|
|
@@ -30,6 +31,9 @@ const defaultMaxPerOwner = 100
|
|
|
30
31
|
*/
|
|
31
32
|
const mppTtlMs = 30 * 24 * 60 * 60 * 1_000
|
|
32
33
|
|
|
34
|
+
const redactedBetterstackToken = '[redacted]'
|
|
35
|
+
const redactedSlackUrl = 'https://hooks.slack.com/…'
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* The subscribable webhook event types, in alphabetical order. The source of
|
|
35
39
|
* truth for the event-type *set*: the {@link EventType} union and the route zod
|
|
@@ -97,12 +101,11 @@ export namespace schema {
|
|
|
97
101
|
z.meta({ examples: ['https://example.com/webhooks'] }),
|
|
98
102
|
)
|
|
99
103
|
|
|
100
|
-
const SlackUrl = z
|
|
101
|
-
.url
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
104
|
+
const SlackUrl = z.url().check(
|
|
105
|
+
z.refine((url) => url !== redactedSlackUrl, { error: 'A Slack webhook URL is required.' }),
|
|
106
|
+
z.describe('Your Slack incoming-webhook URL (`https://hooks.slack.com/services/…`).'),
|
|
107
|
+
z.meta({ examples: ['https://hooks.slack.com/services/T000/B000/XXXXXXXX'] }),
|
|
108
|
+
)
|
|
106
109
|
|
|
107
110
|
const BetterstackUrl = z
|
|
108
111
|
.url()
|
|
@@ -113,13 +116,14 @@ export namespace schema {
|
|
|
113
116
|
z.meta({ examples: ['https://s1234567.eu-nbg-2.betterstackdata.com'] }),
|
|
114
117
|
)
|
|
115
118
|
|
|
116
|
-
const BetterstackToken = z
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
119
|
+
const BetterstackToken = z.string().check(
|
|
120
|
+
z.minLength(1),
|
|
121
|
+
z.refine((token) => token !== redactedBetterstackToken, {
|
|
122
|
+
error: 'A Better Stack source token is required.',
|
|
123
|
+
}),
|
|
124
|
+
z.describe('Your Better Stack source token (sent as `Authorization: Bearer …`).'),
|
|
125
|
+
z.meta({ examples: ['FczKcxEhjEDE58dBX7XaeX1q'] }),
|
|
126
|
+
)
|
|
123
127
|
|
|
124
128
|
/**
|
|
125
129
|
* Where matched events are delivered: a `url` destination (a signed event POST
|
|
@@ -149,6 +153,26 @@ export namespace schema {
|
|
|
149
153
|
),
|
|
150
154
|
)
|
|
151
155
|
|
|
156
|
+
const PublicDestination = z
|
|
157
|
+
.discriminatedUnion('type', [
|
|
158
|
+
z.object({
|
|
159
|
+
type: z.literal('url'),
|
|
160
|
+
url: Url,
|
|
161
|
+
}),
|
|
162
|
+
z.object({
|
|
163
|
+
type: z.literal('slack'),
|
|
164
|
+
url: z.literal(redactedSlackUrl).check(z.describe('Redacted Slack webhook URL.')),
|
|
165
|
+
}),
|
|
166
|
+
z.object({
|
|
167
|
+
token: z
|
|
168
|
+
.literal(redactedBetterstackToken)
|
|
169
|
+
.check(z.describe('Redacted Better Stack source token.')),
|
|
170
|
+
type: z.literal('betterstack'),
|
|
171
|
+
url: BetterstackUrl,
|
|
172
|
+
}),
|
|
173
|
+
])
|
|
174
|
+
.check(z.describe('Delivery destination with bearer credentials redacted.'))
|
|
175
|
+
|
|
152
176
|
/**
|
|
153
177
|
* Equality / membership operator for a filterable field. A consumer can pass a
|
|
154
178
|
* bare value (shorthand for exact equality, the common case) or an operator
|
|
@@ -399,15 +423,10 @@ export namespace schema {
|
|
|
399
423
|
z.refine((f) => !f.args || f.signature, {
|
|
400
424
|
error: '`args` requires `signature` to decode the event.',
|
|
401
425
|
}),
|
|
402
|
-
z.refine(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
error:
|
|
408
|
-
'Addressless log filters must include an event signature or topic0 plus at least one indexed topic or argument.',
|
|
409
|
-
},
|
|
410
|
-
),
|
|
426
|
+
z.refine((f) => hasLogPushdown(f), {
|
|
427
|
+
error:
|
|
428
|
+
'Log filters must include a pushdown-capable address or an event/topic0 anchor plus an indexed topic or argument.',
|
|
429
|
+
}),
|
|
411
430
|
)
|
|
412
431
|
|
|
413
432
|
/**
|
|
@@ -507,7 +526,7 @@ export namespace schema {
|
|
|
507
526
|
z.describe('When the subscription was created, as an ISO 8601 timestamp.'),
|
|
508
527
|
z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
|
|
509
528
|
),
|
|
510
|
-
destination:
|
|
529
|
+
destination: PublicDestination,
|
|
511
530
|
eventType: EventType.check(z.meta({ examples: ['token:transfer'] })),
|
|
512
531
|
expiresAt: z
|
|
513
532
|
.optional(z.iso.datetime())
|
|
@@ -1032,7 +1051,9 @@ export namespace schema {
|
|
|
1032
1051
|
.check(z.describe('Details for creating a webhook subscription.'))
|
|
1033
1052
|
|
|
1034
1053
|
export const Response = z
|
|
1035
|
-
.
|
|
1054
|
+
.object({
|
|
1055
|
+
...Subscription.shape,
|
|
1056
|
+
destination: Destination,
|
|
1036
1057
|
secret: z
|
|
1037
1058
|
.string()
|
|
1038
1059
|
.check(
|
|
@@ -2294,6 +2315,7 @@ export function matchesOperator(
|
|
|
2294
2315
|
return value.toLowerCase().startsWith(String(o['startsWith']).toLowerCase())
|
|
2295
2316
|
if ('in' in o) return (o['in'] as readonly unknown[]).some((v) => operandEquals(value, v, kind))
|
|
2296
2317
|
if ('not' in o) return !operandEquals(value, o['not'], kind)
|
|
2318
|
+
if ('eq' in o && kind === 'string') return operandEquals(value, o['eq'], kind)
|
|
2297
2319
|
// Comparison object: AND-combine whichever of eq/gt/gte/lt/lte are present.
|
|
2298
2320
|
const n = toBig(value)
|
|
2299
2321
|
let ok = true
|
|
@@ -2510,31 +2532,124 @@ function toLogEvent(
|
|
|
2510
2532
|
}
|
|
2511
2533
|
}
|
|
2512
2534
|
|
|
2535
|
+
/** Narrows an exact or membership operator into an RPC filter value. */
|
|
2536
|
+
function pushValue(op: unknown): unknown {
|
|
2537
|
+
const value = (() => {
|
|
2538
|
+
if (typeof op !== 'object' || op === null) return op
|
|
2539
|
+
const operator = op as Record<string, unknown>
|
|
2540
|
+
if ('eq' in operator) return operator['eq']
|
|
2541
|
+
if (Array.isArray(operator['in']) && operator['in'].length > 0) return operator['in']
|
|
2542
|
+
return undefined
|
|
2543
|
+
})()
|
|
2544
|
+
const scalar = (item: unknown) => ['bigint', 'boolean', 'number', 'string'].includes(typeof item)
|
|
2545
|
+
if (scalar(value)) return value
|
|
2546
|
+
if (Array.isArray(value) && value.length > 0 && value.every(scalar)) return value
|
|
2547
|
+
return undefined
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
/** Narrows an `eq`/`in` operator into a hex RPC filter value. */
|
|
2551
|
+
function pushHex(op: unknown): `0x${string}` | `0x${string}`[] | undefined {
|
|
2552
|
+
const value = pushValue(op)
|
|
2553
|
+
if (typeof value === 'string') return value as `0x${string}`
|
|
2554
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
|
2555
|
+
return value as `0x${string}`[]
|
|
2556
|
+
return undefined
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2513
2559
|
/** Narrows an `eq`/`in` address operator into the `getLogs` address pushdown. */
|
|
2514
|
-
function pushAddress(op: unknown)
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2560
|
+
function pushAddress(op: unknown) {
|
|
2561
|
+
return pushHex(op)
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
/** Parses a human-readable event signature without letting refinements throw. */
|
|
2565
|
+
function parseEvent(signature: string | undefined): AbiEvent | undefined {
|
|
2566
|
+
if (!signature) return undefined
|
|
2567
|
+
try {
|
|
2568
|
+
const item = parseAbiItem(signature)
|
|
2569
|
+
return item.type === 'event' ? item : undefined
|
|
2570
|
+
} catch {
|
|
2571
|
+
return undefined
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/** Returns exact indexed arguments that viem can push into `eth_getLogs`. */
|
|
2576
|
+
function pushEventArgs(args: Record<string, unknown> | undefined, event: AbiEvent) {
|
|
2577
|
+
const pushed: Record<string, unknown> = {}
|
|
2578
|
+
for (const [name, op] of Object.entries(args ?? {})) {
|
|
2579
|
+
const input = event.inputs.find((item) => item.indexed && item.name === name)
|
|
2580
|
+
const value = pushValue(op)
|
|
2581
|
+
if (input && value !== undefined) pushed[name] = value
|
|
2582
|
+
}
|
|
2583
|
+
return pushed
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/** Checks that the accepted filter has a selective `eth_getLogs` representation. */
|
|
2587
|
+
function hasLogPushdown(f: z.output<typeof schema.LogFilters>): boolean {
|
|
2588
|
+
if (pushAddress(f.address) !== undefined) return true
|
|
2589
|
+
|
|
2590
|
+
const secondaryTopic = [f.topic1, f.topic2, f.topic3].some(
|
|
2591
|
+
(topic) => pushHex(topic) !== undefined,
|
|
2592
|
+
)
|
|
2593
|
+
const event = parseEvent(f.signature)
|
|
2594
|
+
if (event && !event.anonymous)
|
|
2595
|
+
return secondaryTopic || Object.keys(pushEventArgs(f.args, event)).length > 0
|
|
2596
|
+
return pushHex(f.topic0) !== undefined && secondaryTopic
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/** Builds raw topic filters, preserving null wildcards between constrained positions. */
|
|
2600
|
+
function pushTopics(f: z.output<typeof schema.LogFilters>, event: AbiEvent | undefined) {
|
|
2601
|
+
const topics: (`0x${string}` | `0x${string}`[] | null | undefined)[] = [
|
|
2602
|
+
event ? encodeEventTopics({ abi: [event] })[0] : pushHex(f.topic0),
|
|
2603
|
+
pushHex(f.topic1),
|
|
2604
|
+
pushHex(f.topic2),
|
|
2605
|
+
pushHex(f.topic3),
|
|
2606
|
+
]
|
|
2607
|
+
while (topics.at(-1) === undefined) topics.pop()
|
|
2608
|
+
return topics.map((topic) => topic ?? null)
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/** Converts one raw JSON-RPC log into the viem-shaped scanner input. */
|
|
2612
|
+
function fromRpcLog(log: RpcLog): ScanLog {
|
|
2613
|
+
return {
|
|
2614
|
+
address: log.address,
|
|
2615
|
+
blockNumber: log.blockNumber === null ? null : BigInt(log.blockNumber),
|
|
2616
|
+
data: log.data,
|
|
2617
|
+
logIndex: Value.hexToNumber(log.logIndex) ?? null,
|
|
2618
|
+
topics: log.topics,
|
|
2619
|
+
transactionHash: log.transactionHash,
|
|
2620
|
+
transactionIndex: Value.hexToNumber(log.transactionIndex) ?? null,
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
/** Best-effort decodes a raw log using the subscription event. */
|
|
2625
|
+
function decodeScanLog(log: ScanLog, event: AbiEvent): ScanLog {
|
|
2626
|
+
try {
|
|
2627
|
+
const decoded = decodeEventLog({
|
|
2628
|
+
abi: [event],
|
|
2629
|
+
data: log.data as `0x${string}`,
|
|
2630
|
+
strict: false,
|
|
2631
|
+
topics: log.topics as [`0x${string}`, ...`0x${string}`[]],
|
|
2632
|
+
})
|
|
2633
|
+
return {
|
|
2634
|
+
...log,
|
|
2635
|
+
args: decoded.args,
|
|
2636
|
+
eventName: decoded.eventName,
|
|
2637
|
+
}
|
|
2638
|
+
} catch {
|
|
2639
|
+
return log
|
|
2522
2640
|
}
|
|
2523
|
-
// `not`/range forms can't be pushed; widen here, re-check in JS.
|
|
2524
|
-
return undefined
|
|
2525
2641
|
}
|
|
2526
2642
|
|
|
2527
2643
|
/**
|
|
2528
|
-
* Scans contract event logs over the confirmed window via
|
|
2529
|
-
* then re-checks **every** filter in JS
|
|
2530
|
-
* `not`, ranges,
|
|
2644
|
+
* Scans contract event logs over the confirmed window via `eth_getLogs`,
|
|
2645
|
+
* then re-checks **every** filter in JS. Operators the RPC cannot express
|
|
2646
|
+
* (`not`, ranges, non-indexed `args`) still apply. Two modes:
|
|
2531
2647
|
* - **raw** (no `signature`): `topic0..3` are matched directly.
|
|
2532
|
-
* - **decoded** (`signature` present):
|
|
2533
|
-
*
|
|
2534
|
-
* decodes `args` — which the JS pass then matches by name.
|
|
2648
|
+
* - **decoded** (`signature` present): the selector, raw topics, and supported
|
|
2649
|
+
* indexed `args` are pushed down before the JS pass matches decoded values.
|
|
2535
2650
|
*
|
|
2536
|
-
*
|
|
2537
|
-
* re-checked in JS over the narrow window.
|
|
2651
|
+
* Exact and membership address/topic operators are pushed down to the node;
|
|
2652
|
+
* all operators are re-checked in JS over the narrow window.
|
|
2538
2653
|
*/
|
|
2539
2654
|
async function scanLogs(options: {
|
|
2540
2655
|
cache: ChainCache
|
|
@@ -2549,8 +2664,8 @@ async function scanLogs(options: {
|
|
|
2549
2664
|
const window = scanWindow(cursor, safeBlock, maxBlocks)
|
|
2550
2665
|
if (!window) return { items: [], scannedThrough: undefined }
|
|
2551
2666
|
|
|
2552
|
-
const event =
|
|
2553
|
-
if (
|
|
2667
|
+
const event = parseEvent(f.signature)
|
|
2668
|
+
if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name)))
|
|
2554
2669
|
throw new Webhooks.InvalidFilterError('log:emitted', [
|
|
2555
2670
|
{ message: 'Unsupported event signature.', path: ['signature'] },
|
|
2556
2671
|
])
|
|
@@ -2558,18 +2673,29 @@ async function scanLogs(options: {
|
|
|
2558
2673
|
const address = pushAddress(f.address)
|
|
2559
2674
|
const fromBlock = BigInt(window.from)
|
|
2560
2675
|
const toBlock = BigInt(window.to)
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2676
|
+
const topics = pushTopics(f, event)
|
|
2677
|
+
const hasRawTopics = topics.slice(1).some((topic) => topic !== null)
|
|
2678
|
+
const args = event ? pushEventArgs(f.args, event) : {}
|
|
2679
|
+
const logs = (!event || hasRawTopics
|
|
2680
|
+
? (
|
|
2681
|
+
await rpc<RpcLog[]>(client, 'eth_getLogs', [
|
|
2682
|
+
{
|
|
2683
|
+
...(address ? { address } : {}),
|
|
2684
|
+
fromBlock: blockTag(window.from),
|
|
2685
|
+
...(topics.length > 0 ? { topics } : {}),
|
|
2686
|
+
toBlock: blockTag(window.to),
|
|
2687
|
+
},
|
|
2688
|
+
])
|
|
2689
|
+
)
|
|
2690
|
+
.map(fromRpcLog)
|
|
2691
|
+
.map((log) => (event ? decodeScanLog(log, event) : log))
|
|
2692
|
+
: await client.getLogs({
|
|
2563
2693
|
...(address ? { address } : {}),
|
|
2694
|
+
...(Object.keys(args).length > 0 ? { args: args as never } : {}),
|
|
2564
2695
|
event,
|
|
2565
2696
|
fromBlock,
|
|
2566
2697
|
strict: false,
|
|
2567
2698
|
toBlock,
|
|
2568
|
-
})
|
|
2569
|
-
: await client.getLogs({
|
|
2570
|
-
...(address ? { address } : {}),
|
|
2571
|
-
fromBlock,
|
|
2572
|
-
toBlock,
|
|
2573
2699
|
})) as unknown as ScanLog[]
|
|
2574
2700
|
|
|
2575
2701
|
logs.sort((a, b) => {
|
|
@@ -2884,7 +3010,7 @@ function toPublic(subscription: Webhooks.Subscription) {
|
|
|
2884
3010
|
chainId: subscription.chainId,
|
|
2885
3011
|
...(subscription.context === undefined ? {} : { context: subscription.context }),
|
|
2886
3012
|
createdAt: subscription.createdAt,
|
|
2887
|
-
destination: subscription.destination,
|
|
3013
|
+
destination: redactDestination(subscription.destination),
|
|
2888
3014
|
eventType: subscription.eventType,
|
|
2889
3015
|
...(subscription.expiresAt === undefined ? {} : { expiresAt: subscription.expiresAt }),
|
|
2890
3016
|
failureCount: subscription.failureCount,
|
|
@@ -2900,7 +3026,18 @@ function toPublic(subscription: Webhooks.Subscription) {
|
|
|
2900
3026
|
|
|
2901
3027
|
/** Public read shape plus the one-time signing secret (creation only). */
|
|
2902
3028
|
function toCreated(subscription: Webhooks.Subscription) {
|
|
2903
|
-
return {
|
|
3029
|
+
return {
|
|
3030
|
+
...toPublic(subscription),
|
|
3031
|
+
destination: subscription.destination,
|
|
3032
|
+
secret: subscription.secret,
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
/** Redacts bearer credentials from the destination returned by read endpoints. */
|
|
3037
|
+
function redactDestination(destination: WebhookDestination.Destination) {
|
|
3038
|
+
if (destination.type === 'slack') return { ...destination, url: redactedSlackUrl }
|
|
3039
|
+
if (destination.type === 'betterstack') return { ...destination, token: redactedBetterstackToken }
|
|
3040
|
+
return destination
|
|
2904
3041
|
}
|
|
2905
3042
|
|
|
2906
3043
|
function mutationError(c: Context<App.Environment>, cause: unknown) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
import type Stripe from 'stripe'
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
5
5
|
import * as ApiKeys from '../../ApiKeys.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
7
|
import * as RequestUsage from '../../db/tables/requestUsage.js'
|
|
@@ -11,7 +11,7 @@ import * as Store from '../../internal/Store.js'
|
|
|
11
11
|
import * as Viem from '../../internal/Viem.js'
|
|
12
12
|
import * as Billing from './Billing.js'
|
|
13
13
|
|
|
14
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
14
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
15
15
|
|
|
16
16
|
/** Baseline billable mainnet sponsorship input; tests override per case. */
|
|
17
17
|
const input = {
|
|
@@ -62,6 +62,7 @@ type StubSubscription = { items?: readonly Billing.MeterKind[]; status: string }
|
|
|
62
62
|
function stubStripe(options: {
|
|
63
63
|
meterEvent?: ((params: { identifier: string }) => void) | undefined
|
|
64
64
|
subscriptions?: readonly StubSubscription[] | undefined
|
|
65
|
+
subscriptionsList?: (() => Promise<void>) | undefined
|
|
65
66
|
}) {
|
|
66
67
|
const meterEvents: {
|
|
67
68
|
event_name: string
|
|
@@ -127,7 +128,10 @@ function stubStripe(options: {
|
|
|
127
128
|
subscriptionsCreated.push(params)
|
|
128
129
|
return {}
|
|
129
130
|
},
|
|
130
|
-
list: async () =>
|
|
131
|
+
list: async () => {
|
|
132
|
+
await options.subscriptionsList?.()
|
|
133
|
+
return { data: subscriptions }
|
|
134
|
+
},
|
|
131
135
|
},
|
|
132
136
|
}
|
|
133
137
|
return { itemsCreated, meterEvents, stripe: stripe as never as Stripe, subscriptionsCreated }
|
|
@@ -433,6 +437,51 @@ describe('createRequestUsageReporter', () => {
|
|
|
433
437
|
await db.close()
|
|
434
438
|
})
|
|
435
439
|
|
|
440
|
+
test('behavior: overlapping ticks cannot claim the same observed watermark twice', async () => {
|
|
441
|
+
const db = create()
|
|
442
|
+
await db.migrate()
|
|
443
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
444
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
445
|
+
|
|
446
|
+
const paused = Promise.withResolvers<void>()
|
|
447
|
+
const resume = Promise.withResolvers<void>()
|
|
448
|
+
const delayed = stubStripe({
|
|
449
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
450
|
+
subscriptionsList: async () => {
|
|
451
|
+
paused.resolve()
|
|
452
|
+
await resume.promise
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
const delayedTick = Billing.createRequestUsageReporter({
|
|
456
|
+
db,
|
|
457
|
+
now,
|
|
458
|
+
readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
|
|
459
|
+
stripe: delayed.stripe,
|
|
460
|
+
}).tick()
|
|
461
|
+
|
|
462
|
+
// Pause after this tick observes an empty watermark, then let an
|
|
463
|
+
// overlapping tick report and settle that same count first.
|
|
464
|
+
await paused.promise
|
|
465
|
+
const winner = stubStripe({
|
|
466
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
467
|
+
})
|
|
468
|
+
expect(
|
|
469
|
+
await Billing.createRequestUsageReporter({
|
|
470
|
+
db,
|
|
471
|
+
now,
|
|
472
|
+
readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
|
|
473
|
+
stripe: winner.stripe,
|
|
474
|
+
}).tick(),
|
|
475
|
+
).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
476
|
+
|
|
477
|
+
resume.resolve()
|
|
478
|
+
expect(await delayedTick).toStrictEqual({ failed: 0, reported: 0, skipped: 0 })
|
|
479
|
+
expect(winner.meterEvents).toHaveLength(1)
|
|
480
|
+
expect(delayed.meterEvents).toHaveLength(0)
|
|
481
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(5) // prettier-ignore
|
|
482
|
+
await db.close()
|
|
483
|
+
})
|
|
484
|
+
|
|
436
485
|
test('behavior: seals only hours older than the settlement delay', async () => {
|
|
437
486
|
const db = create()
|
|
438
487
|
await db.migrate()
|
|
@@ -510,7 +510,8 @@ export function createRequestUsageReporter(
|
|
|
510
510
|
subscriptions.set(orgId, subscription)
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
|
|
513
|
+
// The observed watermark is stable for every tick claiming this delta.
|
|
514
|
+
const sequence = watermark?.reportedCount ?? 0
|
|
514
515
|
const identifier = usageIdentifier(environment, orgId, bucketStart, sequence)
|
|
515
516
|
const inserted = await RequestUsage.insertPending(database, {
|
|
516
517
|
...key,
|
|
@@ -599,17 +600,17 @@ function startOfHour(ms: number) {
|
|
|
599
600
|
|
|
600
601
|
/**
|
|
601
602
|
* Deterministic meter-event identifier, bounded under Stripe's 100-char limit:
|
|
602
|
-
* `rq_${env}_${sha(orgId)[:12]}_${bucketEpochHour}_${
|
|
603
|
+
* `rq_${env}_${sha(orgId)[:12]}_${bucketEpochHour}_${observedWatermark}`.
|
|
603
604
|
*/
|
|
604
605
|
function usageIdentifier(
|
|
605
606
|
environment: StripeCustomers.Record['environment'],
|
|
606
607
|
orgId: string,
|
|
607
608
|
bucketStart: string,
|
|
608
|
-
|
|
609
|
+
observedWatermark: number,
|
|
609
610
|
) {
|
|
610
611
|
const hash = Hash.keccak256(Hex.fromString(orgId)).slice(2, 14)
|
|
611
612
|
const epochHour = Math.floor(new Date(bucketStart).getTime() / hourMs)
|
|
612
|
-
return `rq_${environment}_${hash}_${epochHour}_${
|
|
613
|
+
return `rq_${environment}_${hash}_${epochHour}_${observedWatermark}`
|
|
613
614
|
}
|
|
614
615
|
|
|
615
616
|
/**
|
|
@@ -750,7 +750,12 @@ export function billing() {
|
|
|
750
750
|
// the gate never opens (`Billing.status` reads the stored status)
|
|
751
751
|
// without a subscription to invoice the spend against. A failure
|
|
752
752
|
// here surfaces as 5xx and Stripe redelivers.
|
|
753
|
-
if (status === 'active')
|
|
753
|
+
if (status === 'active')
|
|
754
|
+
await core_Billing.ensureSubscription(
|
|
755
|
+
client,
|
|
756
|
+
customer,
|
|
757
|
+
core_Billing.kindsFor(record.environment),
|
|
758
|
+
)
|
|
754
759
|
await StripeCustomers.setStatus(db, record.orgId, status, record.environment)
|
|
755
760
|
await syncKeyBilling(c, { environment: record.environment, orgId: record.orgId, status })
|
|
756
761
|
return c.json({ received: true }, 200)
|
|
@@ -298,7 +298,13 @@ export function orgs() {
|
|
|
298
298
|
description: 'Delete an organization and all of its projects.',
|
|
299
299
|
operationId: 'deleteOrganization',
|
|
300
300
|
responses: OpenApi.responses({
|
|
301
|
-
errors: {
|
|
301
|
+
errors: {
|
|
302
|
+
404: 'No accessible organization was found for this id.',
|
|
303
|
+
409: {
|
|
304
|
+
codes: ['sponsorships_unreported'],
|
|
305
|
+
description: 'Billable sponsorships are still awaiting settlement.',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
302
308
|
success: {
|
|
303
309
|
description: 'Confirmation that the organization was deleted.',
|
|
304
310
|
schema: schema.deleteOrganization.Response,
|
|
@@ -318,6 +324,8 @@ export function orgs() {
|
|
|
318
324
|
|
|
319
325
|
const db = Db.get(c.get('db'))
|
|
320
326
|
try {
|
|
327
|
+
if (await Organizations.hasUnreportedSponsorships(db, Auth.org(c).id))
|
|
328
|
+
return sponsorshipsUnreported(c)
|
|
321
329
|
const kv = c.get('kv')
|
|
322
330
|
if (kv)
|
|
323
331
|
for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
@@ -331,12 +339,22 @@ export function orgs() {
|
|
|
331
339
|
200,
|
|
332
340
|
)
|
|
333
341
|
} catch (cause) {
|
|
342
|
+
if (cause instanceof Organizations.UnreportedSponsorshipsError)
|
|
343
|
+
return sponsorshipsUnreported(c)
|
|
334
344
|
return Response.upstream(c, cause)
|
|
335
345
|
}
|
|
336
346
|
},
|
|
337
347
|
)
|
|
338
348
|
}
|
|
339
349
|
|
|
350
|
+
function sponsorshipsUnreported(c: Context<App.Environment>) {
|
|
351
|
+
return Response.error(c, {
|
|
352
|
+
code: 'sponsorships_unreported',
|
|
353
|
+
message: 'Billable sponsorships are still awaiting settlement',
|
|
354
|
+
status: 409,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
340
358
|
function serializeOrganization(
|
|
341
359
|
record: Organizations.Record & { role?: Memberships.Role | undefined },
|
|
342
360
|
) {
|
package/src/apps/mcp.test.ts
CHANGED
|
@@ -269,3 +269,21 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
|
|
|
269
269
|
)
|
|
270
270
|
expect(rejected.status).toBe(401)
|
|
271
271
|
})
|
|
272
|
+
|
|
273
|
+
test('applies the MCP anonymous rate limit', async () => {
|
|
274
|
+
const app = TestApp.create()
|
|
275
|
+
app.route('/', mcp({ docs: false, fetch: (request) => app.fetch(request) }))
|
|
276
|
+
|
|
277
|
+
const response = await app.fetch(
|
|
278
|
+
new Request(new URL('/mcp', origin), {
|
|
279
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tools/list', params: {} }),
|
|
280
|
+
headers: {
|
|
281
|
+
accept: 'application/json, text/event-stream',
|
|
282
|
+
'content-type': 'application/json',
|
|
283
|
+
},
|
|
284
|
+
method: 'POST',
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"100"`)
|
|
289
|
+
})
|
package/src/apps/mcp.ts
CHANGED
|
@@ -125,10 +125,10 @@ export function mcp(options: mcp.Options) {
|
|
|
125
125
|
const app = new Hono<App.Environment>()
|
|
126
126
|
.post(
|
|
127
127
|
'/mcp',
|
|
128
|
-
Auth.policy({ apiKey: { scopes: [] }, public:
|
|
128
|
+
Auth.policy({ apiKey: { scopes: [] }, public: { rateLimit: { perMinute: 100 } } }),
|
|
129
129
|
OpenApi.describeRoute({
|
|
130
130
|
description:
|
|
131
|
-
"A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests
|
|
131
|
+
"A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests allow 100 requests per minute; send `Authorization: Bearer <api key>` for protected tools.",
|
|
132
132
|
operationId: 'mcpRequest',
|
|
133
133
|
// The JSON-RPC request envelope is protocol-owned (like `/rpc`), so the
|
|
134
134
|
// body documents an example rather than a schema.
|
|
@@ -450,7 +450,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
450
450
|
`)
|
|
451
451
|
})
|
|
452
452
|
|
|
453
|
-
test('behavior: production keys record billable rows', async () => {
|
|
453
|
+
test('behavior: non-mainnet production keys record billable rows', async () => {
|
|
454
454
|
const db = TestApp.database()
|
|
455
455
|
const app = TestApp.create({
|
|
456
456
|
auth: { keys },
|
|
@@ -1141,15 +1141,15 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
1141
1141
|
() => {
|
|
1142
1142
|
// prettier-ignore
|
|
1143
1143
|
const webhookSecret = `whsec_${'c'.repeat(32)}`
|
|
1144
|
-
let node: Awaited<ReturnType<typeof Containers.
|
|
1144
|
+
let node: Awaited<ReturnType<typeof Containers.tempo>> | undefined
|
|
1145
1145
|
|
|
1146
1146
|
beforeAll(async () => {
|
|
1147
|
-
node = await Containers.
|
|
1147
|
+
node = await Containers.tempo({ chainId: Viem.chainId.mainnet })
|
|
1148
1148
|
}, 240_000)
|
|
1149
1149
|
|
|
1150
1150
|
afterAll(async () => {
|
|
1151
1151
|
await TestStripe.sweep()
|
|
1152
|
-
await node?.
|
|
1152
|
+
await node?.stop()
|
|
1153
1153
|
})
|
|
1154
1154
|
|
|
1155
1155
|
test(
|
|
@@ -1157,7 +1157,8 @@ describe.runIf(TestStripe.secretKey)(
|
|
|
1157
1157
|
{ retry: 0, timeout: 300_000 },
|
|
1158
1158
|
async () => {
|
|
1159
1159
|
// prettier-ignore
|
|
1160
|
-
const
|
|
1160
|
+
const endpoint = node!.endpoints.default
|
|
1161
|
+
const url = `http://${endpoint.host}:${endpoint.port}`
|
|
1161
1162
|
const stripe = TestStripe.client()
|
|
1162
1163
|
const db = TestApp.database()
|
|
1163
1164
|
// The key attributes to org_1; the row must exist for the usage read.
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -20,8 +20,8 @@ import * as Scope from '../../Scope.js'
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* The relay route group exposes wallet features at `/rpc/relay` and fee
|
|
23
|
-
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
|
|
24
|
-
*
|
|
23
|
+
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes the
|
|
24
|
+
* verified-token and zone context used by relay handlers.
|
|
25
25
|
*
|
|
26
26
|
* ```ts
|
|
27
27
|
* App.create(options).route('/', data()).route('/', relay())
|
|
@@ -2,12 +2,12 @@ import { nanoid } from 'nanoid'
|
|
|
2
2
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
3
|
import type { Client } from 'viem'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Runtime from '../../../test/runtime.js'
|
|
6
6
|
import * as Db from '../../db/Db.js'
|
|
7
7
|
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
8
8
|
import * as Sponsorships from './Sponsorships.js'
|
|
9
9
|
|
|
10
|
-
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
10
|
+
const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
|
|
11
11
|
|
|
12
12
|
/** Baseline insert input; tests override per case. */
|
|
13
13
|
const input = {
|