tapimo 0.4.4 → 0.5.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 +7403 -6127
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +4 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +24 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +115 -115
- package/dist/apps/data/App.d.ts +1838 -1534
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +135 -44
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/{FundingRouteProviders.d.ts → FundingQuoteProviders.d.ts} +7 -7
- package/dist/apps/data/{FundingRouteProviders.d.ts.map → FundingQuoteProviders.d.ts.map} +1 -1
- package/dist/apps/data/{FundingRouteProviders.js → FundingQuoteProviders.js} +36 -91
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +242 -242
- package/dist/apps/data/routes/addresses.d.ts +227 -227
- package/dist/apps/data/routes/balances.d.ts +12 -12
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +125 -126
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +55 -54
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +207 -388
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +143 -221
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +33 -33
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1090 -108
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +118 -118
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +207 -207
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/members.d.ts +71 -71
- package/dist/apps/management/routes/orgs.d.ts +12 -12
- package/dist/apps/management/routes/projects.d.ts +70 -70
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +75 -75
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +42 -4
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +9 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +130 -10
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +185 -428
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -218
- package/dist/internal/FundingQuotes.js.map +1 -0
- package/dist/internal/Viem.d.ts +3 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +11 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/package.json +1 -1
- package/src/ApiKeys.test.ts +23 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.ts +6 -0
- package/src/Client.test-d.ts +20 -11
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +30 -4
- package/src/apps/data/App.test.ts +354 -0
- package/src/apps/data/App.ts +159 -52
- package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
- package/src/apps/data/{FundingRouteProviders.ts → FundingQuoteProviders.ts} +40 -101
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +151 -206
- package/src/apps/data/routes/funding.ts +156 -291
- package/src/apps/data/routes/indexer.test.ts +97 -1
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +21 -0
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/relay/App.test.ts +324 -4
- package/src/apps/relay/App.ts +40 -4
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.ts +2 -0
- package/src/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +132 -22
- package/src/internal/Auth.ts +171 -14
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +174 -0
- package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +153 -312
- package/src/internal/Viem.ts +14 -5
- package/src/internal/index.test.ts +1 -1
- package/src/internal/index.ts +1 -1
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -225
- package/src/internal/FundingRoutes.test.ts +0 -213
package/src/apps/data/App.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
1
|
+
import { type Context, Hono } from 'hono'
|
|
2
|
+
import { matchedRoutes } from 'hono/route'
|
|
3
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
2
4
|
import * as z from 'zod/mini'
|
|
3
5
|
|
|
4
6
|
import type * as Assets from '../../Assets.js'
|
|
5
7
|
import type * as App from '../../App.js'
|
|
6
|
-
import * as Metadata from '../metadata.js'
|
|
7
8
|
import * as Auth from '../../internal/Auth.js'
|
|
9
|
+
import * as EdgeCache from '../../internal/EdgeCache.js'
|
|
8
10
|
import * as Path from '../../internal/Path.js'
|
|
9
11
|
import * as Response from '../../internal/Response.js'
|
|
10
12
|
import * as Schema from '../../internal/Schema.js'
|
|
11
13
|
import * as Tidx from '../../internal/Tidx.js'
|
|
12
14
|
import * as Viem from '../../internal/Viem.js'
|
|
15
|
+
import * as Scope from '../../Scope.js'
|
|
16
|
+
import * as Metadata from '../metadata.js'
|
|
13
17
|
|
|
14
18
|
import { transactions as transactionActivities } from './routes/activities.js'
|
|
15
19
|
import { addresses } from './routes/addresses.js'
|
|
@@ -40,6 +44,10 @@ import { schema as webhookSchema, webhooks } from './routes/webhooks.js'
|
|
|
40
44
|
export function data(options: data.Options = {}) {
|
|
41
45
|
const { assets, defaultChainId = Viem.defaultChainId, rpc: rpc_option, tidx } = options
|
|
42
46
|
|
|
47
|
+
// Zone endpoints are explicit trust boundaries; the API never derives them.
|
|
48
|
+
const zones = new Map((options.zones ?? []).map((zone) => [zone.chainId, zone]))
|
|
49
|
+
const dataRouteHandlers = new Set<unknown>()
|
|
50
|
+
|
|
43
51
|
// The chain ids this deployment serves (built-in public chains plus
|
|
44
52
|
// `defaultChainId` and any explicit `supportedChainIds`). Published on context
|
|
45
53
|
// so handlers with a path `chainId` can guard it too.
|
|
@@ -48,6 +56,7 @@ export function data(options: data.Options = {}) {
|
|
|
48
56
|
...Object.keys(Tidx.url).map(Number),
|
|
49
57
|
defaultChainId,
|
|
50
58
|
...(options.supportedChainIds ?? []),
|
|
59
|
+
...zones.keys(),
|
|
51
60
|
])
|
|
52
61
|
|
|
53
62
|
// Memoize one viem client per resolved upstream (stateless HTTP transports).
|
|
@@ -64,8 +73,7 @@ export function data(options: data.Options = {}) {
|
|
|
64
73
|
return client
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
// Memoize TIDX clients per
|
|
68
|
-
// chain instead of on every request.
|
|
76
|
+
// Memoize TIDX clients per resolved upstream.
|
|
69
77
|
const getTidx = Tidx.createGetClient({ defaultChainId, tidx })
|
|
70
78
|
|
|
71
79
|
const app = new Hono<App.Environment>()
|
|
@@ -82,49 +90,75 @@ export function data(options: data.Options = {}) {
|
|
|
82
90
|
c.set('tidx', tidx)
|
|
83
91
|
c.set('verifiedTokens', options.verifiedTokens === false ? undefined : (options.verifiedTokens ?? {})) // prettier-ignore
|
|
84
92
|
c.set('webhook', options.webhook === false ? undefined : (options.webhook ?? {}))
|
|
93
|
+
c.set('zones', zones)
|
|
85
94
|
await next()
|
|
86
95
|
})
|
|
87
|
-
//
|
|
88
|
-
// upstream, which would otherwise return an opaque `upstream_error`.
|
|
96
|
+
// Resolve one chain for every data route before auth and handlers use it.
|
|
89
97
|
.use('*', async (c, next) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
const selection = resolveChainSelector({
|
|
99
|
+
c,
|
|
100
|
+
defaultChainId,
|
|
101
|
+
handlers: dataRouteHandlers,
|
|
94
102
|
})
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
if (!selection.matched) return next()
|
|
104
|
+
if (selection.conflict || selection.invalid)
|
|
105
|
+
return Response.error(c, {
|
|
106
|
+
code: 'chain_id_invalid',
|
|
107
|
+
message: selection.conflict ? 'Conflicting chain ids' : 'Invalid chain id',
|
|
108
|
+
status: 400,
|
|
109
|
+
})
|
|
110
|
+
if (!supportedChainIds.has(selection.chainId))
|
|
111
|
+
return Response.unsupportedChainId(c, selection.chainId, supportedChainIds)
|
|
112
|
+
c.set('chainId', selection.chainId)
|
|
113
|
+
return next()
|
|
114
|
+
})
|
|
115
|
+
// Zone data requires an issued scope. Raw RPC may instead forward the
|
|
116
|
+
// caller's Zone token to the node that enforces account scoping.
|
|
117
|
+
.use('*', async (c, next) => {
|
|
118
|
+
if (zones.size === 0) return next()
|
|
119
|
+
const selection = resolveChainSelector({
|
|
120
|
+
c,
|
|
121
|
+
defaultChainId,
|
|
122
|
+
handlers: dataRouteHandlers,
|
|
123
|
+
})
|
|
124
|
+
if (!selection.matched) return next()
|
|
125
|
+
const chainId = c.get('chainId')
|
|
126
|
+
if (!zones.has(chainId)) return next()
|
|
127
|
+
|
|
128
|
+
const principal = Auth.getPrincipal(c)
|
|
129
|
+
const scopes = principal?.type === 'api_key' ? principal.apiKey.scopes : undefined
|
|
130
|
+
const allowed =
|
|
131
|
+
scopes?.includes(Scope.wildcard) ||
|
|
132
|
+
scopes?.includes(`zone:${chainId}:read`) ||
|
|
133
|
+
(selection.rpc && Boolean(c.req.header(ZoneRpcAuthentication.headerName)?.trim()))
|
|
134
|
+
if (!allowed)
|
|
135
|
+
return Response.error(c, {
|
|
136
|
+
code: 'api_key_forbidden',
|
|
137
|
+
message: selection.rpc
|
|
138
|
+
? `Chain id ${chainId} is a zone. Use an API key granting \`zone:${chainId}:read\`, or pass a Zone token via \`X-Authorization-Token\`.`
|
|
139
|
+
: `Chain id ${chainId} is a zone. Use an API key granting \`zone:${chainId}:read\`.`,
|
|
140
|
+
status: 403,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await next()
|
|
144
|
+
// Defense in depth for cache misses; pre-auth lookups are disabled by
|
|
145
|
+
// the route eligibility predicate below.
|
|
146
|
+
c.set('edgeCache', undefined)
|
|
101
147
|
})
|
|
102
148
|
// Sandbox keys are testnet-only. A chain-scoped data route defaults to
|
|
103
149
|
// mainnet, so a sandbox key that omits `chainId` (or targets mainnet) is
|
|
104
150
|
// refused with a clear message telling the caller to pass a testnet
|
|
105
|
-
// `chainId`.
|
|
106
|
-
// query, so it is skipped and enforces the rule in its sponsor validation.
|
|
151
|
+
// `chainId`.
|
|
107
152
|
.use('*', async (c, next) => {
|
|
108
|
-
const rpc = Path.join(c.get('basePath') ?? '', 'rpc')
|
|
109
|
-
const relay = Path.join(rpc, 'relay')
|
|
110
|
-
if (c.req.path === relay || c.req.path.startsWith(`${relay}/`)) return next()
|
|
111
|
-
|
|
112
153
|
const principal = Auth.getPrincipal(c)
|
|
113
154
|
if (principal?.type !== 'api_key' || principal.environment !== 'sandbox') return next()
|
|
114
155
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
const selection = resolveChainSelector({
|
|
157
|
+
c,
|
|
158
|
+
defaultChainId,
|
|
159
|
+
handlers: dataRouteHandlers,
|
|
119
160
|
})
|
|
120
|
-
|
|
121
|
-
// stays with the route's own validator rather than the sandbox message.
|
|
122
|
-
const chainId = (() => {
|
|
123
|
-
if (raw === undefined) return defaultChainId
|
|
124
|
-
const parsed = Schema.ChainId.safeParse(raw)
|
|
125
|
-
return parsed.success ? parsed.data : undefined
|
|
126
|
-
})()
|
|
127
|
-
if (chainId === undefined || !Viem.isMainnet(chainId)) return next()
|
|
161
|
+
if (!selection.matched || !Viem.isMainnet(c.get('chainId'))) return next()
|
|
128
162
|
|
|
129
163
|
return Response.error(c, {
|
|
130
164
|
code: 'api_key_forbidden',
|
|
@@ -149,7 +183,7 @@ export function data(options: data.Options = {}) {
|
|
|
149
183
|
.route('/', blocks())
|
|
150
184
|
.route('/', exchanges())
|
|
151
185
|
.route('/', feeAmm())
|
|
152
|
-
.route('/', funding(options.
|
|
186
|
+
.route('/', funding(options.funding))
|
|
153
187
|
.route('/', indexer())
|
|
154
188
|
.route('/', receipts())
|
|
155
189
|
.route('/', tokenlist())
|
|
@@ -162,21 +196,23 @@ export function data(options: data.Options = {}) {
|
|
|
162
196
|
.route('/', rpc())
|
|
163
197
|
.route('/', coingecko())
|
|
164
198
|
.route('/', blocksByTimestampCompat())
|
|
199
|
+
const edgeCacheEligible = (c: Context<App.Environment>) => {
|
|
200
|
+
const selection = resolveChainSelector({ c, defaultChainId, handlers: dataRouteHandlers })
|
|
201
|
+
return (
|
|
202
|
+
selection.matched &&
|
|
203
|
+
!selection.conflict &&
|
|
204
|
+
!selection.invalid &&
|
|
205
|
+
!zones.has(selection.chainId)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
for (const route of app.routes) {
|
|
209
|
+
if (route.method === 'ALL') continue
|
|
210
|
+
dataRouteHandlers.add(route.handler)
|
|
211
|
+
if (zones.size > 0) EdgeCache.setEligibility(route.handler, edgeCacheEligible)
|
|
212
|
+
}
|
|
165
213
|
return Metadata.attach(app, openapi)
|
|
166
214
|
}
|
|
167
215
|
|
|
168
|
-
function resolveChainSelector(options: {
|
|
169
|
-
basePath: string
|
|
170
|
-
path: string
|
|
171
|
-
query: string | undefined
|
|
172
|
-
}) {
|
|
173
|
-
const rpc = Path.join(options.basePath, 'rpc')
|
|
174
|
-
if (options.path === rpc) return undefined
|
|
175
|
-
if (options.path.startsWith(`${rpc}/`))
|
|
176
|
-
return options.path.slice(rpc.length + 1).split('/')[0] || undefined
|
|
177
|
-
return options.query
|
|
178
|
-
}
|
|
179
|
-
|
|
180
216
|
export declare namespace data {
|
|
181
217
|
/** Options for the data route group. */
|
|
182
218
|
type Options = {
|
|
@@ -184,8 +220,8 @@ export declare namespace data {
|
|
|
184
220
|
assets?: Assets.Assets | undefined
|
|
185
221
|
/** Default Tempo chain id for requests that omit `chainId`. */
|
|
186
222
|
defaultChainId?: Viem.ChainId | undefined
|
|
187
|
-
/** Funding
|
|
188
|
-
|
|
223
|
+
/** Funding quote provider configuration. */
|
|
224
|
+
funding?: funding.Options | undefined
|
|
189
225
|
/** Tempo RPC options: a static config, or a per-request resolver that routes by request principal to a separate (e.g. public, no-auth) RPC. */
|
|
190
226
|
rpc?: Viem.getClient.Rpc | undefined
|
|
191
227
|
/** Extra chain ids this deployment serves, beyond the built-in public chains and `defaultChainId`. A `chainId` outside this set is rejected with a 400. */
|
|
@@ -196,6 +232,19 @@ export declare namespace data {
|
|
|
196
232
|
verifiedTokens?: VerifiedTokens | false | undefined
|
|
197
233
|
/** Webhook feature configuration. Enabled by default (rows live in `db`); pass `false` to disable, leaving `/webhooks` mounted but returning `404` and hidden from the OpenAPI document. */
|
|
198
234
|
webhook?: Webhook | false | undefined
|
|
235
|
+
/**
|
|
236
|
+
* Zone chains served by this deployment. REST and TIDX require a
|
|
237
|
+
* `zone:<chainId>:read` scope; raw RPC may forward a Zone token instead.
|
|
238
|
+
*/
|
|
239
|
+
zones?: readonly Zone[] | undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** One zone chain served by this deployment. */
|
|
243
|
+
type Zone = {
|
|
244
|
+
/** Zone chain id. Consumer-supplied literal; the API never derives zone chain ids. */
|
|
245
|
+
chainId: number
|
|
246
|
+
/** Zone RPC used when forwarding a caller-supplied Zone token. */
|
|
247
|
+
forwardRpcUrl: string
|
|
199
248
|
}
|
|
200
249
|
|
|
201
250
|
/** Verified-token feature configuration. */
|
|
@@ -219,6 +268,66 @@ export declare namespace data {
|
|
|
219
268
|
}
|
|
220
269
|
}
|
|
221
270
|
|
|
271
|
+
function resolveChainSelector(options: {
|
|
272
|
+
c: Context<App.Environment>
|
|
273
|
+
defaultChainId: Viem.ChainId
|
|
274
|
+
handlers: ReadonlySet<unknown>
|
|
275
|
+
}) {
|
|
276
|
+
let matched = false
|
|
277
|
+
let path: string | undefined
|
|
278
|
+
let rpc = false
|
|
279
|
+
for (const route of matchedRoutes(options.c)) {
|
|
280
|
+
if (!options.handlers.has(route.handler)) continue
|
|
281
|
+
matched = true
|
|
282
|
+
const selector = pathChainSelector(route.path, options.c.req.path)
|
|
283
|
+
if (selector.name === undefined) continue
|
|
284
|
+
path = selector.value
|
|
285
|
+
rpc = selector.name === 'chain'
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!matched)
|
|
290
|
+
return {
|
|
291
|
+
chainId: options.defaultChainId,
|
|
292
|
+
conflict: false,
|
|
293
|
+
invalid: false,
|
|
294
|
+
matched: false,
|
|
295
|
+
rpc: false,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const search = new URL(options.c.req.url).searchParams
|
|
299
|
+
const raw = [
|
|
300
|
+
...(path === undefined ? [] : [path]),
|
|
301
|
+
...(rpc ? [] : search.getAll('chainId')),
|
|
302
|
+
...(rpc ? [] : search.getAll('chain_id')),
|
|
303
|
+
]
|
|
304
|
+
const parsed = raw.map((value) => Schema.ChainId.safeParse(value))
|
|
305
|
+
const chainIds = [...new Set(parsed.flatMap((result) => (result.success ? [result.data] : [])))]
|
|
306
|
+
return {
|
|
307
|
+
chainId: chainIds[0] ?? options.defaultChainId,
|
|
308
|
+
conflict: chainIds.length > 1,
|
|
309
|
+
invalid: parsed.some((result) => !result.success),
|
|
310
|
+
matched: true,
|
|
311
|
+
rpc,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function pathChainSelector(routePath: string, requestPath: string) {
|
|
316
|
+
const route = routePath.split('/')
|
|
317
|
+
const request = requestPath.split('/')
|
|
318
|
+
for (let index = 0; index < route.length; index++) {
|
|
319
|
+
const segment = route[index] ?? ''
|
|
320
|
+
for (const name of ['chainId', 'chain'] as const) {
|
|
321
|
+
const prefix = `:${name}`
|
|
322
|
+
if (!segment.startsWith(prefix)) continue
|
|
323
|
+
const suffix = segment.slice(prefix.length)
|
|
324
|
+
if (suffix !== '' && suffix !== '?' && !suffix.startsWith('{')) continue
|
|
325
|
+
return { name, value: request[index] || undefined }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { name: undefined, value: undefined }
|
|
329
|
+
}
|
|
330
|
+
|
|
222
331
|
/**
|
|
223
332
|
* The OpenAPI metadata this group owns: its operation tags and the outbound
|
|
224
333
|
* `webhooks` block. Tagged onto the {@link data} instance so `App.create`
|
|
@@ -231,8 +340,7 @@ const openapi = {
|
|
|
231
340
|
{ name: 'Blocks', description: 'The ordered batches of transactions making up the chain.' },
|
|
232
341
|
{ name: 'Exchange', description: "Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices." }, // prettier-ignore
|
|
233
342
|
{ name: 'Fee AMM', description: 'Pools that convert stablecoins to pay fees.' },
|
|
234
|
-
{ name: 'Funding', description: '
|
|
235
|
-
{ name: 'Providers', description: 'Provider metadata, capabilities, and integration status.' },
|
|
343
|
+
{ name: 'Funding', description: 'Live quotes and supported inputs for funding Tempo.' },
|
|
236
344
|
{ name: 'Tokens', description: 'TIP-20 token details, supply, and holders.' },
|
|
237
345
|
{ name: 'Transactions', description: 'Transactions submitted to Tempo, and their receipts.' },
|
|
238
346
|
{ name: 'Transfers', description: 'Token movements from executing transactions.' },
|
|
@@ -252,7 +360,6 @@ const openapi = {
|
|
|
252
360
|
'Exchange',
|
|
253
361
|
'Fee AMM',
|
|
254
362
|
'Funding',
|
|
255
|
-
'Providers',
|
|
256
363
|
'Tokens',
|
|
257
364
|
'Transactions',
|
|
258
365
|
'Transfers',
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as FundingQuotes from '../../internal/FundingQuotes.js'
|
|
2
|
+
import * as FundingQuoteProviders from './FundingQuoteProviders.js'
|
|
3
|
+
|
|
4
|
+
const now = () => new Date('2026-01-01T00:00:00.000Z')
|
|
5
|
+
|
|
6
|
+
describe('FundingQuoteProviders', () => {
|
|
7
|
+
test('maps successful Relay quotes to available quote probes', async () => {
|
|
8
|
+
const requests: RecordedRequest[] = []
|
|
9
|
+
const candidate = quoteCandidate()
|
|
10
|
+
const fetch = recordingFetch(requests, async () =>
|
|
11
|
+
jsonResponse({
|
|
12
|
+
details: {
|
|
13
|
+
currencyOut: {
|
|
14
|
+
amount: '990000',
|
|
15
|
+
minimumAmount: '980000',
|
|
16
|
+
},
|
|
17
|
+
timeEstimate: 15,
|
|
18
|
+
totalImpact: { percent: '-1.20' },
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
const adapter = FundingQuoteProviders.relay({
|
|
23
|
+
apiKey: 'relay-api-key',
|
|
24
|
+
fetch,
|
|
25
|
+
now,
|
|
26
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
|
|
30
|
+
|
|
31
|
+
expect(requests.map((request) => request.url.pathname)).toEqual(['/quote/v2'])
|
|
32
|
+
expect(new Headers(requests[0]?.init?.headers).get('x-api-key')).toBe('relay-api-key')
|
|
33
|
+
expect(jsonRequestBody(requests[0])).toMatchObject({
|
|
34
|
+
amount: '1000000',
|
|
35
|
+
destinationChainId: 4217,
|
|
36
|
+
destinationCurrency: candidate.destinationToken.address,
|
|
37
|
+
originChainId: 8453,
|
|
38
|
+
originCurrency: candidate.sourceToken.address,
|
|
39
|
+
tradeType: 'EXACT_INPUT',
|
|
40
|
+
user: '0x1111111111111111111111111111111111111111',
|
|
41
|
+
})
|
|
42
|
+
expect(result).toMatchObject({
|
|
43
|
+
minimumOutputAmount: '980000',
|
|
44
|
+
outputAmount: '990000',
|
|
45
|
+
outputAmountFormatted: '0.99',
|
|
46
|
+
quality: {
|
|
47
|
+
estimatedSeconds: 15,
|
|
48
|
+
liquiditySource: 'providerQuote',
|
|
49
|
+
sourceDetail: 'relay:quote-v2',
|
|
50
|
+
tier: 'liquid',
|
|
51
|
+
},
|
|
52
|
+
sampledAt: '2026-01-01T00:00:00.000Z',
|
|
53
|
+
status: 'available',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('calls the runtime fetch method with its global receiver', async () => {
|
|
58
|
+
const candidate = quoteCandidate()
|
|
59
|
+
const fetch = vi.fn(function (this: unknown) {
|
|
60
|
+
expect(this).toBe(globalThis)
|
|
61
|
+
return Promise.resolve(
|
|
62
|
+
jsonResponse({
|
|
63
|
+
details: {
|
|
64
|
+
currencyOut: { amount: '990000', minimumAmount: '980000' },
|
|
65
|
+
timeEstimate: 15,
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
vi.stubGlobal('fetch', fetch)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const adapter = FundingQuoteProviders.relay({
|
|
74
|
+
now,
|
|
75
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
76
|
+
})
|
|
77
|
+
const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
|
|
78
|
+
|
|
79
|
+
expect(result.outputAmount).toBe('990000')
|
|
80
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
81
|
+
} finally {
|
|
82
|
+
vi.unstubAllGlobals()
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
type RecordedRequest = {
|
|
88
|
+
init: RequestInit | undefined
|
|
89
|
+
url: URL
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function quoteCandidate(): FundingQuotes.QuoteProbeInput['candidate'] {
|
|
93
|
+
const [candidate] = FundingQuotes.listQuoteCandidates({
|
|
94
|
+
provider: 'relay',
|
|
95
|
+
sourceAmount: '1000000',
|
|
96
|
+
sourceAmountUnits: 'baseUnits',
|
|
97
|
+
sourceToken: 'USDC',
|
|
98
|
+
sourceChain: 'base',
|
|
99
|
+
})
|
|
100
|
+
if (!candidate) throw new Error('Missing Relay quote candidate')
|
|
101
|
+
return candidate
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function probeInput(
|
|
105
|
+
candidate: FundingQuotes.QuoteProbeInput['candidate'],
|
|
106
|
+
): FundingQuotes.QuoteProbeInput {
|
|
107
|
+
return {
|
|
108
|
+
candidate,
|
|
109
|
+
cache: { bypass: false },
|
|
110
|
+
destinationChain: candidate.destinationChain,
|
|
111
|
+
destinationToken: candidate.destinationToken,
|
|
112
|
+
request: {
|
|
113
|
+
provider: candidate.provider.id,
|
|
114
|
+
sourceAmount: candidate.sourceAmount.amount,
|
|
115
|
+
sourceAmountUnits: 'baseUnits',
|
|
116
|
+
sourceChain: candidate.sourceChain.id,
|
|
117
|
+
sourceToken: candidate.sourceToken.symbol,
|
|
118
|
+
},
|
|
119
|
+
sourceAmount: candidate.sourceAmount,
|
|
120
|
+
sourceChain: candidate.sourceChain,
|
|
121
|
+
sourceToken: candidate.sourceToken,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function recordingFetch(
|
|
126
|
+
requests: RecordedRequest[],
|
|
127
|
+
respond: (request: RecordedRequest) => Response | Promise<Response>,
|
|
128
|
+
): typeof globalThis.fetch {
|
|
129
|
+
return async (input, init) => {
|
|
130
|
+
const request = { init, url: toUrl(input) }
|
|
131
|
+
requests.push(request)
|
|
132
|
+
return respond(request)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toUrl(input: Parameters<typeof globalThis.fetch>[0]) {
|
|
137
|
+
if (input instanceof Request) return new URL(input.url)
|
|
138
|
+
return new URL(String(input))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function jsonResponse(body: unknown, status = 200) {
|
|
142
|
+
return new Response(JSON.stringify(body), {
|
|
143
|
+
headers: { 'content-type': 'application/json' },
|
|
144
|
+
status,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function jsonRequestBody(request: RecordedRequest | undefined) {
|
|
149
|
+
if (typeof request?.init?.body !== 'string') throw new Error('Expected JSON request body')
|
|
150
|
+
return JSON.parse(request.init.body) as unknown
|
|
151
|
+
}
|