tapimo 0.5.2 → 0.6.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 +13987 -5755
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +910 -412
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +4 -0
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/early-access.d.ts +148 -0
- package/dist/admin/apps/early-access.d.ts.map +1 -0
- package/dist/admin/apps/early-access.js +133 -0
- package/dist/admin/apps/early-access.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +643 -118
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +214 -1
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- 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 +45 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +3733 -2428
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +193 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +71 -0
- package/dist/apps/data/FxOracle.d.ts.map +1 -0
- package/dist/apps/data/FxOracle.js +97 -0
- package/dist/apps/data/FxOracle.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +3575 -1358
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +230 -88
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +963 -342
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/addresses.js +11 -9
- package/dist/apps/data/routes/addresses.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +108 -57
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +88 -45
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/exchanges.d.ts +952 -201
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +518 -76
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/funding.d.ts +8 -2
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +32 -6
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts +392 -315
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +152 -77
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +166 -173
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +14 -12
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +277 -536
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +96 -77
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +82 -17
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +70 -13
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +307 -0
- package/dist/apps/data/routes/valuation.d.ts.map +1 -0
- package/dist/apps/data/routes/valuation.js +292 -0
- package/dist/apps/data/routes/valuation.js.map +1 -0
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +7 -5
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +459 -479
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +124 -25
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +8606 -1681
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +12 -1
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +23 -0
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +5 -3
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +88 -48
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +10 -2
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +218 -0
- package/dist/apps/management/routes/faucet.d.ts.map +1 -0
- package/dist/apps/management/routes/faucet.js +139 -0
- package/dist/apps/management/routes/faucet.js.map +1 -0
- package/dist/apps/management/routes/invitations.d.ts +121 -121
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +3 -0
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +197 -168
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
- package/dist/apps/management/routes/invite-links.js +26 -1
- package/dist/apps/management/routes/invite-links.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +2 -0
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +4 -0
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +49 -49
- package/dist/apps/management/routes/orgs.d.ts +91 -91
- package/dist/apps/management/routes/projects.d.ts +115 -115
- package/dist/apps/management/routes/usage.d.ts +35 -32
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +4 -0
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
- package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
- package/dist/apps/management/routes/verified-token-requests.js +271 -0
- package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
- package/dist/apps/management/routes/webhooks.d.ts +6977 -0
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
- package/dist/apps/management/routes/webhooks.js +573 -0
- package/dist/apps/management/routes/webhooks.js.map +1 -0
- package/dist/apps/mcp.d.ts +9 -2
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +9 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +7 -2
- package/dist/cli/Cli.js.map +1 -1
- package/dist/db/Db.d.ts +4 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +83 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +41 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +45 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earlyAccess.d.ts +50 -0
- package/dist/db/tables/earlyAccess.d.ts.map +1 -0
- package/dist/db/tables/earlyAccess.js +63 -0
- package/dist/db/tables/earlyAccess.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +12 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +23 -0
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +10 -2
- package/dist/db/tables/inviteLinks.d.ts.map +1 -1
- package/dist/db/tables/inviteLinks.js +10 -1
- package/dist/db/tables/inviteLinks.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +2 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
- package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokenRequests.js +120 -0
- package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +16 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +43 -2
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +60 -45
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +69 -15
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +74 -25
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +19 -1
- 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/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +9 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +2 -2
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +3 -0
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Schema.d.ts +14 -5
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +23 -5
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Value.d.ts +18 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +11 -1
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +10 -1
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/Webhooks.d.ts +24 -3
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +34 -8
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +2 -2
- package/src/App.ts +9 -1
- package/src/Client.test-d.ts +638 -6
- package/src/admin/App.ts +4 -0
- package/src/admin/apps/early-access.test.ts +125 -0
- package/src/admin/apps/early-access.ts +175 -0
- package/src/admin/apps/verified-tokens.test.ts +201 -0
- package/src/admin/apps/verified-tokens.ts +532 -286
- package/src/admin/ui/plugin.ts +2 -1
- package/src/admin/ui/src/lib/early-access.ts +63 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
- package/src/admin/ui/src/routeTree.gen.ts +33 -2
- package/src/admin/ui/src/routes/__root.tsx +2 -1
- package/src/admin/ui/src/routes/early-access.tsx +157 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
- package/src/admin/ui.generated.ts +48 -33
- package/src/apps/data/App.ts +22 -14
- package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
- package/src/apps/data/FundingQuoteProviders.ts +257 -4
- package/src/apps/data/FxOracle.test.ts +118 -0
- package/src/apps/data/FxOracle.ts +153 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/activities.test.ts +173 -3
- package/src/apps/data/routes/activities.ts +403 -132
- package/src/apps/data/routes/addresses.ts +19 -9
- package/src/apps/data/routes/balances.test.ts +144 -11
- package/src/apps/data/routes/balances.ts +140 -51
- package/src/apps/data/routes/exchanges.test.ts +540 -9
- package/src/apps/data/routes/exchanges.ts +690 -103
- package/src/apps/data/routes/funding.test.ts +84 -0
- package/src/apps/data/routes/funding.ts +38 -6
- package/src/apps/data/routes/receipts.test.ts +125 -23
- package/src/apps/data/routes/receipts.ts +205 -100
- package/src/apps/data/routes/tokens.test.ts +43 -0
- package/src/apps/data/routes/tokens.ts +27 -12
- package/src/apps/data/routes/transactions.test.ts +72 -19
- package/src/apps/data/routes/transactions.ts +162 -92
- package/src/apps/data/routes/transfers.test.ts +146 -4
- package/src/apps/data/routes/transfers.ts +91 -13
- package/src/apps/data/routes/valuation.test.ts +422 -0
- package/src/apps/data/routes/valuation.ts +423 -0
- package/src/apps/data/routes/verified-tokens.ts +14 -2
- package/src/apps/data/routes/webhooks.test.ts +56 -8
- package/src/apps/data/routes/webhooks.ts +144 -27
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +13 -0
- package/src/apps/management/routes/api-keys.test.ts +87 -0
- package/src/apps/management/routes/api-keys.ts +4 -3
- package/src/apps/management/routes/billing.test.ts +112 -1
- package/src/apps/management/routes/billing.ts +7 -2
- package/src/apps/management/routes/faucet.test.ts +79 -0
- package/src/apps/management/routes/faucet.ts +185 -0
- package/src/apps/management/routes/invitations.test.ts +172 -0
- package/src/apps/management/routes/invitations.ts +3 -0
- package/src/apps/management/routes/invite-links.test.ts +117 -0
- package/src/apps/management/routes/invite-links.ts +43 -1
- package/src/apps/management/routes/me.test.ts +140 -88
- package/src/apps/management/routes/me.ts +6 -0
- package/src/apps/management/routes/usage.test.ts +6 -0
- package/src/apps/management/routes/usage.ts +7 -0
- package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
- package/src/apps/management/routes/verified-token-requests.ts +397 -0
- package/src/apps/management/routes/webhooks.test.ts +426 -0
- package/src/apps/management/routes/webhooks.ts +672 -0
- package/src/apps/mcp.test.ts +13 -0
- package/src/apps/mcp.ts +18 -5
- package/src/cli/Cli.test.ts +36 -0
- package/src/cli/Cli.ts +7 -2
- package/src/db/Db.test.ts +19 -0
- package/src/db/Db.ts +95 -0
- package/src/db/Schema.ts +53 -0
- package/src/db/tables/earlyAccess.ts +86 -0
- package/src/db/tables/invitations.ts +26 -1
- package/src/db/tables/inviteLinks.ts +22 -3
- package/src/db/tables/sponsoredTransactions.ts +4 -0
- package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
- package/src/db/tables/verifiedTokenRequests.ts +182 -0
- package/src/db/tables/webhookSubscriptions.ts +31 -0
- package/src/internal/Auth.ts +73 -45
- package/src/internal/FundingQuotes.test.ts +143 -1
- package/src/internal/FundingQuotes.ts +95 -28
- package/src/internal/Log.test-d.ts +21 -0
- package/src/internal/Log.test.ts +50 -0
- package/src/internal/Log.ts +24 -1
- package/src/internal/MetricSink.test.ts +160 -0
- package/src/internal/MetricSink.ts +10 -0
- package/src/internal/OpenApi.ts +3 -0
- package/src/internal/Schema.ts +50 -5
- package/src/internal/Value.test.ts +20 -0
- package/src/internal/Value.ts +24 -1
- package/src/internal/VerifiedTokens.ts +9 -1
- package/src/internal/Webhooks.test.ts +140 -0
- package/src/internal/Webhooks.ts +53 -9
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from 'zod/mini'
|
|
2
2
|
|
|
3
3
|
import * as FundingQuotes from '../../internal/FundingQuotes.js'
|
|
4
|
+
import * as Store from '../../internal/Store.js'
|
|
4
5
|
|
|
5
6
|
type Fetch = typeof globalThis.fetch
|
|
6
7
|
|
|
@@ -10,13 +11,23 @@ const defaultAcrossBaseUrl = 'https://app.across.to/api'
|
|
|
10
11
|
const defaultRelayBaseUrl = 'https://api.relay.link'
|
|
11
12
|
const defaultRhinoBaseUrl = 'https://api.rhino.fi/bridge'
|
|
12
13
|
const defaultSquidBaseUrl = 'https://v2.api.squidrouter.com/v2'
|
|
14
|
+
const defaultSymbiosisBaseUrl = 'https://api.symbiosis.finance/crosschain'
|
|
15
|
+
// Keep one upstream slot free for deployment smoke tests and operational probes.
|
|
16
|
+
const symbiosisRateLimit = 4
|
|
17
|
+
const symbiosisRateLimitWindowMs = 1_000
|
|
13
18
|
const providerOperations = {
|
|
14
19
|
across: 'swap/approval',
|
|
15
20
|
relay: 'quote/v2',
|
|
16
21
|
rhino: 'quote/bridge-swap/public',
|
|
17
22
|
squid: 'v2/route',
|
|
23
|
+
symbiosis: 'v2/quote',
|
|
18
24
|
} as const
|
|
19
25
|
|
|
26
|
+
/** Returns the stable upstream operation used by a funding quote provider. */
|
|
27
|
+
export function providerOperation(provider: FundingQuotes.ProviderId) {
|
|
28
|
+
return providerOperations[provider]
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
namespace schema {
|
|
21
32
|
const IntegerString = z.string().check(z.regex(/^\d+$/))
|
|
22
33
|
|
|
@@ -65,6 +76,30 @@ namespace schema {
|
|
|
65
76
|
}),
|
|
66
77
|
}),
|
|
67
78
|
})
|
|
79
|
+
|
|
80
|
+
export const SymbiosisToken = z.object({
|
|
81
|
+
address: z.string(),
|
|
82
|
+
chainId: z.number().check(z.int(), z.nonnegative()),
|
|
83
|
+
decimals: z.number().check(z.int(), z.nonnegative()),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const SymbiosisTokenAmount = z.extend(SymbiosisToken, { amount: IntegerString })
|
|
87
|
+
|
|
88
|
+
export const SymbiosisQuote = z.object({
|
|
89
|
+
estimatedTime: z.optional(z.number().check(z.nonnegative())),
|
|
90
|
+
kind: z.union([z.literal('bridge'), z.literal('crosschain-swap'), z.literal('intent-swap')]),
|
|
91
|
+
routes: z
|
|
92
|
+
.array(
|
|
93
|
+
z.object({
|
|
94
|
+
tokens: z.array(SymbiosisToken).check(z.minLength(1)),
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
.check(z.minLength(1)),
|
|
98
|
+
tokenAmountOut: SymbiosisTokenAmount,
|
|
99
|
+
tokenAmountOutMin: SymbiosisTokenAmount,
|
|
100
|
+
tx: z.object({ chainId: z.number().check(z.int(), z.nonnegative()) }),
|
|
101
|
+
type: z.union([z.literal('evm'), z.literal('tron')]),
|
|
102
|
+
})
|
|
68
103
|
}
|
|
69
104
|
|
|
70
105
|
/** Creates the live funding quote adapters configured for this deployment. */
|
|
@@ -74,6 +109,7 @@ export function createAdapters(options: createAdapters.Options = {}) {
|
|
|
74
109
|
...(options.relay ? { relay: relay(options.relay) } : {}),
|
|
75
110
|
...(options.rhino ? { rhino: rhino(options.rhino) } : {}),
|
|
76
111
|
...(options.squid ? { squid: squid(options.squid) } : {}),
|
|
112
|
+
...(options.symbiosis ? { symbiosis: symbiosis(options.symbiosis) } : {}),
|
|
77
113
|
} satisfies FundingQuotes.ProviderAdapters
|
|
78
114
|
}
|
|
79
115
|
|
|
@@ -88,6 +124,8 @@ export declare namespace createAdapters {
|
|
|
88
124
|
rhino?: rhino.Options | false | undefined
|
|
89
125
|
/** Squid quote adapter options. */
|
|
90
126
|
squid?: squid.Options | false | undefined
|
|
127
|
+
/** Symbiosis quote adapter options. */
|
|
128
|
+
symbiosis?: symbiosis.Options | false | undefined
|
|
91
129
|
}
|
|
92
130
|
}
|
|
93
131
|
|
|
@@ -137,6 +175,22 @@ export declare namespace configureSquid {
|
|
|
137
175
|
}
|
|
138
176
|
}
|
|
139
177
|
|
|
178
|
+
/** Resolves Symbiosis deployment configuration. */
|
|
179
|
+
export function configureSymbiosis(
|
|
180
|
+
options: configureSymbiosis.Options = {},
|
|
181
|
+
): symbiosis.Options | undefined {
|
|
182
|
+
if (!options.userAddress) return undefined
|
|
183
|
+
return { userAddress: options.userAddress }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export declare namespace configureSymbiosis {
|
|
187
|
+
/** Optional deployment binding required to enable Symbiosis. */
|
|
188
|
+
type Options = {
|
|
189
|
+
/** Symbiosis quote-user binding. */
|
|
190
|
+
userAddress?: string | undefined
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
140
194
|
/** Creates an Across quote adapter backed by `/swap/approval`. */
|
|
141
195
|
export function across(options: across.Options): FundingQuotes.ProviderAdapter {
|
|
142
196
|
assertAcrossConfiguration(options)
|
|
@@ -397,9 +451,87 @@ export declare namespace squid {
|
|
|
397
451
|
}
|
|
398
452
|
}
|
|
399
453
|
|
|
454
|
+
/** Creates a Symbiosis quote adapter backed by `/v2/quote`. */
|
|
455
|
+
export function symbiosis(options: symbiosis.Options): FundingQuotes.ProviderAdapter {
|
|
456
|
+
assertUserAddress(options.userAddress)
|
|
457
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultSymbiosisBaseUrl)
|
|
458
|
+
const fetcher = options.fetch ?? defaultFetch
|
|
459
|
+
const now = options.now ?? (() => new Date())
|
|
460
|
+
const rateLimitStore = options.rateLimitStore ?? Store.memory()
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
async probeQuote({ candidate }, signal) {
|
|
464
|
+
const fromChainId = Number(providerChainId(candidate.sourceChain, 'symbiosis'))
|
|
465
|
+
const fromToken = symbiosisSourceToken(candidate.sourceChain, candidate.sourceToken)
|
|
466
|
+
const toChainId = Number(eip155ChainId(candidate.destinationChain))
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
await consumeSymbiosisQuota(rateLimitStore, now())
|
|
470
|
+
const quote = await requestJson(new URL(`${baseUrl}/v2/quote`), {
|
|
471
|
+
body: {
|
|
472
|
+
from: options.userAddress,
|
|
473
|
+
slippage: 100,
|
|
474
|
+
to: options.userAddress,
|
|
475
|
+
tokenAmountIn: {
|
|
476
|
+
address: fromToken,
|
|
477
|
+
amount: candidate.sourceAmount.amount,
|
|
478
|
+
chainId: fromChainId,
|
|
479
|
+
decimals: candidate.sourceToken.decimals,
|
|
480
|
+
symbol: candidate.sourceToken.symbol,
|
|
481
|
+
},
|
|
482
|
+
tokenOut: {
|
|
483
|
+
address: candidate.destinationToken.address,
|
|
484
|
+
chainId: toChainId,
|
|
485
|
+
decimals: candidate.destinationToken.decimals,
|
|
486
|
+
symbol: candidate.destinationToken.symbol,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
fetcher,
|
|
490
|
+
headers: { 'x-partner-id': 'tempo' },
|
|
491
|
+
method: 'POST',
|
|
492
|
+
signal,
|
|
493
|
+
})
|
|
494
|
+
return formatSymbiosisResult(candidate, quote, now())
|
|
495
|
+
} catch (cause) {
|
|
496
|
+
const unavailable =
|
|
497
|
+
providerUnavailable(cause, [
|
|
498
|
+
'AMOUNT_TOO_LOW',
|
|
499
|
+
'NO_ROUTE',
|
|
500
|
+
'NO_ROUTE_FOUND',
|
|
501
|
+
'ROUTE_NOT_FOUND',
|
|
502
|
+
'UNSUPPORTED_ROUTE',
|
|
503
|
+
]) ?? symbiosisUnavailable(cause)
|
|
504
|
+
if (!unavailable) throw cause
|
|
505
|
+
return unavailableResult({
|
|
506
|
+
detail: 'symbiosis:quote-v2',
|
|
507
|
+
message: unavailable,
|
|
508
|
+
now: now(),
|
|
509
|
+
source: 'providerQuote',
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export declare namespace symbiosis {
|
|
517
|
+
/** Symbiosis quote adapter options. */
|
|
518
|
+
type Options = {
|
|
519
|
+
/** Override for tests or Symbiosis test environments. */
|
|
520
|
+
baseUrl?: string | undefined
|
|
521
|
+
/** Fetch implementation override for tests. */
|
|
522
|
+
fetch?: Fetch | undefined
|
|
523
|
+
/** Clock override for deterministic tests. */
|
|
524
|
+
now?: (() => Date) | undefined
|
|
525
|
+
/** Atomic store used to enforce the provider-wide quote quota. */
|
|
526
|
+
rateLimitStore?: Store.Store | undefined
|
|
527
|
+
/** Probe wallet address used as the Symbiosis sender and recipient. */
|
|
528
|
+
userAddress: string
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
400
532
|
/** Returns bounded provider failure details suitable for structured request logs. */
|
|
401
533
|
export function providerFailure(options: providerFailure.Options): providerFailure.Result {
|
|
402
|
-
const operation =
|
|
534
|
+
const operation = providerOperation(options.provider)
|
|
403
535
|
if (
|
|
404
536
|
options.cause instanceof FundingQuotes.ProviderAdapterTimeoutError ||
|
|
405
537
|
(options.cause instanceof DOMException && options.cause.name === 'AbortError')
|
|
@@ -417,6 +549,8 @@ export function providerFailure(options: providerFailure.Options): providerFailu
|
|
|
417
549
|
}
|
|
418
550
|
if (options.cause instanceof ProviderPayloadError)
|
|
419
551
|
return { failure: 'payload', id: options.provider, operation }
|
|
552
|
+
if (options.cause instanceof ProviderRateLimitError)
|
|
553
|
+
return { failure: 'rate_limit', id: options.provider, operation }
|
|
420
554
|
if (options.cause instanceof TypeError)
|
|
421
555
|
return { failure: 'network', id: options.provider, operation }
|
|
422
556
|
return { failure: 'unknown', id: options.provider, operation }
|
|
@@ -431,7 +565,7 @@ export declare namespace providerFailure {
|
|
|
431
565
|
/** Stable provider error code, when one is safe to retain. */
|
|
432
566
|
code?: string | undefined
|
|
433
567
|
/** Failure boundary that rejected the provider request. */
|
|
434
|
-
failure: 'http' | 'network' | 'payload' | 'timeout' | 'unknown'
|
|
568
|
+
failure: 'http' | 'network' | 'payload' | 'rate_limit' | 'timeout' | 'unknown'
|
|
435
569
|
/** Provider identifier. */
|
|
436
570
|
id: FundingQuotes.ProviderId
|
|
437
571
|
/** Provider operation identifier. */
|
|
@@ -585,6 +719,68 @@ function formatSquidResult(
|
|
|
585
719
|
}
|
|
586
720
|
}
|
|
587
721
|
|
|
722
|
+
function formatSymbiosisResult(
|
|
723
|
+
candidate: FundingQuotes.QuoteCandidate,
|
|
724
|
+
quote: unknown,
|
|
725
|
+
now: Date,
|
|
726
|
+
): FundingQuotes.QuoteProbeResult {
|
|
727
|
+
const parsed = schema.SymbiosisQuote.safeParse(quote)
|
|
728
|
+
if (!parsed.success) throw new ProviderPayloadError()
|
|
729
|
+
const response = parsed.data
|
|
730
|
+
const sourceChainId = Number(providerChainId(candidate.sourceChain, 'symbiosis'))
|
|
731
|
+
const sourceToken = symbiosisSourceToken(candidate.sourceChain, candidate.sourceToken)
|
|
732
|
+
const firstToken = response.routes[0]!.tokens[0]!
|
|
733
|
+
const lastRoute = response.routes[response.routes.length - 1]!
|
|
734
|
+
const lastToken = lastRoute.tokens[lastRoute.tokens.length - 1]!
|
|
735
|
+
if (
|
|
736
|
+
response.tx.chainId !== sourceChainId ||
|
|
737
|
+
response.type !== candidate.sourceChain.kind ||
|
|
738
|
+
!matchesSymbiosisToken(firstToken, {
|
|
739
|
+
address: sourceToken,
|
|
740
|
+
chainId: sourceChainId,
|
|
741
|
+
decimals: candidate.sourceToken.decimals,
|
|
742
|
+
}) ||
|
|
743
|
+
!matchesSymbiosisToken(lastToken, {
|
|
744
|
+
address: candidate.destinationToken.address,
|
|
745
|
+
chainId: Number(eip155ChainId(candidate.destinationChain)),
|
|
746
|
+
decimals: candidate.destinationToken.decimals,
|
|
747
|
+
}) ||
|
|
748
|
+
!matchesSymbiosisToken(response.tokenAmountOut, {
|
|
749
|
+
address: candidate.destinationToken.address,
|
|
750
|
+
chainId: Number(eip155ChainId(candidate.destinationChain)),
|
|
751
|
+
decimals: candidate.destinationToken.decimals,
|
|
752
|
+
}) ||
|
|
753
|
+
!matchesSymbiosisToken(response.tokenAmountOutMin, {
|
|
754
|
+
address: candidate.destinationToken.address,
|
|
755
|
+
chainId: Number(eip155ChainId(candidate.destinationChain)),
|
|
756
|
+
decimals: candidate.destinationToken.decimals,
|
|
757
|
+
}) ||
|
|
758
|
+
BigInt(response.tokenAmountOut.amount) <= 0n ||
|
|
759
|
+
BigInt(response.tokenAmountOutMin.amount) <= 0n ||
|
|
760
|
+
BigInt(response.tokenAmountOutMin.amount) > BigInt(response.tokenAmountOut.amount)
|
|
761
|
+
)
|
|
762
|
+
throw new ProviderPayloadError()
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
minimumOutputAmount: response.tokenAmountOutMin.amount,
|
|
766
|
+
outputAmount: response.tokenAmountOut.amount,
|
|
767
|
+
outputAmountFormatted: decimalFromBaseUnits(
|
|
768
|
+
response.tokenAmountOut.amount,
|
|
769
|
+
candidate.destinationToken.decimals,
|
|
770
|
+
),
|
|
771
|
+
quality: {
|
|
772
|
+
...(response.estimatedTime === undefined
|
|
773
|
+
? {}
|
|
774
|
+
: { estimatedSeconds: Math.ceil(response.estimatedTime) }),
|
|
775
|
+
liquiditySource: 'providerQuote',
|
|
776
|
+
sourceDetail: 'symbiosis:quote-v2',
|
|
777
|
+
tier: 'liquid',
|
|
778
|
+
},
|
|
779
|
+
sampledAt: now.toISOString(),
|
|
780
|
+
status: 'available',
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
588
784
|
function unavailableResult(options: {
|
|
589
785
|
detail: string
|
|
590
786
|
message: string
|
|
@@ -664,6 +860,22 @@ function squidUnavailable(cause: unknown) {
|
|
|
664
860
|
return undefined
|
|
665
861
|
}
|
|
666
862
|
|
|
863
|
+
function symbiosisUnavailable(cause: unknown) {
|
|
864
|
+
if (!(cause instanceof ProviderResponseError) || cause.status !== 400) return undefined
|
|
865
|
+
const body = record(cause.body)
|
|
866
|
+
if (body['message'] === 'Amount is too low to cover fees') return 'AMOUNT_TOO_LOW'
|
|
867
|
+
return undefined
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function consumeSymbiosisQuota(store: Store.Store, now: Date) {
|
|
871
|
+
const time = now.getTime()
|
|
872
|
+
const bucket = Math.floor(time / symbiosisRateLimitWindowMs)
|
|
873
|
+
const count = await Store.increment(store, `funding:symbiosis:quote:${bucket}`, {
|
|
874
|
+
ttl: symbiosisRateLimitWindowMs * 2,
|
|
875
|
+
})
|
|
876
|
+
if (count > symbiosisRateLimit) throw new ProviderRateLimitError()
|
|
877
|
+
}
|
|
878
|
+
|
|
667
879
|
function providerChainId(
|
|
668
880
|
quoteChain: FundingQuotes.QuoteCandidate['sourceChain'],
|
|
669
881
|
provider: string,
|
|
@@ -694,6 +906,16 @@ function rhinoToken(token: FundingQuotes.TokenRef) {
|
|
|
694
906
|
return id
|
|
695
907
|
}
|
|
696
908
|
|
|
909
|
+
function symbiosisSourceToken(chain: FundingQuotes.ChainRef, token: FundingQuotes.TokenRef) {
|
|
910
|
+
if (chain.kind !== 'tron') return token.address
|
|
911
|
+
const tokens: Record<string, string> = {
|
|
912
|
+
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t: '0xa614f803b6fd780986a42c78ec9c7f77e6ded13c',
|
|
913
|
+
}
|
|
914
|
+
const id = tokens[token.address]
|
|
915
|
+
if (!id) throw new UnsupportedProviderTokenError()
|
|
916
|
+
return id
|
|
917
|
+
}
|
|
918
|
+
|
|
697
919
|
function matchesToken(
|
|
698
920
|
token: z.output<typeof schema.AcrossQuote>['inputToken'],
|
|
699
921
|
chain: FundingQuotes.ChainRef,
|
|
@@ -718,6 +940,29 @@ function matchesSquidToken(
|
|
|
718
940
|
)
|
|
719
941
|
}
|
|
720
942
|
|
|
943
|
+
function matchesSymbiosisToken(
|
|
944
|
+
token: z.output<typeof schema.SymbiosisToken>,
|
|
945
|
+
options: matchesSymbiosisToken.Options,
|
|
946
|
+
) {
|
|
947
|
+
return (
|
|
948
|
+
token.address.toLowerCase() === options.address.toLowerCase() &&
|
|
949
|
+
token.chainId === options.chainId &&
|
|
950
|
+
token.decimals === options.decimals
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
declare namespace matchesSymbiosisToken {
|
|
955
|
+
/** Expected provider token identity. */
|
|
956
|
+
type Options = {
|
|
957
|
+
/** Provider-specific token address. */
|
|
958
|
+
address: string
|
|
959
|
+
/** Provider-specific chain id. */
|
|
960
|
+
chainId: number
|
|
961
|
+
/** Token decimal precision. */
|
|
962
|
+
decimals: number
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
721
966
|
function eip155ChainId(chain: { id: string; kind: string }) {
|
|
722
967
|
if (chain.kind !== 'evm') throw new UnsupportedProviderChainError()
|
|
723
968
|
const match = /^eip155:(\d+)$/.exec(chain.id)
|
|
@@ -785,8 +1030,12 @@ function assertAcrossConfiguration(options: across.Options) {
|
|
|
785
1030
|
}
|
|
786
1031
|
|
|
787
1032
|
function assertSquidConfiguration(options: squid.Options) {
|
|
788
|
-
if (options.integratorId.length === 0
|
|
789
|
-
|
|
1033
|
+
if (options.integratorId.length === 0) throw new ProviderConfigurationError()
|
|
1034
|
+
assertUserAddress(options.userAddress)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function assertUserAddress(userAddress: string) {
|
|
1038
|
+
if (!/^0x[\da-fA-F]{40}$/.test(userAddress)) throw new ProviderConfigurationError()
|
|
790
1039
|
}
|
|
791
1040
|
|
|
792
1041
|
class ProviderConfigurationError extends Error {
|
|
@@ -797,6 +1046,10 @@ class ProviderPayloadError extends Error {
|
|
|
797
1046
|
override name = 'FundingQuoteProviders.ProviderPayloadError'
|
|
798
1047
|
}
|
|
799
1048
|
|
|
1049
|
+
class ProviderRateLimitError extends Error {
|
|
1050
|
+
override name = 'FundingQuoteProviders.ProviderRateLimitError'
|
|
1051
|
+
}
|
|
1052
|
+
|
|
800
1053
|
class ProviderResponseError extends Error {
|
|
801
1054
|
override name = 'FundingQuoteProviders.ProviderResponseError'
|
|
802
1055
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as FxOracle from './FxOracle.js'
|
|
2
|
+
|
|
3
|
+
const set = {
|
|
4
|
+
asOf: '2026-01-01',
|
|
5
|
+
base: 'EUR',
|
|
6
|
+
rates: { AUD: '1.6', USD: '1.25' },
|
|
7
|
+
} satisfies FxOracle.RateSet
|
|
8
|
+
|
|
9
|
+
function feedUrl(usd: string) {
|
|
10
|
+
const body = `<Envelope><Cube><Cube time="2026-01-01"><Cube currency="AUD" rate="1.6"/><Cube currency="USD" rate="${usd}"/></Cube></Cube></Envelope>`
|
|
11
|
+
return `data:application/xml,${encodeURIComponent(body)}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('from', () => {
|
|
15
|
+
test('applies the default ttl and preserves overrides', () => {
|
|
16
|
+
const oracle = FxOracle.from({ name: 'test', rates: async () => set })
|
|
17
|
+
expect(oracle.name).toBe('test')
|
|
18
|
+
expect(oracle.ttl).toMatchInlineSnapshot(`3600000`)
|
|
19
|
+
expect(FxOracle.from({ name: 'test', rates: async () => set, ttl: 5 }).ttl).toBe(5)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('resolves the definition rates', async () => {
|
|
23
|
+
const oracle = FxOracle.from({ name: 'test', rates: async () => set })
|
|
24
|
+
await expect(oracle.rates()).resolves.toEqual(set)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('rethrows rates failures as RatesError', async () => {
|
|
28
|
+
const oracle = FxOracle.from({
|
|
29
|
+
name: 'test',
|
|
30
|
+
rates: async () => {
|
|
31
|
+
throw new Error('boom')
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
const error = await oracle.rates().catch((error: unknown) => error)
|
|
35
|
+
expect(error).toBeInstanceOf(FxOracle.RatesError)
|
|
36
|
+
expect(error).toMatchInlineSnapshot(
|
|
37
|
+
`[FxOracle.RatesError: FX oracle "test" failed to produce rates]`,
|
|
38
|
+
)
|
|
39
|
+
expect((error as Error).cause).toMatchInlineSnapshot(`[Error: boom]`)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('ecb', () => {
|
|
44
|
+
test('fetches and parses the daily reference rates', async () => {
|
|
45
|
+
const rates = await FxOracle.ecb({ url: feedUrl('1.25') }).rates()
|
|
46
|
+
expect(rates).toMatchInlineSnapshot(`
|
|
47
|
+
{
|
|
48
|
+
"asOf": "2026-01-01",
|
|
49
|
+
"base": "EUR",
|
|
50
|
+
"rates": {
|
|
51
|
+
"AUD": "1.6",
|
|
52
|
+
"USD": "1.25",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test.each(['', '-1.25', '1..25'])('rejects malformed rate %j', async (value) => {
|
|
59
|
+
const error = await FxOracle.ecb({ url: feedUrl(value) })
|
|
60
|
+
.rates()
|
|
61
|
+
.catch((error: unknown) => error)
|
|
62
|
+
expect(error).toBeInstanceOf(FxOracle.RatesError)
|
|
63
|
+
expect((error as Error).cause).toMatchInlineSnapshot(
|
|
64
|
+
`[FxOracle.ParseError: Rate feed returned an unrecognized payload]`,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('rejects non-positive rates', async () => {
|
|
69
|
+
const error = await FxOracle.ecb({ url: feedUrl('0.0') })
|
|
70
|
+
.rates()
|
|
71
|
+
.catch((error: unknown) => error)
|
|
72
|
+
expect(error).toBeInstanceOf(FxOracle.RatesError)
|
|
73
|
+
expect((error as Error).cause).toMatchInlineSnapshot(
|
|
74
|
+
`[FxOracle.ParseError: Rate feed returned an unrecognized payload]`,
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('rejects rates below fixed-point precision', async () => {
|
|
79
|
+
const error = await FxOracle.ecb({ url: feedUrl('0.0000000000000000001') })
|
|
80
|
+
.rates()
|
|
81
|
+
.catch((error: unknown) => error)
|
|
82
|
+
expect(error).toBeInstanceOf(FxOracle.RatesError)
|
|
83
|
+
expect((error as Error).cause).toMatchInlineSnapshot(
|
|
84
|
+
`[FxOracle.ParseError: Rate feed returned an unrecognized payload]`,
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('rejects with RatesError when the feed is unreachable', async () => {
|
|
89
|
+
const oracle = FxOracle.ecb({ url: 'http://127.0.0.1:1' })
|
|
90
|
+
const error = await oracle.rates().catch((error: unknown) => error)
|
|
91
|
+
expect(error).toBeInstanceOf(FxOracle.RatesError)
|
|
92
|
+
expect(error).toMatchInlineSnapshot(
|
|
93
|
+
`[FxOracle.RatesError: FX oracle "ecb" failed to produce rates]`,
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('rate', () => {
|
|
99
|
+
test('derives cross rates through the base', () => {
|
|
100
|
+
expect(FxOracle.rate(set, { from: 'USD', to: 'AUD' })).toMatchInlineSnapshot(
|
|
101
|
+
`1280000000000000000n`,
|
|
102
|
+
)
|
|
103
|
+
expect(FxOracle.rate(set, { from: 'EUR', to: 'USD' })).toMatchInlineSnapshot(
|
|
104
|
+
`1250000000000000000n`,
|
|
105
|
+
)
|
|
106
|
+
expect(FxOracle.rate(set, { from: 'USD', to: 'EUR' })).toMatchInlineSnapshot(
|
|
107
|
+
`800000000000000000n`,
|
|
108
|
+
)
|
|
109
|
+
expect(FxOracle.rate(set, { from: 'USD', to: 'USD' })).toMatchInlineSnapshot(
|
|
110
|
+
`1000000000000000000n`,
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('returns undefined for currencies the set does not price', () => {
|
|
115
|
+
expect(FxOracle.rate(set, { from: 'BTC', to: 'USD' })).toBeUndefined()
|
|
116
|
+
expect(FxOracle.rate(set, { from: 'USD', to: 'XXX' })).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Value as core_Value } from 'ox'
|
|
2
|
+
|
|
3
|
+
import * as Ttl from '../../internal/Ttl.js'
|
|
4
|
+
|
|
5
|
+
/** A pluggable fiat FX rate oracle consumed by the valuation endpoint. */
|
|
6
|
+
export type Oracle = {
|
|
7
|
+
/** Oracle identifier, surfaced as the valuation's `pricing.source` (e.g. `ecb`). */
|
|
8
|
+
name: string
|
|
9
|
+
/** Fetches the oracle's current rate set. */
|
|
10
|
+
rates: () => Promise<RateSet>
|
|
11
|
+
/** Cache lifetime for a fetched rate set, in milliseconds. */
|
|
12
|
+
ttl: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One oracle-published set of exchange rates quoted against a base currency. */
|
|
16
|
+
export type RateSet = {
|
|
17
|
+
/** Publication date of the rate set (e.g. `2026-07-20`). */
|
|
18
|
+
asOf: string
|
|
19
|
+
/** Currency the rates are quoted against (e.g. `EUR`). */
|
|
20
|
+
base: string
|
|
21
|
+
/** Currency code to units of that currency per one `base` unit, as decimal strings. */
|
|
22
|
+
rates: Record<string, string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fixed-point scale (decimal places) of cross rates returned by {@link rate}. */
|
|
26
|
+
export const precision = 18
|
|
27
|
+
|
|
28
|
+
/** Canonical ECB daily reference-rate feed (public, no key required). */
|
|
29
|
+
export const ecbUrl = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes an oracle definition into an {@link Oracle}: applies the default
|
|
33
|
+
* `ttl` and rethrows `rates()` failures as {@link RatesError}.
|
|
34
|
+
*/
|
|
35
|
+
export function from(oracle: from.Value): Oracle {
|
|
36
|
+
return {
|
|
37
|
+
name: oracle.name,
|
|
38
|
+
rates: async () => {
|
|
39
|
+
try {
|
|
40
|
+
return await oracle.rates()
|
|
41
|
+
} catch (cause) {
|
|
42
|
+
throw new RatesError(oracle.name, { cause })
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
ttl: oracle.ttl ?? Ttl.hours(1),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export declare namespace from {
|
|
50
|
+
/** Oracle definition accepted by {@link from}. */
|
|
51
|
+
type Value = Pick<Oracle, 'name' | 'rates'> & {
|
|
52
|
+
/** Cache lifetime for a fetched rate set, in milliseconds. @default `Ttl.hours(1)` */
|
|
53
|
+
ttl?: number | undefined
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* European Central Bank euro reference rates: one EUR-based set covering ~30
|
|
59
|
+
* currencies, published once per working day.
|
|
60
|
+
*/
|
|
61
|
+
export function ecb(options: ecb.Options = {}): Oracle {
|
|
62
|
+
const { fetch = globalThis.fetch, url = ecbUrl } = options
|
|
63
|
+
return from({
|
|
64
|
+
name: 'ecb',
|
|
65
|
+
rates: async () => {
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
headers: { accept: 'application/xml' },
|
|
68
|
+
signal: AbortSignal.timeout(10_000),
|
|
69
|
+
})
|
|
70
|
+
if (!response.ok) throw new ResponseError(response.status)
|
|
71
|
+
const text = await response.text()
|
|
72
|
+
|
|
73
|
+
// The daily file is a flat, stable XML shape; extract the `Cube`
|
|
74
|
+
// attributes directly rather than pulling in an XML parser.
|
|
75
|
+
const asOf = text.match(/<Cube time=['"](\d{4}-\d{2}-\d{2})['"]/)?.[1]
|
|
76
|
+
const rates: Record<string, string> = {}
|
|
77
|
+
for (const match of text.matchAll(
|
|
78
|
+
/<Cube currency=['"]([A-Z]{3})['"] rate=['"]([^'"]*)['"]/g,
|
|
79
|
+
)) {
|
|
80
|
+
const [, code, value] = match
|
|
81
|
+
if (
|
|
82
|
+
!code ||
|
|
83
|
+
!value ||
|
|
84
|
+
!/^\d+(?:\.\d+)?$/.test(value) ||
|
|
85
|
+
core_Value.from(value, precision) <= 0n
|
|
86
|
+
)
|
|
87
|
+
throw new ParseError()
|
|
88
|
+
rates[code] = value
|
|
89
|
+
}
|
|
90
|
+
if (!asOf || rates['USD'] === undefined) throw new ParseError()
|
|
91
|
+
return { asOf, base: 'EUR', rates }
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export declare namespace ecb {
|
|
97
|
+
/** Options for the ECB oracle. */
|
|
98
|
+
type Options = {
|
|
99
|
+
/** Fetch implementation used to retrieve the feed. @default `globalThis.fetch` */
|
|
100
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
101
|
+
/** Feed URL. @default {@link ecbUrl} */
|
|
102
|
+
url?: string | undefined
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Derives the cross rate from one currency to another through the set's base,
|
|
108
|
+
* scaled to {@link precision} decimal places. Returns `undefined` when the set
|
|
109
|
+
* prices neither leg of a currency.
|
|
110
|
+
*/
|
|
111
|
+
export function rate(set: RateSet, options: rate.Options): bigint | undefined {
|
|
112
|
+
const scaled = (code: string) => {
|
|
113
|
+
if (code === set.base) return 10n ** BigInt(precision)
|
|
114
|
+
const value = set.rates[code]
|
|
115
|
+
return value === undefined ? undefined : core_Value.from(value, precision)
|
|
116
|
+
}
|
|
117
|
+
const source = scaled(options.from)
|
|
118
|
+
const target = scaled(options.to)
|
|
119
|
+
if (source === undefined || target === undefined || source === 0n) return undefined
|
|
120
|
+
return (target * 10n ** BigInt(precision)) / source
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export declare namespace rate {
|
|
124
|
+
/** Options for deriving a cross rate. */
|
|
125
|
+
type Options = {
|
|
126
|
+
/** Currency code to convert from. */
|
|
127
|
+
from: string
|
|
128
|
+
/** Currency code to convert to. */
|
|
129
|
+
to: string
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Thrown when an oracle fails to produce its rate set. */
|
|
134
|
+
export class RatesError extends Error {
|
|
135
|
+
override name = 'FxOracle.RatesError'
|
|
136
|
+
constructor(oracle: string, options: { cause: unknown }) {
|
|
137
|
+
super(`FX oracle "${oracle}" failed to produce rates`, options)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class ParseError extends Error {
|
|
142
|
+
override name = 'FxOracle.ParseError'
|
|
143
|
+
constructor() {
|
|
144
|
+
super('Rate feed returned an unrecognized payload')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class ResponseError extends Error {
|
|
149
|
+
override name = 'FxOracle.ResponseError'
|
|
150
|
+
constructor(status: number) {
|
|
151
|
+
super(`Rate feed request failed with status ${status}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/apps/data/index.ts
CHANGED