tapimo 0.12.0 → 0.13.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 +5561 -5026
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +38 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +2 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +17 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +123 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +80 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +54 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +15 -15
- package/dist/admin/apps/earn-vaults.js +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +81 -81
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -151
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +16 -16
- package/dist/apps/Zones.js +4 -4
- package/dist/apps/data/App.js +18 -18
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/ZoneSelection.d.ts +8 -8
- package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
- package/dist/apps/data/ZoneSelection.js +1 -1
- package/dist/apps/data/ZoneSelection.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +588 -584
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +1561 -1239
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +43 -16
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +44 -44
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +21 -11
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +3 -3
- package/dist/apps/data/routes/earn.d.ts +18 -18
- package/dist/apps/data/routes/earn.js +13 -13
- package/dist/apps/data/routes/exchanges.d.ts +145 -145
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/receipts.d.ts +155 -55
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +28 -10
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +13 -13
- package/dist/apps/data/routes/tokens.d.ts +98 -98
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +51 -20
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +7 -2
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +37 -17
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +18 -12
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +12 -7
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
- package/dist/apps/data/routes/webhooks.d.ts +11 -11
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/funding/App.d.ts +11 -0
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +16 -11
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +60 -48
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +27 -19
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +4487 -3963
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +710 -159
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +236 -4
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
- package/dist/apps/management/routes/webhooks.d.ts +55 -55
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +12 -1
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +3 -2
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +5 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingCatalog.d.ts +4 -2
- package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
- package/dist/db/tables/fundingCatalog.js +5 -1
- package/dist/db/tables/fundingCatalog.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +9 -0
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -3
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EarnVaults.d.ts +3 -3
- package/dist/internal/EarnVaults.js +3 -3
- package/dist/internal/Fetch.d.ts +23 -0
- package/dist/internal/Fetch.d.ts.map +1 -0
- package/dist/internal/Fetch.js +26 -0
- package/dist/internal/Fetch.js.map +1 -0
- package/dist/internal/Log.d.ts +3 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +16 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +5 -3
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +13 -3
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +6 -3
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +8 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +16 -3
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +82 -16
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +7 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Timeout.d.ts +36 -0
- package/dist/internal/Timeout.d.ts.map +1 -0
- package/dist/internal/Timeout.js +37 -0
- package/dist/internal/Timeout.js.map +1 -0
- package/dist/internal/Timing.d.ts +9 -0
- package/dist/internal/Timing.d.ts.map +1 -1
- package/dist/internal/Timing.js +7 -0
- package/dist/internal/Timing.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +3 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/funding/Action.d.ts +9 -0
- package/dist/internal/funding/Action.d.ts.map +1 -1
- package/dist/internal/funding/Action.js +16 -8
- package/dist/internal/funding/Action.js.map +1 -1
- package/dist/internal/funding/Catalog.d.ts +9 -2
- package/dist/internal/funding/Catalog.d.ts.map +1 -1
- package/dist/internal/funding/Catalog.js +19 -8
- package/dist/internal/funding/Catalog.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +68 -13
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +29 -8
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +13 -12
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +23 -21
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +28 -9
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +251 -117
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +2 -2
- package/src/App.test.ts +100 -0
- package/src/App.ts +42 -3
- package/src/Client.test-d.ts +46 -0
- package/src/Scope.test.ts +20 -0
- package/src/Scope.ts +16 -0
- package/src/admin/apps/api-keys.test.ts +75 -0
- package/src/admin/apps/api-keys.ts +64 -0
- package/src/admin/apps/earn-vaults.test.ts +1 -1
- package/src/admin/apps/earn-vaults.ts +1 -1
- package/src/admin/ui/src/lib/api-keys.ts +19 -0
- package/src/admin/ui/src/routes/api-keys.tsx +89 -2
- package/src/admin/ui.generated.ts +189 -151
- package/src/apps/Zones.test.ts +4 -4
- package/src/apps/Zones.ts +4 -4
- package/src/apps/composition.test.ts +1 -1
- package/src/apps/data/App.test.ts +41 -0
- package/src/apps/data/App.ts +18 -18
- package/src/apps/data/ZoneSelection.test.ts +2 -2
- package/src/apps/data/ZoneSelection.ts +8 -8
- package/src/apps/data/routes/activities.test.ts +1526 -12
- package/src/apps/data/routes/activities.ts +1774 -1356
- package/src/apps/data/routes/balances.ts +133 -97
- package/src/apps/data/routes/blocks.test.ts +533 -0
- package/src/apps/data/routes/blocks.ts +35 -16
- package/src/apps/data/routes/earn.test.ts +4 -2
- package/src/apps/data/routes/earn.ts +13 -13
- package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
- package/src/apps/data/routes/receipts.ts +47 -16
- package/src/apps/data/routes/tokens.test.ts +578 -0
- package/src/apps/data/routes/tokens.ts +283 -225
- package/src/apps/data/routes/transactions.ts +68 -29
- package/src/apps/data/routes/transfers.test.ts +421 -0
- package/src/apps/data/routes/transfers.ts +45 -27
- package/src/apps/data/routes/valuation.ts +94 -78
- package/src/apps/data/routes/zones.test.ts +2 -2
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
- package/src/apps/funding/routes/deposit-addresses.ts +17 -12
- package/src/apps/funding/routes/quotes.test.ts +5 -9
- package/src/apps/funding/routes/transfers.test.ts +395 -8
- package/src/apps/funding/routes/transfers.ts +76 -59
- package/src/apps/management/routes/api-keys.test.ts +220 -0
- package/src/apps/management/routes/api-keys.ts +280 -3
- package/src/db/Db.ts +13 -1
- package/src/db/Schema.ts +5 -2
- package/src/db/tables/earnVaults.test.ts +9 -9
- package/src/db/tables/fundingCatalog.test.ts +16 -2
- package/src/db/tables/fundingCatalog.ts +9 -3
- package/src/handlers/internal/utils.test.ts +53 -0
- package/src/handlers/internal/utils.ts +15 -0
- package/src/internal/Auth.ts +12 -4
- package/src/internal/EarnVaults.capabilities.test.ts +2 -2
- package/src/internal/EarnVaults.test.ts +1 -1
- package/src/internal/EarnVaults.ts +3 -3
- package/src/internal/Fetch.test.ts +46 -0
- package/src/internal/Fetch.ts +56 -0
- package/src/internal/Log.test.ts +21 -0
- package/src/internal/Log.ts +20 -5
- package/src/internal/OpenApi.ts +14 -4
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.test.ts +2 -0
- package/src/internal/Schema.ts +12 -2
- package/src/internal/Store.test.ts +178 -0
- package/src/internal/Store.ts +104 -18
- package/src/internal/Tidx.test.ts +2 -1
- package/src/internal/Tidx.ts +7 -2
- package/src/internal/Timeout.test.ts +35 -0
- package/src/internal/Timeout.ts +73 -0
- package/src/internal/Timing.ts +16 -0
- package/src/internal/WebhookDestination.ts +3 -0
- package/src/internal/funding/Action.test.ts +41 -1
- package/src/internal/funding/Action.ts +69 -41
- package/src/internal/funding/Catalog.test.ts +34 -0
- package/src/internal/funding/Catalog.ts +28 -11
- package/src/internal/funding/Provider.test.ts +73 -0
- package/src/internal/funding/Provider.ts +82 -28
- package/src/internal/funding/Transfer.ts +133 -110
- package/src/internal/funding/providers/index.test.ts +4 -167
- package/src/internal/funding/providers/relay.test.ts +2 -2
- package/src/internal/funding/providers/stargate.test.ts +163 -0
- package/src/internal/funding/providers/stargate.ts +359 -134
- package/src/internal/index.test.ts +1 -0
- package/src/internal/index.ts +1 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AbiFunction, Address, type Hex, Solidity } from 'ox'
|
|
2
2
|
import * as z from 'zod/mini'
|
|
3
3
|
|
|
4
|
+
import * as OpenApi from '../OpenApi.js'
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Executable source-chain actions returned by funding transfer creation. A
|
|
6
8
|
* strict discriminated union: transfer reads never include an action.
|
|
@@ -12,49 +14,58 @@ const approveSelector = AbiFunction.getSelector(approve)
|
|
|
12
14
|
/** Zod schemas owned by funding actions. */
|
|
13
15
|
export namespace schema {
|
|
14
16
|
/** One unsigned EVM call, in the exchange quote API's call shape. */
|
|
15
|
-
export const Call =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
export const Call = OpenApi.component(
|
|
18
|
+
z
|
|
19
|
+
.object({
|
|
20
|
+
data: z
|
|
21
|
+
.string()
|
|
22
|
+
.check(
|
|
23
|
+
z.regex(/^0x(?:[0-9a-fA-F]{2})*$/),
|
|
24
|
+
z.describe('ABI-encoded calldata.'),
|
|
25
|
+
z.meta({ examples: ['0x095ea7b3'] }),
|
|
26
|
+
),
|
|
27
|
+
to: z
|
|
28
|
+
.string()
|
|
29
|
+
.check(
|
|
30
|
+
z.regex(/^0x[0-9a-fA-F]{40}$/),
|
|
31
|
+
z.describe('Contract the call targets.'),
|
|
32
|
+
z.meta({ examples: [`0x${'44'.repeat(20)}`] }),
|
|
33
|
+
),
|
|
34
|
+
value: z
|
|
35
|
+
.string()
|
|
36
|
+
.check(
|
|
37
|
+
z.regex(/^0x[0-9a-fA-F]+$/),
|
|
38
|
+
z.describe('Native value to send, as hex.'),
|
|
39
|
+
z.meta({ examples: ['0x0'] }),
|
|
40
|
+
),
|
|
41
|
+
})
|
|
42
|
+
.check(z.describe('One unsigned EVM call.')),
|
|
43
|
+
'FundingActionCall',
|
|
44
|
+
)
|
|
40
45
|
|
|
41
46
|
/** Ordered unsigned EVM calls the caller signs and submits on the source chain. */
|
|
42
|
-
export const EvmCalls =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
export const EvmCalls = OpenApi.component(
|
|
48
|
+
z
|
|
49
|
+
.object({
|
|
50
|
+
calls: z
|
|
51
|
+
.array(Call)
|
|
52
|
+
.check(
|
|
53
|
+
z.minLength(1),
|
|
54
|
+
z.describe('Calls in submission order; they may require separate transactions.'),
|
|
55
|
+
),
|
|
56
|
+
type: z.literal('evm:calls').check(z.describe('Action kind.')),
|
|
57
|
+
})
|
|
58
|
+
.check(z.describe('Ordered unsigned EVM calls to sign and submit on the source chain.')),
|
|
59
|
+
'FundingEvmCallsAction',
|
|
60
|
+
)
|
|
53
61
|
|
|
54
62
|
/** Executable source-chain action attached to a creation response. */
|
|
55
|
-
export const Action =
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
export const Action = OpenApi.component(
|
|
64
|
+
z
|
|
65
|
+
.discriminatedUnion('type', [EvmCalls])
|
|
66
|
+
.check(z.describe('Executable source-chain action. Never returned by transfer reads.')),
|
|
67
|
+
'FundingAction',
|
|
68
|
+
)
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/** One unsigned EVM call (see {@link schema.Call}). */
|
|
@@ -66,6 +77,14 @@ export type EvmCalls = z.output<typeof schema.EvmCalls>
|
|
|
66
77
|
/** Executable source-chain action (see {@link schema.Action}). */
|
|
67
78
|
export type Action = z.output<typeof schema.Action>
|
|
68
79
|
|
|
80
|
+
/** One exact native-value allowance established by provider-specific validation. */
|
|
81
|
+
export type NativeValue = {
|
|
82
|
+
/** Contract that may receive the native value. */
|
|
83
|
+
to: string
|
|
84
|
+
/** Exact native value allowed, as a JSON-RPC quantity. */
|
|
85
|
+
value: string
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
/**
|
|
70
89
|
* Validates prepared EVM calls before persistence. Token calls, approval
|
|
71
90
|
* targets, spenders, amounts, selectors, and native value must match the route.
|
|
@@ -73,9 +92,16 @@ export type Action = z.output<typeof schema.Action>
|
|
|
73
92
|
export function validateEvmCalls(options: validateEvmCalls.Options): void {
|
|
74
93
|
const action = schema.EvmCalls.safeParse(options.action)
|
|
75
94
|
if (!action.success) throw new InvalidActionError('Malformed EVM calls.')
|
|
95
|
+
const nativeValues = new Set<string>()
|
|
96
|
+
for (const { to, value } of options.nativeValues ?? []) {
|
|
97
|
+
if (!Address.validate(to, { strict: false }) || !/^0x[0-9a-fA-F]+$/.test(value))
|
|
98
|
+
throw new InvalidActionError('Malformed native value allowance.')
|
|
99
|
+
nativeValues.add(`${to.toLowerCase()}:${BigInt(value).toString()}`)
|
|
100
|
+
}
|
|
76
101
|
for (const [index, call] of action.data.calls.entries()) {
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
const callValue = BigInt(call.value)
|
|
103
|
+
if (callValue > 0n && !nativeValues.has(`${call.to.toLowerCase()}:${callValue.toString()}`))
|
|
104
|
+
throw new InvalidActionError('Native value must match a validated provider fee.')
|
|
79
105
|
const targetsSourceToken = Address.isEqual(
|
|
80
106
|
call.to as Address.Address,
|
|
81
107
|
options.sourceTokenAddress as Address.Address,
|
|
@@ -104,6 +130,8 @@ export declare namespace validateEvmCalls {
|
|
|
104
130
|
type Options = {
|
|
105
131
|
/** The prepared action to validate. */
|
|
106
132
|
action: EvmCalls
|
|
133
|
+
/** Exact native values established by provider-specific validation. */
|
|
134
|
+
nativeValues?: readonly NativeValue[] | undefined
|
|
107
135
|
/** Source amount in base units. */
|
|
108
136
|
sourceAmount: string
|
|
109
137
|
/** Source token contract address. */
|
|
@@ -309,3 +309,37 @@ describe('route capabilities', () => {
|
|
|
309
309
|
).toThrow()
|
|
310
310
|
})
|
|
311
311
|
})
|
|
312
|
+
|
|
313
|
+
describe('route configuration', () => {
|
|
314
|
+
const configuration = { poolAddress: `0x${'11'.repeat(20)}` }
|
|
315
|
+
|
|
316
|
+
test('carries configuration into hydrated provider routes', () => {
|
|
317
|
+
const snapshot = Catalog.compile(rows({ routes: [{ ...data.routes[0]!, configuration }] }))
|
|
318
|
+
expect(snapshot.routes[0]?.configuration).toStrictEqual(configuration)
|
|
319
|
+
expect(snapshot.routesByProvider.get('relay')?.[0]?.configuration).toStrictEqual(configuration)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('omits configuration when it is absent or null', () => {
|
|
323
|
+
const absent = Catalog.compile(rows())
|
|
324
|
+
const nulled = Catalog.compile(rows({ routes: [{ ...data.routes[0]!, configuration: null }] }))
|
|
325
|
+
expect('configuration' in absent.routes[0]!).toBe(false)
|
|
326
|
+
expect('configuration' in nulled.routes[0]!).toBe(false)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('publishes configuration through the write boundary', async () => {
|
|
330
|
+
const db = TestApp.database()
|
|
331
|
+
const snapshot = await Catalog.publish(db, {
|
|
332
|
+
...data,
|
|
333
|
+
routes: [{ ...data.routes[0]!, configuration }],
|
|
334
|
+
})
|
|
335
|
+
expect(snapshot.routes[0]?.configuration).toStrictEqual(configuration)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('rejects non-object configuration', () => {
|
|
339
|
+
expect(() =>
|
|
340
|
+
Catalog.compile(
|
|
341
|
+
rows({ routes: [{ ...data.routes[0]!, configuration: 'invalid' as never }] }),
|
|
342
|
+
),
|
|
343
|
+
).toThrow()
|
|
344
|
+
})
|
|
345
|
+
})
|
|
@@ -55,10 +55,16 @@ export namespace schema {
|
|
|
55
55
|
z.describe('Funding capabilities enabled on one provider route.'),
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/** Provider-specific configuration for one funding route. */
|
|
59
|
+
export const Configuration = z
|
|
60
|
+
.record(z.string(), z.unknown())
|
|
61
|
+
.check(z.describe('Provider-specific configuration for one funding route.'))
|
|
62
|
+
|
|
63
|
+
// Absent or null `capabilities` keeps a route quote-only. Absent or null
|
|
64
|
+
// `configuration` lets providers decide whether a route is configured.
|
|
60
65
|
const Route = z.extend(db_Schema.FundingRoute, {
|
|
61
66
|
capabilities: z.optional(z.nullable(Capabilities)),
|
|
67
|
+
configuration: z.optional(z.nullable(Configuration)),
|
|
62
68
|
})
|
|
63
69
|
|
|
64
70
|
/** Complete funding catalog content accepted by the publish boundary. */
|
|
@@ -77,10 +83,15 @@ export namespace schema {
|
|
|
77
83
|
/** Funding capabilities enabled on one provider route. */
|
|
78
84
|
export type Capabilities = z.output<typeof schema.Capabilities>
|
|
79
85
|
|
|
86
|
+
/** Provider-specific configuration for one funding route. */
|
|
87
|
+
export type Configuration = z.output<typeof schema.Configuration>
|
|
88
|
+
|
|
80
89
|
/** One hydrated provider route in a funding catalog snapshot. */
|
|
81
90
|
export type ProviderRoute = {
|
|
82
91
|
/** Funding capabilities; absent means the route is quote-only. */
|
|
83
92
|
capabilities?: Capabilities | undefined
|
|
93
|
+
/** Provider-specific route configuration. */
|
|
94
|
+
configuration?: Configuration | undefined
|
|
84
95
|
/** Funding provider that supports the route. */
|
|
85
96
|
providerId: string
|
|
86
97
|
/** Hydrated source-to-destination funding route. */
|
|
@@ -93,8 +104,8 @@ export type Snapshot = {
|
|
|
93
104
|
chainsByKey: ReadonlyMap<string, Chain.Chain>
|
|
94
105
|
/** Hydrated provider routes in stable catalog order. */
|
|
95
106
|
routes: readonly ProviderRoute[]
|
|
96
|
-
/** Hydrated routes grouped by provider id. */
|
|
97
|
-
routesByProvider: ReadonlyMap<string, readonly
|
|
107
|
+
/** Hydrated provider routes grouped by provider id. */
|
|
108
|
+
routesByProvider: ReadonlyMap<string, readonly ProviderRoute[]>
|
|
98
109
|
/** Canonical funding tokens indexed by stable id. */
|
|
99
110
|
tokensById: ReadonlyMap<string, Token.Token>
|
|
100
111
|
/** ISO timestamp of the catalog's last publish. */
|
|
@@ -176,7 +187,7 @@ export function compile(rows: core_Catalog.Rows): Snapshot {
|
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
const routes: ProviderRoute[] = []
|
|
179
|
-
const routesByProvider = new Map<string,
|
|
190
|
+
const routesByProvider = new Map<string, ProviderRoute[]>()
|
|
180
191
|
for (const row of data.routes) {
|
|
181
192
|
const route = {
|
|
182
193
|
destination: resolvePlacement(
|
|
@@ -187,14 +198,16 @@ export function compile(rows: core_Catalog.Rows): Snapshot {
|
|
|
187
198
|
),
|
|
188
199
|
source: resolvePlacement(chainsById, tokensById, row.sourceChainId, row.sourceTokenId),
|
|
189
200
|
}
|
|
190
|
-
|
|
201
|
+
const entry = {
|
|
191
202
|
...(row.capabilities ? { capabilities: row.capabilities } : {}),
|
|
203
|
+
...(row.configuration ? { configuration: row.configuration } : {}),
|
|
192
204
|
providerId: row.providerId,
|
|
193
205
|
route,
|
|
194
|
-
}
|
|
206
|
+
}
|
|
207
|
+
routes.push(entry)
|
|
195
208
|
const providerRoutes = routesByProvider.get(row.providerId)
|
|
196
|
-
if (providerRoutes) providerRoutes.push(
|
|
197
|
-
else routesByProvider.set(row.providerId, [
|
|
209
|
+
if (providerRoutes) providerRoutes.push(entry)
|
|
210
|
+
else routesByProvider.set(row.providerId, [entry])
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
return {
|
|
@@ -219,10 +232,14 @@ export async function publish(db: Db.Db, input: publish.Input): Promise<Snapshot
|
|
|
219
232
|
catalog: { id: 'default', updatedAt: emptyUpdatedAt, version: 'validation' },
|
|
220
233
|
...data,
|
|
221
234
|
})
|
|
222
|
-
//
|
|
235
|
+
// Optional route data persists as null so every stored row has one shape.
|
|
223
236
|
await core_Catalog.publish(db, {
|
|
224
237
|
...data,
|
|
225
|
-
routes: data.routes.map((route) => ({
|
|
238
|
+
routes: data.routes.map((route) => ({
|
|
239
|
+
...route,
|
|
240
|
+
capabilities: route.capabilities ?? null,
|
|
241
|
+
configuration: route.configuration ?? null,
|
|
242
|
+
})),
|
|
226
243
|
})
|
|
227
244
|
return prime(db, await read(db))
|
|
228
245
|
}
|
|
@@ -5,6 +5,7 @@ import * as Across from './providers/across.js'
|
|
|
5
5
|
import * as Relay from './providers/relay.js'
|
|
6
6
|
import * as Rhino from './providers/rhino.js'
|
|
7
7
|
import * as Squid from './providers/squid.js'
|
|
8
|
+
import * as Stargate from './providers/stargate.js'
|
|
8
9
|
import * as Symbiosis from './providers/symbiosis.js'
|
|
9
10
|
|
|
10
11
|
const providers = [
|
|
@@ -19,6 +20,7 @@ const providers = [
|
|
|
19
20
|
integratorId: 'squid-integrator-id',
|
|
20
21
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
21
22
|
}),
|
|
23
|
+
Stargate.stargate(),
|
|
22
24
|
Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
23
25
|
] as const
|
|
24
26
|
|
|
@@ -831,9 +833,63 @@ describe('prepareTransfer', () => {
|
|
|
831
833
|
{ operation: 'prepareTransfer', outcome: 'unavailable', provider: 'relay' },
|
|
832
834
|
])
|
|
833
835
|
})
|
|
836
|
+
|
|
837
|
+
test('accepts preparations without correlation', async () => {
|
|
838
|
+
const { correlation: _correlation, ...result } = preparedResult()
|
|
839
|
+
const spender = `0x${'44'.repeat(20)}`
|
|
840
|
+
const selected = FundingProvider.prepareTransfer({
|
|
841
|
+
...prepareOptions,
|
|
842
|
+
providers: [
|
|
843
|
+
withPrepareTransfer('relay', async () => {
|
|
844
|
+
return {
|
|
845
|
+
...result,
|
|
846
|
+
action: {
|
|
847
|
+
calls: [
|
|
848
|
+
{
|
|
849
|
+
data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${(1_000_000).toString(16).padStart(64, '0')}`,
|
|
850
|
+
to: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
851
|
+
value: '0x0',
|
|
852
|
+
},
|
|
853
|
+
{ data: '0xe8017952', to: spender, value: '0x0' },
|
|
854
|
+
],
|
|
855
|
+
type: 'evm:calls',
|
|
856
|
+
},
|
|
857
|
+
}
|
|
858
|
+
}),
|
|
859
|
+
],
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
expect((await selected)?.result.correlation).toBeUndefined()
|
|
863
|
+
})
|
|
834
864
|
})
|
|
835
865
|
|
|
836
866
|
describe('getPrepareCandidates', () => {
|
|
867
|
+
test('restricts candidates to the requested provider', () => {
|
|
868
|
+
const relay = withPrepareTransfer('relay', async () => preparedResult())
|
|
869
|
+
const stargate = withPrepareTransfer('stargate', async () => preparedResult())
|
|
870
|
+
const route = TestFunding.data.routes.find(
|
|
871
|
+
(entry) => entry.providerId === 'relay' && entry.capabilities?.transfer,
|
|
872
|
+
)!
|
|
873
|
+
const catalog = FundingCatalog.compile({
|
|
874
|
+
catalog: {
|
|
875
|
+
id: 'default',
|
|
876
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
877
|
+
version: 'test',
|
|
878
|
+
},
|
|
879
|
+
...TestFunding.data,
|
|
880
|
+
routes: [...TestFunding.data.routes, { ...route, providerId: 'stargate' }],
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
expect(
|
|
884
|
+
FundingProvider.getPrepareCandidates({
|
|
885
|
+
...prepareOptions,
|
|
886
|
+
catalog,
|
|
887
|
+
provider: 'stargate',
|
|
888
|
+
providers: [relay, stargate],
|
|
889
|
+
}).map(({ provider }) => provider.id),
|
|
890
|
+
).toEqual(['stargate'])
|
|
891
|
+
})
|
|
892
|
+
|
|
837
893
|
test('restricts candidates to the selected destination chain', () => {
|
|
838
894
|
const provider = withPrepareTransfer('relay', async () => preparedResult())
|
|
839
895
|
expect(
|
|
@@ -1110,6 +1166,19 @@ describe('getQuoteCandidates', () => {
|
|
|
1110
1166
|
expect(candidate?.sourceAmount).toEqual({ amount: '1250000', formatted: '1.25' })
|
|
1111
1167
|
})
|
|
1112
1168
|
|
|
1169
|
+
test('carries provider route configuration without duplicating token metadata', () => {
|
|
1170
|
+
const [candidate] = FundingProvider.getQuoteCandidates({
|
|
1171
|
+
...baseUsdcRequest,
|
|
1172
|
+
provider: 'stargate',
|
|
1173
|
+
providers,
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1176
|
+
expect(candidate?.configuration).toEqual({
|
|
1177
|
+
poolAddress: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26',
|
|
1178
|
+
})
|
|
1179
|
+
expect(candidate?.sourceToken.address).toBe('0x833589fcd6edb6e08f4c7c32d4f71b54bda02913')
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1113
1182
|
test('resolves normalized catalog aliases', () => {
|
|
1114
1183
|
const catalog = FundingCatalog.compile({
|
|
1115
1184
|
catalog: {
|
|
@@ -1341,6 +1410,10 @@ describe('getProviders', () => {
|
|
|
1341
1410
|
"id": "squid",
|
|
1342
1411
|
"name": "Squid",
|
|
1343
1412
|
},
|
|
1413
|
+
{
|
|
1414
|
+
"id": "stargate",
|
|
1415
|
+
"name": "Stargate",
|
|
1416
|
+
},
|
|
1344
1417
|
{
|
|
1345
1418
|
"id": "symbiosis",
|
|
1346
1419
|
"name": "Symbiosis",
|
|
@@ -216,10 +216,12 @@ export namespace schema {
|
|
|
216
216
|
/** Executable preparation returned by a funding provider. */
|
|
217
217
|
export const PrepareResult = z.object({
|
|
218
218
|
action: Action.schema.Action,
|
|
219
|
-
correlation: z.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
correlation: z.optional(
|
|
220
|
+
z.object({
|
|
221
|
+
checkEndpoint: z.optional(z.string()),
|
|
222
|
+
requestId: z.string().check(z.minLength(1)),
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
223
225
|
destinationAmount: PositiveInteger,
|
|
224
226
|
destinationAmountMin: PositiveInteger,
|
|
225
227
|
expiresAt: z.optional(z.iso.datetime()),
|
|
@@ -227,6 +229,7 @@ export namespace schema {
|
|
|
227
229
|
z.object({
|
|
228
230
|
amount: Schema.DecimalString,
|
|
229
231
|
side: z.enum(['destination', 'source']),
|
|
232
|
+
token: z.optional(TokenRef),
|
|
230
233
|
}),
|
|
231
234
|
),
|
|
232
235
|
quality: GetQuoteResult.shape.quality,
|
|
@@ -467,6 +470,21 @@ export declare namespace prepareTransfer {
|
|
|
467
470
|
type ReturnType = z.output<typeof schema.PrepareResult>
|
|
468
471
|
}
|
|
469
472
|
|
|
473
|
+
/** Contract for provider-specific executable action validation. */
|
|
474
|
+
export declare namespace validateTransferAction {
|
|
475
|
+
/** Prepared action and its curated route terms. */
|
|
476
|
+
type Parameters = prepareTransfer.Parameters & {
|
|
477
|
+
/** Executable action returned by the provider. */
|
|
478
|
+
action: Action.Action
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Provider-specific constraints established by validation. */
|
|
482
|
+
type ReturnType = {
|
|
483
|
+
/** Exact native values independently validated against provider calldata. */
|
|
484
|
+
nativeValues?: readonly Action.NativeValue[] | undefined
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
470
488
|
/** Contract of the provider `createDepositAddress` method. */
|
|
471
489
|
export declare namespace createDepositAddress {
|
|
472
490
|
/** Input passed to a provider provisioning one reusable deposit address. */
|
|
@@ -683,6 +701,10 @@ export type Provider<id extends string = string> = core_Provider.Provider<id, 'f
|
|
|
683
701
|
input: prepareTransfer.Parameters,
|
|
684
702
|
signal: AbortSignal,
|
|
685
703
|
): Promise<prepareTransfer.ReturnType>
|
|
704
|
+
/** Validates provider-specific action semantics before generic persistence checks. */
|
|
705
|
+
validateTransferAction?(
|
|
706
|
+
input: validateTransferAction.Parameters,
|
|
707
|
+
): validateTransferAction.ReturnType
|
|
686
708
|
/** Receives authenticated provider status events, when configured. */
|
|
687
709
|
webhook?: Webhook | undefined
|
|
688
710
|
}
|
|
@@ -952,6 +974,7 @@ export function getQuoteCandidates(options: getQuoteCandidates.Options) {
|
|
|
952
974
|
.filter((entry) => provider === undefined || entry.provider.id === provider.id)
|
|
953
975
|
.map((entry) =>
|
|
954
976
|
createQuoteCandidate({
|
|
977
|
+
configuration: entry.configuration,
|
|
955
978
|
destinationToken: entry.route.destination,
|
|
956
979
|
provider: entry.provider,
|
|
957
980
|
route: entry.route,
|
|
@@ -1124,20 +1147,17 @@ export async function prepareTransfer(
|
|
|
1124
1147
|
candidates.map(async ({ candidate, provider }) => {
|
|
1125
1148
|
const start = performance.now()
|
|
1126
1149
|
try {
|
|
1150
|
+
const parameters = {
|
|
1151
|
+
candidate,
|
|
1152
|
+
method: options.method,
|
|
1153
|
+
mode: options.mode,
|
|
1154
|
+
recipient: options.recipient,
|
|
1155
|
+
sender: options.sender,
|
|
1156
|
+
slippageBps: options.slippageBps,
|
|
1157
|
+
}
|
|
1127
1158
|
const parsed = schema.PrepareResult.safeParse(
|
|
1128
1159
|
await withTimeout(
|
|
1129
|
-
(signal) =>
|
|
1130
|
-
provider.prepareTransfer(
|
|
1131
|
-
{
|
|
1132
|
-
candidate,
|
|
1133
|
-
method: options.method,
|
|
1134
|
-
mode: options.mode,
|
|
1135
|
-
recipient: options.recipient,
|
|
1136
|
-
sender: options.sender,
|
|
1137
|
-
slippageBps: options.slippageBps,
|
|
1138
|
-
},
|
|
1139
|
-
signal,
|
|
1140
|
-
),
|
|
1160
|
+
(signal) => provider.prepareTransfer(parameters, signal),
|
|
1141
1161
|
options.providerTimeoutMs ?? defaultPrepareTimeoutMs,
|
|
1142
1162
|
),
|
|
1143
1163
|
)
|
|
@@ -1160,8 +1180,13 @@ export async function prepareTransfer(
|
|
|
1160
1180
|
}
|
|
1161
1181
|
if (result.action.type === 'evm:calls')
|
|
1162
1182
|
try {
|
|
1183
|
+
const constraints = provider.validateTransferAction?.({
|
|
1184
|
+
...parameters,
|
|
1185
|
+
action: result.action,
|
|
1186
|
+
})
|
|
1163
1187
|
Action.validateEvmCalls({
|
|
1164
1188
|
action: result.action,
|
|
1189
|
+
nativeValues: constraints?.nativeValues,
|
|
1165
1190
|
sourceAmount: candidate.sourceAmount.amount,
|
|
1166
1191
|
sourceTokenAddress: candidate.sourceToken.address,
|
|
1167
1192
|
})
|
|
@@ -1275,7 +1300,14 @@ export function getDepositAddressCandidates(options: getDepositAddressCandidates
|
|
|
1275
1300
|
const provider = options.providers.find((candidate) => candidate.id === entry.providerId)
|
|
1276
1301
|
if (!provider || !canCreateDepositAddress(provider)) return []
|
|
1277
1302
|
return [
|
|
1278
|
-
{
|
|
1303
|
+
{
|
|
1304
|
+
candidate: createDepositAddressCandidate({
|
|
1305
|
+
configuration: entry.configuration,
|
|
1306
|
+
provider,
|
|
1307
|
+
route: entry.route,
|
|
1308
|
+
}),
|
|
1309
|
+
provider,
|
|
1310
|
+
},
|
|
1279
1311
|
]
|
|
1280
1312
|
})
|
|
1281
1313
|
}
|
|
@@ -1308,6 +1340,9 @@ export function getPrepareCandidates(options: getPrepareCandidates.Options) {
|
|
|
1308
1340
|
if (!amount.success) throw new InvalidAmountError()
|
|
1309
1341
|
const sourceChain = resolveSourceChain(options.sourceChain, options.catalog)
|
|
1310
1342
|
const destinationChainId = `eip155:${options.destinationChainId}`
|
|
1343
|
+
const selectedProvider = options.provider
|
|
1344
|
+
? resolveProvider(options.provider, options.providers)
|
|
1345
|
+
: undefined
|
|
1311
1346
|
|
|
1312
1347
|
return options.catalog.routes.flatMap((entry) => {
|
|
1313
1348
|
if (entry.route.source.chain.id !== sourceChain.id) return []
|
|
@@ -1315,11 +1350,13 @@ export function getPrepareCandidates(options: getPrepareCandidates.Options) {
|
|
|
1315
1350
|
if (!matchesToken(entry.route.source, options.sourceToken)) return []
|
|
1316
1351
|
if (!matchesToken(entry.route.destination, options.destinationToken)) return []
|
|
1317
1352
|
if (!entry.capabilities?.transfer?.modes.includes(options.mode)) return []
|
|
1318
|
-
const provider =
|
|
1319
|
-
|
|
1353
|
+
const provider =
|
|
1354
|
+
selectedProvider ?? options.providers.find((candidate) => candidate.id === entry.providerId)
|
|
1355
|
+
if (!provider || provider.id !== entry.providerId || !canPrepareTransfer(provider)) return []
|
|
1320
1356
|
return [
|
|
1321
1357
|
{
|
|
1322
1358
|
candidate: createQuoteCandidate({
|
|
1359
|
+
configuration: entry.configuration,
|
|
1323
1360
|
destinationToken: entry.route.destination,
|
|
1324
1361
|
provider,
|
|
1325
1362
|
route: entry.route,
|
|
@@ -1349,6 +1386,8 @@ export declare namespace getPrepareCandidates {
|
|
|
1349
1386
|
method: Transfer.Method
|
|
1350
1387
|
/** Amount mode the caller selected. */
|
|
1351
1388
|
mode: Transfer.Mode
|
|
1389
|
+
/** Funding provider ID selected by the caller. */
|
|
1390
|
+
provider?: string | undefined
|
|
1352
1391
|
/** Funding providers considered by the request. */
|
|
1353
1392
|
providers: readonly Provider[]
|
|
1354
1393
|
/** Source amount in base units. */
|
|
@@ -1572,8 +1611,9 @@ function chainRef(chain: Chain): ChainRef {
|
|
|
1572
1611
|
})
|
|
1573
1612
|
}
|
|
1574
1613
|
|
|
1575
|
-
function createDepositAddressCandidate(options:
|
|
1614
|
+
function createDepositAddressCandidate(options: createDepositAddressCandidate.Options) {
|
|
1576
1615
|
return {
|
|
1616
|
+
...(options.configuration ? { configuration: options.configuration } : {}),
|
|
1577
1617
|
destinationChain: chainRef(options.route.destination.chain),
|
|
1578
1618
|
destinationToken: tokenRef(options.route.destination),
|
|
1579
1619
|
provider: schema.ProviderRef.parse(options.provider),
|
|
@@ -1583,14 +1623,17 @@ function createDepositAddressCandidate(options: { provider: Provider; route: cor
|
|
|
1583
1623
|
} as const
|
|
1584
1624
|
}
|
|
1585
1625
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1626
|
+
declare namespace createDepositAddressCandidate {
|
|
1627
|
+
type Options = {
|
|
1628
|
+
configuration?: core_Catalog.Configuration | undefined
|
|
1629
|
+
provider: Provider
|
|
1630
|
+
route: core_Route.Route
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function createQuoteCandidate(options: createQuoteCandidate.Options) {
|
|
1593
1635
|
return {
|
|
1636
|
+
...(options.configuration ? { configuration: options.configuration } : {}),
|
|
1594
1637
|
destinationChain: chainRef(options.destinationToken.chain),
|
|
1595
1638
|
destinationToken: tokenRef(options.destinationToken),
|
|
1596
1639
|
id: [
|
|
@@ -1608,6 +1651,17 @@ function createQuoteCandidate(options: {
|
|
|
1608
1651
|
} as const
|
|
1609
1652
|
}
|
|
1610
1653
|
|
|
1654
|
+
declare namespace createQuoteCandidate {
|
|
1655
|
+
type Options = {
|
|
1656
|
+
configuration?: core_Catalog.Configuration | undefined
|
|
1657
|
+
destinationToken: Token
|
|
1658
|
+
provider: Provider
|
|
1659
|
+
route: core_Route.Route
|
|
1660
|
+
sourceAmount: ReturnType<typeof resolveSourceAmount>
|
|
1661
|
+
sourceToken: Token
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1611
1665
|
function normalizeInput(value: string) {
|
|
1612
1666
|
return value.trim().toLowerCase()
|
|
1613
1667
|
}
|
|
@@ -1807,7 +1861,7 @@ function resolveSourceChain(input: string, catalog: core_Catalog.Snapshot) {
|
|
|
1807
1861
|
|
|
1808
1862
|
function getRoutes(catalog: core_Catalog.Snapshot, providers: readonly Provider[]) {
|
|
1809
1863
|
return providers.flatMap((provider) =>
|
|
1810
|
-
(catalog.routesByProvider.get(provider.id) ?? []).map((
|
|
1864
|
+
(catalog.routesByProvider.get(provider.id) ?? []).map((entry) => ({ ...entry, provider })),
|
|
1811
1865
|
)
|
|
1812
1866
|
}
|
|
1813
1867
|
|