tapimo 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/ApiKey.d.ts +65 -0
- package/dist/ApiKey.d.ts.map +1 -0
- package/dist/ApiKey.js +105 -0
- package/dist/ApiKey.js.map +1 -0
- package/dist/ApiKeySource.d.ts +166 -0
- package/dist/ApiKeySource.d.ts.map +1 -0
- package/dist/ApiKeySource.js +234 -0
- package/dist/ApiKeySource.js.map +1 -0
- package/dist/App.d.ts +19317 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +612 -0
- package/dist/App.js.map +1 -0
- package/dist/Assets.d.ts +44 -0
- package/dist/Assets.d.ts.map +1 -0
- package/dist/Assets.js +23 -0
- package/dist/Assets.js.map +1 -0
- package/dist/Auth.d.ts +2 -0
- package/dist/Auth.d.ts.map +1 -0
- package/dist/Auth.js +2 -0
- package/dist/Auth.js.map +1 -0
- package/dist/Cli.d.ts +21 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +13 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Client.d.ts +20 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +24 -0
- package/dist/Client.js.map +1 -0
- package/dist/Metrics.d.ts +70 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/Metrics.js +91 -0
- package/dist/Metrics.js.map +1 -0
- package/dist/Scope.d.ts +35 -0
- package/dist/Scope.d.ts.map +1 -0
- package/dist/Scope.js +38 -0
- package/dist/Scope.js.map +1 -0
- package/dist/Webhooks.d.ts +79 -0
- package/dist/Webhooks.d.ts.map +1 -0
- package/dist/Webhooks.js +107 -0
- package/dist/Webhooks.js.map +1 -0
- package/dist/admin/Access.d.ts +41 -0
- package/dist/admin/Access.d.ts.map +1 -0
- package/dist/admin/Access.js +75 -0
- package/dist/admin/Access.js.map +1 -0
- package/dist/admin/App.d.ts +1221 -0
- package/dist/admin/App.d.ts.map +1 -0
- package/dist/admin/App.js +113 -0
- package/dist/admin/App.js.map +1 -0
- package/dist/admin/apps/api-keys.d.ts +209 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -0
- package/dist/admin/apps/api-keys.js +129 -0
- package/dist/admin/apps/api-keys.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +46 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -0
- package/dist/admin/apps/scopes.js +41 -0
- package/dist/admin/apps/scopes.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +1206 -0
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
- package/dist/admin/apps/verified-tokens.js +485 -0
- package/dist/admin/apps/verified-tokens.js.map +1 -0
- package/dist/admin/index.d.ts +3 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/ui.generated.d.ts +11 -0
- package/dist/admin/ui.generated.d.ts.map +1 -0
- package/dist/admin/ui.generated.js +43 -0
- package/dist/admin/ui.generated.js.map +1 -0
- package/dist/apps/activities.d.ts +18725 -0
- package/dist/apps/activities.d.ts.map +1 -0
- package/dist/apps/activities.js +3138 -0
- package/dist/apps/activities.js.map +1 -0
- package/dist/apps/addresses.d.ts +3662 -0
- package/dist/apps/addresses.d.ts.map +1 -0
- package/dist/apps/addresses.js +22 -0
- package/dist/apps/addresses.js.map +1 -0
- package/dist/apps/balances.d.ts +342 -0
- package/dist/apps/balances.d.ts.map +1 -0
- package/dist/apps/balances.js +393 -0
- package/dist/apps/balances.js.map +1 -0
- package/dist/apps/blocks.d.ts +931 -0
- package/dist/apps/blocks.d.ts.map +1 -0
- package/dist/apps/blocks.js +689 -0
- package/dist/apps/blocks.js.map +1 -0
- package/dist/apps/coingecko.d.ts +1053 -0
- package/dist/apps/coingecko.d.ts.map +1 -0
- package/dist/apps/coingecko.js +1015 -0
- package/dist/apps/coingecko.js.map +1 -0
- package/dist/apps/exchanges.d.ts +3000 -0
- package/dist/apps/exchanges.d.ts.map +1 -0
- package/dist/apps/exchanges.js +3570 -0
- package/dist/apps/exchanges.js.map +1 -0
- package/dist/apps/fee-amm.d.ts +640 -0
- package/dist/apps/fee-amm.d.ts.map +1 -0
- package/dist/apps/fee-amm.js +815 -0
- package/dist/apps/fee-amm.js.map +1 -0
- package/dist/apps/indexer.d.ts +116 -0
- package/dist/apps/indexer.d.ts.map +1 -0
- package/dist/apps/indexer.js +270 -0
- package/dist/apps/indexer.js.map +1 -0
- package/dist/apps/receipts.d.ts +1438 -0
- package/dist/apps/receipts.d.ts.map +1 -0
- package/dist/apps/receipts.js +874 -0
- package/dist/apps/receipts.js.map +1 -0
- package/dist/apps/rpc.d.ts +173 -0
- package/dist/apps/rpc.d.ts.map +1 -0
- package/dist/apps/rpc.js +191 -0
- package/dist/apps/rpc.js.map +1 -0
- package/dist/apps/tokenlist.d.ts +192 -0
- package/dist/apps/tokenlist.d.ts.map +1 -0
- package/dist/apps/tokenlist.js +153 -0
- package/dist/apps/tokenlist.js.map +1 -0
- package/dist/apps/tokens.d.ts +2246 -0
- package/dist/apps/tokens.d.ts.map +1 -0
- package/dist/apps/tokens.js +1789 -0
- package/dist/apps/tokens.js.map +1 -0
- package/dist/apps/transactions.d.ts +2327 -0
- package/dist/apps/transactions.d.ts.map +1 -0
- package/dist/apps/transactions.js +975 -0
- package/dist/apps/transactions.js.map +1 -0
- package/dist/apps/transfers.d.ts +589 -0
- package/dist/apps/transfers.d.ts.map +1 -0
- package/dist/apps/transfers.js +769 -0
- package/dist/apps/transfers.js.map +1 -0
- package/dist/apps/verified-tokens.d.ts +544 -0
- package/dist/apps/verified-tokens.d.ts.map +1 -0
- package/dist/apps/verified-tokens.js +234 -0
- package/dist/apps/verified-tokens.js.map +1 -0
- package/dist/apps/webhooks.d.ts +7684 -0
- package/dist/apps/webhooks.d.ts.map +1 -0
- package/dist/apps/webhooks.js +2112 -0
- package/dist/apps/webhooks.js.map +1 -0
- package/dist/cloudflare.d.ts +32 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +45 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/Auth.d.ts +298 -0
- package/dist/internal/Auth.d.ts.map +1 -0
- package/dist/internal/Auth.js +618 -0
- package/dist/internal/Auth.js.map +1 -0
- package/dist/internal/Cache.d.ts +74 -0
- package/dist/internal/Cache.d.ts.map +1 -0
- package/dist/internal/Cache.js +215 -0
- package/dist/internal/Cache.js.map +1 -0
- package/dist/internal/Cursor.d.ts +79 -0
- package/dist/internal/Cursor.d.ts.map +1 -0
- package/dist/internal/Cursor.js +116 -0
- package/dist/internal/Cursor.js.map +1 -0
- package/dist/internal/EdgeCache.d.ts +59 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -0
- package/dist/internal/EdgeCache.js +174 -0
- package/dist/internal/EdgeCache.js.map +1 -0
- package/dist/internal/Log.d.ts +104 -0
- package/dist/internal/Log.d.ts.map +1 -0
- package/dist/internal/Log.js +118 -0
- package/dist/internal/Log.js.map +1 -0
- package/dist/internal/MetricSink.d.ts +65 -0
- package/dist/internal/MetricSink.d.ts.map +1 -0
- package/dist/internal/MetricSink.js +146 -0
- package/dist/internal/MetricSink.js.map +1 -0
- package/dist/internal/Mpp.d.ts +71 -0
- package/dist/internal/Mpp.d.ts.map +1 -0
- package/dist/internal/Mpp.js +105 -0
- package/dist/internal/Mpp.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +313 -0
- package/dist/internal/OpenApi.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.d.ts +2 -0
- package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
- package/dist/internal/OpenApi.fonts.js +39 -0
- package/dist/internal/OpenApi.fonts.js.map +1 -0
- package/dist/internal/OpenApi.js +335 -0
- package/dist/internal/OpenApi.js.map +1 -0
- package/dist/internal/OpenApi.logo.d.ts +6 -0
- package/dist/internal/OpenApi.logo.d.ts.map +1 -0
- package/dist/internal/OpenApi.logo.js +19 -0
- package/dist/internal/OpenApi.logo.js.map +1 -0
- package/dist/internal/Path.d.ts +5 -0
- package/dist/internal/Path.d.ts.map +1 -0
- package/dist/internal/Path.js +16 -0
- package/dist/internal/Path.js.map +1 -0
- package/dist/internal/RateLimit.d.ts +60 -0
- package/dist/internal/RateLimit.d.ts.map +1 -0
- package/dist/internal/RateLimit.js +48 -0
- package/dist/internal/RateLimit.js.map +1 -0
- package/dist/internal/Response.d.ts +132 -0
- package/dist/internal/Response.d.ts.map +1 -0
- package/dist/internal/Response.js +72 -0
- package/dist/internal/Response.js.map +1 -0
- package/dist/internal/Schema.d.ts +257 -0
- package/dist/internal/Schema.d.ts.map +1 -0
- package/dist/internal/Schema.js +360 -0
- package/dist/internal/Schema.js.map +1 -0
- package/dist/internal/Store.d.ts +204 -0
- package/dist/internal/Store.d.ts.map +1 -0
- package/dist/internal/Store.js +291 -0
- package/dist/internal/Store.js.map +1 -0
- package/dist/internal/Tidx.d.ts +120 -0
- package/dist/internal/Tidx.d.ts.map +1 -0
- package/dist/internal/Tidx.js +156 -0
- package/dist/internal/Tidx.js.map +1 -0
- package/dist/internal/Timing.d.ts +22 -0
- package/dist/internal/Timing.d.ts.map +1 -0
- package/dist/internal/Timing.js +32 -0
- package/dist/internal/Timing.js.map +1 -0
- package/dist/internal/Ttl.d.ts +25 -0
- package/dist/internal/Ttl.d.ts.map +1 -0
- package/dist/internal/Ttl.js +49 -0
- package/dist/internal/Ttl.js.map +1 -0
- package/dist/internal/Value.d.ts +47 -0
- package/dist/internal/Value.d.ts.map +1 -0
- package/dist/internal/Value.js +122 -0
- package/dist/internal/Value.js.map +1 -0
- package/dist/internal/VerifiedTokens.d.ts +228 -0
- package/dist/internal/VerifiedTokens.d.ts.map +1 -0
- package/dist/internal/VerifiedTokens.js +450 -0
- package/dist/internal/VerifiedTokens.js.map +1 -0
- package/dist/internal/Viem.d.ts +2082 -0
- package/dist/internal/Viem.d.ts.map +1 -0
- package/dist/internal/Viem.js +63 -0
- package/dist/internal/Viem.js.map +1 -0
- package/dist/internal/WebhookDestination.d.ts +198 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -0
- package/dist/internal/WebhookDestination.js +170 -0
- package/dist/internal/WebhookDestination.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +433 -0
- package/dist/internal/Webhooks.d.ts.map +1 -0
- package/dist/internal/Webhooks.js +489 -0
- package/dist/internal/Webhooks.js.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
- package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/betterStack.js +76 -0
- package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
- package/dist/internal/webhookDestinations/slack.d.ts +59 -0
- package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/slack.js +422 -0
- package/dist/internal/webhookDestinations/slack.js.map +1 -0
- package/dist/internal/webhookDestinations/url.d.ts +17 -0
- package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
- package/dist/internal/webhookDestinations/url.js +130 -0
- package/dist/internal/webhookDestinations/url.js.map +1 -0
- package/dist/openrpc.json +5302 -0
- package/package.json +46 -4
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/31318/verified-tokens.json +30 -0
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +177 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
- package/seeds/42431/verified-tokens.json +44 -0
- package/seeds/openrpc/README.md +27 -0
- package/seeds/openrpc/execution-apis.json +8989 -0
- package/src/ApiKey.test.ts +91 -0
- package/src/ApiKey.ts +122 -0
- package/src/ApiKeySource.test-d.ts +11 -0
- package/src/ApiKeySource.test.ts +295 -0
- package/src/ApiKeySource.ts +303 -0
- package/src/App.test-d.ts +38 -0
- package/src/App.test.ts +65927 -0
- package/src/App.ts +855 -0
- package/src/Assets.ts +58 -0
- package/src/Auth.ts +1 -0
- package/src/Cli.test.ts +81 -0
- package/src/Cli.ts +38 -0
- package/src/Client.test-d.ts +1438 -0
- package/src/Client.test.ts +134 -0
- package/src/Client.ts +45 -0
- package/src/Metrics.ts +154 -0
- package/src/Scope.ts +50 -0
- package/src/Webhooks.test.ts +49 -0
- package/src/Webhooks.ts +172 -0
- package/src/admin/Access.test.ts +130 -0
- package/src/admin/Access.ts +87 -0
- package/src/admin/App.test.ts +225 -0
- package/src/admin/App.ts +216 -0
- package/src/admin/apps/api-keys.test.ts +156 -0
- package/src/admin/apps/api-keys.ts +157 -0
- package/src/admin/apps/scopes.test.ts +52 -0
- package/src/admin/apps/scopes.ts +47 -0
- package/src/admin/apps/verified-tokens.test.ts +415 -0
- package/src/admin/apps/verified-tokens.ts +566 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/ui/plugin.ts +135 -0
- package/src/admin/ui/src/components/Checkbox.tsx +22 -0
- package/src/admin/ui/src/components/Dialog.tsx +32 -0
- package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
- package/src/admin/ui/src/components/Page.tsx +53 -0
- package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
- package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
- package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
- package/src/admin/ui/src/lib/api-keys.ts +63 -0
- package/src/admin/ui/src/lib/client.ts +15 -0
- package/src/admin/ui/src/lib/config.ts +57 -0
- package/src/admin/ui/src/lib/query-client.ts +26 -0
- package/src/admin/ui/src/lib/scopes.ts +25 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
- package/src/admin/ui/src/lib/wagmi.ts +18 -0
- package/src/admin/ui/src/routeTree.gen.ts +104 -0
- package/src/admin/ui/src/router.tsx +25 -0
- package/src/admin/ui/src/routes/__root.tsx +219 -0
- package/src/admin/ui/src/routes/api-keys.tsx +329 -0
- package/src/admin/ui/src/routes/index.tsx +7 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
- package/src/admin/ui/src/styles.css +20 -0
- package/src/admin/ui/tsconfig.json +16 -0
- package/src/admin/ui/vite.config.ts +29 -0
- package/src/admin/ui.generated.ts +60 -0
- package/src/apps/activities.test.ts +1020 -0
- package/src/apps/activities.ts +3934 -0
- package/src/apps/addresses.ts +24 -0
- package/src/apps/balances.test.ts +274 -0
- package/src/apps/balances.ts +533 -0
- package/src/apps/blocks.test.ts +385 -0
- package/src/apps/blocks.ts +896 -0
- package/src/apps/coingecko.test.ts +218 -0
- package/src/apps/coingecko.ts +1383 -0
- package/src/apps/exchanges.test.ts +958 -0
- package/src/apps/exchanges.ts +4773 -0
- package/src/apps/fee-amm.test.ts +321 -0
- package/src/apps/fee-amm.ts +1121 -0
- package/src/apps/indexer.test.ts +153 -0
- package/src/apps/indexer.ts +330 -0
- package/src/apps/receipts.test.ts +285 -0
- package/src/apps/receipts.ts +1193 -0
- package/src/apps/rpc.test.ts +302 -0
- package/src/apps/rpc.ts +222 -0
- package/src/apps/tokenlist.test.ts +80 -0
- package/src/apps/tokenlist.ts +207 -0
- package/src/apps/tokens.test.ts +1062 -0
- package/src/apps/tokens.ts +2292 -0
- package/src/apps/transactions.test.ts +408 -0
- package/src/apps/transactions.ts +1363 -0
- package/src/apps/transfers.test.ts +371 -0
- package/src/apps/transfers.ts +1116 -0
- package/src/apps/verified-tokens.test.ts +114 -0
- package/src/apps/verified-tokens.ts +295 -0
- package/src/apps/webhooks.test.ts +1937 -0
- package/src/apps/webhooks.ts +2926 -0
- package/src/cloudflare.ts +55 -0
- package/src/index.ts +15 -0
- package/src/internal/Auth.test.ts +980 -0
- package/src/internal/Auth.ts +1012 -0
- package/src/internal/Cache.test.ts +240 -0
- package/src/internal/Cache.ts +262 -0
- package/src/internal/Cursor.test.ts +100 -0
- package/src/internal/Cursor.ts +149 -0
- package/src/internal/EdgeCache.test.ts +234 -0
- package/src/internal/EdgeCache.ts +224 -0
- package/src/internal/Errors.ts +14 -0
- package/src/internal/Log.test.ts +244 -0
- package/src/internal/Log.ts +216 -0
- package/src/internal/MetricSink.ts +220 -0
- package/src/internal/Mpp.test.ts +64 -0
- package/src/internal/Mpp.ts +141 -0
- package/src/internal/OpenApi.fonts.ts +38 -0
- package/src/internal/OpenApi.logo.ts +18 -0
- package/src/internal/OpenApi.ts +488 -0
- package/src/internal/Path.test.ts +14 -0
- package/src/internal/Path.ts +15 -0
- package/src/internal/RateLimit.test.ts +93 -0
- package/src/internal/RateLimit.ts +93 -0
- package/src/internal/Response.ts +131 -0
- package/src/internal/Schema.test.ts +34 -0
- package/src/internal/Schema.ts +523 -0
- package/src/internal/Store.test.ts +545 -0
- package/src/internal/Store.ts +526 -0
- package/src/internal/Tidx.local.test.ts +33 -0
- package/src/internal/Tidx.test.ts +294 -0
- package/src/internal/Tidx.ts +238 -0
- package/src/internal/Timing.test.ts +22 -0
- package/src/internal/Timing.ts +62 -0
- package/src/internal/Ttl.test.ts +18 -0
- package/src/internal/Ttl.ts +59 -0
- package/src/internal/Value.ts +114 -0
- package/src/internal/VerifiedTokens.test.ts +333 -0
- package/src/internal/VerifiedTokens.ts +645 -0
- package/src/internal/Viem.test.ts +115 -0
- package/src/internal/Viem.ts +165 -0
- package/src/internal/WebhookDestination.test.ts +648 -0
- package/src/internal/WebhookDestination.ts +331 -0
- package/src/internal/Webhooks.test.ts +668 -0
- package/src/internal/Webhooks.ts +940 -0
- package/src/internal/webhookDestinations/betterStack.ts +91 -0
- package/src/internal/webhookDestinations/slack.ts +496 -0
- package/src/internal/webhookDestinations/url.ts +128 -0
- package/src/openrpc.json +5302 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
import type * as z from 'zod/mini'
|
|
2
|
+
import { Schema } from 'cadent'
|
|
3
|
+
|
|
4
|
+
import * as TestApp from '../../test/App.js'
|
|
5
|
+
import * as Exchanges from './exchanges.js'
|
|
6
|
+
|
|
7
|
+
type OrdersResponse = z.output<typeof Exchanges.schema.getOrders.Response>
|
|
8
|
+
type PairsResponse = z.output<typeof Exchanges.schema.getPairs.Response>
|
|
9
|
+
|
|
10
|
+
type Client = ReturnType<typeof TestApp.client>
|
|
11
|
+
type OrdersQuery = {
|
|
12
|
+
chainId?: string
|
|
13
|
+
cursor?: string
|
|
14
|
+
include?: string
|
|
15
|
+
limit?: string
|
|
16
|
+
maker?: string
|
|
17
|
+
order?: 'asc' | 'desc'
|
|
18
|
+
side?: 'bid' | 'ask'
|
|
19
|
+
sort?: 'tick' | 'time'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find a real DEX pair that currently has at least one resting order. The
|
|
24
|
+
* exchange's small pair set means most pairs have empty books at any given
|
|
25
|
+
* moment, so we scan the pairs listing until one returns orders for the
|
|
26
|
+
* supplied query (scoped via `?base=`). Returns `null` when *every* scanned
|
|
27
|
+
* pair returns a 200 with an empty page, which is rare but valid on quiet
|
|
28
|
+
* testnets — callers should skip the assertion rather than fail.
|
|
29
|
+
*
|
|
30
|
+
* A non-200 response is treated as a hard failure: it surfaces real bugs (such
|
|
31
|
+
* as upstream schema mismatches) instead of letting them silently degrade to a
|
|
32
|
+
* skipped test. Empty 200 responses are tolerated and counted, with a final
|
|
33
|
+
* `expect` ensuring at least one pair was scanned so a 0-pair listing also
|
|
34
|
+
* fails loudly.
|
|
35
|
+
*/
|
|
36
|
+
async function findPairWithOrders(
|
|
37
|
+
client: Client,
|
|
38
|
+
query: OrdersQuery = {},
|
|
39
|
+
): Promise<{ body: OrdersResponse; pair: PairsResponse['data'][number] } | null> {
|
|
40
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
41
|
+
{ query: { limit: '10' } },
|
|
42
|
+
TestApp.auth,
|
|
43
|
+
)
|
|
44
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
45
|
+
expect(pairs.data.length).toBeGreaterThan(0)
|
|
46
|
+
for (const pair of pairs.data) {
|
|
47
|
+
const response = await client.v1.exchange.orders.$get(
|
|
48
|
+
{ query: { ...query, base: pair.base.address } },
|
|
49
|
+
TestApp.auth,
|
|
50
|
+
)
|
|
51
|
+
// A 5xx from the orders endpoint indicates a real regression (e.g. an
|
|
52
|
+
// upstream schema mismatch). Fail fast with the error body for context
|
|
53
|
+
// instead of silently skipping the pair.
|
|
54
|
+
if (response.status !== 200) {
|
|
55
|
+
const body = await response.text()
|
|
56
|
+
throw new Error(`orders ${response.status} for base=${pair.base.address}: ${body}`)
|
|
57
|
+
}
|
|
58
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
59
|
+
if (body.data.length > 0) return { body, pair }
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convenience wrapper around `findPairWithOrders` for tests that only care
|
|
66
|
+
* about the response body under a given query. Returns `null` when no pair
|
|
67
|
+
* had orders.
|
|
68
|
+
*/
|
|
69
|
+
async function fetchOrdersForBookedPair(query: OrdersQuery): Promise<OrdersResponse | null> {
|
|
70
|
+
const found = await findPairWithOrders(TestApp.client(), query)
|
|
71
|
+
return found?.body ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('GET /exchange/pairs', () => {
|
|
75
|
+
test('returns a page of trading pairs ordered newest-first by default', async () => {
|
|
76
|
+
const client = TestApp.client()
|
|
77
|
+
const response = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
78
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
|
|
79
|
+
|
|
80
|
+
expect(response.status).toBe(200)
|
|
81
|
+
expect(body.data.length).toBeLessThanOrEqual(5)
|
|
82
|
+
if (body.data.length > 0) {
|
|
83
|
+
const pair = body.data[0]!
|
|
84
|
+
expect(pair.key).toMatch(/^0x[0-9a-f]{64}$/)
|
|
85
|
+
expect(pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
86
|
+
expect(pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
87
|
+
// Newest-first by default.
|
|
88
|
+
for (let i = 1; i < body.data.length; i++) {
|
|
89
|
+
expect(body.data[i - 1]!.blockNumber).toBeGreaterThanOrEqual(body.data[i]!.blockNumber)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, 30000)
|
|
93
|
+
|
|
94
|
+
test('embeds an exact total count on demand via include=totalCount', async () => {
|
|
95
|
+
const client = TestApp.client()
|
|
96
|
+
const response = await client.v1.exchange.pairs.$get(
|
|
97
|
+
{ query: { include: 'totalCount', limit: '5' } },
|
|
98
|
+
TestApp.auth,
|
|
99
|
+
)
|
|
100
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
|
|
101
|
+
|
|
102
|
+
expect(response.status).toBe(200)
|
|
103
|
+
expect(response.headers.get('server-timing')?.includes('pairs_count;dur=')).toBe(true)
|
|
104
|
+
expect(body.meta).toBeDefined()
|
|
105
|
+
expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
|
|
106
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
107
|
+
// The count is sort-key-pruned on ClickHouse, so it is exact (never capped).
|
|
108
|
+
expect(body.meta!.totalCountCapped).toBe(false)
|
|
109
|
+
|
|
110
|
+
const bare = await client.v1.exchange.pairs.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
111
|
+
const bareBody = await TestApp.json(bare, Exchanges.schema.getPairs.Response)
|
|
112
|
+
expect(bare.status).toBe(200)
|
|
113
|
+
expect(bareBody.meta).toBeUndefined()
|
|
114
|
+
}, 30000)
|
|
115
|
+
|
|
116
|
+
test('serves a bounded positional page via `page`', async () => {
|
|
117
|
+
const client = TestApp.client()
|
|
118
|
+
// Ascending creation order anchors the feed at the first pair, so
|
|
119
|
+
// positional pages are deterministic against the head page.
|
|
120
|
+
const headResponse = await client.v1.exchange.pairs.$get(
|
|
121
|
+
{ query: { limit: '10', order: 'asc' } },
|
|
122
|
+
TestApp.auth,
|
|
123
|
+
)
|
|
124
|
+
const head = await TestApp.json(headResponse, Exchanges.schema.getPairs.Response)
|
|
125
|
+
expect(headResponse.status).toBe(200)
|
|
126
|
+
if (head.data.length < 10) return
|
|
127
|
+
|
|
128
|
+
const offsetResponse = await client.v1.exchange.pairs.$get(
|
|
129
|
+
{ query: { limit: '5', order: 'asc', page: '2' } },
|
|
130
|
+
TestApp.auth,
|
|
131
|
+
)
|
|
132
|
+
const page = await TestApp.json(offsetResponse, Exchanges.schema.getPairs.Response)
|
|
133
|
+
expect(offsetResponse.status).toBe(200)
|
|
134
|
+
expect(page.data.map((pair) => pair.key)).toEqual(
|
|
135
|
+
head.data.slice(5, 10).map((pair) => pair.key),
|
|
136
|
+
)
|
|
137
|
+
}, 30000)
|
|
138
|
+
|
|
139
|
+
test('embeds token metadata for both sides when requested via `include=tokens`', async () => {
|
|
140
|
+
const client = TestApp.client()
|
|
141
|
+
const response = await client.v1.exchange.pairs.$get(
|
|
142
|
+
{ query: { limit: '5', include: 'tokens' } },
|
|
143
|
+
TestApp.auth,
|
|
144
|
+
)
|
|
145
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
|
|
146
|
+
|
|
147
|
+
expect(response.status).toBe(200)
|
|
148
|
+
if (body.data.length === 0) return
|
|
149
|
+
const enriched = body.data.find((pair) => typeof pair.base.symbol === 'string')
|
|
150
|
+
// At least one row should resolve metadata against the live RPC; both sides
|
|
151
|
+
// share the same metadata path so when `base.symbol` is present, so is the
|
|
152
|
+
// rest of the trimmed token reference.
|
|
153
|
+
if (enriched) {
|
|
154
|
+
expect(typeof enriched.base.symbol).toBe('string')
|
|
155
|
+
expect(typeof enriched.base.decimals).toBe('number')
|
|
156
|
+
expect(typeof enriched.quote.symbol).toBe('string')
|
|
157
|
+
}
|
|
158
|
+
}, 60000)
|
|
159
|
+
|
|
160
|
+
test('ranks pairs by DEX-escrow base balance when `sort=liquidity`', async () => {
|
|
161
|
+
const client = TestApp.client()
|
|
162
|
+
const response = await client.v1.exchange.pairs.$get(
|
|
163
|
+
{ query: { limit: '5', sort: 'liquidity' } },
|
|
164
|
+
TestApp.auth,
|
|
165
|
+
)
|
|
166
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairs.Response)
|
|
167
|
+
|
|
168
|
+
expect(response.status).toBe(200)
|
|
169
|
+
expect(body.data.length).toBeLessThanOrEqual(5)
|
|
170
|
+
if (body.data.length > 0) {
|
|
171
|
+
for (const pair of body.data) {
|
|
172
|
+
expect(typeof pair.liquidity).toBe('string')
|
|
173
|
+
expect(pair.liquidity).toMatch(/^\d+$/)
|
|
174
|
+
expect(BigInt(pair.liquidity!)).toBeGreaterThan(0n)
|
|
175
|
+
}
|
|
176
|
+
// Newest-first balance ordering.
|
|
177
|
+
for (let i = 1; i < body.data.length; i++) {
|
|
178
|
+
expect(BigInt(body.data[i - 1]!.liquidity!)).toBeGreaterThanOrEqual(
|
|
179
|
+
BigInt(body.data[i]!.liquidity!),
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, 30000)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('GET /exchange/pairs/:base', () => {
|
|
187
|
+
test('returns the pair identified by its base token', async () => {
|
|
188
|
+
const client = TestApp.client()
|
|
189
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
190
|
+
{ query: { limit: '5' } },
|
|
191
|
+
TestApp.auth,
|
|
192
|
+
)
|
|
193
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
194
|
+
if (pairs.data.length === 0) return
|
|
195
|
+
const pair = pairs.data[0]!
|
|
196
|
+
|
|
197
|
+
const response = await client.v1.exchange.pairs[':base'].$get(
|
|
198
|
+
{ param: { base: pair.base.address }, query: {} },
|
|
199
|
+
TestApp.auth,
|
|
200
|
+
)
|
|
201
|
+
expect(response.status).toBe(200)
|
|
202
|
+
const body = await TestApp.json(response, Exchanges.schema.getPair.Response)
|
|
203
|
+
|
|
204
|
+
expect(body.key).toBe(pair.key)
|
|
205
|
+
expect(body.base.address).toBe(pair.base.address)
|
|
206
|
+
expect(body.quote.address).toBe(pair.quote.address)
|
|
207
|
+
expect(Number.isInteger(body.blockNumber)).toBe(true)
|
|
208
|
+
expect(body.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
209
|
+
expect(Number.isNaN(Date.parse(body.timestamp))).toBe(false)
|
|
210
|
+
// Token metadata is opt-in: the base response carries bare addresses.
|
|
211
|
+
expect(body.base.symbol).toBeUndefined()
|
|
212
|
+
}, 30000)
|
|
213
|
+
|
|
214
|
+
test('embeds token metadata when requested via `include=tokens`', async () => {
|
|
215
|
+
const client = TestApp.client()
|
|
216
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
217
|
+
{ query: { limit: '5' } },
|
|
218
|
+
TestApp.auth,
|
|
219
|
+
)
|
|
220
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
221
|
+
if (pairs.data.length === 0) return
|
|
222
|
+
const pair = pairs.data[0]!
|
|
223
|
+
|
|
224
|
+
const response = await client.v1.exchange.pairs[':base'].$get(
|
|
225
|
+
{ param: { base: pair.base.address }, query: { include: 'tokens' } },
|
|
226
|
+
TestApp.auth,
|
|
227
|
+
)
|
|
228
|
+
expect(response.status).toBe(200)
|
|
229
|
+
const body = await TestApp.json(response, Exchanges.schema.getPair.Response)
|
|
230
|
+
// Best-effort: at least one side should resolve metadata for a real pair.
|
|
231
|
+
const sides = [body.base, body.quote]
|
|
232
|
+
expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true)
|
|
233
|
+
}, 60000)
|
|
234
|
+
|
|
235
|
+
test('returns 404 for an unknown base token', async () => {
|
|
236
|
+
const client = TestApp.client()
|
|
237
|
+
const response = await client.v1.exchange.pairs[':base'].$get(
|
|
238
|
+
{ param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
|
|
239
|
+
TestApp.auth,
|
|
240
|
+
)
|
|
241
|
+
expect(response.status).toBe(404)
|
|
242
|
+
}, 30000)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('GET /exchange/pairs/:base/ohlc', () => {
|
|
246
|
+
test('returns OHLC buckets for a real pair', async () => {
|
|
247
|
+
const client = TestApp.client()
|
|
248
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
249
|
+
{ query: { limit: '5' } },
|
|
250
|
+
TestApp.auth,
|
|
251
|
+
)
|
|
252
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
253
|
+
if (pairs.data.length === 0) return
|
|
254
|
+
const pair = pairs.data[0]!
|
|
255
|
+
|
|
256
|
+
const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
|
|
257
|
+
{ param: { base: pair.base.address }, query: { interval: '1h', window: '7d' } },
|
|
258
|
+
TestApp.auth,
|
|
259
|
+
)
|
|
260
|
+
expect(response.status).toBe(200)
|
|
261
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response)
|
|
262
|
+
|
|
263
|
+
expect(body.interval).toBe('1h')
|
|
264
|
+
expect(body.window).toBe('7d')
|
|
265
|
+
expect(body.base.address).toBe(pair.base.address)
|
|
266
|
+
expect(body.quote.address).toBe(pair.quote.address)
|
|
267
|
+
expect(typeof body.truncated).toBe('boolean')
|
|
268
|
+
expect(Array.isArray(body.data)).toBe(true)
|
|
269
|
+
|
|
270
|
+
// Buckets, if any, are ordered oldest → newest with strictly increasing
|
|
271
|
+
// timestamps and well-formed rate / volume shapes.
|
|
272
|
+
for (let i = 0; i < body.data.length; i++) {
|
|
273
|
+
const bucket = body.data[i]!
|
|
274
|
+
expect(bucket.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
275
|
+
expect(bucket.open).toMatch(/^\d+(\.\d+)?$/)
|
|
276
|
+
expect(bucket.close).toMatch(/^\d+(\.\d+)?$/)
|
|
277
|
+
expect(bucket.high).toMatch(/^\d+(\.\d+)?$/)
|
|
278
|
+
expect(bucket.low).toMatch(/^\d+(\.\d+)?$/)
|
|
279
|
+
expect(Number(bucket.high)).toBeGreaterThanOrEqual(Number(bucket.low))
|
|
280
|
+
expect(bucket.fillCount).toBeGreaterThan(0)
|
|
281
|
+
expect(BigInt(bucket.volume.base)).toBeGreaterThanOrEqual(0n)
|
|
282
|
+
expect(BigInt(bucket.volume.quote)).toBeGreaterThanOrEqual(0n)
|
|
283
|
+
if (i > 0) {
|
|
284
|
+
expect(Date.parse(bucket.timestamp)).toBeGreaterThan(
|
|
285
|
+
Date.parse(body.data[i - 1]!.timestamp),
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}, 60000)
|
|
290
|
+
|
|
291
|
+
test('rejects OHLC request whose `interval × window` exceeds the bucket cap', async () => {
|
|
292
|
+
const client = TestApp.client()
|
|
293
|
+
const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
|
|
294
|
+
{
|
|
295
|
+
// `1m` × `24h` = 1440 buckets, well above the 500-bucket cap.
|
|
296
|
+
param: { base: `0x${'1'.repeat(40)}` as const },
|
|
297
|
+
query: { interval: '1m', window: '24h' },
|
|
298
|
+
},
|
|
299
|
+
TestApp.auth,
|
|
300
|
+
)
|
|
301
|
+
expect(response.status).toBe(400)
|
|
302
|
+
}, 15000)
|
|
303
|
+
|
|
304
|
+
test('returns 404 OHLC for an unknown pair', async () => {
|
|
305
|
+
const client = TestApp.client()
|
|
306
|
+
const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
|
|
307
|
+
{ param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
|
|
308
|
+
TestApp.auth,
|
|
309
|
+
)
|
|
310
|
+
expect(response.status).toBe(404)
|
|
311
|
+
}, 30000)
|
|
312
|
+
|
|
313
|
+
test('embeds token metadata on OHLC when requested via `include=tokens`', async () => {
|
|
314
|
+
const client = TestApp.client()
|
|
315
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
316
|
+
{ query: { limit: '5' } },
|
|
317
|
+
TestApp.auth,
|
|
318
|
+
)
|
|
319
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
320
|
+
if (pairs.data.length === 0) return
|
|
321
|
+
const pair = pairs.data[0]!
|
|
322
|
+
|
|
323
|
+
const response = await client.v1.exchange.pairs[':base'].ohlc.$get(
|
|
324
|
+
{
|
|
325
|
+
param: { base: pair.base.address },
|
|
326
|
+
query: { include: 'tokens', interval: '1h', window: '24h' },
|
|
327
|
+
},
|
|
328
|
+
TestApp.auth,
|
|
329
|
+
)
|
|
330
|
+
expect(response.status).toBe(200)
|
|
331
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairOhlc.Response)
|
|
332
|
+
// Best-effort: at least one side should resolve metadata for a real pair.
|
|
333
|
+
const sides = [body.base, body.quote]
|
|
334
|
+
expect(sides.some((side) => typeof side.decimals === 'number')).toBe(true)
|
|
335
|
+
}, 60000)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
describe('GET /exchange/orders?base=… (pair-scoped orders)', () => {
|
|
339
|
+
test('returns a page of resting orders for a real pair', async () => {
|
|
340
|
+
const client = TestApp.client()
|
|
341
|
+
const found = await findPairWithOrders(client, { limit: '5' })
|
|
342
|
+
if (!found) return
|
|
343
|
+
const { body, pair } = found
|
|
344
|
+
|
|
345
|
+
expect(body.data.length).toBeGreaterThan(0)
|
|
346
|
+
expect(body.data.length).toBeLessThanOrEqual(5)
|
|
347
|
+
expect(typeof body.truncated).toBe('boolean')
|
|
348
|
+
// `nextCursor` is `null` when the page exhausts the snapshot.
|
|
349
|
+
expect(body.nextCursor === null || typeof body.nextCursor === 'string').toBe(true)
|
|
350
|
+
|
|
351
|
+
for (const order of body.data) {
|
|
352
|
+
expect(order.orderId).toMatch(/^\d+$/)
|
|
353
|
+
expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
354
|
+
expect(order.amount).toMatch(/^\d+$/)
|
|
355
|
+
expect(order.remaining).toMatch(/^\d+$/)
|
|
356
|
+
expect(BigInt(order.remaining)).toBeGreaterThan(0n)
|
|
357
|
+
expect(BigInt(order.remaining)).toBeLessThanOrEqual(BigInt(order.amount))
|
|
358
|
+
expect(['bid', 'ask']).toContain(order.side)
|
|
359
|
+
expect(Number.isInteger(order.tick)).toBe(true)
|
|
360
|
+
expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
|
|
361
|
+
// `price` is quote-per-base regardless of side; `rate` is the
|
|
362
|
+
// taker-perspective ratio (price for bids, 1/price for asks). A tick=0
|
|
363
|
+
// order — peg — must render both as exactly 1.00000.
|
|
364
|
+
expect(order.price).toMatch(/^\d+(\.\d+)?$/)
|
|
365
|
+
if (order.tick === 0) {
|
|
366
|
+
expect(Number(order.price)).toBe(1)
|
|
367
|
+
expect(Number(order.rate)).toBe(1)
|
|
368
|
+
}
|
|
369
|
+
expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
370
|
+
expect(Number.isInteger(order.blockNumber)).toBe(true)
|
|
371
|
+
expect(order.blockNumber).toBeGreaterThanOrEqual(0)
|
|
372
|
+
expect(Number.isInteger(order.logIndex)).toBe(true)
|
|
373
|
+
expect(order.logIndex).toBeGreaterThanOrEqual(0)
|
|
374
|
+
expect(Number.isNaN(Date.parse(order.placedAt))).toBe(false)
|
|
375
|
+
// Every row is annotated with the pair we scoped to.
|
|
376
|
+
expect(order.pair.key).toBe(pair.key)
|
|
377
|
+
expect(order.pair.base.address).toBe(pair.base.address)
|
|
378
|
+
expect(order.pair.quote.address).toBe(pair.quote.address)
|
|
379
|
+
}
|
|
380
|
+
}, 120000)
|
|
381
|
+
|
|
382
|
+
test('sorts resting orders by tick descending by default', async () => {
|
|
383
|
+
const body = await fetchOrdersForBookedPair({ limit: '10' })
|
|
384
|
+
if (!body) return
|
|
385
|
+
for (let i = 1; i < body.data.length; i++)
|
|
386
|
+
expect(body.data[i - 1]!.tick).toBeGreaterThanOrEqual(body.data[i]!.tick)
|
|
387
|
+
}, 120000)
|
|
388
|
+
|
|
389
|
+
test('sorts resting orders by tick ascending when `order=asc`', async () => {
|
|
390
|
+
const body = await fetchOrdersForBookedPair({ limit: '10', order: 'asc' })
|
|
391
|
+
if (!body) return
|
|
392
|
+
for (let i = 1; i < body.data.length; i++)
|
|
393
|
+
expect(body.data[i - 1]!.tick).toBeLessThanOrEqual(body.data[i]!.tick)
|
|
394
|
+
}, 120000)
|
|
395
|
+
|
|
396
|
+
test('sorts resting orders by placement time when `sort=time`', async () => {
|
|
397
|
+
const body = await fetchOrdersForBookedPair({ limit: '10', sort: 'time' })
|
|
398
|
+
if (!body) return
|
|
399
|
+
for (let i = 1; i < body.data.length; i++) {
|
|
400
|
+
const previous = body.data[i - 1]!
|
|
401
|
+
const current = body.data[i]!
|
|
402
|
+
// Default `order=desc` against `sort=time`: newest placement first,
|
|
403
|
+
// tie-breaking on log index.
|
|
404
|
+
expect(
|
|
405
|
+
previous.blockNumber > current.blockNumber ||
|
|
406
|
+
(previous.blockNumber === current.blockNumber && previous.logIndex >= current.logIndex),
|
|
407
|
+
).toBe(true)
|
|
408
|
+
}
|
|
409
|
+
}, 120000)
|
|
410
|
+
|
|
411
|
+
test('filters resting orders by `side`', async () => {
|
|
412
|
+
const bids = await fetchOrdersForBookedPair({ limit: '10', side: 'bid' })
|
|
413
|
+
if (bids) for (const order of bids.data) expect(order.side).toBe('bid')
|
|
414
|
+
|
|
415
|
+
// Asks are not always present alongside bids on the same pair, so this
|
|
416
|
+
// assertion runs against whichever pair happens to have ask orders.
|
|
417
|
+
const asks = await fetchOrdersForBookedPair({ limit: '10', side: 'ask' })
|
|
418
|
+
if (asks) for (const order of asks.data) expect(order.side).toBe('ask')
|
|
419
|
+
}, 180000)
|
|
420
|
+
|
|
421
|
+
test('filters resting orders to a single maker', async () => {
|
|
422
|
+
// Seed off the default-sort head page to discover a maker that has at
|
|
423
|
+
// least one resting order on a real pair, then filter to just them.
|
|
424
|
+
const seed = await findPairWithOrders(TestApp.client(), { limit: '5' })
|
|
425
|
+
if (!seed) return
|
|
426
|
+
const maker = seed.body.data[0]!.maker
|
|
427
|
+
const client = TestApp.client()
|
|
428
|
+
const response = await client.v1.exchange.orders.$get(
|
|
429
|
+
{ query: { base: seed.pair.base.address, limit: '10', maker } },
|
|
430
|
+
TestApp.auth,
|
|
431
|
+
)
|
|
432
|
+
expect(response.status).toBe(200)
|
|
433
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
434
|
+
expect(body.data.length).toBeGreaterThan(0)
|
|
435
|
+
for (const order of body.data) expect(order.maker).toBe(maker)
|
|
436
|
+
}, 120000)
|
|
437
|
+
|
|
438
|
+
test('embeds pair token metadata per row when requested via `include=tokens`', async () => {
|
|
439
|
+
const seed = await findPairWithOrders(TestApp.client(), {
|
|
440
|
+
include: 'tokens',
|
|
441
|
+
limit: '5',
|
|
442
|
+
})
|
|
443
|
+
if (!seed) return
|
|
444
|
+
const { body, pair } = seed
|
|
445
|
+
// At least one side resolves metadata against the live RPC; both sides
|
|
446
|
+
// share the same resolution path so this gates the embedding shape.
|
|
447
|
+
const row = body.data[0]!
|
|
448
|
+
expect(row.pair.base.address).toBe(pair.base.address)
|
|
449
|
+
expect(row.pair.quote.address).toBe(pair.quote.address)
|
|
450
|
+
const sides = [row.pair.base, row.pair.quote]
|
|
451
|
+
expect(sides.some((side) => typeof side.symbol === 'string')).toBe(true)
|
|
452
|
+
}, 120000)
|
|
453
|
+
|
|
454
|
+
test('paginates resting orders without skipping or duplicating rows', async () => {
|
|
455
|
+
const client = TestApp.client()
|
|
456
|
+
const seed = await findPairWithOrders(client, { limit: '5' })
|
|
457
|
+
if (!seed) return
|
|
458
|
+
if (seed.body.nextCursor === null) return
|
|
459
|
+
|
|
460
|
+
const next = await client.v1.exchange.orders.$get(
|
|
461
|
+
{ query: { base: seed.pair.base.address, cursor: seed.body.nextCursor, limit: '5' } },
|
|
462
|
+
TestApp.auth,
|
|
463
|
+
)
|
|
464
|
+
expect(next.status).toBe(200)
|
|
465
|
+
const second = await TestApp.json(next, Exchanges.schema.getOrders.Response)
|
|
466
|
+
const firstIds = new Set(seed.body.data.map((order) => order.orderId))
|
|
467
|
+
for (const order of second.data) expect(firstIds.has(order.orderId)).toBe(false)
|
|
468
|
+
}, 180000)
|
|
469
|
+
|
|
470
|
+
test('returns 404 for an unknown pair on the orders endpoint', async () => {
|
|
471
|
+
const client = TestApp.client()
|
|
472
|
+
const response = await client.v1.exchange.orders.$get(
|
|
473
|
+
{ query: { base: `0x${'1'.repeat(40)}` as const } },
|
|
474
|
+
TestApp.auth,
|
|
475
|
+
)
|
|
476
|
+
expect(response.status).toBe(404)
|
|
477
|
+
}, 30000)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('GET /exchange/pairs/:base/depth', () => {
|
|
481
|
+
test('returns orderbook depth for a real pair', async () => {
|
|
482
|
+
const client = TestApp.client()
|
|
483
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
484
|
+
{ query: { limit: '5' } },
|
|
485
|
+
TestApp.auth,
|
|
486
|
+
)
|
|
487
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
488
|
+
if (pairs.data.length === 0) return
|
|
489
|
+
|
|
490
|
+
// The RPC fans out ~400 `getTickLevel` reads through a single deployless
|
|
491
|
+
// multicall; on testnet that occasionally exceeds the upstream RPC's
|
|
492
|
+
// multicall return-size budget and surfaces as 502 from our handler.
|
|
493
|
+
// Treat that as "skip" so the test stays green even when the upstream
|
|
494
|
+
// throttles the multicall response.
|
|
495
|
+
let body: z.output<typeof Exchanges.schema.getPairDepth.Response> | null = null
|
|
496
|
+
let pair: (typeof pairs.data)[number] | null = null
|
|
497
|
+
for (const candidate of pairs.data) {
|
|
498
|
+
const response = await client.v1.exchange.pairs[':base'].depth.$get(
|
|
499
|
+
{ param: { base: candidate.base.address }, query: {} },
|
|
500
|
+
TestApp.auth,
|
|
501
|
+
)
|
|
502
|
+
if (response.status === 502) return
|
|
503
|
+
expect(response.status).toBe(200)
|
|
504
|
+
body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response)
|
|
505
|
+
pair = candidate
|
|
506
|
+
if (body.bids.length > 0 || body.asks.length > 0) break
|
|
507
|
+
}
|
|
508
|
+
if (!body || !pair) return
|
|
509
|
+
|
|
510
|
+
expect(body.base.address).toBe(pair.base.address)
|
|
511
|
+
expect(body.quote.address).toBe(pair.quote.address)
|
|
512
|
+
|
|
513
|
+
// Asks are emitted lowest-tick first (best ask outward); bids are
|
|
514
|
+
// emitted highest-tick first (best bid outward). Per-row `cumulativeSize`
|
|
515
|
+
// must equal the running sum of `size` from the head of the same side.
|
|
516
|
+
const checkSide = (side: typeof body.asks, label: 'asks' | 'bids') => {
|
|
517
|
+
let expected = 0n
|
|
518
|
+
let previousTick: number | undefined
|
|
519
|
+
for (const level of side) {
|
|
520
|
+
expect(level.size).toMatch(/^\d+$/)
|
|
521
|
+
expect(level.price).toMatch(/^\d+(\.\d+)?$/)
|
|
522
|
+
expect(BigInt(level.size)).toBeGreaterThan(0n)
|
|
523
|
+
expected += BigInt(level.size)
|
|
524
|
+
expect(BigInt(level.cumulativeSize)).toBe(expected)
|
|
525
|
+
// Ticks are monotone outward from peg per side.
|
|
526
|
+
if (previousTick !== undefined) {
|
|
527
|
+
if (label === 'asks') expect(level.tick).toBeGreaterThan(previousTick)
|
|
528
|
+
else expect(level.tick).toBeLessThan(previousTick)
|
|
529
|
+
}
|
|
530
|
+
previousTick = level.tick
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
checkSide(body.asks, 'asks')
|
|
534
|
+
checkSide(body.bids, 'bids')
|
|
535
|
+
}, 60000)
|
|
536
|
+
|
|
537
|
+
test('caps depth levels per side when `levels` is set', async () => {
|
|
538
|
+
const client = TestApp.client()
|
|
539
|
+
const pairsResponse = await client.v1.exchange.pairs.$get(
|
|
540
|
+
{ query: { limit: '5' } },
|
|
541
|
+
TestApp.auth,
|
|
542
|
+
)
|
|
543
|
+
const pairs = await TestApp.json(pairsResponse, Exchanges.schema.getPairs.Response)
|
|
544
|
+
if (pairs.data.length === 0) return
|
|
545
|
+
|
|
546
|
+
for (const pair of pairs.data) {
|
|
547
|
+
const response = await client.v1.exchange.pairs[':base'].depth.$get(
|
|
548
|
+
{ param: { base: pair.base.address }, query: { levels: '3' } },
|
|
549
|
+
TestApp.auth,
|
|
550
|
+
)
|
|
551
|
+
if (response.status === 502) return
|
|
552
|
+
expect(response.status).toBe(200)
|
|
553
|
+
const body = await TestApp.json(response, Exchanges.schema.getPairDepth.Response)
|
|
554
|
+
expect(body.asks.length).toBeLessThanOrEqual(3)
|
|
555
|
+
expect(body.bids.length).toBeLessThanOrEqual(3)
|
|
556
|
+
}
|
|
557
|
+
}, 60000)
|
|
558
|
+
|
|
559
|
+
test('returns 404 for an unknown depth pair', async () => {
|
|
560
|
+
const client = TestApp.client()
|
|
561
|
+
const response = await client.v1.exchange.pairs[':base'].depth.$get(
|
|
562
|
+
{ param: { base: `0x${'1'.repeat(40)}` as const }, query: {} },
|
|
563
|
+
TestApp.auth,
|
|
564
|
+
)
|
|
565
|
+
expect(response.status).toBe(404)
|
|
566
|
+
}, 30000)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe('GET /exchange/swaps', () => {
|
|
570
|
+
type GlobalSwapsResponse = z.output<typeof Exchanges.schema.getSwaps.Response>
|
|
571
|
+
|
|
572
|
+
test('returns a global, newest-first page of swaps across all pairs', async () => {
|
|
573
|
+
const client = TestApp.client()
|
|
574
|
+
const response = await client.v1.exchange.swaps.$get({ query: { limit: '10' } }, TestApp.auth)
|
|
575
|
+
expect(response.status).toBe(200)
|
|
576
|
+
const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
|
|
577
|
+
expect(body.data.length).toBeLessThanOrEqual(10)
|
|
578
|
+
if (body.data.length === 0) return
|
|
579
|
+
|
|
580
|
+
for (const swap of body.data) {
|
|
581
|
+
expect(swap.taker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
582
|
+
expect(swap.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
583
|
+
expect(['exactSource', 'exactDestination', null]).toContain(swap.mode)
|
|
584
|
+
expect(swap.sourceToken.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
585
|
+
expect(swap.sourceToken.amount).toMatch(/^\d+$/)
|
|
586
|
+
expect(swap.destinationToken.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
587
|
+
expect(swap.destinationToken.amount).toMatch(/^\d+$/)
|
|
588
|
+
expect(swap.rate).toMatch(/^\d+(\.\d+)?$/)
|
|
589
|
+
expect(swap.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
590
|
+
// `route` spans source → destination; its ends mirror the side tokens.
|
|
591
|
+
expect(swap.route.length).toBeGreaterThanOrEqual(2)
|
|
592
|
+
expect(swap.route[0]).toBe(swap.sourceToken.address)
|
|
593
|
+
expect(swap.route.at(-1)).toBe(swap.destinationToken.address)
|
|
594
|
+
// Per-fill detail is nested, in execution order, and chains hop-wise:
|
|
595
|
+
// every fill trades adjacent tokens of the route.
|
|
596
|
+
expect(swap.fills.length).toBeGreaterThan(0)
|
|
597
|
+
for (const fill of swap.fills) {
|
|
598
|
+
expect(fill.orderId).toMatch(/^\d+$/)
|
|
599
|
+
expect(fill.maker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
600
|
+
expect(fill.price).toMatch(/^\d+(\.\d+)?$/)
|
|
601
|
+
expect(swap.route).toContain(fill.sourceToken.address)
|
|
602
|
+
expect(swap.route).toContain(fill.destinationToken.address)
|
|
603
|
+
}
|
|
604
|
+
for (let i = 1; i < swap.fills.length; i++)
|
|
605
|
+
expect(swap.fills[i]!.logIndex).toBeGreaterThan(swap.fills[i - 1]!.logIndex)
|
|
606
|
+
}
|
|
607
|
+
// Newest-first (rows order by block, then first fill within the block).
|
|
608
|
+
for (let i = 1; i < body.data.length; i++) {
|
|
609
|
+
const prev = body.data[i - 1]!
|
|
610
|
+
const curr = body.data[i]!
|
|
611
|
+
expect(
|
|
612
|
+
prev.blockNumber > curr.blockNumber ||
|
|
613
|
+
(prev.blockNumber === curr.blockNumber &&
|
|
614
|
+
prev.fills[0]!.logIndex >= curr.fills[0]!.logIndex),
|
|
615
|
+
).toBe(true)
|
|
616
|
+
}
|
|
617
|
+
}, 60000)
|
|
618
|
+
|
|
619
|
+
test('paginates with an opaque cursor without skipping or duplicating rows', async () => {
|
|
620
|
+
const client = TestApp.client()
|
|
621
|
+
const first = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
622
|
+
const firstBody = await TestApp.json(first, Exchanges.schema.getSwaps.Response)
|
|
623
|
+
if (firstBody.nextCursor === null) return
|
|
624
|
+
|
|
625
|
+
const second = await client.v1.exchange.swaps.$get(
|
|
626
|
+
{ query: { cursor: firstBody.nextCursor, limit: '5' } },
|
|
627
|
+
TestApp.auth,
|
|
628
|
+
)
|
|
629
|
+
const secondBody = await TestApp.json(second, Exchanges.schema.getSwaps.Response)
|
|
630
|
+
|
|
631
|
+
// A swap row is unique on `(block, first fill logIndex)`.
|
|
632
|
+
const key = (swap: GlobalSwapsResponse['data'][number]) =>
|
|
633
|
+
`${swap.blockNumber}:${swap.fills[0]!.logIndex}`
|
|
634
|
+
const firstKeys = new Set(firstBody.data.map(key))
|
|
635
|
+
for (const swap of secondBody.data) expect(firstKeys.has(key(swap))).toBe(false)
|
|
636
|
+
}, 60000)
|
|
637
|
+
|
|
638
|
+
test('filters the global feed by transactionHash', async () => {
|
|
639
|
+
const client = TestApp.client()
|
|
640
|
+
const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
641
|
+
const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response)
|
|
642
|
+
const row = seedBody.data[0]
|
|
643
|
+
if (!row) return
|
|
644
|
+
|
|
645
|
+
const response = await client.v1.exchange.swaps.$get(
|
|
646
|
+
{ query: { limit: '5', transactionHash: row.transactionHash } },
|
|
647
|
+
TestApp.auth,
|
|
648
|
+
)
|
|
649
|
+
const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
|
|
650
|
+
// The seeded swap's transaction is on the head page, so it must match,
|
|
651
|
+
// and every returned swap belongs to that transaction.
|
|
652
|
+
expect(body.data.length).toBeGreaterThan(0)
|
|
653
|
+
for (const swap of body.data) expect(swap.transactionHash).toBe(row.transactionHash)
|
|
654
|
+
}, 60000)
|
|
655
|
+
|
|
656
|
+
test('filters the global feed by maker', async () => {
|
|
657
|
+
const client = TestApp.client()
|
|
658
|
+
const seed = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
659
|
+
const seedBody = await TestApp.json(seed, Exchanges.schema.getSwaps.Response)
|
|
660
|
+
const row = seedBody.data[0]
|
|
661
|
+
if (!row) return
|
|
662
|
+
const maker = row.fills[0]!.maker
|
|
663
|
+
|
|
664
|
+
const response = await client.v1.exchange.swaps.$get(
|
|
665
|
+
{ query: { limit: '5', maker } },
|
|
666
|
+
TestApp.auth,
|
|
667
|
+
)
|
|
668
|
+
const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
|
|
669
|
+
for (const swap of body.data) expect(swap.fills.some((fill) => fill.maker === maker)).toBe(true)
|
|
670
|
+
}, 60000)
|
|
671
|
+
|
|
672
|
+
test('embeds token metadata on swap sides when requested via `include=tokens`', async () => {
|
|
673
|
+
const client = TestApp.client()
|
|
674
|
+
const response = await client.v1.exchange.swaps.$get(
|
|
675
|
+
{ query: { include: 'tokens', limit: '10' } },
|
|
676
|
+
TestApp.auth,
|
|
677
|
+
)
|
|
678
|
+
expect(response.status).toBe(200)
|
|
679
|
+
const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
|
|
680
|
+
if (body.data.length === 0) return
|
|
681
|
+
|
|
682
|
+
const enriched = body.data.find((swap) => typeof swap.sourceToken.symbol === 'string')
|
|
683
|
+
if (enriched) {
|
|
684
|
+
expect(typeof enriched.sourceToken.symbol).toBe('string')
|
|
685
|
+
expect(typeof enriched.sourceToken.decimals).toBe('number')
|
|
686
|
+
}
|
|
687
|
+
}, 60000)
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
describe('GET /exchange/orders', () => {
|
|
691
|
+
type GlobalOrdersResponse = z.output<typeof Exchanges.schema.getOrders.Response>
|
|
692
|
+
|
|
693
|
+
test('returns a global page of resting orders across all pairs', async () => {
|
|
694
|
+
const client = TestApp.client()
|
|
695
|
+
const response = await client.v1.exchange.orders.$get({ query: { limit: '10' } }, TestApp.auth)
|
|
696
|
+
expect(response.status).toBe(200)
|
|
697
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
698
|
+
expect(body.data.length).toBeLessThanOrEqual(10)
|
|
699
|
+
if (body.data.length === 0) return
|
|
700
|
+
|
|
701
|
+
for (const order of body.data) {
|
|
702
|
+
expect(order.orderId).toMatch(/^\d+$/)
|
|
703
|
+
expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
704
|
+
expect(order.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
705
|
+
expect(order.amount).toMatch(/^\d+$/)
|
|
706
|
+
expect(order.remaining).toMatch(/^\d+$/)
|
|
707
|
+
expect(['bid', 'ask']).toContain(order.side)
|
|
708
|
+
expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
|
|
709
|
+
expect(order.placedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
710
|
+
// Each row carries the pair it belongs to (a global page spans many pairs).
|
|
711
|
+
expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/)
|
|
712
|
+
expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
713
|
+
expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
714
|
+
}
|
|
715
|
+
}, 60000)
|
|
716
|
+
|
|
717
|
+
test('embeds a capped total count on demand via include=totalCount', async () => {
|
|
718
|
+
const client = TestApp.client()
|
|
719
|
+
const response = await client.v1.exchange.orders.$get(
|
|
720
|
+
{ query: { include: 'totalCount', limit: '5' } },
|
|
721
|
+
TestApp.auth,
|
|
722
|
+
)
|
|
723
|
+
expect(response.status).toBe(200)
|
|
724
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
725
|
+
expect(body.meta).toBeDefined()
|
|
726
|
+
expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
|
|
727
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
728
|
+
expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
|
|
729
|
+
// The in-memory total is capped when the page was truncated or hit the cap.
|
|
730
|
+
expect(body.meta!.totalCountCapped).toBe(
|
|
731
|
+
body.meta!.totalCount >= Schema.countCap || body.truncated,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
const bare = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
735
|
+
const bareBody = await TestApp.json(bare, Exchanges.schema.getOrders.Response)
|
|
736
|
+
expect(bare.status).toBe(200)
|
|
737
|
+
expect(bareBody.meta).toBeUndefined()
|
|
738
|
+
}, 60000)
|
|
739
|
+
|
|
740
|
+
test('paginates with an opaque cursor without skipping or duplicating rows', async () => {
|
|
741
|
+
const client = TestApp.client()
|
|
742
|
+
const first = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
743
|
+
const firstBody = await TestApp.json(first, Exchanges.schema.getOrders.Response)
|
|
744
|
+
if (firstBody.nextCursor === null) return
|
|
745
|
+
|
|
746
|
+
const second = await client.v1.exchange.orders.$get(
|
|
747
|
+
{ query: { cursor: firstBody.nextCursor, limit: '5' } },
|
|
748
|
+
TestApp.auth,
|
|
749
|
+
)
|
|
750
|
+
const secondBody = await TestApp.json(second, Exchanges.schema.getOrders.Response)
|
|
751
|
+
|
|
752
|
+
const key = (order: GlobalOrdersResponse['data'][number]) => order.orderId
|
|
753
|
+
const firstKeys = new Set(firstBody.data.map(key))
|
|
754
|
+
for (const order of secondBody.data) expect(firstKeys.has(key(order))).toBe(false)
|
|
755
|
+
}, 60000)
|
|
756
|
+
|
|
757
|
+
test('filters the global feed by maker', async () => {
|
|
758
|
+
const client = TestApp.client()
|
|
759
|
+
const seed = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
760
|
+
const seedBody = await TestApp.json(seed, Exchanges.schema.getOrders.Response)
|
|
761
|
+
const row = seedBody.data[0]
|
|
762
|
+
if (!row) return
|
|
763
|
+
|
|
764
|
+
const response = await client.v1.exchange.orders.$get(
|
|
765
|
+
{ query: { limit: '5', maker: row.maker } },
|
|
766
|
+
TestApp.auth,
|
|
767
|
+
)
|
|
768
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
769
|
+
for (const order of body.data) expect(order.maker).toBe(row.maker)
|
|
770
|
+
}, 60000)
|
|
771
|
+
|
|
772
|
+
test('embeds pair token metadata when requested via `include=tokens`', async () => {
|
|
773
|
+
const client = TestApp.client()
|
|
774
|
+
const response = await client.v1.exchange.orders.$get(
|
|
775
|
+
{ query: { include: 'tokens', limit: '10' } },
|
|
776
|
+
TestApp.auth,
|
|
777
|
+
)
|
|
778
|
+
expect(response.status).toBe(200)
|
|
779
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
780
|
+
if (body.data.length === 0) return
|
|
781
|
+
|
|
782
|
+
const enriched = body.data.find((order) => typeof order.pair.base.symbol === 'string')
|
|
783
|
+
if (enriched) {
|
|
784
|
+
expect(typeof enriched.pair.base.symbol).toBe('string')
|
|
785
|
+
expect(typeof enriched.pair.base.decimals).toBe('number')
|
|
786
|
+
}
|
|
787
|
+
}, 60000)
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
describe('GET /exchange/orders/:orderId', () => {
|
|
791
|
+
test('rejects a non-decimal order id with 400', async () => {
|
|
792
|
+
const client = TestApp.client()
|
|
793
|
+
const response = await client.v1.exchange.orders[':orderId'].$get(
|
|
794
|
+
{ param: { orderId: 'not-a-number' }, query: {} },
|
|
795
|
+
TestApp.auth,
|
|
796
|
+
)
|
|
797
|
+
expect(response.status).toBe(400)
|
|
798
|
+
}, 30000)
|
|
799
|
+
|
|
800
|
+
test('returns 404 for an unknown order id', async () => {
|
|
801
|
+
const client = TestApp.client()
|
|
802
|
+
// A very large id is overwhelmingly likely to not exist on testnet. The
|
|
803
|
+
// on-chain `dex.getOrder` view reverts with `OrderDoesNotExist()` for ids
|
|
804
|
+
// it does not hold, which the handler maps to a 404 (not a 502).
|
|
805
|
+
const unknownOrderId = '99999999999999999999'
|
|
806
|
+
const response = await client.v1.exchange.orders[':orderId'].$get(
|
|
807
|
+
{ param: { orderId: unknownOrderId }, query: {} },
|
|
808
|
+
TestApp.auth,
|
|
809
|
+
)
|
|
810
|
+
// Surface a genuine RPC transport failure as a transient skip; the unknown
|
|
811
|
+
// id itself must resolve to a clean 404.
|
|
812
|
+
if (response.status === 502) return
|
|
813
|
+
expect(response.status).toBe(404)
|
|
814
|
+
}, 30000)
|
|
815
|
+
|
|
816
|
+
test('returns live order state and pair for a real id', async () => {
|
|
817
|
+
const client = TestApp.client()
|
|
818
|
+
const sampleOrderId = await sampleOrderIdFromOrders(client)
|
|
819
|
+
if (!sampleOrderId) return
|
|
820
|
+
|
|
821
|
+
const response = await client.v1.exchange.orders[':orderId'].$get(
|
|
822
|
+
{ param: { orderId: sampleOrderId }, query: {} },
|
|
823
|
+
TestApp.auth,
|
|
824
|
+
)
|
|
825
|
+
// The on-chain `dex.getOrder` view is a single RPC; surface 502 as a
|
|
826
|
+
// transient-skip so the test stays green when the route's contract is
|
|
827
|
+
// intact.
|
|
828
|
+
if (response.status === 502) return
|
|
829
|
+
expect(response.status).toBe(200)
|
|
830
|
+
const order = await TestApp.json(response, Exchanges.schema.getOrder.Response)
|
|
831
|
+
|
|
832
|
+
expect(order.orderId).toBe(sampleOrderId)
|
|
833
|
+
expect(order.maker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
834
|
+
expect(order.amount).toMatch(/^\d+$/)
|
|
835
|
+
expect(order.remaining).toMatch(/^\d+$/)
|
|
836
|
+
expect(['exactSource', 'exactDestination']).toContain(order.mode)
|
|
837
|
+
expect(order.rate).toMatch(/^\d+(\.\d+)?$/)
|
|
838
|
+
expect(order.price).toMatch(/^\d+(\.\d+)?$/)
|
|
839
|
+
expect(order.pair.key).toMatch(/^0x[0-9a-f]{64}$/)
|
|
840
|
+
expect(order.pair.base.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
841
|
+
expect(order.pair.quote.address).toMatch(/^0x[0-9a-f]{40}$/)
|
|
842
|
+
}, 60000)
|
|
843
|
+
|
|
844
|
+
test('embeds pair token metadata when requested via `include=tokens`', async () => {
|
|
845
|
+
const client = TestApp.client()
|
|
846
|
+
const sampleOrderId = await sampleOrderIdFromOrders(client)
|
|
847
|
+
if (!sampleOrderId) return
|
|
848
|
+
|
|
849
|
+
const response = await client.v1.exchange.orders[':orderId'].$get(
|
|
850
|
+
{ param: { orderId: sampleOrderId }, query: { include: 'tokens' } },
|
|
851
|
+
TestApp.auth,
|
|
852
|
+
)
|
|
853
|
+
if (response.status === 502) return
|
|
854
|
+
expect(response.status).toBe(200)
|
|
855
|
+
const order = await TestApp.json(response, Exchanges.schema.getOrder.Response)
|
|
856
|
+
|
|
857
|
+
// Both pair tokens share the same metadata path: when one resolves a
|
|
858
|
+
// symbol, both sides of the trimmed token reference are present.
|
|
859
|
+
if (typeof order.pair.base.symbol === 'string') {
|
|
860
|
+
expect(typeof order.pair.base.decimals).toBe('number')
|
|
861
|
+
expect(typeof order.pair.quote.symbol).toBe('string')
|
|
862
|
+
}
|
|
863
|
+
}, 60000)
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
describe('GET /exchange/orders/:orderId/fills', () => {
|
|
867
|
+
test('rejects a non-decimal order id with 400', async () => {
|
|
868
|
+
const client = TestApp.client()
|
|
869
|
+
const response = await client.v1.exchange.orders[':orderId'].fills.$get(
|
|
870
|
+
{ param: { orderId: 'not-a-number' }, query: {} },
|
|
871
|
+
TestApp.auth,
|
|
872
|
+
)
|
|
873
|
+
expect(response.status).toBe(400)
|
|
874
|
+
}, 30000)
|
|
875
|
+
|
|
876
|
+
test('returns a paginated newest-first fill page for a real order id', async () => {
|
|
877
|
+
const client = TestApp.client()
|
|
878
|
+
const sampleOrderId = await sampleOrderIdFromSwaps(client)
|
|
879
|
+
if (!sampleOrderId) return
|
|
880
|
+
|
|
881
|
+
const response = await client.v1.exchange.orders[':orderId'].fills.$get(
|
|
882
|
+
{ param: { orderId: sampleOrderId }, query: { limit: '5' } },
|
|
883
|
+
TestApp.auth,
|
|
884
|
+
)
|
|
885
|
+
// Treat transient indexer 502s as a skip — see order-detail test.
|
|
886
|
+
if (response.status === 502) return
|
|
887
|
+
expect(response.status).toBe(200)
|
|
888
|
+
const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response)
|
|
889
|
+
|
|
890
|
+
// The sample order was discovered via the swap feed, so it has at least
|
|
891
|
+
// one fill.
|
|
892
|
+
expect(page.data.length).toBeGreaterThan(0)
|
|
893
|
+
for (const fill of page.data) {
|
|
894
|
+
expect(fill.amountFilled).toMatch(/^\d+$/)
|
|
895
|
+
expect(fill.taker).toMatch(/^0x[0-9a-f]{40}$/)
|
|
896
|
+
expect(fill.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
|
|
897
|
+
expect(fill.filledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
898
|
+
}
|
|
899
|
+
// Newest-first ordering.
|
|
900
|
+
for (let i = 1; i < page.data.length; i++) {
|
|
901
|
+
const prev = page.data[i - 1]!
|
|
902
|
+
const curr = page.data[i]!
|
|
903
|
+
expect(
|
|
904
|
+
prev.blockNumber > curr.blockNumber ||
|
|
905
|
+
(prev.blockNumber === curr.blockNumber && prev.logIndex >= curr.logIndex),
|
|
906
|
+
).toBe(true)
|
|
907
|
+
}
|
|
908
|
+
// The base page does not run or embed the count.
|
|
909
|
+
expect(page.meta).toBeUndefined()
|
|
910
|
+
}, 60000)
|
|
911
|
+
|
|
912
|
+
test('embeds an exact fill total on demand via include=totalCount', async () => {
|
|
913
|
+
const client = TestApp.client()
|
|
914
|
+
const sampleOrderId = await sampleOrderIdFromSwaps(client)
|
|
915
|
+
if (!sampleOrderId) return
|
|
916
|
+
|
|
917
|
+
const response = await client.v1.exchange.orders[':orderId'].fills.$get(
|
|
918
|
+
{ param: { orderId: sampleOrderId }, query: { include: 'totalCount', limit: '5' } },
|
|
919
|
+
TestApp.auth,
|
|
920
|
+
)
|
|
921
|
+
if (response.status === 502) return
|
|
922
|
+
expect(response.status).toBe(200)
|
|
923
|
+
const page = await TestApp.json(response, Exchanges.schema.getOrderFills.Response)
|
|
924
|
+
|
|
925
|
+
expect(response.headers.get('server-timing')?.includes('order_fills_count;dur=')).toBe(true)
|
|
926
|
+
expect(Number.isInteger(page.meta?.totalCount)).toBe(true)
|
|
927
|
+
expect(page.meta!.totalCount).toBeGreaterThanOrEqual(page.data.length)
|
|
928
|
+
// The count is sort-key-pruned on ClickHouse, so it is exact (never capped).
|
|
929
|
+
expect(page.meta!.totalCountCapped).toBe(false)
|
|
930
|
+
}, 60000)
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Discovers a real order id from the recent global swap feed so the
|
|
935
|
+
* order-fills test can run against live testnet data without hard-coding
|
|
936
|
+
* ids (a swap-feed id is guaranteed to have at least one fill). Returns
|
|
937
|
+
* `null` when the feed is empty.
|
|
938
|
+
*/
|
|
939
|
+
async function sampleOrderIdFromSwaps(client: ReturnType<typeof TestApp.client>) {
|
|
940
|
+
const response = await client.v1.exchange.swaps.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
941
|
+
if (response.status !== 200) return null
|
|
942
|
+
const body = await TestApp.json(response, Exchanges.schema.getSwaps.Response)
|
|
943
|
+
return body.data[0]?.fills[0]?.orderId ?? null
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Discovers a live resting-order id from the global orders feed for the
|
|
948
|
+
* order-detail tests. Unlike the swap feed — which (since the point-in-time
|
|
949
|
+
* state rework) also surfaces fills on flip-only orders and on books missing
|
|
950
|
+
* from `dex_pairs` — the orders feed only lists ids the order-detail
|
|
951
|
+
* endpoint can resolve. Returns `null` when the book is empty.
|
|
952
|
+
*/
|
|
953
|
+
async function sampleOrderIdFromOrders(client: ReturnType<typeof TestApp.client>) {
|
|
954
|
+
const response = await client.v1.exchange.orders.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
955
|
+
if (response.status !== 200) return null
|
|
956
|
+
const body = await TestApp.json(response, Exchanges.schema.getOrders.Response)
|
|
957
|
+
return body.data[0]?.orderId ?? null
|
|
958
|
+
}
|