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,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { AbiEvent } from 'ox';
|
|
2
|
+
import { AbiError, AbiEvent } from 'ox';
|
|
3
3
|
import { BaseError, ContractFunctionRevertedError, decodeFunctionData, parseAbi, } from 'viem';
|
|
4
|
-
import { Actions, Addresses, Tick } from 'viem/tempo';
|
|
4
|
+
import { Abis, Actions, Addresses, Tick } from 'viem/tempo';
|
|
5
5
|
import * as z from 'zod/mini';
|
|
6
6
|
import * as Auth from '../../../internal/Auth.js';
|
|
7
7
|
import * as Cache from '../../../internal/Cache.js';
|
|
@@ -14,10 +14,13 @@ import * as Timing from '../../../internal/Timing.js';
|
|
|
14
14
|
import * as Ttl from '../../../internal/Ttl.js';
|
|
15
15
|
import * as Value from '../../../internal/Value.js';
|
|
16
16
|
import * as VerifiedTokens from '../../../internal/VerifiedTokens.js';
|
|
17
|
+
import * as FxOracle from '../FxOracle.js';
|
|
17
18
|
import * as Tokens from './tokens.js';
|
|
19
|
+
import * as Valuation from './valuation.js';
|
|
18
20
|
// Stablecoin DEX precompile. Pair lifecycle is announced by `PairCreated` from
|
|
19
21
|
// this address; the indexer pre-decodes those into the `dex_pairs` table.
|
|
20
22
|
const stablecoinDex = Addresses.stablecoinDex.toLowerCase();
|
|
23
|
+
const contractPausedSelector = AbiError.getSelector(AbiError.fromAbi(Abis.tip20, 'ContractPaused'));
|
|
21
24
|
// `OrderCancelled(uint128 orderId)` — emitted when a maker cancels a resting
|
|
22
25
|
// order. Used by the orders endpoint to exclude cancelled orders from the
|
|
23
26
|
// resting set.
|
|
@@ -53,6 +56,18 @@ const swapAbi = parseAbi([
|
|
|
53
56
|
// pathUSD on-chain). Uses a fresh `Schema.tokenAddress(...)` so the example
|
|
54
57
|
// reaches the pipe input that path-param OpenAPI generation reads from.
|
|
55
58
|
const exampleBase = '0x20c000000000000000000000b9537d11c60e8b50';
|
|
59
|
+
const exampleSourceToken = '0x20c0000000000000000000008f5425160ebe5525';
|
|
60
|
+
const exampleApprovalCall = Actions.token.approve.call({
|
|
61
|
+
amount: 1000000n,
|
|
62
|
+
spender: Addresses.stablecoinDex,
|
|
63
|
+
token: exampleSourceToken,
|
|
64
|
+
});
|
|
65
|
+
const exampleSwapCall = Actions.dex.sell.call({
|
|
66
|
+
amountIn: 1000000n,
|
|
67
|
+
minAmountOut: 995000n,
|
|
68
|
+
tokenIn: exampleSourceToken,
|
|
69
|
+
tokenOut: exampleBase,
|
|
70
|
+
});
|
|
56
71
|
/**
|
|
57
72
|
* Tick spacing enforced by the stablecoin DEX precompile: orders must be
|
|
58
73
|
* placed at ticks divisible by 10 (a 1 bp price grid). Walking the depth at
|
|
@@ -82,6 +97,11 @@ const maxDepthLevels = 200;
|
|
|
82
97
|
* namespace because the OHLC `Query` refine references it at validation time.
|
|
83
98
|
*/
|
|
84
99
|
const maxBuckets = 500;
|
|
100
|
+
/** Largest amount accepted by the DEX precompile's uint128 swap arguments. */
|
|
101
|
+
const maxSwapAmount = 2n ** 128n - 1n;
|
|
102
|
+
/** Basis-point denominator used for caller-selected swap slippage. */
|
|
103
|
+
const slippageDenominator = 10000n;
|
|
104
|
+
const tokenFields = ['token.logoUri', 'token.verified'];
|
|
85
105
|
/** Converts the OHLC `interval` enum to a millisecond duration. */
|
|
86
106
|
function intervalMs(interval) {
|
|
87
107
|
switch (interval) {
|
|
@@ -157,6 +177,126 @@ export var schema;
|
|
|
157
177
|
.optional(z.boolean())
|
|
158
178
|
.check(z.describe('Whether Tempo has verified this token metadata.'), z.meta({ examples: [true] })),
|
|
159
179
|
}), 'One side of a trading pair (with metadata when requested via `include=tokens`).');
|
|
180
|
+
/** A token amount carrying its nominal value in the requested denomination. */
|
|
181
|
+
const ValuedAmount = z.extend(Schema.TokenAmount, {
|
|
182
|
+
valuation: z
|
|
183
|
+
.optional(z.nullable(Valuation.schema.Value))
|
|
184
|
+
.check(z.describe('The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
|
|
185
|
+
'unverified, its display currency has no rate, or rates were unavailable.')),
|
|
186
|
+
});
|
|
187
|
+
/** Schemas for creating an executable native DEX exchange quote. */
|
|
188
|
+
let createQuote;
|
|
189
|
+
(function (createQuote) {
|
|
190
|
+
const DestinationTokenAddress = Schema.tokenAddress(exampleBase);
|
|
191
|
+
const SourceTokenAddress = Schema.tokenAddress(exampleSourceToken);
|
|
192
|
+
/** Positive token amount accepted by the DEX's uint128 arguments. */
|
|
193
|
+
createQuote.Amount = Schema.DecimalString.check(z.refine((value) => {
|
|
194
|
+
const amount = BigInt(value);
|
|
195
|
+
return amount > 0n && amount <= maxSwapAmount;
|
|
196
|
+
}, { error: 'Amount must be between 1 and the maximum uint128 value.' }), z.describe('Positive token amount in the token’s smallest unit, encoded as a decimal string.'), z.meta({ examples: ['1000000'] }));
|
|
197
|
+
/** Optional token fields that require sources beyond RPC metadata. */
|
|
198
|
+
createQuote.Include = z
|
|
199
|
+
.enum(tokenFields)
|
|
200
|
+
.check(z.describe('Additional token fields to include in the quote response.'));
|
|
201
|
+
/** Parses comma-separated optional token fields. */
|
|
202
|
+
createQuote.includeQuery = Schema.includeQuery(createQuote.Include, 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.');
|
|
203
|
+
/** Query parameters selecting the Tempo chain and optional token fields. */
|
|
204
|
+
createQuote.Query = z
|
|
205
|
+
.strictObject({
|
|
206
|
+
chainId: Schema.ChainIdQuery,
|
|
207
|
+
denomination: Schema.Denomination,
|
|
208
|
+
include: createQuote.includeQuery,
|
|
209
|
+
})
|
|
210
|
+
.check(z.describe('Query parameters for creating a native DEX exchange quote.'));
|
|
211
|
+
/** Quote request with one pinned side and caller-selected slippage. */
|
|
212
|
+
createQuote.Request = z
|
|
213
|
+
.strictObject({
|
|
214
|
+
amount: createQuote.Amount.check(z.describe('Pinned amount in base units: source amount for `exactSource`, destination amount for `exactDestination`.')),
|
|
215
|
+
destinationToken: DestinationTokenAddress.check(z.describe('TIP-20 token the swap receives.')),
|
|
216
|
+
mode: z
|
|
217
|
+
.enum(['exactSource', 'exactDestination'])
|
|
218
|
+
.check(z.describe('Which side of the swap keeps the requested amount exact.'), z.meta({ examples: ['exactSource'] })),
|
|
219
|
+
slippageBps: z
|
|
220
|
+
.number()
|
|
221
|
+
.check(z.int(), z.gte(0), z.lt(10_000), z.describe('Allowed execution slippage in basis points, where 100 is 1%.'), z.meta({ examples: [50] })),
|
|
222
|
+
sourceToken: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
|
|
223
|
+
})
|
|
224
|
+
.check(z.refine((request) => request.sourceToken !== request.destinationToken, {
|
|
225
|
+
error: 'Source and destination tokens must differ.',
|
|
226
|
+
}), z.describe('Request for an executable quote on Tempo’s native stablecoin DEX.'));
|
|
227
|
+
/** One call in the unsigned Tempo transaction plan. */
|
|
228
|
+
createQuote.Call = z
|
|
229
|
+
.object({
|
|
230
|
+
data: Schema.Hex.check(z.describe('ABI-encoded call data.')),
|
|
231
|
+
to: Schema.Address.check(z.describe('Contract or precompile that receives the call.')),
|
|
232
|
+
value: Schema.Quantity.check(z.describe('Native token value sent with the call; native DEX swap calls use `0x0`.'), z.meta({ examples: ['0x0'] })),
|
|
233
|
+
})
|
|
234
|
+
.check(z.describe('One unsigned call in the Tempo transaction plan.'));
|
|
235
|
+
const QuoteToken = Schema.describe(z.partial(z.omit(Tokens.schema.Token, { id: true }), {
|
|
236
|
+
logoUri: true,
|
|
237
|
+
verified: true,
|
|
238
|
+
}), 'A token referenced by an exchange quote, with RPC metadata and optional enrichments.');
|
|
239
|
+
const DestinationToken = z.extend(QuoteToken, {
|
|
240
|
+
address: DestinationTokenAddress.check(z.describe('TIP-20 token the swap receives.')),
|
|
241
|
+
});
|
|
242
|
+
const SourceToken = z.extend(QuoteToken, {
|
|
243
|
+
address: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
|
|
244
|
+
});
|
|
245
|
+
const Transaction = z
|
|
246
|
+
.object({
|
|
247
|
+
calls: z.array(createQuote.Call).check(z.length(2), z.describe('Approval followed by the native DEX swap call.'), z.meta({
|
|
248
|
+
examples: [
|
|
249
|
+
[
|
|
250
|
+
{
|
|
251
|
+
data: exampleApprovalCall.data,
|
|
252
|
+
to: exampleApprovalCall.to,
|
|
253
|
+
value: '0x0',
|
|
254
|
+
},
|
|
255
|
+
{ data: exampleSwapCall.data, to: exampleSwapCall.to, value: '0x0' },
|
|
256
|
+
],
|
|
257
|
+
],
|
|
258
|
+
})),
|
|
259
|
+
chainId: Schema.ChainId.check(z.describe('Tempo chain where these calls execute.')),
|
|
260
|
+
})
|
|
261
|
+
.check(z.describe('Unsigned Tempo transaction plan.'));
|
|
262
|
+
/** Response-level valuation rate provenance. */
|
|
263
|
+
createQuote.Meta = z
|
|
264
|
+
.object({
|
|
265
|
+
pricing: z
|
|
266
|
+
.optional(Valuation.schema.Pricing)
|
|
267
|
+
.check(z.describe('Rate provenance for amount valuations. Present when conversion rates were consulted.')),
|
|
268
|
+
})
|
|
269
|
+
.check(z.describe('Response-level resources attached to this quote.'));
|
|
270
|
+
/** Native DEX quote plus the approval and swap calls needed to execute it. */
|
|
271
|
+
createQuote.Response = z
|
|
272
|
+
.discriminatedUnion('mode', [
|
|
273
|
+
z.object({
|
|
274
|
+
destinationAmount: ValuedAmount.check(z.describe('Quoted destination amount.')),
|
|
275
|
+
destinationAmountMin: ValuedAmount.check(z.describe('Minimum destination amount after slippage.')),
|
|
276
|
+
destinationToken: DestinationToken,
|
|
277
|
+
meta: z.optional(createQuote.Meta).check(z.describe('Valuation rate provenance for this quote.')),
|
|
278
|
+
mode: z
|
|
279
|
+
.literal('exactSource')
|
|
280
|
+
.check(z.describe('The source amount is fixed and the destination has a minimum.'), z.meta({ examples: ['exactSource'] })),
|
|
281
|
+
sourceAmount: ValuedAmount.check(z.describe('Exact source amount.')),
|
|
282
|
+
sourceToken: SourceToken.check(z.describe('Token spent by the swap.')),
|
|
283
|
+
transaction: Transaction,
|
|
284
|
+
}),
|
|
285
|
+
z.object({
|
|
286
|
+
destinationAmount: ValuedAmount.check(z.describe('Exact destination amount.')),
|
|
287
|
+
destinationToken: DestinationToken.check(z.describe('Token received by the swap.')),
|
|
288
|
+
meta: z.optional(createQuote.Meta).check(z.describe('Valuation rate provenance for this quote.')),
|
|
289
|
+
mode: z
|
|
290
|
+
.literal('exactDestination')
|
|
291
|
+
.check(z.describe('The destination amount is fixed and the source has a maximum.'), z.meta({ examples: ['exactDestination'] })),
|
|
292
|
+
sourceAmount: ValuedAmount.check(z.describe('Quoted source amount.')),
|
|
293
|
+
sourceAmountMax: ValuedAmount.check(z.describe('Maximum source amount after slippage.')),
|
|
294
|
+
sourceToken: SourceToken,
|
|
295
|
+
transaction: Transaction,
|
|
296
|
+
}),
|
|
297
|
+
])
|
|
298
|
+
.check(z.describe('Executable native DEX quote with slippage-protected calls.'));
|
|
299
|
+
})(createQuote = schema.createQuote || (schema.createQuote = {}));
|
|
160
300
|
/** Schemas for the getPairs operation. */
|
|
161
301
|
let getPairs;
|
|
162
302
|
(function (getPairs) {
|
|
@@ -255,20 +395,12 @@ export var schema;
|
|
|
255
395
|
/** Schemas for the getSwaps operation (the swap feed, optionally pair-scoped). */
|
|
256
396
|
let getSwaps;
|
|
257
397
|
(function (getSwaps) {
|
|
258
|
-
/**
|
|
259
|
-
* Optional related resource that callers opt into via `include`. Currently
|
|
260
|
-
* only `tokens` (embeds metadata for both pair tokens on each row's
|
|
261
|
-
* `sourceToken`/`destinationToken`).
|
|
262
|
-
*/
|
|
398
|
+
/** Optional token fields that require sources beyond RPC metadata. */
|
|
263
399
|
getSwaps.Include = z
|
|
264
|
-
.enum(
|
|
265
|
-
.check(z.describe('
|
|
266
|
-
/**
|
|
267
|
-
|
|
268
|
-
* resources to embed. Extra lookups (e.g. token metadata) only run when
|
|
269
|
-
* explicitly requested, keeping the base swap feed fast.
|
|
270
|
-
*/
|
|
271
|
-
getSwaps.includeQuery = Schema.includeQuery(getSwaps.Include, 'Comma-separated optional resources to embed, e.g. `tokens`.');
|
|
400
|
+
.enum(tokenFields)
|
|
401
|
+
.check(z.describe('Additional token fields to include in swap responses.'));
|
|
402
|
+
/** Parses comma-separated optional token fields. */
|
|
403
|
+
getSwaps.includeQuery = Schema.includeQuery(getSwaps.Include, 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.');
|
|
272
404
|
/** Query parameters for swap feed requests. */
|
|
273
405
|
getSwaps.Query = z
|
|
274
406
|
.strictObject({
|
|
@@ -276,6 +408,7 @@ export var schema;
|
|
|
276
408
|
'blockNumber.to': Schema.blockNumberBound('swaps', 'to'),
|
|
277
409
|
chainId: Schema.ChainIdQuery,
|
|
278
410
|
cursor: Schema.Cursor,
|
|
411
|
+
denomination: Schema.Denomination,
|
|
279
412
|
include: getSwaps.includeQuery,
|
|
280
413
|
limit: Schema.Limit,
|
|
281
414
|
maker: z
|
|
@@ -297,20 +430,13 @@ export var schema;
|
|
|
297
430
|
.check(z.describe('Query parameters for swap feed requests. Cursor-paginated only — a batched ' +
|
|
298
431
|
'transaction expands one fill group into a variable number of swap rows, so a ' +
|
|
299
432
|
'positional `page` has no stable meaning on this feed.'));
|
|
300
|
-
/**
|
|
301
|
-
|
|
302
|
-
* requested via `include=tokens`) annotated with the `amount` that moved
|
|
303
|
-
* on this side, as a decimal integer string in base units.
|
|
304
|
-
*/
|
|
305
|
-
getSwaps.SwapSide = z
|
|
306
|
-
.extend(schema.PairToken, {
|
|
307
|
-
amount: Schema.DecimalString.check(z.describe('Amount moved on this side, returned as a decimal integer string in the token’s smallest units.'), z.meta({ examples: ['1000000'] })),
|
|
308
|
-
})
|
|
309
|
-
.check(z.describe('One side of a swap: the token and amount that moved.'));
|
|
433
|
+
/** One swap-side token with RPC metadata and optional curated fields. */
|
|
434
|
+
getSwaps.SwapSide = Schema.describe(Tokens.schema.TokenReference, 'A token referenced by one side of a swap.');
|
|
310
435
|
/** A single maker-order fill within a swap, framed from the taker's perspective. */
|
|
311
436
|
getSwaps.Fill = z
|
|
312
437
|
.object({
|
|
313
|
-
|
|
438
|
+
destinationAmount: ValuedAmount.check(z.describe('Amount the taker received in this fill.')),
|
|
439
|
+
destinationToken: getSwaps.SwapSide.check(z.describe('Token the taker received in this fill.')),
|
|
314
440
|
logIndex: z
|
|
315
441
|
.number()
|
|
316
442
|
.check(z.int(), z.nonnegative(), z.describe('Log index of this fill within its block.'), z.meta({ examples: [0] })),
|
|
@@ -322,7 +448,8 @@ export var schema;
|
|
|
322
448
|
price: z
|
|
323
449
|
.string()
|
|
324
450
|
.check(z.regex(/^\d+(\.\d+)?$/), z.describe('Quote-per-base price at fill time, returned as a fixed-decimal string with 5 decimal places. This value is direction-independent and works well for charts.'), z.meta({ examples: ['1.00000'] })),
|
|
325
|
-
|
|
451
|
+
sourceAmount: ValuedAmount.check(z.describe('Amount the taker sent in this fill.')),
|
|
452
|
+
sourceToken: getSwaps.SwapSide.check(z.describe('Token the taker sent in this fill.')),
|
|
326
453
|
})
|
|
327
454
|
.check(z.describe('One maker-order fill within a swap, described from the taker’s perspective.'));
|
|
328
455
|
/**
|
|
@@ -337,7 +464,8 @@ export var schema;
|
|
|
337
464
|
blockNumber: z
|
|
338
465
|
.number()
|
|
339
466
|
.check(z.int(), z.nonnegative(), z.describe('Block number where the swap was included.'), z.meta({ examples: [23456789] })),
|
|
340
|
-
|
|
467
|
+
destinationAmount: ValuedAmount.check(z.describe('Total amount the taker received in this swap.')),
|
|
468
|
+
destinationToken: getSwaps.SwapSide.check(z.describe('Destination token for the final hop.')),
|
|
341
469
|
filledAt: z.iso
|
|
342
470
|
.datetime()
|
|
343
471
|
.check(z.describe('Block timestamp when the swap was filled onchain.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
|
|
@@ -359,7 +487,8 @@ export var schema;
|
|
|
359
487
|
route: z
|
|
360
488
|
.array(Schema.TokenAddress)
|
|
361
489
|
.check(z.describe('Token path for the swap, from source through any intermediate tokens to destination. `route.length - 1` is the hop count; two entries means a direct swap.')),
|
|
362
|
-
|
|
490
|
+
sourceAmount: ValuedAmount.check(z.describe('Total amount the taker sent in this swap.')),
|
|
491
|
+
sourceToken: getSwaps.SwapSide.check(z.describe('Source token for the first hop.')),
|
|
363
492
|
taker: Schema.Address.check(z.describe('Taker address that initiated the swap.')),
|
|
364
493
|
transactionHash: Schema.Hash.check(z.describe('Transaction hash for the transaction containing the swap.')),
|
|
365
494
|
})
|
|
@@ -371,9 +500,21 @@ export var schema;
|
|
|
371
500
|
* rows per page; a transaction batching several unrelated swaps by one
|
|
372
501
|
* taker may have its rows split across consecutive pages.
|
|
373
502
|
*/
|
|
503
|
+
/** Page-level resources: valuation rate provenance. */
|
|
504
|
+
getSwaps.Meta = z
|
|
505
|
+
.object({
|
|
506
|
+
pricing: z
|
|
507
|
+
.optional(Valuation.schema.Pricing)
|
|
508
|
+
.check(z.describe('Rate provenance for amount valuations. Present when conversion rates were ' +
|
|
509
|
+
'consulted; absent when every value was identity-valued or rates were unavailable.')),
|
|
510
|
+
})
|
|
511
|
+
.check(z.describe('Page-level resources attached to this response.'));
|
|
374
512
|
getSwaps.Response = z
|
|
375
513
|
.object({
|
|
376
514
|
data: z.array(getSwaps.Swap).check(z.describe('Swaps returned on this page.')),
|
|
515
|
+
meta: z
|
|
516
|
+
.optional(getSwaps.Meta)
|
|
517
|
+
.check(z.describe('Page-level resources, such as valuation rate provenance.')),
|
|
377
518
|
nextCursor: Schema.NextCursor,
|
|
378
519
|
})
|
|
379
520
|
.check(z.describe('A paginated list of swaps on Tempo’s built-in stablecoin exchange.'));
|
|
@@ -802,8 +943,206 @@ export var schema;
|
|
|
802
943
|
* `token_balances_snapshot` row attribute cleanly to one pair. Pairs whose
|
|
803
944
|
* DEX-escrow balance is zero are excluded from the liquidity ranking.
|
|
804
945
|
*/
|
|
805
|
-
export function exchanges() {
|
|
946
|
+
export function exchanges(options = {}) {
|
|
947
|
+
const oracle = options.fx?.oracle ?? FxOracle.ecb();
|
|
806
948
|
return new Hono()
|
|
949
|
+
.post('/v1/exchange/quotes', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('query', schema.createQuote.Query, {
|
|
950
|
+
code: 'query_invalid',
|
|
951
|
+
message: 'Invalid query parameters',
|
|
952
|
+
}), OpenApi.validate('json', schema.createQuote.Request, {
|
|
953
|
+
code: 'body_invalid',
|
|
954
|
+
message: 'Invalid request body',
|
|
955
|
+
}), OpenApi.describeRoute({
|
|
956
|
+
description: 'Creates a priced exchange preview on Tempo’s native stablecoin DEX with unsigned approval and swap calls. This endpoint does not submit a transaction.',
|
|
957
|
+
operationId: 'createExchangeQuote',
|
|
958
|
+
responses: OpenApi.responses({
|
|
959
|
+
errors: {
|
|
960
|
+
400: { codes: ['body_invalid', 'query_invalid', 'quote_amount_out_of_range'] },
|
|
961
|
+
404: {
|
|
962
|
+
codes: ['quote_not_available'],
|
|
963
|
+
description: 'The native DEX cannot price an executable exchange for these terms.',
|
|
964
|
+
},
|
|
965
|
+
502: 'The Tempo RPC could not quote the swap or resolve token metadata.',
|
|
966
|
+
},
|
|
967
|
+
success: {
|
|
968
|
+
description: 'A native DEX quote and the unsigned calls needed to execute it.',
|
|
969
|
+
schema: schema.createQuote.Response,
|
|
970
|
+
},
|
|
971
|
+
}),
|
|
972
|
+
summary: 'Create quote',
|
|
973
|
+
tags: ['Exchange'],
|
|
974
|
+
}), async (c) => {
|
|
975
|
+
if (Auth.narrowAccess)
|
|
976
|
+
return Auth.paidAccessError(c);
|
|
977
|
+
if (OpenApi.narrowValidation)
|
|
978
|
+
return OpenApi.validationError(c, {
|
|
979
|
+
code: 'query_invalid',
|
|
980
|
+
message: 'Invalid query parameters',
|
|
981
|
+
});
|
|
982
|
+
if (OpenApi.narrowValidation)
|
|
983
|
+
return OpenApi.validationError(c, {
|
|
984
|
+
code: 'body_invalid',
|
|
985
|
+
message: 'Invalid request body',
|
|
986
|
+
});
|
|
987
|
+
const request = c.req.valid('json');
|
|
988
|
+
const options = c.req.valid('query');
|
|
989
|
+
const chainId = options.chainId ?? c.get('chainId');
|
|
990
|
+
const client = c.get('getClient')(chainId);
|
|
991
|
+
const amount = BigInt(request.amount);
|
|
992
|
+
const slippageBps = BigInt(request.slippageBps);
|
|
993
|
+
c.header('Cache-Control', 'no-store');
|
|
994
|
+
try {
|
|
995
|
+
// Value quoted amounts via their curated display currency; snapshot
|
|
996
|
+
// or rate failures degrade the values to `null`, never the quote.
|
|
997
|
+
const denomination = options.denomination ?? 'USD';
|
|
998
|
+
const snapshot = await VerifiedTokens.snapshot(c, chainId).catch(() => undefined);
|
|
999
|
+
const rates = await Valuation.ratesFor(c, {
|
|
1000
|
+
currencies: snapshot
|
|
1001
|
+
? [request.destinationToken, request.sourceToken].flatMap((address) => {
|
|
1002
|
+
const held = snapshot.byAddress.get(address.toLowerCase())?.currency;
|
|
1003
|
+
return held === undefined ? [] : [held];
|
|
1004
|
+
})
|
|
1005
|
+
: [],
|
|
1006
|
+
denomination,
|
|
1007
|
+
oracle,
|
|
1008
|
+
});
|
|
1009
|
+
const valued = (baseUnits, address, token) => ({
|
|
1010
|
+
...Value.tokenAmount({
|
|
1011
|
+
baseUnits,
|
|
1012
|
+
currency: token.currency,
|
|
1013
|
+
decimals: token.decimals,
|
|
1014
|
+
}),
|
|
1015
|
+
valuation: Valuation.valuationFor({
|
|
1016
|
+
amount: BigInt(baseUnits),
|
|
1017
|
+
denomination,
|
|
1018
|
+
rates,
|
|
1019
|
+
token: snapshot?.byAddress.get(address.toLowerCase()),
|
|
1020
|
+
}),
|
|
1021
|
+
});
|
|
1022
|
+
if (request.mode === 'exactSource') {
|
|
1023
|
+
const destinationAmount = await Actions.dex.getSellQuote(client, {
|
|
1024
|
+
amountIn: amount,
|
|
1025
|
+
tokenIn: request.sourceToken,
|
|
1026
|
+
tokenOut: request.destinationToken,
|
|
1027
|
+
});
|
|
1028
|
+
if (destinationAmount === 0n)
|
|
1029
|
+
return quoteNotAvailable(c);
|
|
1030
|
+
const minimumAmount = (destinationAmount * (slippageDenominator - slippageBps) + slippageDenominator - 1n) /
|
|
1031
|
+
slippageDenominator;
|
|
1032
|
+
const approve = Actions.token.approve.call(client, {
|
|
1033
|
+
amount,
|
|
1034
|
+
spender: Addresses.stablecoinDex,
|
|
1035
|
+
token: request.sourceToken,
|
|
1036
|
+
});
|
|
1037
|
+
const swap = Actions.dex.sell.call({
|
|
1038
|
+
amountIn: amount,
|
|
1039
|
+
minAmountOut: minimumAmount,
|
|
1040
|
+
tokenIn: request.sourceToken,
|
|
1041
|
+
tokenOut: request.destinationToken,
|
|
1042
|
+
});
|
|
1043
|
+
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1044
|
+
addresses: [request.destinationToken, request.sourceToken],
|
|
1045
|
+
chainId,
|
|
1046
|
+
include: options.include,
|
|
1047
|
+
});
|
|
1048
|
+
const destinationToken = tokensByAddress.get(request.destinationToken);
|
|
1049
|
+
const sourceToken = tokensByAddress.get(request.sourceToken);
|
|
1050
|
+
if (!destinationToken || !sourceToken)
|
|
1051
|
+
throw new Error('Unable to resolve exchange quote token metadata');
|
|
1052
|
+
return c.json(Response.validated(schema.createQuote.Response, {
|
|
1053
|
+
destinationAmount: valued(destinationAmount, request.destinationToken, destinationToken),
|
|
1054
|
+
destinationAmountMin: valued(minimumAmount, request.destinationToken, destinationToken),
|
|
1055
|
+
destinationToken: {
|
|
1056
|
+
...destinationToken,
|
|
1057
|
+
address: request.destinationToken,
|
|
1058
|
+
},
|
|
1059
|
+
...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
|
|
1060
|
+
mode: request.mode,
|
|
1061
|
+
sourceAmount: valued(request.amount, request.sourceToken, sourceToken),
|
|
1062
|
+
sourceToken: {
|
|
1063
|
+
...sourceToken,
|
|
1064
|
+
address: request.sourceToken,
|
|
1065
|
+
},
|
|
1066
|
+
transaction: {
|
|
1067
|
+
calls: [
|
|
1068
|
+
{ data: approve.data, to: approve.to, value: '0x0' },
|
|
1069
|
+
{ data: swap.data, to: swap.to, value: '0x0' },
|
|
1070
|
+
],
|
|
1071
|
+
chainId,
|
|
1072
|
+
},
|
|
1073
|
+
}), 200);
|
|
1074
|
+
}
|
|
1075
|
+
const sourceAmount = await Actions.dex.getBuyQuote(client, {
|
|
1076
|
+
amountOut: amount,
|
|
1077
|
+
tokenIn: request.sourceToken,
|
|
1078
|
+
tokenOut: request.destinationToken,
|
|
1079
|
+
});
|
|
1080
|
+
if (sourceAmount === 0n)
|
|
1081
|
+
return quoteNotAvailable(c);
|
|
1082
|
+
const maximumAmount = (sourceAmount * (slippageDenominator + slippageBps)) / slippageDenominator;
|
|
1083
|
+
if (maximumAmount > maxSwapAmount)
|
|
1084
|
+
return Response.error(c, {
|
|
1085
|
+
code: 'quote_amount_out_of_range',
|
|
1086
|
+
message: 'Slippage-adjusted source amount exceeds the native DEX limit',
|
|
1087
|
+
status: 400,
|
|
1088
|
+
});
|
|
1089
|
+
const approve = Actions.token.approve.call(client, {
|
|
1090
|
+
amount: maximumAmount,
|
|
1091
|
+
spender: Addresses.stablecoinDex,
|
|
1092
|
+
token: request.sourceToken,
|
|
1093
|
+
});
|
|
1094
|
+
const swap = Actions.dex.buy.call({
|
|
1095
|
+
amountOut: amount,
|
|
1096
|
+
maxAmountIn: maximumAmount,
|
|
1097
|
+
tokenIn: request.sourceToken,
|
|
1098
|
+
tokenOut: request.destinationToken,
|
|
1099
|
+
});
|
|
1100
|
+
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1101
|
+
addresses: [request.destinationToken, request.sourceToken],
|
|
1102
|
+
chainId,
|
|
1103
|
+
include: options.include,
|
|
1104
|
+
});
|
|
1105
|
+
const destinationToken = tokensByAddress.get(request.destinationToken);
|
|
1106
|
+
const sourceToken = tokensByAddress.get(request.sourceToken);
|
|
1107
|
+
if (!destinationToken || !sourceToken)
|
|
1108
|
+
throw new Error('Unable to resolve exchange quote token metadata');
|
|
1109
|
+
return c.json(Response.validated(schema.createQuote.Response, {
|
|
1110
|
+
destinationAmount: valued(request.amount, request.destinationToken, destinationToken),
|
|
1111
|
+
destinationToken: {
|
|
1112
|
+
...destinationToken,
|
|
1113
|
+
address: request.destinationToken,
|
|
1114
|
+
},
|
|
1115
|
+
...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
|
|
1116
|
+
mode: request.mode,
|
|
1117
|
+
sourceAmount: valued(sourceAmount, request.sourceToken, sourceToken),
|
|
1118
|
+
sourceAmountMax: valued(maximumAmount, request.sourceToken, sourceToken),
|
|
1119
|
+
sourceToken: {
|
|
1120
|
+
...sourceToken,
|
|
1121
|
+
address: request.sourceToken,
|
|
1122
|
+
},
|
|
1123
|
+
transaction: {
|
|
1124
|
+
calls: [
|
|
1125
|
+
{ data: approve.data, to: approve.to, value: '0x0' },
|
|
1126
|
+
{ data: swap.data, to: swap.to, value: '0x0' },
|
|
1127
|
+
],
|
|
1128
|
+
chainId,
|
|
1129
|
+
},
|
|
1130
|
+
}), 200);
|
|
1131
|
+
}
|
|
1132
|
+
catch (cause) {
|
|
1133
|
+
if (request.mode === 'exactSource' && isQuoteAmountOutOfRange(cause))
|
|
1134
|
+
return Response.error(c, {
|
|
1135
|
+
code: 'quote_amount_out_of_range',
|
|
1136
|
+
message: 'Quote amount exceeds the native DEX limit',
|
|
1137
|
+
status: 400,
|
|
1138
|
+
});
|
|
1139
|
+
if (isQuoteUnavailable(cause))
|
|
1140
|
+
return quoteNotAvailable(c);
|
|
1141
|
+
if (cause instanceof Valuation.UnsupportedDenominationError)
|
|
1142
|
+
return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 });
|
|
1143
|
+
return Response.upstream(c, cause);
|
|
1144
|
+
}
|
|
1145
|
+
})
|
|
807
1146
|
.get('/v1/exchange/swaps', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('query', schema.getSwaps.Query, {
|
|
808
1147
|
code: 'query_invalid',
|
|
809
1148
|
message: 'Invalid query parameters',
|
|
@@ -812,7 +1151,7 @@ export function exchanges() {
|
|
|
812
1151
|
operationId: 'getSwaps',
|
|
813
1152
|
responses: OpenApi.responses({
|
|
814
1153
|
errors: {
|
|
815
|
-
502: 'The indexer could not serve the exchange data right now.',
|
|
1154
|
+
502: 'The indexer or Tempo RPC could not serve the exchange data right now.',
|
|
816
1155
|
},
|
|
817
1156
|
success: {
|
|
818
1157
|
description: 'A page of swaps from Tempo’s built-in stablecoin exchange.',
|
|
@@ -822,11 +1161,10 @@ export function exchanges() {
|
|
|
822
1161
|
summary: 'List swaps',
|
|
823
1162
|
tags: ['Exchange'],
|
|
824
1163
|
}), Cache.response({
|
|
825
|
-
// Exchanges are a newest-first feed; refresh quickly. The `
|
|
826
|
-
// segment isolates
|
|
827
|
-
// per-fill pages (cursor pages cache as `immutable`).
|
|
1164
|
+
// Exchanges are a newest-first feed; refresh quickly. The `v3` cache
|
|
1165
|
+
// segment isolates this response shape from earlier swap pages.
|
|
828
1166
|
cacheControl: Cache.policies.feed,
|
|
829
|
-
name: 'tempo-api:exchange:
|
|
1167
|
+
name: 'tempo-api:exchange:v3:swaps',
|
|
830
1168
|
key: (c) => Cache.urlKey(c, schema.getSwaps.Query),
|
|
831
1169
|
}), async (c) => {
|
|
832
1170
|
if (Auth.narrowAccess)
|
|
@@ -838,11 +1176,9 @@ export function exchanges() {
|
|
|
838
1176
|
});
|
|
839
1177
|
const options = c.req.valid('query');
|
|
840
1178
|
const chainId = options.chainId ?? c.get('chainId');
|
|
841
|
-
// Cursor pages
|
|
842
|
-
// upgrade their cache policy; the head page keeps the route's `feed`
|
|
843
|
-
// default.
|
|
1179
|
+
// Cursor pages carry current valuations and curated verification data.
|
|
844
1180
|
if (options.cursor !== undefined)
|
|
845
|
-
Cache.setPolicy(c, Cache.policies.
|
|
1181
|
+
Cache.setPolicy(c, Cache.policies.metadata);
|
|
846
1182
|
try {
|
|
847
1183
|
// The pair index (base address → pair) resolves each fill's book
|
|
848
1184
|
// orientation (the order events only carry the base-side `token`;
|
|
@@ -863,39 +1199,89 @@ export function exchanges() {
|
|
|
863
1199
|
toTimestamp: options['timestamp.to'],
|
|
864
1200
|
transactionHash: options.transactionHash,
|
|
865
1201
|
});
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1202
|
+
// `route` spans every swap and fill side, so one lookup resolves each
|
|
1203
|
+
// token once for required RPC metadata and requested curated fields.
|
|
1204
|
+
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1205
|
+
addresses: page.data.flatMap((swap) => swap.route),
|
|
1206
|
+
chainId,
|
|
1207
|
+
include: options.include,
|
|
1208
|
+
});
|
|
1209
|
+
// Value amounts via their curated display currency; snapshot or rate
|
|
1210
|
+
// failures degrade the values to `null`, never the feed.
|
|
1211
|
+
const denomination = options.denomination ?? 'USD';
|
|
1212
|
+
const snapshot = page.data.length > 0
|
|
1213
|
+
? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
|
|
1214
|
+
: undefined;
|
|
1215
|
+
const rates = await Valuation.ratesFor(c, {
|
|
1216
|
+
currencies: snapshot
|
|
1217
|
+
? page.data.flatMap((swap) => swap.route.flatMap((address) => {
|
|
1218
|
+
const held = snapshot.byAddress.get(address.toLowerCase())?.currency;
|
|
1219
|
+
return held === undefined ? [] : [held];
|
|
1220
|
+
}))
|
|
1221
|
+
: [],
|
|
1222
|
+
denomination,
|
|
1223
|
+
oracle,
|
|
1224
|
+
});
|
|
1225
|
+
const enrichSide = (side) => {
|
|
1226
|
+
const token = tokensByAddress.get(side.address);
|
|
1227
|
+
if (!token)
|
|
1228
|
+
throw new Error(`Token metadata unavailable for ${side.address}`);
|
|
1229
|
+
return {
|
|
1230
|
+
amount: {
|
|
1231
|
+
...Value.tokenAmount({
|
|
1232
|
+
baseUnits: side.amount,
|
|
1233
|
+
currency: token.currency,
|
|
1234
|
+
decimals: token.decimals,
|
|
1235
|
+
}),
|
|
1236
|
+
valuation: Valuation.valuationFor({
|
|
1237
|
+
amount: BigInt(side.amount),
|
|
1238
|
+
denomination,
|
|
1239
|
+
rates,
|
|
1240
|
+
token: snapshot?.byAddress.get(side.address.toLowerCase()),
|
|
1241
|
+
}),
|
|
1242
|
+
},
|
|
1243
|
+
token: {
|
|
1244
|
+
address: side.address,
|
|
1245
|
+
currency: token.currency,
|
|
1246
|
+
decimals: token.decimals,
|
|
1247
|
+
...(token.logoUri !== undefined ? { logoUri: token.logoUri } : {}),
|
|
1248
|
+
name: token.name,
|
|
1249
|
+
symbol: token.symbol,
|
|
1250
|
+
...(token.verified !== undefined ? { verified: token.verified } : {}),
|
|
1251
|
+
},
|
|
881
1252
|
};
|
|
882
|
-
|
|
1253
|
+
};
|
|
1254
|
+
const data = page.data.map((swap) => {
|
|
1255
|
+
const destination = enrichSide(swap.destinationToken);
|
|
1256
|
+
const source = enrichSide(swap.sourceToken);
|
|
1257
|
+
return {
|
|
883
1258
|
...swap,
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1259
|
+
destinationAmount: destination.amount,
|
|
1260
|
+
destinationToken: destination.token,
|
|
1261
|
+
fills: swap.fills.map((fill) => {
|
|
1262
|
+
const destination_fill = enrichSide(fill.destinationToken);
|
|
1263
|
+
const source_fill = enrichSide(fill.sourceToken);
|
|
1264
|
+
return {
|
|
1265
|
+
...fill,
|
|
1266
|
+
destinationAmount: destination_fill.amount,
|
|
1267
|
+
destinationToken: destination_fill.token,
|
|
1268
|
+
sourceAmount: source_fill.amount,
|
|
1269
|
+
sourceToken: source_fill.token,
|
|
1270
|
+
};
|
|
1271
|
+
}),
|
|
1272
|
+
sourceAmount: source.amount,
|
|
1273
|
+
sourceToken: source.token,
|
|
1274
|
+
};
|
|
1275
|
+
});
|
|
893
1276
|
return c.json(Response.validated(schema.getSwaps.Response, {
|
|
894
1277
|
data,
|
|
1278
|
+
...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
|
|
895
1279
|
nextCursor: page.nextCursor,
|
|
896
1280
|
}), 200);
|
|
897
1281
|
}
|
|
898
1282
|
catch (cause) {
|
|
1283
|
+
if (cause instanceof Valuation.UnsupportedDenominationError)
|
|
1284
|
+
return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 });
|
|
899
1285
|
return Response.upstream(c, cause);
|
|
900
1286
|
}
|
|
901
1287
|
})
|
|
@@ -969,6 +1355,7 @@ export function exchanges() {
|
|
|
969
1355
|
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
970
1356
|
addresses: data.flatMap((pair) => [pair.base.address, pair.quote.address]),
|
|
971
1357
|
chainId,
|
|
1358
|
+
include: tokenFields,
|
|
972
1359
|
});
|
|
973
1360
|
data = data.map((pair) => {
|
|
974
1361
|
const base = tokensByAddress.get(pair.base.address);
|
|
@@ -1053,6 +1440,7 @@ export function exchanges() {
|
|
|
1053
1440
|
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1054
1441
|
addresses: [target.base, target.quote],
|
|
1055
1442
|
chainId,
|
|
1443
|
+
include: tokenFields,
|
|
1056
1444
|
});
|
|
1057
1445
|
const baseMeta = tokensByAddress.get(target.base);
|
|
1058
1446
|
const quoteMeta = tokensByAddress.get(target.quote);
|
|
@@ -1150,6 +1538,7 @@ export function exchanges() {
|
|
|
1150
1538
|
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1151
1539
|
addresses: data.flatMap((o) => [o.pair.base.address, o.pair.quote.address]),
|
|
1152
1540
|
chainId,
|
|
1541
|
+
include: tokenFields,
|
|
1153
1542
|
});
|
|
1154
1543
|
data = data.map((o) => {
|
|
1155
1544
|
const base = tokensByAddress.get(o.pair.base.address);
|
|
@@ -1429,6 +1818,7 @@ export function exchanges() {
|
|
|
1429
1818
|
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1430
1819
|
addresses: [target.base, target.quote],
|
|
1431
1820
|
chainId,
|
|
1821
|
+
include: tokenFields,
|
|
1432
1822
|
});
|
|
1433
1823
|
const baseMeta = tokensByAddress.get(target.base);
|
|
1434
1824
|
const quoteMeta = tokensByAddress.get(target.quote);
|
|
@@ -1519,6 +1909,7 @@ export function exchanges() {
|
|
|
1519
1909
|
const tokensByAddress = await resolveTokensByAddress(c, {
|
|
1520
1910
|
addresses: [target.base, target.quote],
|
|
1521
1911
|
chainId,
|
|
1912
|
+
include: tokenFields,
|
|
1522
1913
|
});
|
|
1523
1914
|
const baseMeta = tokensByAddress.get(target.base);
|
|
1524
1915
|
const quoteMeta = tokensByAddress.get(target.quote);
|
|
@@ -1552,23 +1943,46 @@ function pairNotFoundError(c, options) {
|
|
|
1552
1943
|
: 'Pair not found';
|
|
1553
1944
|
return Response.error(c, { code: 'pair_not_found', message, status: 404 });
|
|
1554
1945
|
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Batch-resolves token metadata for a set of addresses, sharing one
|
|
1557
|
-
* verified-token snapshot and one created-at lookup across the whole set.
|
|
1558
|
-
* Best-effort: addresses whose metadata cannot be resolved are absent from
|
|
1559
|
-
* the returned map, so callers keep their bare `{ address }` shapes. Shared
|
|
1560
|
-
* by every `include=tokens` embed in this module.
|
|
1561
|
-
*/
|
|
1946
|
+
/** Resolves RPC token metadata and explicitly requested enrichments. Unresolvable addresses are omitted. */
|
|
1562
1947
|
async function resolveTokensByAddress(c, options) {
|
|
1563
|
-
const { addresses, chainId } = options;
|
|
1948
|
+
const { addresses, chainId, include } = options;
|
|
1949
|
+
const includeLogoUri = include.includes('token.logoUri');
|
|
1950
|
+
const includeVerified = include.includes('token.verified');
|
|
1564
1951
|
const tokensByAddress = new Map();
|
|
1565
1952
|
const uniqueTokens = [...new Set(addresses)];
|
|
1566
1953
|
if (uniqueTokens.length === 0)
|
|
1567
1954
|
return tokensByAddress;
|
|
1568
|
-
const snapshot =
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1955
|
+
const snapshot = includeLogoUri || includeVerified
|
|
1956
|
+
? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
|
|
1957
|
+
: undefined;
|
|
1958
|
+
const resolved = await Timing.time(c, 'tokens', () => Promise.all(uniqueTokens.map(async (token) => {
|
|
1959
|
+
try {
|
|
1960
|
+
const [logoUri, metadata] = await Promise.all([
|
|
1961
|
+
includeLogoUri
|
|
1962
|
+
? Timing.time(c, 'token_logo', () => Tokens.getTokenLogo(c, { address: token, chainId })).catch(() => undefined)
|
|
1963
|
+
: undefined,
|
|
1964
|
+
Timing.time(c, 'token_metadata', () => Tokens.getTokenMetadata(c, { address: token, chainId })),
|
|
1965
|
+
]);
|
|
1966
|
+
return [
|
|
1967
|
+
token,
|
|
1968
|
+
{
|
|
1969
|
+
address: token,
|
|
1970
|
+
currency: metadata.currency,
|
|
1971
|
+
decimals: metadata.decimals,
|
|
1972
|
+
id: token,
|
|
1973
|
+
logoUri: includeLogoUri
|
|
1974
|
+
? (logoUri ?? snapshot?.byAddress.get(token)?.logoUri ?? metadata.logoUri)
|
|
1975
|
+
: undefined,
|
|
1976
|
+
name: metadata.name,
|
|
1977
|
+
symbol: metadata.symbol,
|
|
1978
|
+
verified: includeVerified ? snapshot?.byAddress.has(token) : undefined,
|
|
1979
|
+
},
|
|
1980
|
+
];
|
|
1981
|
+
}
|
|
1982
|
+
catch {
|
|
1983
|
+
return [token, undefined];
|
|
1984
|
+
}
|
|
1985
|
+
})));
|
|
1572
1986
|
for (const [token, metadata] of resolved)
|
|
1573
1987
|
if (metadata)
|
|
1574
1988
|
tokensByAddress.set(token, metadata);
|
|
@@ -3438,6 +3852,34 @@ function isOrderDoesNotExist(error) {
|
|
|
3438
3852
|
return (revert instanceof ContractFunctionRevertedError &&
|
|
3439
3853
|
revert.data?.errorName === 'OrderDoesNotExist');
|
|
3440
3854
|
}
|
|
3855
|
+
/** Returns the native DEX 404 used when no executable quote is available. */
|
|
3856
|
+
function quoteNotAvailable(c) {
|
|
3857
|
+
return Response.error(c, {
|
|
3858
|
+
code: 'quote_not_available',
|
|
3859
|
+
message: 'No executable quote is available on Tempo’s native DEX',
|
|
3860
|
+
status: 404,
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
/** Distinguishes native DEX route failures from RPC transport failures. */
|
|
3864
|
+
function isQuoteUnavailable(error) {
|
|
3865
|
+
if (!(error instanceof BaseError))
|
|
3866
|
+
return false;
|
|
3867
|
+
const revert = error.walk((e) => e instanceof ContractFunctionRevertedError);
|
|
3868
|
+
if (!(revert instanceof ContractFunctionRevertedError))
|
|
3869
|
+
return false;
|
|
3870
|
+
return (revert.raw === contractPausedSelector ||
|
|
3871
|
+
['InsufficientLiquidity', 'InvalidBaseToken', 'InvalidToken', 'PairDoesNotExist'].includes(revert.data?.errorName ?? ''));
|
|
3872
|
+
}
|
|
3873
|
+
/** Detects uint128 arithmetic overflow from exact-source DEX quote calls. */
|
|
3874
|
+
function isQuoteAmountOutOfRange(error) {
|
|
3875
|
+
if (!(error instanceof BaseError))
|
|
3876
|
+
return false;
|
|
3877
|
+
const revert = error.walk((e) => e instanceof ContractFunctionRevertedError);
|
|
3878
|
+
if (!(revert instanceof ContractFunctionRevertedError))
|
|
3879
|
+
return false;
|
|
3880
|
+
const data = revert.data;
|
|
3881
|
+
return data?.errorName === 'Panic' && data.args?.[0] === 0x11n;
|
|
3882
|
+
}
|
|
3441
3883
|
/**
|
|
3442
3884
|
* Resolves a single page of `OrderFilled` events for one order. Pagination is
|
|
3443
3885
|
* keyset on `(block_num, log_idx)`, which is unique and append-only for the
|