tapimo 0.7.0 → 0.7.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/dist/ApiKeys.d.ts +24 -7
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +17 -5
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +12104 -7861
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +191 -128
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +17 -4
- package/dist/Assets.js.map +1 -1
- package/dist/Scope.d.ts +19 -8
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +25 -9
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/admin/App.d.ts +601 -20
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +12 -2
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +94 -11
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +141 -32
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +1069 -0
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -0
- package/dist/admin/apps/earn-vaults.js +409 -0
- package/dist/admin/apps/earn-vaults.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +166 -166
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -95
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +2 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +18 -2
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +22 -1
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +59 -39
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/Zones.d.ts +8335 -0
- package/dist/apps/Zones.d.ts.map +1 -0
- package/dist/apps/Zones.js +38 -0
- package/dist/apps/Zones.js.map +1 -0
- package/dist/apps/data/App.d.ts +14377 -10165
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +91 -19
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +1 -1
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/index.d.ts +0 -1
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +0 -1
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +32 -32
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +130 -65
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +312 -312
- package/dist/apps/data/routes/balances.d.ts +18 -18
- package/dist/apps/data/routes/balances.js +5 -5
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +62 -35
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/earn.d.ts +2144 -0
- package/dist/apps/data/routes/earn.d.ts.map +1 -0
- package/dist/apps/data/routes/earn.js +947 -0
- package/dist/apps/data/routes/earn.js.map +1 -0
- package/dist/apps/data/routes/exchanges.d.ts +1231 -324
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +656 -193
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +59 -8
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +4092 -387
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +1403 -256
- 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/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +19 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +265 -265
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +11 -11
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +5 -5
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +211 -22
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +49 -9
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +116 -88
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +198 -91
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +55 -55
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +78 -39
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +78 -29
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +165 -67
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +10 -10
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +34 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +5 -3
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +175 -65
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +281 -0
- package/dist/apps/data/routes/zones.d.ts.map +1 -0
- package/dist/apps/data/routes/zones.js +252 -0
- package/dist/apps/data/routes/zones.js.map +1 -0
- package/dist/apps/docs/reference.js +6 -5
- package/dist/apps/docs/reference.js.map +1 -1
- package/dist/apps/index.d.ts +2 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +2 -1
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +670 -670
- package/dist/apps/management/Billing.d.ts +8 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +12 -0
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/Error.d.ts +12 -0
- package/dist/apps/management/Error.d.ts.map +1 -0
- package/dist/apps/management/Error.js +10 -0
- package/dist/apps/management/Error.js.map +1 -0
- package/dist/apps/management/routes/api-keys.d.ts +5 -5
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +2 -8
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +119 -119
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/invitations.d.ts +62 -62
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +4 -10
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +30 -30
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +78 -78
- package/dist/apps/management/routes/orgs.d.ts +48 -48
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +2 -8
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +9 -9
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +80 -80
- package/dist/apps/management/routes/verified-token-requests.d.ts +36 -36
- package/dist/apps/management/routes/webhooks.d.ts +50 -50
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +15 -3
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +55 -56
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/App.js +192 -27
- package/dist/apps/mpp/App.js.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +0 -20
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +0 -24
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts +40 -0
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Adapter.js +66 -0
- package/dist/apps/mpp/internal/Adapter.js.map +1 -0
- package/dist/apps/mpp/internal/Error.d.ts +4 -0
- package/dist/apps/mpp/internal/Error.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Error.js +41 -0
- package/dist/apps/mpp/internal/Error.js.map +1 -0
- package/dist/apps/mpp/internal/Idempotency.d.ts +33 -0
- package/dist/apps/mpp/internal/Idempotency.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Idempotency.js +28 -0
- package/dist/apps/mpp/internal/Idempotency.js.map +1 -0
- package/dist/apps/mpp/internal/Policy.d.ts +46 -0
- package/dist/apps/mpp/internal/Policy.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Policy.js +57 -0
- package/dist/apps/mpp/internal/Policy.js.map +1 -0
- package/dist/apps/mpp/internal/ScreenManager.d.ts +27 -0
- package/dist/apps/mpp/internal/ScreenManager.d.ts.map +1 -0
- package/dist/apps/mpp/internal/ScreenManager.js +18 -0
- package/dist/apps/mpp/internal/ScreenManager.js.map +1 -0
- package/dist/apps/mpp/internal/Screening.d.ts +35 -0
- package/dist/apps/mpp/internal/Screening.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Screening.js +20 -0
- package/dist/apps/mpp/internal/Screening.js.map +1 -0
- package/dist/apps/mpp/internal/Sponsorship.d.ts +36 -0
- package/dist/apps/mpp/internal/Sponsorship.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Sponsorship.js +103 -0
- package/dist/apps/mpp/internal/Sponsorship.js.map +1 -0
- package/dist/apps/mpp/internal/Utils.d.ts +7 -0
- package/dist/apps/mpp/internal/Utils.d.ts.map +1 -0
- package/dist/apps/mpp/internal/Utils.js +24 -0
- package/dist/apps/mpp/internal/Utils.js.map +1 -0
- package/dist/apps/relay/App.js +184 -24
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts +4 -0
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +16 -7
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts +20 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +342 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +921 -1
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +269 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earnVaults.d.ts +68 -0
- package/dist/db/tables/earnVaults.d.ts.map +1 -0
- package/dist/db/tables/earnVaults.js +72 -0
- package/dist/db/tables/earnVaults.js.map +1 -0
- package/dist/db/tables/fundingCatalog.d.ts +56 -0
- package/dist/db/tables/fundingCatalog.d.ts.map +1 -0
- package/dist/db/tables/fundingCatalog.js +85 -0
- package/dist/db/tables/fundingCatalog.js.map +1 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts +159 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -0
- package/dist/db/tables/fundingDepositAddresses.js +206 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -0
- package/dist/db/tables/fundingDeposits.d.ts +115 -0
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -0
- package/dist/db/tables/fundingDeposits.js +100 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -0
- package/dist/db/tables/fundingIdempotency.d.ts +91 -0
- package/dist/db/tables/fundingIdempotency.d.ts.map +1 -0
- package/dist/db/tables/fundingIdempotency.js +103 -0
- package/dist/db/tables/fundingIdempotency.js.map +1 -0
- package/dist/db/tables/fundingTransferEvents.d.ts +12 -0
- package/dist/db/tables/fundingTransferEvents.d.ts.map +1 -0
- package/dist/db/tables/fundingTransferEvents.js +18 -0
- package/dist/db/tables/fundingTransferEvents.js.map +1 -0
- package/dist/db/tables/fundingTransferTransactions.d.ts +15 -0
- package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -0
- package/dist/db/tables/fundingTransferTransactions.js +27 -0
- package/dist/db/tables/fundingTransferTransactions.js.map +1 -0
- package/dist/db/tables/fundingTransfers.d.ts +82 -0
- package/dist/db/tables/fundingTransfers.d.ts.map +1 -0
- package/dist/db/tables/fundingTransfers.js +89 -0
- package/dist/db/tables/fundingTransfers.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +8 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +35 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +8 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +16 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +13 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -1
- package/dist/db/tables/requestUsage.js +16 -0
- package/dist/db/tables/requestUsage.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +4 -2
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +14 -4
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/users.d.ts +8 -0
- package/dist/db/tables/users.d.ts.map +1 -1
- package/dist/db/tables/users.js +16 -0
- package/dist/db/tables/users.js.map +1 -1
- package/dist/db/tables/verifiedTokens.d.ts +14 -7
- package/dist/db/tables/verifiedTokens.d.ts.map +1 -1
- package/dist/db/tables/verifiedTokens.js +9 -7
- package/dist/db/tables/verifiedTokens.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +31 -1
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +24 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/handlers/relay.js +108 -108
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +26 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +35 -2
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +16 -10
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/Cursor.js +1 -1
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EarnRates.d.ts +89 -0
- package/dist/internal/EarnRates.d.ts.map +1 -0
- package/dist/internal/EarnRates.js +266 -0
- package/dist/internal/EarnRates.js.map +1 -0
- package/dist/internal/EarnVaults.d.ts +171 -0
- package/dist/internal/EarnVaults.d.ts.map +1 -0
- package/dist/internal/EarnVaults.js +547 -0
- package/dist/internal/EarnVaults.js.map +1 -0
- package/dist/internal/EdgeCache.d.ts +9 -2
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +36 -9
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Id.d.ts +11 -0
- package/dist/internal/Id.d.ts.map +1 -1
- package/dist/internal/Id.js +13 -0
- package/dist/internal/Id.js.map +1 -1
- package/dist/internal/Idempotency.d.ts +83 -0
- package/dist/internal/Idempotency.d.ts.map +1 -0
- package/dist/internal/Idempotency.js +113 -0
- package/dist/internal/Idempotency.js.map +1 -0
- package/dist/internal/Log.d.ts +64 -9
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +29 -7
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +47 -1
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +1 -1
- 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/Provider.d.ts +14 -0
- package/dist/internal/Provider.d.ts.map +1 -0
- package/dist/internal/Provider.js +5 -0
- package/dist/internal/Provider.js.map +1 -0
- package/dist/internal/Schema.d.ts +8 -3
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +24 -13
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +6 -0
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +35 -0
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +43 -24
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +79 -11
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +82 -65
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +83 -12
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +34 -28
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +9 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +15 -10
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/exchange/Provider.d.ts +219 -0
- package/dist/internal/exchange/Provider.d.ts.map +1 -0
- package/dist/internal/exchange/Provider.js +117 -0
- package/dist/internal/exchange/Provider.js.map +1 -0
- package/dist/internal/exchange/index.d.ts +2 -0
- package/dist/internal/exchange/index.d.ts.map +1 -0
- package/dist/internal/exchange/index.js +2 -0
- package/dist/internal/exchange/index.js.map +1 -0
- package/dist/internal/exchange/providers/uniswap.d.ts +57 -0
- package/dist/internal/exchange/providers/uniswap.d.ts.map +1 -0
- package/dist/internal/exchange/providers/uniswap.js +553 -0
- package/dist/internal/exchange/providers/uniswap.js.map +1 -0
- package/dist/internal/funding/Action.d.ts +55 -0
- package/dist/internal/funding/Action.d.ts.map +1 -0
- package/dist/internal/funding/Action.js +74 -0
- package/dist/internal/funding/Action.js.map +1 -0
- package/dist/internal/funding/Catalog.d.ts +107 -0
- package/dist/internal/funding/Catalog.d.ts.map +1 -0
- package/dist/internal/funding/Catalog.js +270 -0
- package/dist/internal/funding/Catalog.js.map +1 -0
- package/dist/internal/funding/Chain.d.ts +76 -0
- package/dist/internal/funding/Chain.d.ts.map +1 -0
- package/dist/internal/funding/Chain.js +38 -0
- package/dist/internal/funding/Chain.js.map +1 -0
- package/dist/internal/funding/Deposit.d.ts +366 -0
- package/dist/internal/funding/Deposit.d.ts.map +1 -0
- package/dist/internal/funding/Deposit.js +286 -0
- package/dist/internal/funding/Deposit.js.map +1 -0
- package/dist/internal/funding/DepositAddress.d.ts +345 -0
- package/dist/internal/funding/DepositAddress.d.ts.map +1 -0
- package/dist/internal/funding/DepositAddress.js +189 -0
- package/dist/internal/funding/DepositAddress.js.map +1 -0
- package/dist/internal/funding/Evidence.d.ts +52 -0
- package/dist/internal/funding/Evidence.d.ts.map +1 -0
- package/dist/internal/funding/Evidence.js +314 -0
- package/dist/internal/funding/Evidence.js.map +1 -0
- package/dist/internal/funding/Provider.d.ts +1186 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -0
- package/dist/internal/funding/Provider.js +1081 -0
- package/dist/internal/funding/Provider.js.map +1 -0
- package/dist/internal/funding/Reconciliation.d.ts +113 -0
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -0
- package/dist/internal/funding/Reconciliation.js +676 -0
- package/dist/internal/funding/Reconciliation.js.map +1 -0
- package/dist/internal/funding/Route.d.ts +21 -0
- package/dist/internal/funding/Route.d.ts.map +1 -0
- package/dist/internal/funding/Route.js +5 -0
- package/dist/internal/funding/Route.js.map +1 -0
- package/dist/internal/funding/Subsidy.d.ts +113 -0
- package/dist/internal/funding/Subsidy.d.ts.map +1 -0
- package/dist/internal/funding/Subsidy.js +138 -0
- package/dist/internal/funding/Subsidy.js.map +1 -0
- package/dist/internal/funding/Token.d.ts +41 -0
- package/dist/internal/funding/Token.d.ts.map +1 -0
- package/dist/internal/funding/Token.js +22 -0
- package/dist/internal/funding/Token.js.map +1 -0
- package/dist/internal/funding/Transfer.d.ts +643 -0
- package/dist/internal/funding/Transfer.d.ts.map +1 -0
- package/dist/internal/funding/Transfer.js +332 -0
- package/dist/internal/funding/Transfer.js.map +1 -0
- package/dist/internal/funding/index.d.ts +13 -0
- package/dist/internal/funding/index.d.ts.map +1 -0
- package/dist/internal/funding/index.js +13 -0
- package/dist/internal/funding/index.js.map +1 -0
- package/dist/internal/funding/providers/across.d.ts +21 -0
- package/dist/internal/funding/providers/across.d.ts.map +1 -0
- package/dist/internal/funding/providers/across.js +117 -0
- package/dist/internal/funding/providers/across.js.map +1 -0
- package/dist/internal/funding/providers/relay.d.ts +46 -0
- package/dist/internal/funding/providers/relay.d.ts.map +1 -0
- package/dist/internal/funding/providers/relay.js +464 -0
- package/dist/internal/funding/providers/relay.js.map +1 -0
- package/dist/internal/funding/providers/rhino.d.ts +15 -0
- package/dist/internal/funding/providers/rhino.d.ts.map +1 -0
- package/dist/internal/funding/providers/rhino.js +116 -0
- package/dist/internal/funding/providers/rhino.js.map +1 -0
- package/dist/internal/funding/providers/squid.d.ts +19 -0
- package/dist/internal/funding/providers/squid.d.ts.map +1 -0
- package/dist/internal/funding/providers/squid.js +120 -0
- package/dist/internal/funding/providers/squid.js.map +1 -0
- package/dist/internal/funding/providers/symbiosis.d.ts +20 -0
- package/dist/internal/funding/providers/symbiosis.d.ts.map +1 -0
- package/dist/internal/funding/providers/symbiosis.js +176 -0
- package/dist/internal/funding/providers/symbiosis.js.map +1 -0
- package/dist/internal/index.d.ts +5 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +5 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/webhookDestinations/url.js +61 -57
- package/dist/internal/webhookDestinations/url.js.map +1 -1
- package/dist/providers/index.d.ts +43 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/package.json +9 -4
- package/src/ApiKeys.test.ts +11 -7
- package/src/ApiKeys.ts +37 -9
- package/src/App.test.ts +85 -10
- package/src/App.ts +224 -137
- package/src/Assets.ts +29 -4
- package/src/Client.test-d.ts +650 -30
- package/src/Scope.test.ts +19 -0
- package/src/Scope.ts +28 -11
- package/src/Webhooks.test.ts +25 -0
- package/src/Webhooks.ts +1 -1
- package/src/admin/App.test.ts +9 -2
- package/src/admin/App.ts +32 -9
- package/src/admin/apps/api-keys.test.ts +167 -23
- package/src/admin/apps/api-keys.ts +166 -28
- package/src/admin/apps/earn-vaults.test.ts +338 -0
- package/src/admin/apps/earn-vaults.ts +572 -0
- package/src/admin/apps/scopes.test.ts +16 -1
- package/src/admin/ui/plugin.ts +2 -2
- package/src/admin/ui/src/components/EarnVaultForm.tsx +137 -0
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -2
- package/src/admin/ui/src/components/Page.tsx +1 -1
- package/src/admin/ui/src/components/ThemeToggle.tsx +1 -1
- package/src/admin/ui/src/lib/api-keys.ts +37 -7
- package/src/admin/ui/src/lib/client.ts +1 -1
- package/src/admin/ui/src/lib/config.ts +1 -1
- package/src/admin/ui/src/lib/early-access.ts +8 -9
- package/src/admin/ui/src/lib/earn-vaults.ts +207 -0
- package/src/admin/ui/src/lib/format.ts +13 -0
- package/src/admin/ui/src/lib/organizations.test.ts +14 -4
- package/src/admin/ui/src/lib/organizations.ts +7 -1
- package/src/admin/ui/src/lib/response.ts +22 -0
- package/src/admin/ui/src/lib/scopes.test.ts +19 -11
- package/src/admin/ui/src/lib/scopes.ts +28 -7
- package/src/admin/ui/src/lib/verified-tokens.ts +10 -9
- package/src/admin/ui/src/routeTree.gen.ts +70 -7
- package/src/admin/ui/src/router.tsx +2 -2
- package/src/admin/ui/src/routes/__root.tsx +9 -8
- package/src/admin/ui/src/routes/api-keys.tsx +475 -64
- package/src/admin/ui/src/routes/early-access.tsx +5 -12
- package/src/admin/ui/src/routes/earn.vaults.tsx +348 -0
- package/src/admin/ui/src/routes/earn.vaults_.$vaultId.edit.tsx +154 -0
- package/src/admin/ui/src/routes/earn.vaults_.new.tsx +190 -0
- package/src/admin/ui/src/routes/organizations.tsx +4 -7
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +56 -54
- package/src/admin/ui/src/routes/verified-tokens.tsx +4 -4
- package/src/admin/ui/vite.config.ts +4 -1
- package/src/admin/ui.generated.ts +189 -95
- package/src/analytics/Analytics.test.ts +69 -10
- package/src/analytics/Analytics.ts +20 -1
- package/src/analytics/tables/requestEvents.ts +79 -37
- package/src/apps/Zones.test.ts +33 -0
- package/src/apps/Zones.ts +40 -0
- package/src/apps/composition.test.ts +84 -1
- package/src/apps/data/App.test.ts +38 -9
- package/src/apps/data/App.ts +96 -33
- package/src/apps/data/FxOracle.ts +1 -1
- package/src/apps/data/index.ts +0 -1
- package/src/apps/data/routes/activities.test.ts +82 -6
- package/src/apps/data/routes/activities.ts +131 -71
- package/src/apps/data/routes/balances.test.ts +50 -37
- package/src/apps/data/routes/balances.ts +5 -5
- package/src/apps/data/routes/blocks.test.ts +2 -0
- package/src/apps/data/routes/coingecko.test.ts +22 -0
- package/src/apps/data/routes/coingecko.ts +84 -48
- package/src/apps/data/routes/earn.test.ts +1108 -0
- package/src/apps/data/routes/earn.ts +1391 -0
- package/src/apps/data/routes/exchanges.test.ts +82 -33
- package/src/apps/data/routes/exchanges.ts +842 -227
- package/src/apps/data/routes/fee-amm.test.ts +4 -6
- package/src/apps/data/routes/fee-amm.ts +69 -12
- package/src/apps/data/routes/funding.test.ts +2712 -221
- package/src/apps/data/routes/funding.ts +1604 -387
- package/src/apps/data/routes/indexer.test.ts +43 -34
- package/src/apps/data/routes/indexer.ts +19 -2
- package/src/apps/data/routes/receipts.test.ts +5 -3
- package/src/apps/data/routes/receipts.ts +13 -11
- package/src/apps/data/routes/rpc.test.ts +446 -28
- package/src/apps/data/routes/rpc.ts +243 -25
- package/src/apps/data/routes/tokenlist.test.ts +15 -0
- package/src/apps/data/routes/tokenlist.ts +88 -22
- package/src/apps/data/routes/tokens.ts +262 -86
- package/src/apps/data/routes/transactions.test.ts +2 -0
- package/src/apps/data/routes/transactions.ts +77 -42
- package/src/apps/data/routes/transfers.test.ts +166 -4
- package/src/apps/data/routes/transfers.ts +216 -78
- package/src/apps/data/routes/valuation.test.ts +62 -9
- package/src/apps/data/routes/valuation.ts +2 -2
- package/src/apps/data/routes/verified-tokens.test.ts +51 -0
- package/src/apps/data/routes/verified-tokens.ts +52 -15
- package/src/apps/data/routes/webhooks.test.ts +432 -33
- package/src/apps/data/routes/webhooks.ts +189 -79
- package/src/apps/data/routes/zones.test.ts +473 -0
- package/src/apps/data/routes/zones.ts +338 -0
- package/src/apps/docs/reference.ts +8 -6
- package/src/apps/index.ts +2 -1
- package/src/apps/management/Billing.test.ts +29 -0
- package/src/apps/management/Billing.ts +23 -0
- package/src/apps/management/Error.ts +12 -0
- package/src/apps/management/routes/api-keys.test.ts +5 -3
- package/src/apps/management/routes/api-keys.ts +2 -9
- package/src/apps/management/routes/invitations.ts +4 -11
- package/src/apps/management/routes/orgs.ts +2 -9
- package/src/apps/management/routes/scopes.test.ts +7 -2
- package/src/apps/management/routes/scopes.ts +2 -2
- package/src/apps/management/routes/webhooks.test.ts +6 -1
- package/src/apps/management/routes/webhooks.ts +15 -3
- package/src/apps/mpp/App.ts +276 -29
- package/src/apps/mpp/Mpp.test.ts +631 -26
- package/src/apps/mpp/Mpp.ts +0 -42
- package/src/apps/mpp/internal/Adapter.ts +108 -3
- package/src/apps/mpp/internal/Error.test.ts +33 -0
- package/src/apps/mpp/internal/Error.ts +44 -6
- package/src/apps/mpp/internal/Idempotency.test.ts +213 -0
- package/src/apps/mpp/internal/Idempotency.ts +59 -5
- package/src/apps/mpp/internal/Policy.test.ts +122 -0
- package/src/apps/mpp/internal/Policy.ts +98 -0
- package/src/apps/mpp/internal/ScreenManager.test.ts +68 -0
- package/src/apps/mpp/internal/ScreenManager.ts +41 -0
- package/src/apps/mpp/internal/Screening.ts +40 -3
- package/src/apps/mpp/internal/Sponsorship.ts +144 -0
- package/src/apps/mpp/internal/Utils.ts +30 -0
- package/src/apps/relay/App.test.ts +495 -64
- package/src/apps/relay/App.ts +234 -24
- package/src/apps/relay/Sponsorships.test.ts +51 -0
- package/src/apps/relay/Sponsorships.ts +19 -7
- package/src/db/Db.ts +439 -0
- package/src/db/Schema.ts +325 -2
- package/src/db/tables/earnVaults.test.ts +185 -0
- package/src/db/tables/earnVaults.ts +151 -0
- package/src/db/tables/fundingCatalog.test.ts +108 -0
- package/src/db/tables/fundingCatalog.ts +157 -0
- package/src/db/tables/fundingDepositAddresses.test.ts +580 -0
- package/src/db/tables/fundingDepositAddresses.ts +376 -0
- package/src/db/tables/fundingDeposits.test.ts +261 -0
- package/src/db/tables/fundingDeposits.ts +229 -0
- package/src/db/tables/fundingIdempotency.test.ts +182 -0
- package/src/db/tables/fundingIdempotency.ts +180 -0
- package/src/db/tables/fundingTransferEvents.ts +28 -0
- package/src/db/tables/fundingTransferTransactions.test.ts +69 -0
- package/src/db/tables/fundingTransferTransactions.ts +37 -0
- package/src/db/tables/fundingTransfers.test.ts +89 -0
- package/src/db/tables/fundingTransfers.ts +153 -0
- package/src/db/tables/organizations.test.ts +88 -0
- package/src/db/tables/organizations.ts +42 -1
- package/src/db/tables/projects.ts +16 -0
- package/src/db/tables/requestUsage.ts +23 -0
- package/src/db/tables/sponsoredTransactions.test.ts +52 -2
- package/src/db/tables/sponsoredTransactions.ts +18 -6
- package/src/db/tables/users.ts +16 -0
- package/src/db/tables/verifiedTokens.ts +19 -14
- package/src/db/tables/webhookSubscriptions.ts +58 -3
- package/src/handlers/relay.test.ts +2 -0
- package/src/handlers/relay.ts +95 -99
- package/src/internal/Auth.test.ts +48 -0
- package/src/internal/Auth.ts +61 -2
- package/src/internal/Cache.ts +13 -9
- package/src/internal/Cursor.test.ts +2 -2
- package/src/internal/Cursor.ts +1 -1
- package/src/internal/EarnRates.test.ts +125 -0
- package/src/internal/EarnRates.ts +409 -0
- package/src/internal/EarnVaults.test.ts +195 -0
- package/src/internal/EarnVaults.ts +876 -0
- package/src/internal/EdgeCache.test.ts +37 -5
- package/src/internal/EdgeCache.ts +39 -7
- package/src/internal/Id.ts +14 -0
- package/src/internal/Idempotency.test.ts +109 -0
- package/src/internal/Idempotency.ts +237 -0
- package/src/internal/Log.test-d.ts +41 -8
- package/src/internal/Log.test.ts +189 -8
- package/src/internal/Log.ts +124 -17
- package/src/internal/MetricSink.test.ts +244 -4
- package/src/internal/MetricSink.ts +51 -1
- package/src/internal/OpenApi.ts +3 -0
- package/src/internal/Provider.test.ts +17 -0
- package/src/internal/Provider.ts +17 -0
- package/src/internal/Schema.test.ts +13 -0
- package/src/internal/Schema.ts +41 -24
- package/src/internal/Store.test.ts +25 -0
- package/src/internal/Store.ts +46 -0
- package/src/internal/Tidx.local.test.ts +78 -3
- package/src/internal/Tidx.test.ts +212 -18
- package/src/internal/Tidx.ts +112 -32
- package/src/internal/VerifiedTokens.test.ts +15 -0
- package/src/internal/Viem.test.ts +170 -17
- package/src/internal/Viem.ts +111 -28
- package/src/internal/WebhookDestination.ts +25 -23
- package/src/internal/Webhooks.test.ts +59 -0
- package/src/internal/Webhooks.ts +34 -10
- package/src/internal/exchange/Provider.test.ts +216 -0
- package/src/internal/exchange/Provider.ts +326 -0
- package/src/internal/exchange/index.test.ts +6 -0
- package/src/internal/exchange/index.ts +1 -0
- package/src/internal/exchange/providers/uniswap.test.ts +122 -0
- package/src/internal/exchange/providers/uniswap.ts +682 -0
- package/src/internal/funding/Action.test.ts +138 -0
- package/src/internal/funding/Action.ts +117 -0
- package/src/internal/funding/Catalog.test.ts +311 -0
- package/src/internal/funding/Catalog.ts +347 -0
- package/src/internal/funding/Chain.test.ts +79 -0
- package/src/internal/funding/Chain.ts +120 -0
- package/src/internal/funding/Deposit.test.ts +26 -0
- package/src/internal/funding/Deposit.ts +453 -0
- package/src/internal/funding/DepositAddress.test.ts +18 -0
- package/src/internal/funding/DepositAddress.ts +312 -0
- package/src/internal/funding/Evidence.test.ts +297 -0
- package/src/internal/funding/Evidence.ts +430 -0
- package/src/internal/funding/Provider.test.ts +1328 -0
- package/src/internal/funding/Provider.ts +1796 -0
- package/src/internal/funding/Reconciliation.local.test.ts +504 -0
- package/src/internal/funding/Reconciliation.test.ts +2020 -0
- package/src/internal/funding/Reconciliation.ts +930 -0
- package/src/internal/funding/Route.test.ts +66 -0
- package/src/internal/funding/Route.ts +29 -0
- package/src/internal/funding/Subsidy.local.test.ts +152 -0
- package/src/internal/funding/Subsidy.test.ts +75 -0
- package/src/internal/funding/Subsidy.ts +260 -0
- package/src/internal/funding/Token.test.ts +47 -0
- package/src/internal/funding/Token.ts +59 -0
- package/src/internal/funding/Transfer.test.ts +369 -0
- package/src/internal/funding/Transfer.ts +522 -0
- package/src/internal/funding/index.test.ts +20 -0
- package/src/internal/funding/index.ts +12 -0
- package/src/internal/funding/providers/across.ts +149 -0
- package/src/{apps/data/FundingQuoteProviders.test.ts → internal/funding/providers/index.test.ts} +178 -157
- package/src/internal/funding/providers/relay.test.ts +474 -0
- package/src/internal/funding/providers/relay.ts +559 -0
- package/src/internal/funding/providers/rhino.ts +139 -0
- package/src/internal/funding/providers/squid.ts +151 -0
- package/src/internal/funding/providers/symbiosis.ts +221 -0
- package/src/internal/index.test.ts +5 -1
- package/src/internal/index.ts +5 -1
- package/src/internal/webhookDestinations/url.ts +49 -45
- package/src/providers/index.test.ts +14 -0
- package/src/providers/index.ts +54 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts +0 -174
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingQuoteProviders.js +0 -775
- package/dist/apps/data/FundingQuoteProviders.js.map +0 -1
- package/dist/internal/FundingQuotes.d.ts +0 -864
- package/dist/internal/FundingQuotes.d.ts.map +0 -1
- package/dist/internal/FundingQuotes.js +0 -672
- package/dist/internal/FundingQuotes.js.map +0 -1
- package/src/apps/data/FundingQuoteProviders.ts +0 -1043
- package/src/internal/FundingQuotes.test.ts +0 -456
- package/src/internal/FundingQuotes.ts +0 -944
|
@@ -1,13 +1,89 @@
|
|
|
1
|
+
import { Hash, Hex, Value } from 'ox'
|
|
1
2
|
import { Schema } from 'tapimo'
|
|
3
|
+
import { decodeFunctionData, erc20Abi, maxUint256 } from 'viem'
|
|
4
|
+
import { base } from 'viem/chains'
|
|
5
|
+
import { tempoMainnet } from 'viem/tempo/chains'
|
|
2
6
|
|
|
3
|
-
import
|
|
7
|
+
import * as Anvil from '../../../../test/Anvil.js'
|
|
8
|
+
import * as FundingDepositAddresses from '../../../db/tables/fundingDepositAddresses.js'
|
|
9
|
+
import * as FundingIdempotencyTable from '../../../db/tables/fundingIdempotency.js'
|
|
10
|
+
import * as Cache from '../../../internal/Cache.js'
|
|
11
|
+
import * as Cursor from '../../../internal/Cursor.js'
|
|
12
|
+
import * as FundingCatalog from '../../../internal/funding/Catalog.js'
|
|
13
|
+
import * as FundingDeposit from '../../../internal/funding/Deposit.js'
|
|
14
|
+
import * as FundingDepositAddress from '../../../internal/funding/DepositAddress.js'
|
|
15
|
+
import * as FundingProvider from '../../../internal/funding/Provider.js'
|
|
16
|
+
import * as FundingTransfer from '../../../internal/funding/Transfer.js'
|
|
17
|
+
import * as Idempotency from '../../../internal/Idempotency.js'
|
|
18
|
+
import * as Across from '../../../internal/funding/providers/across.js'
|
|
19
|
+
import * as Relay from '../../../internal/funding/providers/relay.js'
|
|
20
|
+
import * as Rhino from '../../../internal/funding/providers/rhino.js'
|
|
21
|
+
import * as Squid from '../../../internal/funding/providers/squid.js'
|
|
22
|
+
import * as Symbiosis from '../../../internal/funding/providers/symbiosis.js'
|
|
4
23
|
import type * as Log from '../../../internal/Log.js'
|
|
24
|
+
import * as Store from '../../../internal/Store.js'
|
|
5
25
|
import * as TestApp from '../../../../test/App.js'
|
|
26
|
+
import * as TestFunding from '../../../../test/Funding.js'
|
|
6
27
|
import * as Funding from './funding.js'
|
|
7
28
|
|
|
29
|
+
const db = TestApp.database()
|
|
30
|
+
const relayApiKey = process.env.RELAY_API_KEY
|
|
31
|
+
|
|
32
|
+
const providers = [
|
|
33
|
+
Across.across({
|
|
34
|
+
apiKey: 'across-api-key',
|
|
35
|
+
integratorId: '0xdead',
|
|
36
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
37
|
+
}),
|
|
38
|
+
Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
39
|
+
Rhino.rhino(),
|
|
40
|
+
Squid.squid({
|
|
41
|
+
integratorId: 'squid-integrator-id',
|
|
42
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
43
|
+
}),
|
|
44
|
+
Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
45
|
+
] as const
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Providers with executable coverage on the static Base USDC → pathUSD
|
|
49
|
+
* corridor. Each entry gets a fork execution suite (live quote, calls executed
|
|
50
|
+
* on an anvil Base fork); covering another provider is one entry here.
|
|
51
|
+
*/
|
|
52
|
+
const executableProviders = [
|
|
53
|
+
{
|
|
54
|
+
create: (sender: string) =>
|
|
55
|
+
Relay.relay({
|
|
56
|
+
...(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
|
|
57
|
+
userAddress: sender,
|
|
58
|
+
}),
|
|
59
|
+
id: 'relay',
|
|
60
|
+
},
|
|
61
|
+
] as const
|
|
62
|
+
|
|
63
|
+
beforeAll(() => TestFunding.publish(db))
|
|
64
|
+
|
|
65
|
+
function create(options: TestApp.create.Options = {}) {
|
|
66
|
+
return TestApp.create({ ...options, db })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('funding OpenAPI', () => {
|
|
70
|
+
test('publishes get operation ids for funding inventory', async () => {
|
|
71
|
+
const app = create({ auth: false })
|
|
72
|
+
const spec = await (await app.request('/openapi.json')).json()
|
|
73
|
+
|
|
74
|
+
expect({
|
|
75
|
+
chains: spec.paths['/v1/funding/chains'].get.operationId,
|
|
76
|
+
providers: spec.paths['/v1/funding/providers'].get.operationId,
|
|
77
|
+
}).toEqual({
|
|
78
|
+
chains: 'getFundingChains',
|
|
79
|
+
providers: 'getFundingProviders',
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
8
84
|
describe('GET /funding/quotes', () => {
|
|
9
85
|
test('publishes alphabetically ordered quote example fields', async () => {
|
|
10
|
-
const app =
|
|
86
|
+
const app = create({ auth: false })
|
|
11
87
|
const spec = await (await app.request('/openapi.json')).json()
|
|
12
88
|
const operation = spec.paths['/v1/funding/quotes'].get
|
|
13
89
|
const response = operation.responses['200'].content['application/json']
|
|
@@ -17,7 +93,7 @@ describe('GET /funding/quotes', () => {
|
|
|
17
93
|
})
|
|
18
94
|
|
|
19
95
|
test('returns successful live quotes with output amounts', async () => {
|
|
20
|
-
const app =
|
|
96
|
+
const app = create({ providers: [relayProvider()] })
|
|
21
97
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
22
98
|
expect(response.status).toBe(200)
|
|
23
99
|
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
@@ -47,20 +123,84 @@ describe('GET /funding/quotes', () => {
|
|
|
47
123
|
expect(body.data[0]?.destinationToken).not.toHaveProperty('amount')
|
|
48
124
|
})
|
|
49
125
|
|
|
126
|
+
test('resolves token identifiers through the composed route', async () => {
|
|
127
|
+
const app = create({ providers: [relayProvider()] })
|
|
128
|
+
const identifiers = [
|
|
129
|
+
{ destinationToken: 'usdc.e', name: 'symbol', sourceToken: 'USDC' },
|
|
130
|
+
{ destinationToken: 'usdce', name: 'slug', sourceToken: 'usdc' },
|
|
131
|
+
{
|
|
132
|
+
destinationToken: '0x20c000000000000000000000b9537d11c60e8b50',
|
|
133
|
+
name: 'address',
|
|
134
|
+
sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
destinationToken: 'eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50',
|
|
138
|
+
name: 'token key',
|
|
139
|
+
sourceToken: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
140
|
+
},
|
|
141
|
+
] as const
|
|
142
|
+
const results = []
|
|
143
|
+
for (const identifier of identifiers) {
|
|
144
|
+
const response = await app.request(
|
|
145
|
+
quoteUrl({
|
|
146
|
+
destinationToken: identifier.destinationToken,
|
|
147
|
+
sourceAmount: '1000000',
|
|
148
|
+
sourceChain: 'base',
|
|
149
|
+
sourceToken: identifier.sourceToken,
|
|
150
|
+
}),
|
|
151
|
+
{ headers: TestApp.auth.headers },
|
|
152
|
+
)
|
|
153
|
+
const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
|
|
154
|
+
results.push({
|
|
155
|
+
destinationToken: body.data[0]?.destinationToken.symbol,
|
|
156
|
+
name: identifier.name,
|
|
157
|
+
sourceToken: body.data[0]?.sourceToken.symbol,
|
|
158
|
+
status: response.status,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect(results).toMatchInlineSnapshot(`
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
"destinationToken": "USDC.e",
|
|
166
|
+
"name": "symbol",
|
|
167
|
+
"sourceToken": "USDC",
|
|
168
|
+
"status": 200,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"destinationToken": "USDC.e",
|
|
172
|
+
"name": "slug",
|
|
173
|
+
"sourceToken": "USDC",
|
|
174
|
+
"status": 200,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"destinationToken": "USDC.e",
|
|
178
|
+
"name": "address",
|
|
179
|
+
"sourceToken": "USDC",
|
|
180
|
+
"status": 200,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"destinationToken": "USDC.e",
|
|
184
|
+
"name": "token key",
|
|
185
|
+
"sourceToken": "USDC",
|
|
186
|
+
"status": 200,
|
|
187
|
+
},
|
|
188
|
+
]
|
|
189
|
+
`)
|
|
190
|
+
})
|
|
191
|
+
|
|
50
192
|
test('queries only the requested provider', async () => {
|
|
51
193
|
let acrossCalls = 0
|
|
52
194
|
let relayCalls = 0
|
|
53
|
-
const app =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
},
|
|
195
|
+
const app = create({
|
|
196
|
+
providers: [
|
|
197
|
+
acrossProvider(() => {
|
|
198
|
+
acrossCalls += 1
|
|
199
|
+
}),
|
|
200
|
+
relayProvider(() => {
|
|
201
|
+
relayCalls += 1
|
|
202
|
+
}),
|
|
203
|
+
],
|
|
64
204
|
})
|
|
65
205
|
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
66
206
|
headers: TestApp.auth.headers,
|
|
@@ -74,7 +214,7 @@ describe('GET /funding/quotes', () => {
|
|
|
74
214
|
})
|
|
75
215
|
|
|
76
216
|
test('returns non-EVM Rhino quotes with stable chain and token ids', async () => {
|
|
77
|
-
const app =
|
|
217
|
+
const app = create({ providers: [rhinoProvider()] })
|
|
78
218
|
const response = await app.request(
|
|
79
219
|
quoteUrl({
|
|
80
220
|
destinationToken: 'USDC.e',
|
|
@@ -103,8 +243,8 @@ describe('GET /funding/quotes', () => {
|
|
|
103
243
|
})
|
|
104
244
|
})
|
|
105
245
|
|
|
106
|
-
test('returns an empty list when no provider is
|
|
107
|
-
const response = await
|
|
246
|
+
test('returns an empty list when no provider is available', async () => {
|
|
247
|
+
const response = await create().request(quoteUrl(), {
|
|
108
248
|
headers: TestApp.auth.headers,
|
|
109
249
|
})
|
|
110
250
|
expect(response.status).toBe(200)
|
|
@@ -114,19 +254,16 @@ describe('GET /funding/quotes', () => {
|
|
|
114
254
|
|
|
115
255
|
test('returns an empty list when the provider reports no quote', async () => {
|
|
116
256
|
const entries: Log.Entry[] = []
|
|
117
|
-
const app =
|
|
118
|
-
funding: {
|
|
119
|
-
adapters: {
|
|
120
|
-
relay: {
|
|
121
|
-
async probeQuote() {
|
|
122
|
-
return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
},
|
|
257
|
+
const app = create({
|
|
127
258
|
logger: (entry) => {
|
|
128
259
|
entries.push(entry)
|
|
129
260
|
},
|
|
261
|
+
providers: [
|
|
262
|
+
withGetQuote('relay', async () => ({
|
|
263
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
264
|
+
status: 'unavailable',
|
|
265
|
+
})),
|
|
266
|
+
],
|
|
130
267
|
})
|
|
131
268
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
132
269
|
expect(response.status).toBe(200)
|
|
@@ -136,23 +273,19 @@ describe('GET /funding/quotes', () => {
|
|
|
136
273
|
{
|
|
137
274
|
durationMs: expect.any(Number),
|
|
138
275
|
id: 'relay',
|
|
139
|
-
operation: '
|
|
276
|
+
operation: 'getQuote',
|
|
140
277
|
outcome: 'unavailable',
|
|
141
278
|
},
|
|
142
279
|
])
|
|
143
280
|
})
|
|
144
281
|
|
|
145
282
|
test('returns 502 when every matching provider fails', async () => {
|
|
146
|
-
const app =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
},
|
|
283
|
+
const app = create({
|
|
284
|
+
providers: [
|
|
285
|
+
withGetQuote('relay', async () => {
|
|
286
|
+
throw new Error('upstream unavailable')
|
|
287
|
+
}),
|
|
288
|
+
],
|
|
156
289
|
})
|
|
157
290
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
158
291
|
expect(response.status).toBe(502)
|
|
@@ -161,64 +294,58 @@ describe('GET /funding/quotes', () => {
|
|
|
161
294
|
})
|
|
162
295
|
|
|
163
296
|
test('logs provider quote deadlines as timeouts', async () => {
|
|
297
|
+
vi.useFakeTimers()
|
|
164
298
|
const entries: Log.Entry[] = []
|
|
165
|
-
const app =
|
|
166
|
-
funding: {
|
|
167
|
-
adapterTimeoutMs: 1,
|
|
168
|
-
adapters: {
|
|
169
|
-
across: {
|
|
170
|
-
probeQuote() {
|
|
171
|
-
return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
},
|
|
299
|
+
const app = create({
|
|
176
300
|
logger: (entry) => {
|
|
177
301
|
entries.push(entry)
|
|
178
302
|
},
|
|
303
|
+
providers: [
|
|
304
|
+
withGetQuote('across', () => new Promise<FundingProvider.getQuote.ReturnType>(() => {})),
|
|
305
|
+
],
|
|
179
306
|
})
|
|
180
307
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
308
|
+
try {
|
|
309
|
+
const response_promise = app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
310
|
+
headers: TestApp.auth.headers,
|
|
311
|
+
})
|
|
312
|
+
await vi.advanceTimersByTimeAsync(2_000)
|
|
313
|
+
const response = await response_promise
|
|
184
314
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
failure: 'timeout',
|
|
188
|
-
id: 'across',
|
|
189
|
-
operation: 'swap/approval',
|
|
190
|
-
})
|
|
191
|
-
expect(entries[0]?.fundingProviderAttempts).toEqual([
|
|
192
|
-
{
|
|
193
|
-
durationMs: expect.any(Number),
|
|
315
|
+
expect(response.status).toBe(502)
|
|
316
|
+
expect(entries[0]?.provider).toEqual({
|
|
194
317
|
failure: 'timeout',
|
|
195
318
|
id: 'across',
|
|
196
|
-
operation: '
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
319
|
+
operation: 'getQuote',
|
|
320
|
+
})
|
|
321
|
+
expect(entries[0]?.fundingProviderAttempts).toEqual([
|
|
322
|
+
{
|
|
323
|
+
durationMs: expect.any(Number),
|
|
324
|
+
failure: 'timeout',
|
|
325
|
+
id: 'across',
|
|
326
|
+
operation: 'getQuote',
|
|
327
|
+
outcome: 'failed',
|
|
328
|
+
},
|
|
329
|
+
])
|
|
330
|
+
} finally {
|
|
331
|
+
vi.useRealTimers()
|
|
332
|
+
}
|
|
200
333
|
})
|
|
201
334
|
|
|
202
335
|
test('logs every failure when multiple providers fail', async () => {
|
|
203
336
|
const entries: Log.Entry[] = []
|
|
204
|
-
const app =
|
|
205
|
-
funding: {
|
|
206
|
-
adapters: {
|
|
207
|
-
across: {
|
|
208
|
-
async probeQuote() {
|
|
209
|
-
throw new TypeError('Across network failure')
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
relay: {
|
|
213
|
-
async probeQuote() {
|
|
214
|
-
throw new Error('Relay failure')
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
},
|
|
337
|
+
const app = create({
|
|
219
338
|
logger: (entry) => {
|
|
220
339
|
entries.push(entry)
|
|
221
340
|
},
|
|
341
|
+
providers: [
|
|
342
|
+
withGetQuote('across', async () => {
|
|
343
|
+
throw new TypeError('Across network failure')
|
|
344
|
+
}),
|
|
345
|
+
withGetQuote('relay', async () => {
|
|
346
|
+
throw new Error('Relay failure')
|
|
347
|
+
}),
|
|
348
|
+
],
|
|
222
349
|
})
|
|
223
350
|
|
|
224
351
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
@@ -227,25 +354,25 @@ describe('GET /funding/quotes', () => {
|
|
|
227
354
|
expect(entries[0]?.provider).toEqual({
|
|
228
355
|
failure: 'network',
|
|
229
356
|
id: 'across',
|
|
230
|
-
operation: '
|
|
357
|
+
operation: 'getQuote',
|
|
231
358
|
})
|
|
232
359
|
expect(entries[0]?.providerFailures).toEqual([
|
|
233
|
-
{ failure: 'network', id: 'across', operation: '
|
|
234
|
-
{ failure: 'unknown', id: 'relay', operation: '
|
|
360
|
+
{ failure: 'network', id: 'across', operation: 'getQuote' },
|
|
361
|
+
{ failure: 'unknown', id: 'relay', operation: 'getQuote' },
|
|
235
362
|
])
|
|
236
363
|
expect(entries[0]?.fundingProviderAttempts).toEqual([
|
|
237
364
|
{
|
|
238
365
|
durationMs: expect.any(Number),
|
|
239
366
|
failure: 'network',
|
|
240
367
|
id: 'across',
|
|
241
|
-
operation: '
|
|
368
|
+
operation: 'getQuote',
|
|
242
369
|
outcome: 'failed',
|
|
243
370
|
},
|
|
244
371
|
{
|
|
245
372
|
durationMs: expect.any(Number),
|
|
246
373
|
failure: 'unknown',
|
|
247
374
|
id: 'relay',
|
|
248
|
-
operation: '
|
|
375
|
+
operation: 'getQuote',
|
|
249
376
|
outcome: 'failed',
|
|
250
377
|
},
|
|
251
378
|
])
|
|
@@ -253,20 +380,16 @@ describe('GET /funding/quotes', () => {
|
|
|
253
380
|
|
|
254
381
|
test('logs failed providers when another provider returns a quote', async () => {
|
|
255
382
|
const entries: Log.Entry[] = []
|
|
256
|
-
const app =
|
|
257
|
-
funding: {
|
|
258
|
-
adapters: {
|
|
259
|
-
across: {
|
|
260
|
-
async probeQuote() {
|
|
261
|
-
throw new TypeError(`wallet ${'0xaa'.repeat(20)} could not reach secret-provider`)
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
relay: relayAdapter(),
|
|
265
|
-
},
|
|
266
|
-
},
|
|
383
|
+
const app = create({
|
|
267
384
|
logger: (entry) => {
|
|
268
385
|
entries.push(entry)
|
|
269
386
|
},
|
|
387
|
+
providers: [
|
|
388
|
+
withGetQuote('across', async () => {
|
|
389
|
+
throw new TypeError(`wallet ${'0xaa'.repeat(20)} could not reach secret-provider`)
|
|
390
|
+
}),
|
|
391
|
+
relayProvider(),
|
|
392
|
+
],
|
|
270
393
|
})
|
|
271
394
|
|
|
272
395
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
@@ -277,13 +400,13 @@ describe('GET /funding/quotes', () => {
|
|
|
277
400
|
durationMs: expect.any(Number),
|
|
278
401
|
failure: 'network',
|
|
279
402
|
id: 'across',
|
|
280
|
-
operation: '
|
|
403
|
+
operation: 'getQuote',
|
|
281
404
|
outcome: 'failed',
|
|
282
405
|
},
|
|
283
406
|
{
|
|
284
407
|
durationMs: expect.any(Number),
|
|
285
408
|
id: 'relay',
|
|
286
|
-
operation: '
|
|
409
|
+
operation: 'getQuote',
|
|
287
410
|
outcome: 'available',
|
|
288
411
|
},
|
|
289
412
|
])
|
|
@@ -292,23 +415,20 @@ describe('GET /funding/quotes', () => {
|
|
|
292
415
|
})
|
|
293
416
|
|
|
294
417
|
test('rejects successful provider results without output amounts', async () => {
|
|
295
|
-
const app =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
},
|
|
418
|
+
const app = create({
|
|
419
|
+
providers: [
|
|
420
|
+
withGetQuote('relay', async () => ({
|
|
421
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
422
|
+
status: 'available',
|
|
423
|
+
})),
|
|
424
|
+
],
|
|
305
425
|
})
|
|
306
426
|
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
307
427
|
expect(response.status).toBe(502)
|
|
308
428
|
})
|
|
309
429
|
|
|
310
430
|
test('rejects invalid query parameters', async () => {
|
|
311
|
-
const response = await
|
|
431
|
+
const response = await create().request(
|
|
312
432
|
quoteUrl({ sourceAmount: '1000000', sourceChain: 'base' }),
|
|
313
433
|
{ headers: TestApp.auth.headers },
|
|
314
434
|
)
|
|
@@ -318,19 +438,20 @@ describe('GET /funding/quotes', () => {
|
|
|
318
438
|
})
|
|
319
439
|
|
|
320
440
|
test('rejects unsupported providers', async () => {
|
|
321
|
-
const response = await
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
441
|
+
const response = await create().request(quoteUrl({ ...baseUsdcQuery, provider: 'bridge' }), {
|
|
442
|
+
headers: TestApp.auth.headers,
|
|
443
|
+
})
|
|
325
444
|
expect(response.status).toBe(400)
|
|
326
445
|
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
327
446
|
expect(body.error.code).toBe('query_invalid')
|
|
328
447
|
})
|
|
329
448
|
|
|
330
449
|
test('rejects unsupported source chains', async () => {
|
|
331
|
-
const response = await
|
|
450
|
+
const response = await create().request(
|
|
332
451
|
quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }),
|
|
333
|
-
{
|
|
452
|
+
{
|
|
453
|
+
headers: TestApp.auth.headers,
|
|
454
|
+
},
|
|
334
455
|
)
|
|
335
456
|
expect(response.status).toBe(400)
|
|
336
457
|
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
@@ -338,7 +459,7 @@ describe('GET /funding/quotes', () => {
|
|
|
338
459
|
})
|
|
339
460
|
|
|
340
461
|
test('does not expose the removed routes endpoint', async () => {
|
|
341
|
-
const response = await
|
|
462
|
+
const response = await create().request(
|
|
342
463
|
'/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000',
|
|
343
464
|
{ headers: TestApp.auth.headers },
|
|
344
465
|
)
|
|
@@ -348,14 +469,17 @@ describe('GET /funding/quotes', () => {
|
|
|
348
469
|
|
|
349
470
|
describe('GET /funding/chains', () => {
|
|
350
471
|
test('lists only inputs supported by quote providers', async () => {
|
|
351
|
-
const response = await
|
|
472
|
+
const response = await create({ providers }).request('/v1/funding/chains', {
|
|
473
|
+
headers: TestApp.auth.headers,
|
|
474
|
+
})
|
|
352
475
|
expect(response.status).toBe(200)
|
|
353
|
-
const body = await TestApp.json(response, Funding.schema.
|
|
476
|
+
const body = await TestApp.json(response, Funding.schema.getFundingChains.Response)
|
|
354
477
|
|
|
355
478
|
expect(body.data.map((chain) => chain.id)).toEqual([
|
|
356
479
|
'eip155:1',
|
|
357
|
-
'eip155:8453',
|
|
358
480
|
'eip155:56',
|
|
481
|
+
'eip155:8453',
|
|
482
|
+
'eip155:137',
|
|
359
483
|
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
360
484
|
'tron:0x2b6653dc',
|
|
361
485
|
])
|
|
@@ -369,135 +493,2502 @@ describe('GET /funding/chains', () => {
|
|
|
369
493
|
})
|
|
370
494
|
|
|
371
495
|
describe('GET /funding/providers', () => {
|
|
372
|
-
test('lists quote providers
|
|
373
|
-
const app =
|
|
496
|
+
test('lists supplied quote providers', async () => {
|
|
497
|
+
const app = create({ providers: [relayProvider()] })
|
|
374
498
|
const response = await app.request('/v1/funding/providers', {
|
|
375
499
|
headers: TestApp.auth.headers,
|
|
376
500
|
})
|
|
377
501
|
expect(response.status).toBe(200)
|
|
378
|
-
const body = await TestApp.json(response, Funding.schema.
|
|
502
|
+
const body = await TestApp.json(response, Funding.schema.getFundingProviders.Response)
|
|
379
503
|
|
|
380
504
|
expect(body.data).toEqual([
|
|
381
|
-
{
|
|
382
|
-
id: 'across',
|
|
383
|
-
kind: 'intentBridge',
|
|
384
|
-
name: 'Across',
|
|
385
|
-
requiresApiKey: true,
|
|
386
|
-
status: 'unconfigured',
|
|
387
|
-
},
|
|
388
505
|
{
|
|
389
506
|
id: 'relay',
|
|
390
|
-
kind: 'solverNetwork',
|
|
391
507
|
name: 'Relay',
|
|
392
|
-
requiresApiKey: false,
|
|
393
|
-
status: 'configured',
|
|
394
508
|
},
|
|
509
|
+
])
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
test('isolates cached inventories between apps', async () => {
|
|
513
|
+
const store = Store.memory()
|
|
514
|
+
const relayApp = create({ cache: { store }, providers: [relayProvider()] })
|
|
515
|
+
const acrossApp = create({ cache: { store }, providers: [acrossProvider()] })
|
|
516
|
+
|
|
517
|
+
const relayResponse = await relayApp.request('/v1/funding/providers', {
|
|
518
|
+
headers: TestApp.auth.headers,
|
|
519
|
+
})
|
|
520
|
+
const acrossResponse = await acrossApp.request('/v1/funding/providers', {
|
|
521
|
+
headers: TestApp.auth.headers,
|
|
522
|
+
})
|
|
523
|
+
const relayBody = await TestApp.json(relayResponse, Funding.schema.getFundingProviders.Response)
|
|
524
|
+
const acrossBody = await TestApp.json(
|
|
525
|
+
acrossResponse,
|
|
526
|
+
Funding.schema.getFundingProviders.Response,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
expect(relayBody.data.map((provider) => provider.id)).toEqual(['relay'])
|
|
530
|
+
expect(acrossBody.data.map((provider) => provider.id)).toEqual(['across'])
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test('isolates cached inventories between database sources', async () => {
|
|
534
|
+
const first = TestApp.database()
|
|
535
|
+
const second = TestApp.database()
|
|
536
|
+
const store = Store.memory()
|
|
537
|
+
await TestFunding.publish(first)
|
|
538
|
+
await FundingCatalog.publish(second, { ...TestFunding.data, routes: [] })
|
|
539
|
+
const firstApp = TestApp.create({ cache: { store }, db: first, providers: [relayProvider()] })
|
|
540
|
+
const secondApp = TestApp.create({ cache: { store }, db: second, providers: [relayProvider()] })
|
|
541
|
+
|
|
542
|
+
const firstResponse = await firstApp.request('/v1/funding/providers', {
|
|
543
|
+
headers: TestApp.auth.headers,
|
|
544
|
+
})
|
|
545
|
+
const secondResponse = await secondApp.request('/v1/funding/providers', {
|
|
546
|
+
headers: TestApp.auth.headers,
|
|
547
|
+
})
|
|
548
|
+
const firstBody = await TestApp.json(firstResponse, Funding.schema.getFundingProviders.Response)
|
|
549
|
+
const secondBody = await TestApp.json(
|
|
550
|
+
secondResponse,
|
|
551
|
+
Funding.schema.getFundingProviders.Response,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
expect(firstBody.data.map((provider) => provider.id)).toEqual(['relay'])
|
|
555
|
+
expect(secondBody.data).toEqual([])
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test('does not expose the removed generic provider endpoint', async () => {
|
|
559
|
+
const response = await create().request('/v1/providers', {
|
|
560
|
+
headers: TestApp.auth.headers,
|
|
561
|
+
})
|
|
562
|
+
expect(response.status).toBe(404)
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
describe('POST /funding/deposit-addresses', () => {
|
|
567
|
+
test('requires funding:write and an idempotency key', async () => {
|
|
568
|
+
const app = transfersApp({ providers: [depositAddressProvider()] })
|
|
569
|
+
expect((await postDepositAddress(app, { headers: {} })).status).toBe(401)
|
|
570
|
+
expect((await postDepositAddress(app, { key: fundingReader })).status).toBe(403)
|
|
571
|
+
const response = await postDepositAddress(app, {
|
|
572
|
+
headers: { authorization: `Bearer ${fundingWriter.token}` },
|
|
573
|
+
})
|
|
574
|
+
expect(response.status).toBe(400)
|
|
575
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
576
|
+
'idempotency_key_required',
|
|
577
|
+
)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('requires a source amount', async () => {
|
|
581
|
+
const { amount: _amount, ...body } = depositAddressBody
|
|
582
|
+
const response = await postDepositAddress(
|
|
583
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
584
|
+
{ body },
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
588
|
+
{
|
|
589
|
+
body: { requestId: expect.any(String) },
|
|
590
|
+
},
|
|
591
|
+
`
|
|
592
|
+
{
|
|
593
|
+
"body": {
|
|
594
|
+
"error": {
|
|
595
|
+
"code": "body_invalid",
|
|
596
|
+
"details": [
|
|
597
|
+
{
|
|
598
|
+
"message": "Invalid input: expected string, received undefined",
|
|
599
|
+
"path": [
|
|
600
|
+
"amount",
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
"message": "Check the request body and try again.",
|
|
605
|
+
},
|
|
606
|
+
"requestId": Any<String>,
|
|
607
|
+
},
|
|
608
|
+
"status": 400,
|
|
609
|
+
}
|
|
610
|
+
`,
|
|
611
|
+
)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
test('creates a reusable address and persists private provider correlation', async () => {
|
|
615
|
+
let seen: FundingProvider.createDepositAddress.Parameters | undefined
|
|
616
|
+
const app = transfersApp({
|
|
617
|
+
providers: [
|
|
618
|
+
depositAddressProvider('TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV', (input) => {
|
|
619
|
+
seen = input
|
|
620
|
+
}),
|
|
621
|
+
],
|
|
622
|
+
})
|
|
623
|
+
const response = await postDepositAddress(app, {
|
|
624
|
+
body: { ...depositAddressBody, destinationToken: 'usdt0', sourceToken: 'usdt' },
|
|
625
|
+
})
|
|
626
|
+
expect(response.status).toBe(200)
|
|
627
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
628
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
629
|
+
expect(body).toMatchObject({
|
|
630
|
+
address: 'TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV',
|
|
631
|
+
destinationToken: { symbol: 'USDT0' },
|
|
632
|
+
provider: { id: 'relay', name: 'Relay' },
|
|
633
|
+
recipient: depositAddressBody.recipient,
|
|
634
|
+
refundAddress: depositAddressBody.refundAddress,
|
|
635
|
+
sourceChain: { id: 'tron:0x2b6653dc' },
|
|
636
|
+
sourceToken: { symbol: 'USDT' },
|
|
637
|
+
status: 'active',
|
|
638
|
+
})
|
|
639
|
+
expect(seen).toMatchObject({
|
|
640
|
+
recipient: depositAddressBody.recipient,
|
|
641
|
+
refundAddress: depositAddressBody.refundAddress,
|
|
642
|
+
})
|
|
643
|
+
const record = await FundingDepositAddresses.get(db, body.id)
|
|
644
|
+
expect(record).toMatchObject({
|
|
645
|
+
deliveryStrategy: 'provider',
|
|
646
|
+
providerOutputToken: body.destinationToken,
|
|
647
|
+
providerRequestIds: ['relay_request_1'],
|
|
648
|
+
providerState: { checkEndpoint: '/intents/status/v3?requestId=relay_request_1' },
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
test('resolves token identifiers through the composed route', async () => {
|
|
653
|
+
const app = transfersApp({
|
|
654
|
+
providers: [depositAddressProvider('T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb')],
|
|
655
|
+
})
|
|
656
|
+
const identifiers = [
|
|
657
|
+
{ destinationToken: 'usdt0', name: 'alias', sourceToken: 'usdt' },
|
|
395
658
|
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
requiresApiKey: false,
|
|
400
|
-
status: 'unconfigured',
|
|
659
|
+
destinationToken: depositAddressBody.destinationToken,
|
|
660
|
+
name: 'address',
|
|
661
|
+
sourceToken: depositAddressBody.sourceToken,
|
|
401
662
|
},
|
|
402
663
|
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
requiresApiKey: true,
|
|
407
|
-
status: 'unconfigured',
|
|
664
|
+
destinationToken: 'eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73',
|
|
665
|
+
name: 'token key',
|
|
666
|
+
sourceToken: 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
|
408
667
|
},
|
|
668
|
+
] as const
|
|
669
|
+
const results = []
|
|
670
|
+
for (const identifier of identifiers) {
|
|
671
|
+
const response = await postDepositAddress(app, {
|
|
672
|
+
body: {
|
|
673
|
+
...depositAddressBody,
|
|
674
|
+
destinationToken: identifier.destinationToken,
|
|
675
|
+
recipient: `0x${'55'.repeat(20)}`,
|
|
676
|
+
refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E',
|
|
677
|
+
sourceToken: identifier.sourceToken,
|
|
678
|
+
},
|
|
679
|
+
})
|
|
680
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
681
|
+
results.push({
|
|
682
|
+
destinationToken: body.destinationToken.symbol,
|
|
683
|
+
name: identifier.name,
|
|
684
|
+
sourceToken: body.sourceToken.symbol,
|
|
685
|
+
status: response.status,
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
expect(results).toMatchInlineSnapshot(`
|
|
690
|
+
[
|
|
691
|
+
{
|
|
692
|
+
"destinationToken": "USDT0",
|
|
693
|
+
"name": "alias",
|
|
694
|
+
"sourceToken": "USDT",
|
|
695
|
+
"status": 200,
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"destinationToken": "USDT0",
|
|
699
|
+
"name": "address",
|
|
700
|
+
"sourceToken": "USDT",
|
|
701
|
+
"status": 200,
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
"destinationToken": "USDT0",
|
|
705
|
+
"name": "token key",
|
|
706
|
+
"sourceToken": "USDT",
|
|
707
|
+
"status": 200,
|
|
708
|
+
},
|
|
709
|
+
]
|
|
710
|
+
`)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
test('omits newly quoted terms from a matching reusable address', async () => {
|
|
714
|
+
let calls = 0
|
|
715
|
+
const body = { ...depositAddressBody, refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E' }
|
|
716
|
+
const app = transfersApp({
|
|
717
|
+
providers: [
|
|
718
|
+
depositAddressProvider('TEdvoHEatmDKvTh3o9vBRB9Vdtbhn4QFhy', () => {
|
|
719
|
+
calls += 1
|
|
720
|
+
}),
|
|
721
|
+
],
|
|
722
|
+
})
|
|
723
|
+
const first = await TestApp.json(
|
|
724
|
+
await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
|
|
725
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
726
|
+
)
|
|
727
|
+
const second = await TestApp.json(
|
|
728
|
+
await postDepositAddress(app, {
|
|
729
|
+
body: { ...body, amount: '200000000' },
|
|
730
|
+
idempotencyKey: nextDepositAddressKey(),
|
|
731
|
+
}),
|
|
732
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
733
|
+
)
|
|
734
|
+
if (!('sourceAmount' in first)) throw new Error('Expected newly provisioned terms.')
|
|
735
|
+
|
|
736
|
+
expect({
|
|
737
|
+
calls,
|
|
738
|
+
firstHasQuote: 'quote' in first,
|
|
739
|
+
firstId: first.id,
|
|
740
|
+
firstSourceAmount: first.sourceAmount.baseUnits,
|
|
741
|
+
secondHasQuote: 'quote' in second,
|
|
742
|
+
secondId: second.id,
|
|
743
|
+
}).toMatchInlineSnapshot(
|
|
409
744
|
{
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
name: 'Symbiosis',
|
|
413
|
-
requiresApiKey: false,
|
|
414
|
-
status: 'unconfigured',
|
|
745
|
+
firstId: expect.any(String),
|
|
746
|
+
secondId: expect.any(String),
|
|
415
747
|
},
|
|
416
|
-
|
|
748
|
+
`
|
|
749
|
+
{
|
|
750
|
+
"calls": 2,
|
|
751
|
+
"firstHasQuote": true,
|
|
752
|
+
"firstId": Any<String>,
|
|
753
|
+
"firstSourceAmount": "100000000",
|
|
754
|
+
"secondHasQuote": false,
|
|
755
|
+
"secondId": Any<String>,
|
|
756
|
+
}
|
|
757
|
+
`,
|
|
758
|
+
)
|
|
759
|
+
expect(second.id).toBe(first.id)
|
|
417
760
|
})
|
|
418
761
|
|
|
419
|
-
test('
|
|
420
|
-
const
|
|
421
|
-
|
|
762
|
+
test('stores subsidized addresses as a separate reusable identity', async () => {
|
|
763
|
+
const inputs: FundingProvider.createDepositAddress.Parameters[] = []
|
|
764
|
+
const app = transfersApp({
|
|
765
|
+
providers: [
|
|
766
|
+
withCreateDepositAddress('relay', async (input) => {
|
|
767
|
+
inputs.push(input)
|
|
768
|
+
return {
|
|
769
|
+
address: inputs.length === 1 ? `0x${'31'.repeat(20)}` : `0x${'32'.repeat(20)}`,
|
|
770
|
+
correlation: { requestId: `relay_request_${inputs.length}` },
|
|
771
|
+
destinationAmount: '1000000',
|
|
772
|
+
destinationAmountMin: '1000000',
|
|
773
|
+
fees: [],
|
|
774
|
+
sampledAt: '2026-08-04T00:00:00.000Z',
|
|
775
|
+
}
|
|
776
|
+
}),
|
|
777
|
+
],
|
|
422
778
|
})
|
|
423
|
-
|
|
779
|
+
const body = {
|
|
780
|
+
...depositAddressBody,
|
|
781
|
+
refundAddress: `0x${'33'.repeat(20)}`,
|
|
782
|
+
sourceChain: 'base',
|
|
783
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
784
|
+
}
|
|
785
|
+
const standard = await TestApp.json(
|
|
786
|
+
await postDepositAddress(app, { body }),
|
|
787
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
788
|
+
)
|
|
789
|
+
const subsidized = await TestApp.json(
|
|
790
|
+
await postDepositAddress(app, { body: { ...body, subsidize: true } }),
|
|
791
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
expect({
|
|
795
|
+
distinct: standard.id !== subsidized.id,
|
|
796
|
+
inputs: inputs.map(({ subsidize }) => subsidize),
|
|
797
|
+
standard: standard.subsidize,
|
|
798
|
+
subsidized: subsidized.subsidize,
|
|
799
|
+
}).toMatchInlineSnapshot(`
|
|
800
|
+
{
|
|
801
|
+
"distinct": true,
|
|
802
|
+
"inputs": [
|
|
803
|
+
false,
|
|
804
|
+
true,
|
|
805
|
+
],
|
|
806
|
+
"standard": false,
|
|
807
|
+
"subsidized": true,
|
|
808
|
+
}
|
|
809
|
+
`)
|
|
424
810
|
})
|
|
425
|
-
})
|
|
426
811
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
812
|
+
test('normalizes EVM accounts before provisioning and matching', async () => {
|
|
813
|
+
const inputs: FundingProvider.createDepositAddress.Parameters[] = []
|
|
814
|
+
const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
|
|
815
|
+
const refundAddress = '0x1234567890AbcdEF1234567890aBcdef12345678'
|
|
816
|
+
const body = {
|
|
817
|
+
...depositAddressBody,
|
|
818
|
+
recipient,
|
|
819
|
+
refundAddress,
|
|
820
|
+
sourceChain: 'base',
|
|
821
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
822
|
+
}
|
|
823
|
+
const app = transfersApp({
|
|
824
|
+
providers: [
|
|
825
|
+
depositAddressProvider(`0x${'99'.repeat(20)}`, (input) => {
|
|
826
|
+
inputs.push(input)
|
|
827
|
+
}),
|
|
828
|
+
],
|
|
829
|
+
})
|
|
830
|
+
const first = await TestApp.json(
|
|
831
|
+
await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
|
|
832
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
833
|
+
)
|
|
834
|
+
const second = await TestApp.json(
|
|
835
|
+
await postDepositAddress(app, {
|
|
836
|
+
body: {
|
|
837
|
+
...body,
|
|
838
|
+
recipient: recipient.toLowerCase(),
|
|
839
|
+
refundAddress: refundAddress.toLowerCase(),
|
|
840
|
+
},
|
|
841
|
+
idempotencyKey: nextDepositAddressKey(),
|
|
842
|
+
}),
|
|
843
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
844
|
+
)
|
|
432
845
|
|
|
433
|
-
|
|
846
|
+
expect({
|
|
847
|
+
first: { recipient: first.recipient, refundAddress: first.refundAddress },
|
|
848
|
+
inputs: inputs.map(({ recipient, refundAddress }) => ({ recipient, refundAddress })),
|
|
849
|
+
reused: first.id === second.id,
|
|
850
|
+
second: { recipient: second.recipient, refundAddress: second.refundAddress },
|
|
851
|
+
}).toMatchInlineSnapshot(`
|
|
852
|
+
{
|
|
853
|
+
"first": {
|
|
854
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
855
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
856
|
+
},
|
|
857
|
+
"inputs": [
|
|
858
|
+
{
|
|
859
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
860
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
864
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
"reused": true,
|
|
868
|
+
"second": {
|
|
869
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
870
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
871
|
+
},
|
|
872
|
+
}
|
|
873
|
+
`)
|
|
874
|
+
})
|
|
434
875
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
876
|
+
test('keeps the stored reusable address when the provider rotates addresses', async () => {
|
|
877
|
+
const body = { ...depositAddressBody, recipient: `0x${'22'.repeat(20)}` }
|
|
878
|
+
const first = await TestApp.json(
|
|
879
|
+
await postDepositAddress(
|
|
880
|
+
transfersApp({
|
|
881
|
+
providers: [depositAddressProvider('TGCAjMXComunWZEXCT1LPBdcYbDVuyexBv')],
|
|
882
|
+
}),
|
|
883
|
+
{ body, idempotencyKey: nextDepositAddressKey() },
|
|
884
|
+
),
|
|
885
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
886
|
+
)
|
|
440
887
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
888
|
+
let calls = 0
|
|
889
|
+
const app = transfersApp({
|
|
890
|
+
providers: [
|
|
891
|
+
depositAddressProvider('THkQfRopincF6emzbk6VMC7jTHqJ8MP8g7', () => {
|
|
892
|
+
calls += 1
|
|
893
|
+
}),
|
|
894
|
+
],
|
|
895
|
+
})
|
|
896
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
897
|
+
const response = await postDepositAddress(app, {
|
|
898
|
+
body: { ...body, amount: '200000000' },
|
|
899
|
+
idempotencyKey,
|
|
900
|
+
})
|
|
901
|
+
const second = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
902
|
+
|
|
903
|
+
expect({
|
|
904
|
+
address: second.address,
|
|
905
|
+
calls,
|
|
906
|
+
hasQuote: 'quote' in second,
|
|
907
|
+
reused: second.id === first.id,
|
|
908
|
+
status: response.status,
|
|
909
|
+
}).toMatchInlineSnapshot(
|
|
910
|
+
{
|
|
911
|
+
address: expect.any(String),
|
|
912
|
+
},
|
|
913
|
+
`
|
|
914
|
+
{
|
|
915
|
+
"address": Any<String>,
|
|
916
|
+
"calls": 1,
|
|
917
|
+
"hasQuote": false,
|
|
918
|
+
"reused": true,
|
|
919
|
+
"status": 200,
|
|
458
920
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
921
|
+
`,
|
|
922
|
+
)
|
|
923
|
+
expect(second.address).toBe(first.address)
|
|
924
|
+
expect(
|
|
925
|
+
await TestApp.json(
|
|
926
|
+
await postDepositAddress(app, {
|
|
927
|
+
body: { ...body, amount: '200000000' },
|
|
928
|
+
idempotencyKey,
|
|
929
|
+
}),
|
|
930
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
931
|
+
),
|
|
932
|
+
).toEqual(second)
|
|
933
|
+
expect(calls).toBe(1)
|
|
934
|
+
})
|
|
462
935
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
936
|
+
test('rejects a refund address from another chain', async () => {
|
|
937
|
+
const response = await postDepositAddress(
|
|
938
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
939
|
+
{ body: { ...depositAddressBody, refundAddress: `0x${'11'.repeat(20)}` } },
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
943
|
+
{
|
|
944
|
+
body: { requestId: expect.any(String) },
|
|
945
|
+
},
|
|
946
|
+
`
|
|
947
|
+
{
|
|
948
|
+
"body": {
|
|
949
|
+
"error": {
|
|
950
|
+
"code": "body_invalid",
|
|
951
|
+
"message": "Invalid \`refundAddress\`.",
|
|
952
|
+
},
|
|
953
|
+
"requestId": Any<String>,
|
|
954
|
+
},
|
|
955
|
+
"status": 400,
|
|
481
956
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
957
|
+
`,
|
|
958
|
+
)
|
|
959
|
+
})
|
|
485
960
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
961
|
+
test('replays matching idempotent requests and rejects changed input', async () => {
|
|
962
|
+
const body = { ...depositAddressBody, refundAddress: 'TKJebW6SdoJhgkKU13BeKCbrMzT6HeQhJ6' }
|
|
963
|
+
const app = transfersApp({
|
|
964
|
+
providers: [depositAddressProvider('TLrtXaP4Yp1AGqrwQLGoHD5yGh4tTAvPbe')],
|
|
965
|
+
})
|
|
966
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
967
|
+
const first = await postDepositAddress(app, { body, idempotencyKey })
|
|
968
|
+
const replay = await postDepositAddress(app, { body, idempotencyKey })
|
|
969
|
+
expect(replay.status).toBe(200)
|
|
970
|
+
expect(await replay.json()).toEqual(await first.json())
|
|
971
|
+
|
|
972
|
+
const conflict = await postDepositAddress(app, {
|
|
973
|
+
body: { ...depositAddressBody, refundAddress: 'TNR8TefgTphcrwQQodMxFDa6BPggc2CZeE' },
|
|
974
|
+
idempotencyKey,
|
|
975
|
+
})
|
|
976
|
+
expect(conflict.status).toBe(409)
|
|
977
|
+
expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
|
|
978
|
+
'idempotency_conflict',
|
|
979
|
+
)
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
test('defaults subsidize for legacy idempotency replays', async () => {
|
|
983
|
+
const app = transfersApp({
|
|
984
|
+
providers: [depositAddressProvider(`0x${'45'.repeat(20)}`)],
|
|
985
|
+
})
|
|
986
|
+
const requestBody = {
|
|
987
|
+
...depositAddressBody,
|
|
988
|
+
refundAddress: `0x${'44'.repeat(20)}`,
|
|
989
|
+
sourceChain: 'base',
|
|
990
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
991
|
+
}
|
|
992
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
993
|
+
const first = await postDepositAddress(app, { body: requestBody, idempotencyKey })
|
|
994
|
+
const response = await TestApp.json(first, Funding.schema.createFundingDepositAddress.Response)
|
|
995
|
+
const { subsidize: _, ...legacy } = response
|
|
996
|
+
await db.kysely
|
|
997
|
+
.updateTable('funding_idempotency_requests')
|
|
998
|
+
.set({ response: JSON.stringify(legacy) })
|
|
999
|
+
.where('apiKeyId', '=', fundingWriter.id)
|
|
1000
|
+
.where('keyHash', '=', Idempotency.inputHash(idempotencyKey))
|
|
1001
|
+
.returningAll()
|
|
1002
|
+
.executeTakeFirstOrThrow()
|
|
1003
|
+
|
|
1004
|
+
const replay = await postDepositAddress(app, { body: requestBody, idempotencyKey })
|
|
1005
|
+
const response_replay = await TestApp.json(
|
|
1006
|
+
replay,
|
|
1007
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
expect({ status: replay.status, subsidize: response_replay.subsidize }).toMatchInlineSnapshot(`
|
|
1011
|
+
{
|
|
1012
|
+
"status": 200,
|
|
1013
|
+
"subsidize": false,
|
|
500
1014
|
}
|
|
501
|
-
|
|
502
|
-
}
|
|
1015
|
+
`)
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
test('preserves the idempotency fingerprint when subsidize is omitted', async () => {
|
|
1019
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
1020
|
+
await FundingIdempotencyTable.claim(db, {
|
|
1021
|
+
apiKeyId: fundingWriter.id,
|
|
1022
|
+
keyHash: Idempotency.inputHash(idempotencyKey),
|
|
1023
|
+
requestHash: Idempotency.inputHash({
|
|
1024
|
+
body: depositAddressBody,
|
|
1025
|
+
endpoint: 'POST /v1/funding/deposit-addresses',
|
|
1026
|
+
}),
|
|
1027
|
+
ttlMs: 60_000,
|
|
1028
|
+
})
|
|
1029
|
+
|
|
1030
|
+
const response = await postDepositAddress(transfersApp(), { idempotencyKey })
|
|
1031
|
+
|
|
1032
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
1033
|
+
{
|
|
1034
|
+
body: { requestId: expect.any(String) },
|
|
1035
|
+
},
|
|
1036
|
+
`
|
|
1037
|
+
{
|
|
1038
|
+
"body": {
|
|
1039
|
+
"error": {
|
|
1040
|
+
"code": "idempotency_in_progress",
|
|
1041
|
+
"message": "An equivalent funding deposit address creation is still running.",
|
|
1042
|
+
},
|
|
1043
|
+
"requestId": Any<String>,
|
|
1044
|
+
},
|
|
1045
|
+
"status": 409,
|
|
1046
|
+
}
|
|
1047
|
+
`,
|
|
1048
|
+
)
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
test('includes the creation endpoint in the idempotency fingerprint', async () => {
|
|
1052
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
1053
|
+
await FundingIdempotencyTable.claim(db, {
|
|
1054
|
+
apiKeyId: fundingWriter.id,
|
|
1055
|
+
keyHash: Idempotency.inputHash(idempotencyKey),
|
|
1056
|
+
requestHash: Idempotency.inputHash(depositAddressBody),
|
|
1057
|
+
ttlMs: 60_000,
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
const response = await postDepositAddress(
|
|
1061
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
1062
|
+
{ idempotencyKey },
|
|
1063
|
+
)
|
|
1064
|
+
expect(response.status).toBe(409)
|
|
1065
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1066
|
+
'idempotency_conflict',
|
|
1067
|
+
)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
test('returns not available without a registered provider', async () => {
|
|
1071
|
+
const response = await postDepositAddress(transfersApp({ providers: [] }))
|
|
1072
|
+
expect(response.status).toBe(404)
|
|
1073
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1074
|
+
'deposit_address_not_available',
|
|
1075
|
+
)
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
test('reports unexpected provider failures', async () => {
|
|
1079
|
+
const entries: Log.Entry[] = []
|
|
1080
|
+
const app = transfersApp({
|
|
1081
|
+
logger: (entry) => {
|
|
1082
|
+
entries.push(entry)
|
|
1083
|
+
},
|
|
1084
|
+
providers: [
|
|
1085
|
+
withCreateDepositAddress('relay', async () => {
|
|
1086
|
+
throw new Error('upstream failed')
|
|
1087
|
+
}),
|
|
1088
|
+
],
|
|
1089
|
+
})
|
|
1090
|
+
const response = await postDepositAddress(app, {
|
|
1091
|
+
body: { ...depositAddressBody, refundAddress: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb' },
|
|
1092
|
+
})
|
|
1093
|
+
expect(response.status).toBe(502)
|
|
1094
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1095
|
+
'upstream_error',
|
|
1096
|
+
)
|
|
1097
|
+
expect(entries.at(-1)).toMatchObject({
|
|
1098
|
+
provider: {
|
|
1099
|
+
failure: 'unknown',
|
|
1100
|
+
id: 'relay',
|
|
1101
|
+
operation: 'createDepositAddress',
|
|
1102
|
+
},
|
|
1103
|
+
status: 502,
|
|
1104
|
+
})
|
|
1105
|
+
})
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
describe('POST /webhooks/funding/relay', () => {
|
|
1109
|
+
test('authenticates and enqueues a bounded reconciliation hint', async () => {
|
|
1110
|
+
const secret = 'relay-webhook-secret'
|
|
1111
|
+
const address = await seedDepositAddress({
|
|
1112
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1113
|
+
provider: { id: 'relay', name: 'Relay' },
|
|
1114
|
+
recipient: `0x${'99'.repeat(20)}`,
|
|
1115
|
+
}),
|
|
1116
|
+
})
|
|
1117
|
+
const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
|
|
1118
|
+
const app = transfersApp({
|
|
1119
|
+
providers: [
|
|
1120
|
+
Relay.relay({
|
|
1121
|
+
apiKey: secret,
|
|
1122
|
+
webhook: {
|
|
1123
|
+
db,
|
|
1124
|
+
dispatch: async (message) => {
|
|
1125
|
+
dispatched.push(message)
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
}),
|
|
1129
|
+
],
|
|
1130
|
+
})
|
|
1131
|
+
const body = JSON.stringify({
|
|
1132
|
+
data: {
|
|
1133
|
+
depositAddress: { address: address.address },
|
|
1134
|
+
requestId: 'relay-child-request',
|
|
1135
|
+
},
|
|
1136
|
+
event: 'request.status.updated',
|
|
1137
|
+
})
|
|
1138
|
+
const timestamp = String(Math.floor(Date.now() / 1_000))
|
|
1139
|
+
const signature = Hash.hmac256(
|
|
1140
|
+
Hex.fromString(secret),
|
|
1141
|
+
Hex.fromString(`${timestamp}.${body}`),
|
|
1142
|
+
).slice(2)
|
|
1143
|
+
const response = await app.request('/webhooks/funding/relay', {
|
|
1144
|
+
body,
|
|
1145
|
+
headers: {
|
|
1146
|
+
'x-signature-sha256': signature,
|
|
1147
|
+
'x-signature-timestamp': timestamp,
|
|
1148
|
+
},
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
expect({ dispatched, status: response.status }).toMatchInlineSnapshot(`
|
|
1153
|
+
{
|
|
1154
|
+
"dispatched": [
|
|
1155
|
+
{
|
|
1156
|
+
"addressId": "${address.id}",
|
|
1157
|
+
"requestId": "relay-child-request",
|
|
1158
|
+
"type": "funding:deposit-address:reconcile",
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
"status": 204,
|
|
1162
|
+
}
|
|
1163
|
+
`)
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
test('rejects unauthenticated events', async () => {
|
|
1167
|
+
const app = transfersApp({
|
|
1168
|
+
providers: [
|
|
1169
|
+
Relay.relay({
|
|
1170
|
+
apiKey: 'relay-webhook-secret',
|
|
1171
|
+
webhook: { db, dispatch: async () => {} },
|
|
1172
|
+
}),
|
|
1173
|
+
],
|
|
1174
|
+
})
|
|
1175
|
+
const response = await app.request('/webhooks/funding/relay', {
|
|
1176
|
+
body: '{}',
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
expect(response.status).toMatchInlineSnapshot('401')
|
|
1181
|
+
})
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
describe.skipIf(!relayApiKey)('POST /funding/deposit-addresses (relay)', () => {
|
|
1185
|
+
test('provisions and persists a reusable address through Relay', async () => {
|
|
1186
|
+
const app = transfersApp({
|
|
1187
|
+
providers: [Relay.relay({ apiKey: relayApiKey })],
|
|
1188
|
+
})
|
|
1189
|
+
const response = await postDepositAddress(app, {
|
|
1190
|
+
body: { ...depositAddressBody, recipient: `0x${'77'.repeat(20)}` },
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
expect(response.status).toBe(200)
|
|
1194
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
1195
|
+
expect(body).toMatchInlineSnapshot(
|
|
1196
|
+
{
|
|
1197
|
+
address: expect.any(String),
|
|
1198
|
+
createdAt: expect.any(String),
|
|
1199
|
+
destinationAmount: {
|
|
1200
|
+
baseUnits: expect.any(String),
|
|
1201
|
+
formatted: expect.any(String),
|
|
1202
|
+
},
|
|
1203
|
+
destinationAmountMin: {
|
|
1204
|
+
baseUnits: expect.any(String),
|
|
1205
|
+
formatted: expect.any(String),
|
|
1206
|
+
},
|
|
1207
|
+
fees: expect.any(Array),
|
|
1208
|
+
id: expect.any(String),
|
|
1209
|
+
quote: { sampledAt: expect.any(String) },
|
|
1210
|
+
updatedAt: expect.any(String),
|
|
1211
|
+
},
|
|
1212
|
+
`
|
|
1213
|
+
{
|
|
1214
|
+
"address": Any<String>,
|
|
1215
|
+
"createdAt": Any<String>,
|
|
1216
|
+
"destinationAmount": {
|
|
1217
|
+
"baseUnits": Any<String>,
|
|
1218
|
+
"currency": "USD",
|
|
1219
|
+
"decimals": 6,
|
|
1220
|
+
"formatted": Any<String>,
|
|
1221
|
+
},
|
|
1222
|
+
"destinationAmountMin": {
|
|
1223
|
+
"baseUnits": Any<String>,
|
|
1224
|
+
"currency": "USD",
|
|
1225
|
+
"decimals": 6,
|
|
1226
|
+
"formatted": Any<String>,
|
|
1227
|
+
},
|
|
1228
|
+
"destinationChain": {
|
|
1229
|
+
"addressFormat": "hex",
|
|
1230
|
+
"id": "eip155:4217",
|
|
1231
|
+
"kind": "evm",
|
|
1232
|
+
"name": "Tempo",
|
|
1233
|
+
},
|
|
1234
|
+
"destinationToken": {
|
|
1235
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
1236
|
+
"currency": "USD",
|
|
1237
|
+
"decimals": 6,
|
|
1238
|
+
"name": "USDT0",
|
|
1239
|
+
"standard": "TIP-20",
|
|
1240
|
+
"symbol": "USDT0",
|
|
1241
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1242
|
+
"verified": true,
|
|
1243
|
+
},
|
|
1244
|
+
"fees": Any<Array>,
|
|
1245
|
+
"id": Any<String>,
|
|
1246
|
+
"provider": {
|
|
1247
|
+
"id": "relay",
|
|
1248
|
+
"name": "Relay",
|
|
1249
|
+
},
|
|
1250
|
+
"quote": {
|
|
1251
|
+
"sampledAt": Any<String>,
|
|
1252
|
+
},
|
|
1253
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
1254
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
1255
|
+
"sourceAmount": {
|
|
1256
|
+
"baseUnits": "100000000",
|
|
1257
|
+
"currency": "USD",
|
|
1258
|
+
"decimals": 6,
|
|
1259
|
+
"formatted": "100",
|
|
1260
|
+
},
|
|
1261
|
+
"sourceChain": {
|
|
1262
|
+
"addressFormat": "base58check",
|
|
1263
|
+
"id": "tron:0x2b6653dc",
|
|
1264
|
+
"kind": "tron",
|
|
1265
|
+
"name": "Tron",
|
|
1266
|
+
},
|
|
1267
|
+
"sourceToken": {
|
|
1268
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1269
|
+
"currency": "USD",
|
|
1270
|
+
"decimals": 6,
|
|
1271
|
+
"name": "Tether USD",
|
|
1272
|
+
"standard": "TRC-20",
|
|
1273
|
+
"symbol": "USDT",
|
|
1274
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1275
|
+
"verified": true,
|
|
1276
|
+
},
|
|
1277
|
+
"status": "active",
|
|
1278
|
+
"subsidize": false,
|
|
1279
|
+
"updatedAt": Any<String>,
|
|
1280
|
+
}
|
|
1281
|
+
`,
|
|
1282
|
+
)
|
|
1283
|
+
const record = await FundingDepositAddresses.get(db, body.id)
|
|
1284
|
+
expect(record).toMatchInlineSnapshot(
|
|
1285
|
+
{
|
|
1286
|
+
address: expect.any(String),
|
|
1287
|
+
createdAt: expect.any(String),
|
|
1288
|
+
id: expect.any(String),
|
|
1289
|
+
nextPollAt: expect.any(String),
|
|
1290
|
+
providerRequestIds: [expect.any(String)],
|
|
1291
|
+
providerState: expect.any(Object),
|
|
1292
|
+
snapshot: { address: expect.any(String) },
|
|
1293
|
+
updatedAt: expect.any(String),
|
|
1294
|
+
},
|
|
1295
|
+
`
|
|
1296
|
+
{
|
|
1297
|
+
"address": Any<String>,
|
|
1298
|
+
"apiKeyId": "key_funding_writer",
|
|
1299
|
+
"createdAt": Any<String>,
|
|
1300
|
+
"deliveryStrategy": "provider",
|
|
1301
|
+
"destinationTokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1302
|
+
"environment": "production",
|
|
1303
|
+
"id": Any<String>,
|
|
1304
|
+
"lastPolledAt": null,
|
|
1305
|
+
"nextPollAt": Any<String>,
|
|
1306
|
+
"orgId": "org_funding",
|
|
1307
|
+
"pollFailureCount": 0,
|
|
1308
|
+
"pollLeaseUntil": null,
|
|
1309
|
+
"pollLeaseVersion": 0,
|
|
1310
|
+
"projectId": null,
|
|
1311
|
+
"providerId": "relay",
|
|
1312
|
+
"providerOutputToken": {
|
|
1313
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
1314
|
+
"currency": "USD",
|
|
1315
|
+
"decimals": 6,
|
|
1316
|
+
"name": "USDT0",
|
|
1317
|
+
"standard": "TIP-20",
|
|
1318
|
+
"symbol": "USDT0",
|
|
1319
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1320
|
+
"verified": true,
|
|
1321
|
+
},
|
|
1322
|
+
"providerRequestIds": [
|
|
1323
|
+
Any<String>,
|
|
1324
|
+
],
|
|
1325
|
+
"providerState": Any<Object>,
|
|
1326
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
1327
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
1328
|
+
"snapshot": {
|
|
1329
|
+
"address": Any<String>,
|
|
1330
|
+
"destinationChain": {
|
|
1331
|
+
"addressFormat": "hex",
|
|
1332
|
+
"id": "eip155:4217",
|
|
1333
|
+
"kind": "evm",
|
|
1334
|
+
"name": "Tempo",
|
|
1335
|
+
},
|
|
1336
|
+
"destinationToken": {
|
|
1337
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
1338
|
+
"currency": "USD",
|
|
1339
|
+
"decimals": 6,
|
|
1340
|
+
"name": "USDT0",
|
|
1341
|
+
"standard": "TIP-20",
|
|
1342
|
+
"symbol": "USDT0",
|
|
1343
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1344
|
+
"verified": true,
|
|
1345
|
+
},
|
|
1346
|
+
"provider": {
|
|
1347
|
+
"id": "relay",
|
|
1348
|
+
"name": "Relay",
|
|
1349
|
+
},
|
|
1350
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
1351
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
1352
|
+
"sourceChain": {
|
|
1353
|
+
"addressFormat": "base58check",
|
|
1354
|
+
"id": "tron:0x2b6653dc",
|
|
1355
|
+
"kind": "tron",
|
|
1356
|
+
"name": "Tron",
|
|
1357
|
+
},
|
|
1358
|
+
"sourceToken": {
|
|
1359
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1360
|
+
"currency": "USD",
|
|
1361
|
+
"decimals": 6,
|
|
1362
|
+
"name": "Tether USD",
|
|
1363
|
+
"standard": "TRC-20",
|
|
1364
|
+
"symbol": "USDT",
|
|
1365
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1366
|
+
"verified": true,
|
|
1367
|
+
},
|
|
1368
|
+
"subsidize": false,
|
|
1369
|
+
},
|
|
1370
|
+
"sourceChainId": "tron:0x2b6653dc",
|
|
1371
|
+
"sourceTokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1372
|
+
"status": "active",
|
|
1373
|
+
"subsidize": false,
|
|
1374
|
+
"updatedAt": Any<String>,
|
|
1375
|
+
"version": 1,
|
|
1376
|
+
}
|
|
1377
|
+
`,
|
|
1378
|
+
)
|
|
1379
|
+
})
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
describe('GET /funding/deposit-addresses/:id', () => {
|
|
1383
|
+
test('requires funding:read and returns the owned address', async () => {
|
|
1384
|
+
const app = transfersApp()
|
|
1385
|
+
const record = await seedDepositAddress()
|
|
1386
|
+
const path = `/v1/funding/deposit-addresses/${record.id}`
|
|
1387
|
+
expect((await app.request(path)).status).toBe(401)
|
|
1388
|
+
expect((await app.request(path, as(TestApp.key))).status).toBe(403)
|
|
1389
|
+
|
|
1390
|
+
const response = await app.request(path, as(fundingReader))
|
|
1391
|
+
expect(response.status).toBe(200)
|
|
1392
|
+
expect({
|
|
1393
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1394
|
+
etag: response.headers.get('etag'),
|
|
1395
|
+
}).toMatchInlineSnapshot(`
|
|
1396
|
+
{
|
|
1397
|
+
"cacheControl": "no-store",
|
|
1398
|
+
"etag": null,
|
|
1399
|
+
}
|
|
1400
|
+
`)
|
|
1401
|
+
expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
|
|
1402
|
+
const body = await TestApp.json(response, Funding.schema.getFundingDepositAddress.Response)
|
|
1403
|
+
expect(body).toEqual(FundingDepositAddress.toPublic(record))
|
|
1404
|
+
expect(body).not.toHaveProperty('providerOutputToken')
|
|
1405
|
+
expect(body).not.toHaveProperty('providerRequestIds')
|
|
1406
|
+
})
|
|
1407
|
+
|
|
1408
|
+
test('hides foreign, project-inaccessible, and absent addresses', async () => {
|
|
1409
|
+
const app = transfersApp()
|
|
1410
|
+
const record = await seedDepositAddress()
|
|
1411
|
+
const path = `/v1/funding/deposit-addresses/${record.id}`
|
|
1412
|
+
|
|
1413
|
+
for (const key of [fundingForeign, fundingProjectReader]) {
|
|
1414
|
+
const response = await app.request(path, as(key))
|
|
1415
|
+
expect(response.status).toBe(404)
|
|
1416
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1417
|
+
'funding_deposit_address_not_found',
|
|
1418
|
+
)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const absent = await app.request(
|
|
1422
|
+
'/v1/funding/deposit-addresses/fda_000000000000000_000000000000000000000000',
|
|
1423
|
+
as(fundingReader),
|
|
1424
|
+
)
|
|
1425
|
+
expect(absent.status).toBe(404)
|
|
1426
|
+
})
|
|
1427
|
+
})
|
|
1428
|
+
|
|
1429
|
+
describe('GET /funding/deposit-addresses/:id/deposits', () => {
|
|
1430
|
+
test('pages deposits newest-first under an owned address', async () => {
|
|
1431
|
+
const app = transfersApp()
|
|
1432
|
+
const address = await seedDepositAddress()
|
|
1433
|
+
const first = await seedDeposit(address)
|
|
1434
|
+
const second = await seedDeposit(address)
|
|
1435
|
+
const third = await seedDeposit(address)
|
|
1436
|
+
const fourth = await seedDeposit(address)
|
|
1437
|
+
const fifth = await seedDeposit(address)
|
|
1438
|
+
const sixth = await seedDeposit(address)
|
|
1439
|
+
|
|
1440
|
+
const response = await app.request(
|
|
1441
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits?limit=5`,
|
|
1442
|
+
as(fundingReader),
|
|
1443
|
+
)
|
|
1444
|
+
expect(response.status).toBe(200)
|
|
1445
|
+
expect({
|
|
1446
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1447
|
+
etag: response.headers.get('etag'),
|
|
1448
|
+
}).toMatchInlineSnapshot(`
|
|
1449
|
+
{
|
|
1450
|
+
"cacheControl": "no-store",
|
|
1451
|
+
"etag": null,
|
|
1452
|
+
}
|
|
1453
|
+
`)
|
|
1454
|
+
const body = await TestApp.json(
|
|
1455
|
+
response,
|
|
1456
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1457
|
+
)
|
|
1458
|
+
expect(body.data.map((deposit) => deposit.id)).toEqual([
|
|
1459
|
+
sixth.id,
|
|
1460
|
+
fifth.id,
|
|
1461
|
+
fourth.id,
|
|
1462
|
+
third.id,
|
|
1463
|
+
second.id,
|
|
1464
|
+
])
|
|
1465
|
+
expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
|
|
1466
|
+
|
|
1467
|
+
const next = await TestApp.json(
|
|
1468
|
+
await app.request(
|
|
1469
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits?limit=5&cursor=${body.nextCursor}`,
|
|
1470
|
+
as(fundingReader),
|
|
1471
|
+
),
|
|
1472
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1473
|
+
)
|
|
1474
|
+
expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
|
|
1475
|
+
expect(next.nextCursor).toBeNull()
|
|
1476
|
+
})
|
|
1477
|
+
|
|
1478
|
+
test('returns a provider-observed deposit before source verification', async () => {
|
|
1479
|
+
const app = transfersApp()
|
|
1480
|
+
const address = await seedDepositAddress({
|
|
1481
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1482
|
+
provider: { id: 'relay_observed', name: 'Relay' },
|
|
1483
|
+
recipient: `0x${'31'.repeat(20)}`,
|
|
1484
|
+
}),
|
|
1485
|
+
})
|
|
1486
|
+
const sourceTransactionHash = `0x${'ab'.repeat(32)}`
|
|
1487
|
+
await FundingDeposit.create(db, {
|
|
1488
|
+
now: new Date('2026-01-04T00:00:00.000Z'),
|
|
1489
|
+
providerRequestId: 'relay-observed-request',
|
|
1490
|
+
snapshot: {
|
|
1491
|
+
depositAddressId: address.id,
|
|
1492
|
+
destinationChain: address.snapshot.destinationChain,
|
|
1493
|
+
destinationToken: address.snapshot.destinationToken,
|
|
1494
|
+
provider: address.snapshot.provider,
|
|
1495
|
+
recipient: address.recipient,
|
|
1496
|
+
refundAddress: address.refundAddress,
|
|
1497
|
+
sourceChain: address.snapshot.sourceChain,
|
|
1498
|
+
sourceToken: address.snapshot.sourceToken,
|
|
1499
|
+
sourceTransactionHashes: [sourceTransactionHash],
|
|
1500
|
+
},
|
|
1501
|
+
sourceTransactionHash,
|
|
1502
|
+
})
|
|
1503
|
+
|
|
1504
|
+
const response = await app.request(
|
|
1505
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits`,
|
|
1506
|
+
as(fundingReader),
|
|
1507
|
+
)
|
|
1508
|
+
expect(response.status).toBe(200)
|
|
1509
|
+
const body = await TestApp.json(
|
|
1510
|
+
response,
|
|
1511
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1512
|
+
)
|
|
1513
|
+
expect(body).toMatchInlineSnapshot(
|
|
1514
|
+
{
|
|
1515
|
+
data: [
|
|
1516
|
+
{
|
|
1517
|
+
createdAt: expect.any(String),
|
|
1518
|
+
depositAddressId: expect.any(String),
|
|
1519
|
+
id: expect.any(String),
|
|
1520
|
+
updatedAt: expect.any(String),
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
},
|
|
1524
|
+
`
|
|
1525
|
+
{
|
|
1526
|
+
"data": [
|
|
1527
|
+
{
|
|
1528
|
+
"createdAt": Any<String>,
|
|
1529
|
+
"depositAddressId": Any<String>,
|
|
1530
|
+
"destinationChain": {
|
|
1531
|
+
"addressFormat": "hex",
|
|
1532
|
+
"id": "eip155:4217",
|
|
1533
|
+
"kind": "evm",
|
|
1534
|
+
"name": "Tempo",
|
|
1535
|
+
},
|
|
1536
|
+
"destinationToken": {
|
|
1537
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
1538
|
+
"currency": "USD",
|
|
1539
|
+
"decimals": 6,
|
|
1540
|
+
"name": "USDT0",
|
|
1541
|
+
"standard": "TIP-20",
|
|
1542
|
+
"symbol": "USDT0",
|
|
1543
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1544
|
+
"verified": true,
|
|
1545
|
+
},
|
|
1546
|
+
"id": Any<String>,
|
|
1547
|
+
"provider": {
|
|
1548
|
+
"id": "relay_observed",
|
|
1549
|
+
"name": "Relay",
|
|
1550
|
+
},
|
|
1551
|
+
"recipient": "0x3131313131313131313131313131313131313131",
|
|
1552
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
1553
|
+
"sourceChain": {
|
|
1554
|
+
"addressFormat": "base58check",
|
|
1555
|
+
"id": "tron:0x2b6653dc",
|
|
1556
|
+
"kind": "tron",
|
|
1557
|
+
"name": "Tron",
|
|
1558
|
+
},
|
|
1559
|
+
"sourceToken": {
|
|
1560
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1561
|
+
"currency": "USD",
|
|
1562
|
+
"decimals": 6,
|
|
1563
|
+
"name": "Tether USD",
|
|
1564
|
+
"standard": "TRC-20",
|
|
1565
|
+
"symbol": "USDT",
|
|
1566
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1567
|
+
"verified": true,
|
|
1568
|
+
},
|
|
1569
|
+
"sourceTransactionHashes": [
|
|
1570
|
+
"0xabababababababababababababababababababababababababababababababab",
|
|
1571
|
+
],
|
|
1572
|
+
"status": "detected",
|
|
1573
|
+
"updatedAt": Any<String>,
|
|
1574
|
+
},
|
|
1575
|
+
],
|
|
1576
|
+
"nextCursor": null,
|
|
1577
|
+
}
|
|
1578
|
+
`,
|
|
1579
|
+
)
|
|
1580
|
+
})
|
|
1581
|
+
|
|
1582
|
+
test('returns the parent not-found error before listing deposits', async () => {
|
|
1583
|
+
const app = transfersApp()
|
|
1584
|
+
const address = await seedDepositAddress({ orgId: fundingForeign.orgId })
|
|
1585
|
+
const response = await app.request(
|
|
1586
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits`,
|
|
1587
|
+
as(fundingReader),
|
|
1588
|
+
)
|
|
1589
|
+
expect(response.status).toBe(404)
|
|
1590
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1591
|
+
'funding_deposit_address_not_found',
|
|
1592
|
+
)
|
|
1593
|
+
})
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1596
|
+
describe('GET /funding/deposits/:id', () => {
|
|
1597
|
+
test('returns the owned deposit without private reconciliation state', async () => {
|
|
1598
|
+
const app = transfersApp()
|
|
1599
|
+
const address = await seedDepositAddress()
|
|
1600
|
+
const record = await seedDeposit(address, {
|
|
1601
|
+
providerRequestIds: ['relay-private'],
|
|
1602
|
+
providerState: { request: 'private' },
|
|
1603
|
+
})
|
|
1604
|
+
const response = await app.request(`/v1/funding/deposits/${record.id}`, as(fundingReader))
|
|
1605
|
+
expect(response.status).toBe(200)
|
|
1606
|
+
expect(response.headers.get('cache-control')).toBe(Cache.policies.state)
|
|
1607
|
+
const body = await TestApp.json(response, Funding.schema.getFundingDeposit.Response)
|
|
1608
|
+
expect(body).toEqual(FundingDeposit.toPublic(record))
|
|
1609
|
+
expect(body).not.toHaveProperty('providerRequestIds')
|
|
1610
|
+
expect(body).not.toHaveProperty('providerState')
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
test('hides foreign and absent deposits', async () => {
|
|
1614
|
+
const app = transfersApp()
|
|
1615
|
+
const address = await seedDepositAddress()
|
|
1616
|
+
const record = await seedDeposit(address)
|
|
1617
|
+
expect(
|
|
1618
|
+
(await app.request(`/v1/funding/deposits/${record.id}`, as(fundingForeign))).status,
|
|
1619
|
+
).toBe(404)
|
|
1620
|
+
expect(
|
|
1621
|
+
(
|
|
1622
|
+
await app.request(
|
|
1623
|
+
'/v1/funding/deposits/fdp_000000000000000_000000000000000000000000',
|
|
1624
|
+
as(fundingReader),
|
|
1625
|
+
)
|
|
1626
|
+
).status,
|
|
1627
|
+
).toBe(404)
|
|
1628
|
+
})
|
|
1629
|
+
})
|
|
1630
|
+
|
|
1631
|
+
describe('GET /funding/transfers', () => {
|
|
1632
|
+
test('requires the funding:read scope', async () => {
|
|
1633
|
+
const app = transfersApp()
|
|
1634
|
+
expect((await app.request('/v1/funding/transfers')).status).toBe(401)
|
|
1635
|
+
const forbidden = await app.request('/v1/funding/transfers', as(TestApp.key))
|
|
1636
|
+
expect(forbidden.status).toBe(403)
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
test('pages own transfers newest-first', async () => {
|
|
1640
|
+
const app = transfersApp()
|
|
1641
|
+
const first = await seedTransfer()
|
|
1642
|
+
const second = await seedTransfer()
|
|
1643
|
+
const third = await seedTransfer()
|
|
1644
|
+
await seedTransfer({ orgId: fundingForeign.orgId })
|
|
1645
|
+
|
|
1646
|
+
const response = await app.request('/v1/funding/transfers?limit=5', {
|
|
1647
|
+
headers: { 'tempo-api-key': fundingReader.token },
|
|
1648
|
+
})
|
|
1649
|
+
expect(response.status).toBe(200)
|
|
1650
|
+
expect(response.headers.get('cache-control')).toBe(Cache.policies.feed)
|
|
1651
|
+
const etag = response.headers.get('etag') ?? ''
|
|
1652
|
+
expect(etag).not.toBe('')
|
|
1653
|
+
expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
|
|
1654
|
+
const body = await TestApp.json(response, Funding.schema.listFundingTransfers.Response)
|
|
1655
|
+
expect(body.data.map((transfer) => transfer.id)).toEqual([third.id, second.id, first.id])
|
|
1656
|
+
expect(body.nextCursor).toBeNull()
|
|
1657
|
+
expect(body.meta).toBeUndefined()
|
|
1658
|
+
|
|
1659
|
+
const unchanged = await app.request('/v1/funding/transfers?limit=5', {
|
|
1660
|
+
headers: { 'if-none-match': etag, 'tempo-api-key': fundingReader.token },
|
|
1661
|
+
})
|
|
1662
|
+
expect(unchanged.status).toBe(304)
|
|
1663
|
+
expect(unchanged.headers.get('cache-control')).toBe(Cache.policies.feed)
|
|
1664
|
+
expect(unchanged.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
|
|
1665
|
+
|
|
1666
|
+
const paged = await TestApp.json(
|
|
1667
|
+
await app.request(`/v1/funding/transfers?limit=5&cursor=${second.id}`, as(fundingReader)),
|
|
1668
|
+
Funding.schema.listFundingTransfers.Response,
|
|
1669
|
+
)
|
|
1670
|
+
expect(paged.data.map((transfer) => transfer.id)).toEqual([first.id])
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1673
|
+
test('filters by status and embeds the capped count on request', async () => {
|
|
1674
|
+
const app = transfersApp()
|
|
1675
|
+
const expired = await seedTransfer()
|
|
1676
|
+
await FundingTransfer.transition(db, {
|
|
1677
|
+
expectedVersion: 1,
|
|
1678
|
+
id: expired.id,
|
|
1679
|
+
status: 'expired',
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1682
|
+
const response = await app.request(
|
|
1683
|
+
'/v1/funding/transfers?status=expired&include=totalCount',
|
|
1684
|
+
as(fundingReader),
|
|
1685
|
+
)
|
|
1686
|
+
const body = await TestApp.json(response, Funding.schema.listFundingTransfers.Response)
|
|
1687
|
+
expect(body.data.map((transfer) => transfer.id)).toEqual([expired.id])
|
|
1688
|
+
expect(body.data[0]?.status).toBe('expired')
|
|
1689
|
+
expect(body.meta).toEqual({ totalCount: 1, totalCountCapped: false })
|
|
1690
|
+
})
|
|
1691
|
+
|
|
1692
|
+
test('narrows project-attributed keys to their project', async () => {
|
|
1693
|
+
const app = transfersApp()
|
|
1694
|
+
const scoped = await seedTransfer({ projectId: fundingProjectReader.projectId })
|
|
1695
|
+
|
|
1696
|
+
const body = await TestApp.json(
|
|
1697
|
+
await app.request('/v1/funding/transfers?limit=200', as(fundingProjectReader)),
|
|
1698
|
+
Funding.schema.listFundingTransfers.Response,
|
|
1699
|
+
)
|
|
1700
|
+
expect(body.data.map((transfer) => transfer.id)).toEqual([scoped.id])
|
|
1701
|
+
|
|
1702
|
+
// The organization-attributed key sees the project row too.
|
|
1703
|
+
const orgBody = await TestApp.json(
|
|
1704
|
+
await app.request('/v1/funding/transfers?limit=200', as(fundingReader)),
|
|
1705
|
+
Funding.schema.listFundingTransfers.Response,
|
|
1706
|
+
)
|
|
1707
|
+
expect(orgBody.data.map((transfer) => transfer.id)).toContain(scoped.id)
|
|
1708
|
+
})
|
|
1709
|
+
|
|
1710
|
+
test('rejects invalid query parameters', async () => {
|
|
1711
|
+
const app = transfersApp()
|
|
1712
|
+
const response = await app.request('/v1/funding/transfers?status=bogus', as(fundingReader))
|
|
1713
|
+
expect(response.status).toBe(400)
|
|
1714
|
+
})
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
describe('GET /funding/transfers/:id', () => {
|
|
1718
|
+
test('returns the owned transfer directly, without an action', async () => {
|
|
1719
|
+
const app = transfersApp()
|
|
1720
|
+
const record = await seedTransfer()
|
|
1721
|
+
const response = await app.request(`/v1/funding/transfers/${record.id}`, {
|
|
1722
|
+
headers: { 'tempo-api-key': fundingReader.token },
|
|
1723
|
+
})
|
|
1724
|
+
expect(response.status).toBe(200)
|
|
1725
|
+
expect({
|
|
1726
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1727
|
+
etag: response.headers.get('etag'),
|
|
1728
|
+
}).toMatchInlineSnapshot(`
|
|
1729
|
+
{
|
|
1730
|
+
"cacheControl": "no-store",
|
|
1731
|
+
"etag": null,
|
|
1732
|
+
}
|
|
1733
|
+
`)
|
|
1734
|
+
expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
|
|
1735
|
+
const body = await TestApp.json(response, Funding.schema.getFundingTransfer.Response)
|
|
1736
|
+
expect(body).toEqual(FundingTransfer.toPublic(record))
|
|
1737
|
+
expect('action' in body).toBe(false)
|
|
1738
|
+
|
|
1739
|
+
const repeated = await app.request(`/v1/funding/transfers/${record.id}`, {
|
|
1740
|
+
headers: { 'if-none-match': '"stale"', 'tempo-api-key': fundingReader.token },
|
|
1741
|
+
})
|
|
1742
|
+
expect({
|
|
1743
|
+
cacheControl: repeated.headers.get('cache-control'),
|
|
1744
|
+
etag: repeated.headers.get('etag'),
|
|
1745
|
+
status: repeated.status,
|
|
1746
|
+
}).toMatchInlineSnapshot(`
|
|
1747
|
+
{
|
|
1748
|
+
"cacheControl": "no-store",
|
|
1749
|
+
"etag": null,
|
|
1750
|
+
"status": 200,
|
|
1751
|
+
}
|
|
1752
|
+
`)
|
|
1753
|
+
})
|
|
1754
|
+
|
|
1755
|
+
test('hides foreign and absent transfers behind the same 404', async () => {
|
|
1756
|
+
const app = transfersApp()
|
|
1757
|
+
const record = await seedTransfer()
|
|
1758
|
+
|
|
1759
|
+
const foreign = await app.request(`/v1/funding/transfers/${record.id}`, as(fundingForeign))
|
|
1760
|
+
expect(foreign.status).toBe(404)
|
|
1761
|
+
expect(((await foreign.json()) as { error: { code: string } }).error.code).toBe(
|
|
1762
|
+
'funding_transfer_not_found',
|
|
1763
|
+
)
|
|
1764
|
+
|
|
1765
|
+
const project = await app.request(
|
|
1766
|
+
`/v1/funding/transfers/${record.id}`,
|
|
1767
|
+
as(fundingProjectReader),
|
|
1768
|
+
)
|
|
1769
|
+
expect(project.status).toBe(404)
|
|
1770
|
+
|
|
1771
|
+
const absent = await app.request(
|
|
1772
|
+
'/v1/funding/transfers/ftr_000000000000000_000000000000000000000000',
|
|
1773
|
+
as(fundingReader),
|
|
1774
|
+
)
|
|
1775
|
+
expect(absent.status).toBe(404)
|
|
1776
|
+
})
|
|
1777
|
+
})
|
|
1778
|
+
|
|
1779
|
+
const createBody = {
|
|
1780
|
+
amount: '1000000',
|
|
1781
|
+
destinationToken: '0x20c0000000000000000000000000000000000000',
|
|
1782
|
+
mode: 'exactSource',
|
|
1783
|
+
recipient: `0x${'11'.repeat(20)}`,
|
|
1784
|
+
sender: `0x${'22'.repeat(20)}`,
|
|
1785
|
+
slippageBps: 300,
|
|
1786
|
+
sourceChain: 'base',
|
|
1787
|
+
sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const vaultTransferBody = {
|
|
1791
|
+
amount: createBody.amount,
|
|
1792
|
+
mode: createBody.mode,
|
|
1793
|
+
recipient: createBody.recipient,
|
|
1794
|
+
sender: createBody.sender,
|
|
1795
|
+
slippageBps: createBody.slippageBps,
|
|
1796
|
+
sourceChain: createBody.sourceChain,
|
|
1797
|
+
sourceToken: createBody.sourceToken,
|
|
1798
|
+
vaultAddress: `0x${'f4'.repeat(20)}`,
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const zoneTransferBody = {
|
|
1802
|
+
amount: createBody.amount,
|
|
1803
|
+
destinationChain: 'eip155:421700001',
|
|
1804
|
+
destinationToken: createBody.destinationToken,
|
|
1805
|
+
mode: createBody.mode,
|
|
1806
|
+
recipient: createBody.recipient,
|
|
1807
|
+
recipientFallback: `0x${'33'.repeat(20)}`,
|
|
1808
|
+
sender: createBody.sender,
|
|
1809
|
+
slippageBps: createBody.slippageBps,
|
|
1810
|
+
sourceChain: createBody.sourceChain,
|
|
1811
|
+
sourceToken: createBody.sourceToken,
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const depositAddressBody = {
|
|
1815
|
+
amount: '100000000',
|
|
1816
|
+
destinationToken: '0x20c00000000000000000000014f22ca97301eb73',
|
|
1817
|
+
recipient: `0x${'11'.repeat(20)}`,
|
|
1818
|
+
refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
|
|
1819
|
+
sourceChain: 'tron',
|
|
1820
|
+
sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
let depositAddressIdempotencySequence = 0
|
|
1824
|
+
function nextDepositAddressKey() {
|
|
1825
|
+
return `funding_deposit_address_${depositAddressIdempotencySequence++}`
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function postDepositAddress(
|
|
1829
|
+
app: ReturnType<typeof create>,
|
|
1830
|
+
options: {
|
|
1831
|
+
body?: unknown
|
|
1832
|
+
headers?: Record<string, string> | undefined
|
|
1833
|
+
idempotencyKey?: string | undefined
|
|
1834
|
+
key?: { token: string } | undefined
|
|
1835
|
+
} = {},
|
|
1836
|
+
) {
|
|
1837
|
+
return app.request('/v1/funding/deposit-addresses', {
|
|
1838
|
+
body: JSON.stringify(options.body ?? depositAddressBody),
|
|
1839
|
+
headers: options.headers ?? {
|
|
1840
|
+
authorization: `Bearer ${(options.key ?? fundingWriter).token}`,
|
|
1841
|
+
'content-type': 'application/json',
|
|
1842
|
+
'idempotency-key': options.idempotencyKey ?? nextDepositAddressKey(),
|
|
1843
|
+
},
|
|
1844
|
+
method: 'POST',
|
|
1845
|
+
})
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function postTransfer(
|
|
1849
|
+
app: ReturnType<typeof create>,
|
|
1850
|
+
options: { body?: unknown; idempotencyKey?: string | undefined; key?: { token: string } } = {},
|
|
1851
|
+
) {
|
|
1852
|
+
return app.request('/v1/funding/transfers', {
|
|
1853
|
+
body: JSON.stringify(options.body ?? createBody),
|
|
1854
|
+
headers: {
|
|
1855
|
+
authorization: `Bearer ${(options.key ?? fundingWriter).token}`,
|
|
1856
|
+
'content-type': 'application/json',
|
|
1857
|
+
...(options.idempotencyKey === undefined
|
|
1858
|
+
? {}
|
|
1859
|
+
: { 'idempotency-key': options.idempotencyKey }),
|
|
1860
|
+
},
|
|
1861
|
+
method: 'POST',
|
|
1862
|
+
})
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
describe('POST /funding/transfers', () => {
|
|
1866
|
+
function providerResponse(): FundingProvider.prepareTransfer.ReturnType {
|
|
1867
|
+
const spender = `0x${'44'.repeat(20)}`
|
|
1868
|
+
return {
|
|
1869
|
+
action: {
|
|
1870
|
+
calls: [
|
|
1871
|
+
{
|
|
1872
|
+
data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${(1_000_000).toString(16).padStart(64, '0')}`,
|
|
1873
|
+
to: createBody.sourceToken,
|
|
1874
|
+
value: '0x0',
|
|
1875
|
+
},
|
|
1876
|
+
{ data: '0xe8017952', to: spender, value: '0x0' },
|
|
1877
|
+
],
|
|
1878
|
+
type: 'evm:calls',
|
|
1879
|
+
},
|
|
1880
|
+
correlation: { requestId: `0x${'ab'.repeat(32)}` },
|
|
1881
|
+
fees: [{ amount: '21050', side: 'source' }],
|
|
1882
|
+
destinationAmountMin: '980000',
|
|
1883
|
+
destinationAmount: '990000',
|
|
1884
|
+
quality: {
|
|
1885
|
+
estimatedSeconds: 4,
|
|
1886
|
+
liquiditySource: 'providerQuote',
|
|
1887
|
+
sourceDetail: 'relay:quote-v2',
|
|
1888
|
+
tier: 'liquid',
|
|
1889
|
+
},
|
|
1890
|
+
sampledAt: '2099-01-01T00:00:00.000Z',
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function provider(
|
|
1895
|
+
prepareTransfer: NonNullable<FundingProvider.Provider['prepareTransfer']> = async () =>
|
|
1896
|
+
providerResponse(),
|
|
1897
|
+
) {
|
|
1898
|
+
return FundingProvider.from({
|
|
1899
|
+
...Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
1900
|
+
prepareTransfer,
|
|
1901
|
+
})
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
let idempotencySequence = 0
|
|
1905
|
+
function nextIdempotencyKey() {
|
|
1906
|
+
return `funding_test_${idempotencySequence++}`
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
test('requires funding:write and an Idempotency-Key', async () => {
|
|
1910
|
+
const app = transfersApp({
|
|
1911
|
+
providers: [provider()],
|
|
1912
|
+
})
|
|
1913
|
+
const forbidden = await postTransfer(app, {
|
|
1914
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
1915
|
+
key: fundingReader,
|
|
1916
|
+
})
|
|
1917
|
+
expect(forbidden.status).toBe(403)
|
|
1918
|
+
|
|
1919
|
+
const missing = await postTransfer(app)
|
|
1920
|
+
expect(missing.status).toBe(400)
|
|
1921
|
+
expect(((await missing.json()) as { error: { code: string } }).error.code).toBe(
|
|
1922
|
+
'idempotency_key_required',
|
|
1923
|
+
)
|
|
1924
|
+
})
|
|
1925
|
+
|
|
1926
|
+
test('creates a transfer with the selected executable quote', async () => {
|
|
1927
|
+
const inputs: FundingProvider.prepareTransfer.Parameters[] = []
|
|
1928
|
+
const app = transfersApp({
|
|
1929
|
+
providers: [
|
|
1930
|
+
provider(async (input) => {
|
|
1931
|
+
inputs.push(input)
|
|
1932
|
+
return providerResponse()
|
|
1933
|
+
}),
|
|
1934
|
+
],
|
|
1935
|
+
})
|
|
1936
|
+
const response = await postTransfer(app, {
|
|
1937
|
+
body: { ...createBody, destinationToken: 'pathusd', sourceToken: 'usdc' },
|
|
1938
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
1939
|
+
})
|
|
1940
|
+
expect(response.status).toBe(200)
|
|
1941
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
1942
|
+
const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
|
|
1943
|
+
|
|
1944
|
+
expect(inputs[0]).toMatchObject({
|
|
1945
|
+
method: 'transaction',
|
|
1946
|
+
mode: 'exactSource',
|
|
1947
|
+
recipient: createBody.recipient,
|
|
1948
|
+
sender: createBody.sender,
|
|
1949
|
+
slippageBps: 300,
|
|
1950
|
+
})
|
|
1951
|
+
expect(body).toMatchObject({
|
|
1952
|
+
action: { type: 'evm:calls' },
|
|
1953
|
+
destinationAmountMin: { baseUnits: '980000' },
|
|
1954
|
+
method: 'transaction',
|
|
1955
|
+
mode: 'exactSource',
|
|
1956
|
+
provider: { id: 'relay' },
|
|
1957
|
+
// The provider returned no expiry, so the default validity window applies.
|
|
1958
|
+
quote: { expiresAt: '2099-01-01T00:01:00.000Z', sampledAt: '2099-01-01T00:00:00.000Z' },
|
|
1959
|
+
sourceAmount: { baseUnits: '1000000' },
|
|
1960
|
+
status: 'awaiting-source',
|
|
1961
|
+
version: 1,
|
|
1962
|
+
})
|
|
1963
|
+
expect(body.fees[0]).toMatchObject({ side: 'source', token: { symbol: 'USDC' } })
|
|
1964
|
+
|
|
1965
|
+
// Reads return the same transfer without the action; the private
|
|
1966
|
+
// correlation never leaves the row.
|
|
1967
|
+
const read = await app.request(`/v1/funding/transfers/${body.id}`, as(fundingWriter))
|
|
1968
|
+
const readBody = await TestApp.json(read, Funding.schema.getFundingTransfer.Response)
|
|
1969
|
+
expect('action' in readBody).toBe(false)
|
|
1970
|
+
expect(JSON.stringify(readBody)).not.toContain('ab'.repeat(32))
|
|
1971
|
+
const row = await db.kysely
|
|
1972
|
+
.selectFrom('funding_transfers')
|
|
1973
|
+
.select(['providerState'])
|
|
1974
|
+
.where('id', '=', body.id)
|
|
1975
|
+
.executeTakeFirstOrThrow()
|
|
1976
|
+
expect(row.providerState).toEqual({ requestId: `0x${'ab'.repeat(32)}` })
|
|
1977
|
+
})
|
|
1978
|
+
|
|
1979
|
+
test('resolves token identifiers through the composed route', async () => {
|
|
1980
|
+
const app = transfersApp({ providers: [provider()] })
|
|
1981
|
+
const identifiers = [
|
|
1982
|
+
{ destinationToken: 'pathusd', name: 'alias', sourceToken: 'usdc' },
|
|
1983
|
+
{
|
|
1984
|
+
destinationToken: createBody.destinationToken,
|
|
1985
|
+
name: 'address',
|
|
1986
|
+
sourceToken: createBody.sourceToken,
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
destinationToken: 'eip155:4217/erc20:0x20c0000000000000000000000000000000000000',
|
|
1990
|
+
name: 'token key',
|
|
1991
|
+
sourceToken: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
1992
|
+
},
|
|
1993
|
+
] as const
|
|
1994
|
+
const results = []
|
|
1995
|
+
for (const identifier of identifiers) {
|
|
1996
|
+
const response = await postTransfer(app, {
|
|
1997
|
+
body: {
|
|
1998
|
+
...createBody,
|
|
1999
|
+
destinationToken: identifier.destinationToken,
|
|
2000
|
+
sourceToken: identifier.sourceToken,
|
|
2001
|
+
},
|
|
2002
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
2003
|
+
})
|
|
2004
|
+
const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
|
|
2005
|
+
results.push({
|
|
2006
|
+
destinationToken: body.destinationToken.symbol,
|
|
2007
|
+
name: identifier.name,
|
|
2008
|
+
sourceToken: body.sourceToken.symbol,
|
|
2009
|
+
status: response.status,
|
|
2010
|
+
})
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
expect(results).toMatchInlineSnapshot(`
|
|
2014
|
+
[
|
|
2015
|
+
{
|
|
2016
|
+
"destinationToken": "pathUSD",
|
|
2017
|
+
"name": "alias",
|
|
2018
|
+
"sourceToken": "USDC",
|
|
2019
|
+
"status": 200,
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
"destinationToken": "pathUSD",
|
|
2023
|
+
"name": "address",
|
|
2024
|
+
"sourceToken": "USDC",
|
|
2025
|
+
"status": 200,
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
"destinationToken": "pathUSD",
|
|
2029
|
+
"name": "token key",
|
|
2030
|
+
"sourceToken": "USDC",
|
|
2031
|
+
"status": 200,
|
|
2032
|
+
},
|
|
2033
|
+
]
|
|
2034
|
+
`)
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
test('replays the exact response for the same key and body', async () => {
|
|
2038
|
+
const app = transfersApp({
|
|
2039
|
+
providers: [provider()],
|
|
2040
|
+
})
|
|
2041
|
+
const idempotencyKey = nextIdempotencyKey()
|
|
2042
|
+
const first = await postTransfer(app, { idempotencyKey })
|
|
2043
|
+
const second = await postTransfer(app, { idempotencyKey })
|
|
2044
|
+
expect(second.status).toBe(200)
|
|
2045
|
+
expect(await second.json()).toEqual(await first.json())
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
test('rejects a reused key with different input', async () => {
|
|
2049
|
+
const app = transfersApp({
|
|
2050
|
+
providers: [provider()],
|
|
2051
|
+
})
|
|
2052
|
+
const idempotencyKey = nextIdempotencyKey()
|
|
2053
|
+
await postTransfer(app, { idempotencyKey })
|
|
2054
|
+
const conflict = await postTransfer(app, {
|
|
2055
|
+
body: { ...createBody, amount: '2000000' },
|
|
2056
|
+
idempotencyKey,
|
|
2057
|
+
})
|
|
2058
|
+
expect(conflict.status).toBe(409)
|
|
2059
|
+
expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
|
|
2060
|
+
'idempotency_conflict',
|
|
2061
|
+
)
|
|
2062
|
+
})
|
|
2063
|
+
|
|
2064
|
+
test('rejects an equivalent request while the first is running', async () => {
|
|
2065
|
+
const app = transfersApp({
|
|
2066
|
+
providers: [provider()],
|
|
2067
|
+
})
|
|
2068
|
+
const idempotencyKey = nextIdempotencyKey()
|
|
2069
|
+
await FundingIdempotencyTable.claim(db, {
|
|
2070
|
+
apiKeyId: fundingWriter.id,
|
|
2071
|
+
keyHash: Idempotency.inputHash(idempotencyKey),
|
|
2072
|
+
requestHash: Idempotency.inputHash(createBody),
|
|
2073
|
+
ttlMs: 60_000,
|
|
2074
|
+
})
|
|
2075
|
+
const pending = await postTransfer(app, { idempotencyKey })
|
|
2076
|
+
expect(pending.status).toBe(409)
|
|
2077
|
+
expect(((await pending.json()) as { error: { code: string } }).error.code).toBe(
|
|
2078
|
+
'idempotency_in_progress',
|
|
2079
|
+
)
|
|
2080
|
+
})
|
|
2081
|
+
|
|
2082
|
+
test('rolls back creation after losing the idempotency lease', async () => {
|
|
2083
|
+
const idempotencyKey = nextIdempotencyKey()
|
|
2084
|
+
const keyHash = Idempotency.inputHash(idempotencyKey)
|
|
2085
|
+
const before = await db.kysely
|
|
2086
|
+
.selectFrom('funding_transfers')
|
|
2087
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
2088
|
+
.executeTakeFirstOrThrow()
|
|
2089
|
+
const app = transfersApp({
|
|
2090
|
+
providers: [
|
|
2091
|
+
provider(async () => {
|
|
2092
|
+
await db.kysely
|
|
2093
|
+
.updateTable('funding_idempotency_requests')
|
|
2094
|
+
.set({ expiresAt: new Date(0).toISOString() })
|
|
2095
|
+
.where('apiKeyId', '=', fundingWriter.id)
|
|
2096
|
+
.where('keyHash', '=', keyHash)
|
|
2097
|
+
.execute()
|
|
2098
|
+
await FundingIdempotencyTable.claim(db, {
|
|
2099
|
+
apiKeyId: fundingWriter.id,
|
|
2100
|
+
keyHash,
|
|
2101
|
+
requestHash: Idempotency.inputHash(createBody),
|
|
2102
|
+
ttlMs: 60_000,
|
|
2103
|
+
})
|
|
2104
|
+
return providerResponse()
|
|
2105
|
+
}),
|
|
2106
|
+
],
|
|
2107
|
+
})
|
|
2108
|
+
|
|
2109
|
+
const response = await postTransfer(app, { idempotencyKey })
|
|
2110
|
+
expect(response.status).toBe(500)
|
|
2111
|
+
const after = await db.kysely
|
|
2112
|
+
.selectFrom('funding_transfers')
|
|
2113
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
2114
|
+
.executeTakeFirstOrThrow()
|
|
2115
|
+
expect(after.count).toBe(before.count)
|
|
2116
|
+
})
|
|
2117
|
+
|
|
2118
|
+
test('rejects non-account fields and gates disabled corridors and methods', async () => {
|
|
2119
|
+
const app = transfersApp({
|
|
2120
|
+
providers: [provider()],
|
|
2121
|
+
})
|
|
2122
|
+
|
|
2123
|
+
const ignoredRefund = await postTransfer(app, {
|
|
2124
|
+
body: { ...createBody, refundAddress: createBody.sender },
|
|
2125
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
2126
|
+
})
|
|
2127
|
+
expect(ignoredRefund.status).toBe(400)
|
|
2128
|
+
expect(((await ignoredRefund.json()) as { error: { code: string } }).error.code).toBe(
|
|
2129
|
+
'body_invalid',
|
|
2130
|
+
)
|
|
2131
|
+
|
|
2132
|
+
const unsupportedDestination = await postTransfer(app, {
|
|
2133
|
+
body: { ...createBody, destinationVault: `0x${'f4'.repeat(20)}`, type: 'vault' },
|
|
2134
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
2135
|
+
})
|
|
2136
|
+
expect(unsupportedDestination.status).toBe(400)
|
|
2137
|
+
expect(((await unsupportedDestination.json()) as { error: { code: string } }).error.code).toBe(
|
|
2138
|
+
'body_invalid',
|
|
2139
|
+
)
|
|
2140
|
+
|
|
2141
|
+
const unknownToken = await postTransfer(app, {
|
|
2142
|
+
body: { ...createBody, destinationToken: `0x${'99'.repeat(20)}` },
|
|
2143
|
+
idempotencyKey: nextIdempotencyKey(),
|
|
2144
|
+
})
|
|
2145
|
+
expect(unknownToken.status).toBe(404)
|
|
2146
|
+
})
|
|
2147
|
+
|
|
2148
|
+
test('releases the claim after a provider failure', async () => {
|
|
2149
|
+
const failing = transfersApp({
|
|
2150
|
+
providers: [
|
|
2151
|
+
provider(async () => {
|
|
2152
|
+
throw new Error('upstream unavailable')
|
|
2153
|
+
}),
|
|
2154
|
+
],
|
|
2155
|
+
})
|
|
2156
|
+
const idempotencyKey = nextIdempotencyKey()
|
|
2157
|
+
const failed = await postTransfer(failing, { idempotencyKey })
|
|
2158
|
+
expect(failed.status).toBe(502)
|
|
2159
|
+
expect(((await failed.json()) as { error: { code: string } }).error.code).toBe('upstream_error')
|
|
2160
|
+
|
|
2161
|
+
// The key was released, so the same request retries successfully.
|
|
2162
|
+
const working = transfersApp({
|
|
2163
|
+
providers: [provider()],
|
|
2164
|
+
})
|
|
2165
|
+
expect((await postTransfer(working, { idempotencyKey })).status).toBe(200)
|
|
2166
|
+
})
|
|
2167
|
+
|
|
2168
|
+
test('rejects preparations with unbounded approvals', async () => {
|
|
2169
|
+
const app = transfersApp({
|
|
2170
|
+
providers: [
|
|
2171
|
+
provider(async () => {
|
|
2172
|
+
const result = providerResponse()
|
|
2173
|
+
const spender = `0x${'44'.repeat(20)}`
|
|
2174
|
+
return {
|
|
2175
|
+
...result,
|
|
2176
|
+
action: {
|
|
2177
|
+
calls: [
|
|
2178
|
+
{
|
|
2179
|
+
data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${'f'.repeat(64)}`,
|
|
2180
|
+
to: createBody.sourceToken,
|
|
2181
|
+
value: '0x0',
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
type: 'evm:calls',
|
|
2185
|
+
},
|
|
2186
|
+
}
|
|
2187
|
+
}),
|
|
2188
|
+
],
|
|
2189
|
+
})
|
|
2190
|
+
const response = await postTransfer(app, { idempotencyKey: nextIdempotencyKey() })
|
|
2191
|
+
expect(response.status).toBe(502)
|
|
2192
|
+
})
|
|
2193
|
+
})
|
|
2194
|
+
|
|
2195
|
+
// Quotes come from the live provider API; the returned calls execute on a
|
|
2196
|
+
// disposable anvil fork of Base, proving the action without funding a real
|
|
2197
|
+
// account. Skips without the `anvil` binary.
|
|
2198
|
+
describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', () => {
|
|
2199
|
+
/**
|
|
2200
|
+
* Storage slot of Circle FiatToken's `balanceAndBlacklistStates` mapping.
|
|
2201
|
+
* `anvil_dealERC20` cannot probe it because balances pack a blacklist bit,
|
|
2202
|
+
* so the fork suite writes the slot directly. Every curated USDC is a
|
|
2203
|
+
* FiatToken.
|
|
2204
|
+
*/
|
|
2205
|
+
const fiatTokenBalanceSlot = 9n
|
|
2206
|
+
|
|
2207
|
+
for (const executable of executableProviders) {
|
|
2208
|
+
describe(executable.id, () => {
|
|
2209
|
+
const route = TestFunding.snapshot.routes.find(
|
|
2210
|
+
(entry) =>
|
|
2211
|
+
entry.route.source.chain.id === `eip155:${base.id}` &&
|
|
2212
|
+
entry.route.source.symbol === 'USDC' &&
|
|
2213
|
+
entry.route.destination.slug === 'pathusd',
|
|
2214
|
+
)!
|
|
2215
|
+
const usdc = route.route.source
|
|
2216
|
+
const amount = Value.from('10', usdc.decimals)
|
|
2217
|
+
const sender = Anvil.getAccount(0)
|
|
2218
|
+
|
|
2219
|
+
let anvil: Anvil.start.ReturnType
|
|
2220
|
+
|
|
2221
|
+
beforeAll(async () => {
|
|
2222
|
+
anvil = await Anvil.start({ chain: base, forkUrl: base.rpcUrls.default.http[0]! })
|
|
2223
|
+
})
|
|
2224
|
+
|
|
2225
|
+
afterAll(async () => {
|
|
2226
|
+
await anvil?.stop()
|
|
2227
|
+
})
|
|
2228
|
+
|
|
2229
|
+
function createTransfer(idempotencyKey: string) {
|
|
2230
|
+
const app = transfersApp({ providers: [executable.create(sender.address)] })
|
|
2231
|
+
return postTransfer(app, {
|
|
2232
|
+
body: { ...createBody, amount: amount.toString(), sender: sender.address },
|
|
2233
|
+
idempotencyKey,
|
|
2234
|
+
})
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
test('executes the prepared calls and returns successful receipts', async () => {
|
|
2238
|
+
const { client } = anvil
|
|
2239
|
+
await Anvil.setErc20Balance(client, {
|
|
2240
|
+
address: sender.address,
|
|
2241
|
+
amount,
|
|
2242
|
+
slot: fiatTokenBalanceSlot,
|
|
2243
|
+
token: usdc.address as `0x${string}`,
|
|
2244
|
+
})
|
|
2245
|
+
|
|
2246
|
+
const response = await createTransfer(`bridge_${executable.id}_${sender.address}`)
|
|
2247
|
+
expect(response.status).toBe(200)
|
|
2248
|
+
const transfer = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
|
|
2249
|
+
expect(transfer.action.type).toBe('evm:calls')
|
|
2250
|
+
if (transfer.action.type !== 'evm:calls') return
|
|
2251
|
+
expect(transfer.action.calls.length).toBeGreaterThan(0)
|
|
2252
|
+
|
|
2253
|
+
const receipts = []
|
|
2254
|
+
for (const call of transfer.action.calls) {
|
|
2255
|
+
receipts.push(
|
|
2256
|
+
await client.sendTransactionSync({
|
|
2257
|
+
account: sender,
|
|
2258
|
+
data: call.data as `0x${string}`,
|
|
2259
|
+
to: call.to as `0x${string}`,
|
|
2260
|
+
value: BigInt(call.value),
|
|
2261
|
+
}),
|
|
2262
|
+
)
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
expect(receipts.map((receipt) => receipt.status)).toEqual(
|
|
2266
|
+
transfer.action.calls.map(() => 'success'),
|
|
2267
|
+
)
|
|
2268
|
+
// The deposit is the last call and must emit; an approval alone proves nothing.
|
|
2269
|
+
expect(receipts.at(-1)!.logs.length).toBeGreaterThan(0)
|
|
2270
|
+
})
|
|
2271
|
+
|
|
2272
|
+
test('bounds the approval to the quoted source spend', async () => {
|
|
2273
|
+
const response = await createTransfer(`bridge_bounds_${executable.id}_${sender.address}`)
|
|
2274
|
+
expect(response.status).toBe(200)
|
|
2275
|
+
const transfer = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
|
|
2276
|
+
if (transfer.action.type !== 'evm:calls') return
|
|
2277
|
+
|
|
2278
|
+
const approval = transfer.action.calls.find((call) =>
|
|
2279
|
+
call.data.toLowerCase().startsWith('0x095ea7b3'),
|
|
2280
|
+
)
|
|
2281
|
+
expect(approval).toBeDefined()
|
|
2282
|
+
if (!approval) return
|
|
2283
|
+
|
|
2284
|
+
// Decoded from the live action, so a provider that starts returning
|
|
2285
|
+
// an unlimited allowance fails here.
|
|
2286
|
+
const decoded = decodeFunctionData({ abi: erc20Abi, data: approval.data as `0x${string}` })
|
|
2287
|
+
expect(decoded.functionName).toBe('approve')
|
|
2288
|
+
if (decoded.functionName !== 'approve') return
|
|
2289
|
+
const [, approved] = decoded.args
|
|
2290
|
+
expect(approved).toBeLessThanOrEqual(BigInt(transfer.sourceAmount.baseUnits))
|
|
2291
|
+
expect(approved).toBeLessThan(maxUint256)
|
|
2292
|
+
})
|
|
2293
|
+
})
|
|
2294
|
+
}
|
|
2295
|
+
})
|
|
2296
|
+
|
|
2297
|
+
describe('POST /funding/transfers/{destination}', () => {
|
|
2298
|
+
test.each([
|
|
2299
|
+
{ body: vaultTransferBody, destination: 'vault' },
|
|
2300
|
+
{ body: zoneTransferBody, destination: 'zone' },
|
|
2301
|
+
] as const)('validates and returns 501 for $destination transfers', async (options) => {
|
|
2302
|
+
const app = transfersApp()
|
|
2303
|
+
const before = await db.kysely
|
|
2304
|
+
.selectFrom('funding_transfers')
|
|
2305
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
2306
|
+
.executeTakeFirstOrThrow()
|
|
2307
|
+
const response = await app.request(`/v1/funding/transfers/${options.destination}`, {
|
|
2308
|
+
body: JSON.stringify(options.body),
|
|
2309
|
+
headers: {
|
|
2310
|
+
authorization: `Bearer ${fundingWriter.token}`,
|
|
2311
|
+
'content-type': 'application/json',
|
|
2312
|
+
'idempotency-key': `funding_${options.destination}_unimplemented`,
|
|
2313
|
+
},
|
|
2314
|
+
method: 'POST',
|
|
2315
|
+
})
|
|
2316
|
+
|
|
2317
|
+
expect(response.status).toBe(501)
|
|
2318
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
2319
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
2320
|
+
'not_implemented',
|
|
2321
|
+
)
|
|
2322
|
+
const after = await db.kysely
|
|
2323
|
+
.selectFrom('funding_transfers')
|
|
2324
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
2325
|
+
.executeTakeFirstOrThrow()
|
|
2326
|
+
expect(after.count).toBe(before.count)
|
|
2327
|
+
})
|
|
2328
|
+
|
|
2329
|
+
test('requires the header and validates destination fields', async () => {
|
|
2330
|
+
const app = transfersApp()
|
|
2331
|
+
const missingHeader = await app.request('/v1/funding/transfers/vault', {
|
|
2332
|
+
body: JSON.stringify(vaultTransferBody),
|
|
2333
|
+
headers: {
|
|
2334
|
+
authorization: `Bearer ${fundingWriter.token}`,
|
|
2335
|
+
'content-type': 'application/json',
|
|
2336
|
+
},
|
|
2337
|
+
method: 'POST',
|
|
2338
|
+
})
|
|
2339
|
+
expect(missingHeader.status).toBe(400)
|
|
2340
|
+
expect(((await missingHeader.json()) as { error: { code: string } }).error.code).toBe(
|
|
2341
|
+
'idempotency_key_required',
|
|
2342
|
+
)
|
|
2343
|
+
|
|
2344
|
+
const invalidZone = await app.request('/v1/funding/transfers/zone', {
|
|
2345
|
+
body: JSON.stringify({ ...zoneTransferBody, destinationChain: '421700001' }),
|
|
2346
|
+
headers: {
|
|
2347
|
+
authorization: `Bearer ${fundingWriter.token}`,
|
|
2348
|
+
'content-type': 'application/json',
|
|
2349
|
+
'idempotency-key': 'funding_zone_invalid',
|
|
2350
|
+
},
|
|
2351
|
+
method: 'POST',
|
|
2352
|
+
})
|
|
2353
|
+
expect(invalidZone.status).toBe(400)
|
|
2354
|
+
expect(((await invalidZone.json()) as { error: { code: string } }).error.code).toBe(
|
|
2355
|
+
'body_invalid',
|
|
2356
|
+
)
|
|
2357
|
+
})
|
|
2358
|
+
})
|
|
2359
|
+
|
|
2360
|
+
describe('funding transfers OpenAPI', () => {
|
|
2361
|
+
test('publishes an executable account transfer example', async () => {
|
|
2362
|
+
const app = create({ auth: false })
|
|
2363
|
+
type TokenSchema = { examples: string[] }
|
|
2364
|
+
type TransferExample = { destinationToken: { address: string } }
|
|
2365
|
+
type Operation = {
|
|
2366
|
+
requestBody: {
|
|
2367
|
+
content: Record<string, { schema: { properties: { destinationToken: TokenSchema } } }>
|
|
2368
|
+
}
|
|
2369
|
+
responses: Record<string, { content: Record<string, { example: TransferExample }> }>
|
|
2370
|
+
}
|
|
2371
|
+
type Spec = { paths: Record<string, { post: Operation }> }
|
|
2372
|
+
const spec = (await (await app.request('/openapi.json')).json()) as Spec
|
|
2373
|
+
const operation = spec.paths['/v1/funding/transfers']!.post
|
|
2374
|
+
|
|
2375
|
+
expect({
|
|
2376
|
+
request:
|
|
2377
|
+
operation.requestBody.content['application/json']!.schema.properties.destinationToken
|
|
2378
|
+
.examples,
|
|
2379
|
+
response:
|
|
2380
|
+
operation.responses['200']!.content['application/json']!.example.destinationToken.address,
|
|
2381
|
+
}).toEqual({
|
|
2382
|
+
request: ['pathusd'],
|
|
2383
|
+
response: '0x20c0000000000000000000000000000000000000',
|
|
2384
|
+
})
|
|
2385
|
+
})
|
|
2386
|
+
|
|
2387
|
+
test('publishes human-readable deposit address route examples', async () => {
|
|
2388
|
+
const app = create({ auth: false })
|
|
2389
|
+
type Property = { examples: string[] }
|
|
2390
|
+
type Operation = {
|
|
2391
|
+
requestBody: {
|
|
2392
|
+
content: Record<
|
|
2393
|
+
string,
|
|
2394
|
+
{
|
|
2395
|
+
schema: {
|
|
2396
|
+
properties: {
|
|
2397
|
+
destinationToken: Property
|
|
2398
|
+
sourceChain: Property
|
|
2399
|
+
sourceToken: Property
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
>
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
type Spec = { paths: Record<string, { post: Operation }> }
|
|
2407
|
+
const spec = (await (await app.request('/openapi.json')).json()) as Spec
|
|
2408
|
+
const properties =
|
|
2409
|
+
spec.paths['/v1/funding/deposit-addresses']!.post.requestBody.content['application/json']!
|
|
2410
|
+
.schema.properties
|
|
2411
|
+
|
|
2412
|
+
expect({
|
|
2413
|
+
destinationToken: properties.destinationToken.examples,
|
|
2414
|
+
sourceChain: properties.sourceChain.examples,
|
|
2415
|
+
sourceToken: properties.sourceToken.examples,
|
|
2416
|
+
}).toMatchInlineSnapshot(`
|
|
2417
|
+
{
|
|
2418
|
+
"destinationToken": [
|
|
2419
|
+
"usdt0",
|
|
2420
|
+
],
|
|
2421
|
+
"sourceChain": [
|
|
2422
|
+
"tron",
|
|
2423
|
+
"tron:0x2b6653dc",
|
|
2424
|
+
],
|
|
2425
|
+
"sourceToken": [
|
|
2426
|
+
"usdt",
|
|
2427
|
+
],
|
|
2428
|
+
}
|
|
2429
|
+
`)
|
|
2430
|
+
})
|
|
2431
|
+
|
|
2432
|
+
test('publishes concise operation documentation', async () => {
|
|
2433
|
+
const app = create({ auth: false })
|
|
2434
|
+
type Operation = { description: string; summary: string; tags: string[] }
|
|
2435
|
+
type Spec = {
|
|
2436
|
+
paths: Record<string, { get?: Operation; post?: Operation }>
|
|
2437
|
+
tags: { description?: string; name: string }[]
|
|
2438
|
+
}
|
|
2439
|
+
const spec = (await (await app.request('/openapi.json')).json()) as Spec
|
|
2440
|
+
const detailPath = Object.keys(spec.paths).find((path) =>
|
|
2441
|
+
path.includes('/v1/funding/transfers/{'),
|
|
2442
|
+
)!
|
|
2443
|
+
const depositAddressPath = Object.keys(spec.paths).find(
|
|
2444
|
+
(path) => path.includes('/v1/funding/deposit-addresses/{') && !path.endsWith('/deposits'),
|
|
2445
|
+
)!
|
|
2446
|
+
const depositAddressDepositsPath = Object.keys(spec.paths).find((path) =>
|
|
2447
|
+
path.endsWith('/deposits'),
|
|
2448
|
+
)!
|
|
2449
|
+
const depositPath = Object.keys(spec.paths).find((path) =>
|
|
2450
|
+
path.includes('/v1/funding/deposits/{'),
|
|
2451
|
+
)!
|
|
2452
|
+
const summarize = (operation: Operation) => ({
|
|
2453
|
+
description: operation.description,
|
|
2454
|
+
summary: operation.summary,
|
|
2455
|
+
})
|
|
2456
|
+
const operations = {
|
|
2457
|
+
account: summarize(spec.paths['/v1/funding/transfers']!.post!),
|
|
2458
|
+
chains: summarize(spec.paths['/v1/funding/chains']!.get!),
|
|
2459
|
+
deposit: summarize(spec.paths[depositPath]!.get!),
|
|
2460
|
+
depositAddress: summarize(spec.paths[depositAddressPath]!.get!),
|
|
2461
|
+
depositAddressCreate: summarize(spec.paths['/v1/funding/deposit-addresses']!.post!),
|
|
2462
|
+
depositAddressDeposits: summarize(spec.paths[depositAddressDepositsPath]!.get!),
|
|
2463
|
+
detail: summarize(spec.paths[detailPath]!.get!),
|
|
2464
|
+
list: summarize(spec.paths['/v1/funding/transfers']!.get!),
|
|
2465
|
+
providers: summarize(spec.paths['/v1/funding/providers']!.get!),
|
|
2466
|
+
quotes: summarize(spec.paths['/v1/funding/quotes']!.get!),
|
|
2467
|
+
vault: summarize(spec.paths['/v1/funding/transfers/vault']!.post!),
|
|
2468
|
+
zone: summarize(spec.paths['/v1/funding/transfers/zone']!.post!),
|
|
2469
|
+
}
|
|
2470
|
+
const sidebar = Object.values(spec.paths).flatMap((path) =>
|
|
2471
|
+
[path.get, path.post].flatMap((operation) =>
|
|
2472
|
+
operation?.tags.includes('Funding') ? [operation.summary] : [],
|
|
2473
|
+
),
|
|
2474
|
+
)
|
|
2475
|
+
|
|
2476
|
+
for (const operation of Object.values(operations))
|
|
2477
|
+
expect(operation.description.split(/\s+/).length).toBeLessThan(15)
|
|
2478
|
+
|
|
2479
|
+
expect({
|
|
2480
|
+
category: spec.tags.find((tag) => tag.name === 'Funding')?.description,
|
|
2481
|
+
operations,
|
|
2482
|
+
sidebar,
|
|
2483
|
+
}).toMatchInlineSnapshot(`
|
|
2484
|
+
{
|
|
2485
|
+
"category": "Quote, transfer, and deposit management for funding Tempo from other chains.",
|
|
2486
|
+
"operations": {
|
|
2487
|
+
"account": {
|
|
2488
|
+
"description": "Creates a transfer to a Tempo account.",
|
|
2489
|
+
"summary": "Create transfer",
|
|
2490
|
+
},
|
|
2491
|
+
"chains": {
|
|
2492
|
+
"description": "Lists supported source chains and tokens.",
|
|
2493
|
+
"summary": "Get chains",
|
|
2494
|
+
},
|
|
2495
|
+
"deposit": {
|
|
2496
|
+
"description": "Returns one detected deposit and its delivery status.",
|
|
2497
|
+
"summary": "Get deposit",
|
|
2498
|
+
},
|
|
2499
|
+
"depositAddress": {
|
|
2500
|
+
"description": "Returns one reusable funding deposit address.",
|
|
2501
|
+
"summary": "Get deposit address",
|
|
2502
|
+
},
|
|
2503
|
+
"depositAddressCreate": {
|
|
2504
|
+
"description": "Creates or returns a reusable deposit address for funding a Tempo account.",
|
|
2505
|
+
"summary": "Create deposit address",
|
|
2506
|
+
},
|
|
2507
|
+
"depositAddressDeposits": {
|
|
2508
|
+
"description": "Lists deposits detected for one funding deposit address, newest first.",
|
|
2509
|
+
"summary": "List deposits",
|
|
2510
|
+
},
|
|
2511
|
+
"detail": {
|
|
2512
|
+
"description": "Returns a transfer by ID.",
|
|
2513
|
+
"summary": "Get transfer",
|
|
2514
|
+
},
|
|
2515
|
+
"list": {
|
|
2516
|
+
"description": "Lists transfers from newest to oldest.",
|
|
2517
|
+
"summary": "List transfers",
|
|
2518
|
+
},
|
|
2519
|
+
"providers": {
|
|
2520
|
+
"description": "Lists available transfer providers.",
|
|
2521
|
+
"summary": "Get providers",
|
|
2522
|
+
},
|
|
2523
|
+
"quotes": {
|
|
2524
|
+
"description": "Returns live quotes for transferring stablecoins to Tempo.",
|
|
2525
|
+
"summary": "Get quotes",
|
|
2526
|
+
},
|
|
2527
|
+
"vault": {
|
|
2528
|
+
"description": "Creates a transfer into a Tempo Earn vault.",
|
|
2529
|
+
"summary": "Create transfer into vault",
|
|
2530
|
+
},
|
|
2531
|
+
"zone": {
|
|
2532
|
+
"description": "Creates a transfer into a Tempo Zone.",
|
|
2533
|
+
"summary": "Create transfer into zone",
|
|
2534
|
+
},
|
|
2535
|
+
},
|
|
2536
|
+
"sidebar": [
|
|
2537
|
+
"Get quotes",
|
|
2538
|
+
"Get chains",
|
|
2539
|
+
"Get providers",
|
|
2540
|
+
"Get deposit address",
|
|
2541
|
+
"Get deposit",
|
|
2542
|
+
"List deposits",
|
|
2543
|
+
"Create deposit address",
|
|
2544
|
+
"List transfers",
|
|
2545
|
+
"Create transfer",
|
|
2546
|
+
"Create transfer into vault",
|
|
2547
|
+
"Create transfer into zone",
|
|
2548
|
+
"Get transfer",
|
|
2549
|
+
],
|
|
2550
|
+
}
|
|
2551
|
+
`)
|
|
2552
|
+
})
|
|
2553
|
+
|
|
2554
|
+
test('publishes concrete creation schemas', async () => {
|
|
2555
|
+
const app = create({ auth: false })
|
|
2556
|
+
const spec = (await (await app.request('/openapi.json')).json()) as {
|
|
2557
|
+
paths: Record<
|
|
2558
|
+
string,
|
|
2559
|
+
{
|
|
2560
|
+
post?: {
|
|
2561
|
+
requestBody: {
|
|
2562
|
+
content: Record<
|
|
2563
|
+
string,
|
|
2564
|
+
{
|
|
2565
|
+
schema: {
|
|
2566
|
+
additionalProperties?: boolean
|
|
2567
|
+
properties: object
|
|
2568
|
+
required?: readonly string[] | undefined
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
>
|
|
2572
|
+
}
|
|
2573
|
+
responses: Record<string, unknown>
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
>
|
|
2577
|
+
}
|
|
2578
|
+
const creationSchema = (path: string) =>
|
|
2579
|
+
spec.paths[path]!.post!.requestBody.content['application/json']!.schema
|
|
2580
|
+
expect(
|
|
2581
|
+
creationSchema('/v1/funding/deposit-addresses').required?.includes('subsidize') ?? false,
|
|
2582
|
+
).toMatchInlineSnapshot(`false`)
|
|
2583
|
+
const summarize = (path: string) => {
|
|
2584
|
+
const schema = creationSchema(path)
|
|
2585
|
+
return {
|
|
2586
|
+
additionalProperties: schema.additionalProperties,
|
|
2587
|
+
properties: Object.keys(schema.properties),
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
expect({
|
|
2592
|
+
account: summarize('/v1/funding/transfers'),
|
|
2593
|
+
depositAddress: summarize('/v1/funding/deposit-addresses'),
|
|
2594
|
+
depositAddressResponses: Object.keys(
|
|
2595
|
+
spec.paths['/v1/funding/deposit-addresses']!.post!.responses,
|
|
2596
|
+
),
|
|
2597
|
+
vault: summarize('/v1/funding/transfers/vault'),
|
|
2598
|
+
vaultResponses: Object.keys(spec.paths['/v1/funding/transfers/vault']!.post!.responses),
|
|
2599
|
+
zone: summarize('/v1/funding/transfers/zone'),
|
|
2600
|
+
zoneResponses: Object.keys(spec.paths['/v1/funding/transfers/zone']!.post!.responses),
|
|
2601
|
+
}).toMatchInlineSnapshot(`
|
|
2602
|
+
{
|
|
2603
|
+
"account": {
|
|
2604
|
+
"additionalProperties": false,
|
|
2605
|
+
"properties": [
|
|
2606
|
+
"amount",
|
|
2607
|
+
"destinationToken",
|
|
2608
|
+
"mode",
|
|
2609
|
+
"recipient",
|
|
2610
|
+
"sender",
|
|
2611
|
+
"slippageBps",
|
|
2612
|
+
"sourceChain",
|
|
2613
|
+
"sourceToken",
|
|
2614
|
+
],
|
|
2615
|
+
},
|
|
2616
|
+
"depositAddress": {
|
|
2617
|
+
"additionalProperties": false,
|
|
2618
|
+
"properties": [
|
|
2619
|
+
"amount",
|
|
2620
|
+
"destinationToken",
|
|
2621
|
+
"recipient",
|
|
2622
|
+
"refundAddress",
|
|
2623
|
+
"sourceChain",
|
|
2624
|
+
"sourceToken",
|
|
2625
|
+
"subsidize",
|
|
2626
|
+
],
|
|
2627
|
+
},
|
|
2628
|
+
"depositAddressResponses": [
|
|
2629
|
+
"200",
|
|
2630
|
+
"400",
|
|
2631
|
+
"401",
|
|
2632
|
+
"404",
|
|
2633
|
+
"409",
|
|
2634
|
+
"429",
|
|
2635
|
+
"500",
|
|
2636
|
+
"502",
|
|
2637
|
+
],
|
|
2638
|
+
"vault": {
|
|
2639
|
+
"additionalProperties": false,
|
|
2640
|
+
"properties": [
|
|
2641
|
+
"amount",
|
|
2642
|
+
"mode",
|
|
2643
|
+
"recipient",
|
|
2644
|
+
"sender",
|
|
2645
|
+
"slippageBps",
|
|
2646
|
+
"sourceChain",
|
|
2647
|
+
"sourceToken",
|
|
2648
|
+
"vaultAddress",
|
|
2649
|
+
],
|
|
2650
|
+
},
|
|
2651
|
+
"vaultResponses": [
|
|
2652
|
+
"400",
|
|
2653
|
+
"401",
|
|
2654
|
+
"429",
|
|
2655
|
+
"500",
|
|
2656
|
+
"501",
|
|
2657
|
+
],
|
|
2658
|
+
"zone": {
|
|
2659
|
+
"additionalProperties": false,
|
|
2660
|
+
"properties": [
|
|
2661
|
+
"amount",
|
|
2662
|
+
"destinationChain",
|
|
2663
|
+
"destinationToken",
|
|
2664
|
+
"mode",
|
|
2665
|
+
"recipient",
|
|
2666
|
+
"recipientFallback",
|
|
2667
|
+
"sender",
|
|
2668
|
+
"slippageBps",
|
|
2669
|
+
"sourceChain",
|
|
2670
|
+
"sourceToken",
|
|
2671
|
+
],
|
|
2672
|
+
},
|
|
2673
|
+
"zoneResponses": [
|
|
2674
|
+
"400",
|
|
2675
|
+
"401",
|
|
2676
|
+
"429",
|
|
2677
|
+
"500",
|
|
2678
|
+
"501",
|
|
2679
|
+
],
|
|
2680
|
+
}
|
|
2681
|
+
`)
|
|
2682
|
+
})
|
|
2683
|
+
|
|
2684
|
+
test('publishes alphabetically ordered example fields and parameter examples', async () => {
|
|
2685
|
+
const app = create({ auth: false })
|
|
2686
|
+
const spec = (await (await app.request('/openapi.json')).json()) as {
|
|
2687
|
+
paths: Record<
|
|
2688
|
+
string,
|
|
2689
|
+
{
|
|
2690
|
+
get: {
|
|
2691
|
+
parameters?: { example?: unknown; name: string; schema?: { examples?: unknown } }[]
|
|
2692
|
+
responses: Record<string, { content: Record<string, { example: { data: {}[] } }> }>
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
>
|
|
2696
|
+
}
|
|
2697
|
+
const operation = spec.paths['/v1/funding/transfers']!.get
|
|
2698
|
+
const fields = Object.keys(operation.responses['200']!.content['application/json']!.example.data[0]!) // prettier-ignore
|
|
2699
|
+
expect(fields).toEqual([...fields].sort())
|
|
2700
|
+
for (const parameter of operation.parameters ?? [])
|
|
2701
|
+
expect(parameter.example ?? parameter.schema?.examples, parameter.name).toBeDefined()
|
|
2702
|
+
})
|
|
2703
|
+
|
|
2704
|
+
test('publishes operation ids and required scopes', async () => {
|
|
2705
|
+
const app = create({ auth: false })
|
|
2706
|
+
const spec = (await (await app.request('/openapi.json')).json()) as {
|
|
2707
|
+
paths: Record<
|
|
2708
|
+
string,
|
|
2709
|
+
{
|
|
2710
|
+
get: { operationId: string; 'x-required-scopes': string[] }
|
|
2711
|
+
post?: {
|
|
2712
|
+
operationId: string
|
|
2713
|
+
parameters: { in: string; name: string; required: boolean }[]
|
|
2714
|
+
'x-required-scopes': string[]
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
>
|
|
2718
|
+
}
|
|
2719
|
+
const detailPath = Object.keys(spec.paths).find((path) =>
|
|
2720
|
+
path.includes('/v1/funding/transfers/{'),
|
|
2721
|
+
)
|
|
2722
|
+
const depositAddressPath = Object.keys(spec.paths).find(
|
|
2723
|
+
(path) => path.includes('/v1/funding/deposit-addresses/{') && !path.endsWith('/deposits'),
|
|
2724
|
+
)
|
|
2725
|
+
const depositAddressDepositsPath = Object.keys(spec.paths).find((path) =>
|
|
2726
|
+
path.endsWith('/deposits'),
|
|
2727
|
+
)
|
|
2728
|
+
const depositPath = Object.keys(spec.paths).find((path) =>
|
|
2729
|
+
path.includes('/v1/funding/deposits/{'),
|
|
2730
|
+
)
|
|
2731
|
+
expect({
|
|
2732
|
+
deposit: depositPath && spec.paths[depositPath]?.get.operationId,
|
|
2733
|
+
depositAddress: depositAddressPath && spec.paths[depositAddressPath]?.get.operationId,
|
|
2734
|
+
depositAddressCreate: spec.paths['/v1/funding/deposit-addresses']?.post?.operationId,
|
|
2735
|
+
depositAddressCreateScopes:
|
|
2736
|
+
spec.paths['/v1/funding/deposit-addresses']?.post?.['x-required-scopes'],
|
|
2737
|
+
depositAddressDeposits:
|
|
2738
|
+
depositAddressDepositsPath && spec.paths[depositAddressDepositsPath]?.get.operationId,
|
|
2739
|
+
depositAddressDepositsScopes:
|
|
2740
|
+
depositAddressDepositsPath &&
|
|
2741
|
+
spec.paths[depositAddressDepositsPath]?.get['x-required-scopes'],
|
|
2742
|
+
depositAddressScopes:
|
|
2743
|
+
depositAddressPath && spec.paths[depositAddressPath]?.get['x-required-scopes'],
|
|
2744
|
+
depositScopes: depositPath && spec.paths[depositPath]?.get['x-required-scopes'],
|
|
2745
|
+
detail: detailPath && spec.paths[detailPath]?.get.operationId,
|
|
2746
|
+
detailScopes: detailPath && spec.paths[detailPath]?.get['x-required-scopes'],
|
|
2747
|
+
list: spec.paths['/v1/funding/transfers']?.get.operationId,
|
|
2748
|
+
listScopes: spec.paths['/v1/funding/transfers']?.get['x-required-scopes'],
|
|
2749
|
+
requiredCreateHeader: spec.paths['/v1/funding/transfers']?.post?.parameters.find(
|
|
2750
|
+
(parameter) => parameter.name === 'idempotency-key',
|
|
2751
|
+
),
|
|
2752
|
+
vault: spec.paths['/v1/funding/transfers/vault']?.post?.operationId,
|
|
2753
|
+
vaultScopes: spec.paths['/v1/funding/transfers/vault']?.post?.['x-required-scopes'],
|
|
2754
|
+
zone: spec.paths['/v1/funding/transfers/zone']?.post?.operationId,
|
|
2755
|
+
zoneScopes: spec.paths['/v1/funding/transfers/zone']?.post?.['x-required-scopes'],
|
|
2756
|
+
}).toEqual({
|
|
2757
|
+
deposit: 'getFundingDeposit',
|
|
2758
|
+
depositAddress: 'getFundingDepositAddress',
|
|
2759
|
+
depositAddressCreate: 'createFundingDepositAddress',
|
|
2760
|
+
depositAddressCreateScopes: ['funding:write'],
|
|
2761
|
+
depositAddressDeposits: 'listFundingDepositAddressDeposits',
|
|
2762
|
+
depositAddressDepositsScopes: ['funding:read'],
|
|
2763
|
+
depositAddressScopes: ['funding:read'],
|
|
2764
|
+
depositScopes: ['funding:read'],
|
|
2765
|
+
detail: 'getFundingTransfer',
|
|
2766
|
+
detailScopes: ['funding:read'],
|
|
2767
|
+
list: 'listFundingTransfers',
|
|
2768
|
+
listScopes: ['funding:read'],
|
|
2769
|
+
requiredCreateHeader: expect.objectContaining({
|
|
2770
|
+
description: expect.stringContaining('24 hours'),
|
|
2771
|
+
in: 'header',
|
|
2772
|
+
name: 'idempotency-key',
|
|
2773
|
+
required: true,
|
|
2774
|
+
schema: expect.any(Object),
|
|
2775
|
+
}),
|
|
2776
|
+
vault: 'createFundingTransferVault',
|
|
2777
|
+
vaultScopes: ['funding:write'],
|
|
2778
|
+
zone: 'createFundingTransferZone',
|
|
2779
|
+
zoneScopes: ['funding:write'],
|
|
2780
|
+
})
|
|
2781
|
+
})
|
|
2782
|
+
})
|
|
2783
|
+
|
|
2784
|
+
const baseUsdcQuery = {
|
|
2785
|
+
sourceAmount: '1000000',
|
|
2786
|
+
sourceChain: 'base',
|
|
2787
|
+
sourceToken: 'USDC',
|
|
2788
|
+
} as const
|
|
2789
|
+
|
|
2790
|
+
type QuoteQuery = Record<string, string>
|
|
2791
|
+
|
|
2792
|
+
function quoteUrl(query: QuoteQuery = baseUsdcQuery) {
|
|
2793
|
+
return `/v1/funding/quotes?${new URLSearchParams(
|
|
2794
|
+
Object.entries(query).map(([key, value]) => [key, String(value)]),
|
|
2795
|
+
)}`
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function relayProvider(onGetQuote: () => void = () => {}) {
|
|
2799
|
+
return withGetQuote('relay', async (input, signal) => {
|
|
2800
|
+
onGetQuote()
|
|
2801
|
+
expect(signal.aborted).toBe(false)
|
|
2802
|
+
expect(input.candidate.id).toBe('base-usdc-tempo-usdce-relay')
|
|
2803
|
+
return {
|
|
2804
|
+
destinationAmountMin: '980000',
|
|
2805
|
+
destinationAmount: '990000',
|
|
2806
|
+
quality: {
|
|
2807
|
+
estimatedSeconds: 4,
|
|
2808
|
+
liquiditySource: 'providerQuote',
|
|
2809
|
+
sourceDetail: 'relay:quote-v2',
|
|
2810
|
+
tier: 'liquid',
|
|
2811
|
+
},
|
|
2812
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
2813
|
+
status: 'available',
|
|
2814
|
+
}
|
|
2815
|
+
})
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function acrossProvider(onGetQuote: () => void = () => {}) {
|
|
2819
|
+
return withGetQuote('across', async (input, signal) => {
|
|
2820
|
+
onGetQuote()
|
|
2821
|
+
expect(signal.aborted).toBe(false)
|
|
2822
|
+
expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
|
|
2823
|
+
return {
|
|
2824
|
+
expiresAt: '2026-01-01T00:01:00Z',
|
|
2825
|
+
destinationAmountMin: '988000',
|
|
2826
|
+
destinationAmount: '990000',
|
|
2827
|
+
quality: {
|
|
2828
|
+
estimatedSeconds: 2,
|
|
2829
|
+
liquiditySource: 'providerQuote',
|
|
2830
|
+
sourceDetail: 'across:swap-approval',
|
|
2831
|
+
tier: 'liquid',
|
|
2832
|
+
},
|
|
2833
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
2834
|
+
status: 'available',
|
|
2835
|
+
}
|
|
2836
|
+
})
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
function rhinoProvider() {
|
|
2840
|
+
return withGetQuote('rhino', async (input, signal) => {
|
|
2841
|
+
expect(signal.aborted).toBe(false)
|
|
2842
|
+
expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
|
|
2843
|
+
return {
|
|
2844
|
+
destinationAmount: '983000',
|
|
2845
|
+
quality: {
|
|
2846
|
+
liquiditySource: 'providerQuote',
|
|
2847
|
+
sourceDetail: 'rhino:bridge-swap-public',
|
|
2848
|
+
tier: 'liquid',
|
|
2849
|
+
},
|
|
2850
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
2851
|
+
status: 'available',
|
|
2852
|
+
}
|
|
2853
|
+
})
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
function withGetQuote(
|
|
2857
|
+
id: FundingProvider.ProviderId,
|
|
2858
|
+
getQuote: FundingProvider.Provider['getQuote'],
|
|
2859
|
+
) {
|
|
2860
|
+
const provider = providers.find((entry) => entry.id === id)
|
|
2861
|
+
if (!provider) throw new Error(`Missing ${id} provider`)
|
|
2862
|
+
return FundingProvider.from({ ...provider, getQuote })
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
function withCreateDepositAddress(
|
|
2866
|
+
id: FundingProvider.ProviderId,
|
|
2867
|
+
createDepositAddress: NonNullable<FundingProvider.Provider['createDepositAddress']>,
|
|
2868
|
+
) {
|
|
2869
|
+
const provider = providers.find((entry) => entry.id === id)
|
|
2870
|
+
if (!provider) throw new Error(`Missing ${id} provider`)
|
|
2871
|
+
return FundingProvider.from({ ...provider, createDepositAddress })
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function depositAddressProvider(
|
|
2875
|
+
address = 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
|
|
2876
|
+
onCreate: (input: FundingProvider.createDepositAddress.Parameters) => void = () => {},
|
|
2877
|
+
) {
|
|
2878
|
+
return withCreateDepositAddress('relay', async (input) => {
|
|
2879
|
+
onCreate(input)
|
|
2880
|
+
return {
|
|
2881
|
+
address,
|
|
2882
|
+
correlation: {
|
|
2883
|
+
checkEndpoint: '/intents/status/v3?requestId=relay_request_1',
|
|
2884
|
+
requestId: 'relay_request_1',
|
|
2885
|
+
},
|
|
2886
|
+
destinationAmount: '1000000',
|
|
2887
|
+
destinationAmountMin: '1000000',
|
|
2888
|
+
fees: [],
|
|
2889
|
+
sampledAt: '2026-08-04T00:00:00.000Z',
|
|
2890
|
+
}
|
|
2891
|
+
})
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
/** API key with funding read access, owned by `org_funding`. */
|
|
2895
|
+
const fundingReader = {
|
|
2896
|
+
id: 'key_funding',
|
|
2897
|
+
orgId: 'org_funding',
|
|
2898
|
+
scopes: ['funding:read'],
|
|
2899
|
+
token: 'secret_funding_reader',
|
|
2900
|
+
} satisfies TestApp.kvStore.Key
|
|
2901
|
+
|
|
2902
|
+
/** Project-attributed key in the same org, used to prove project narrowing. */
|
|
2903
|
+
const fundingProjectReader = {
|
|
2904
|
+
id: 'key_funding_project',
|
|
2905
|
+
orgId: 'org_funding',
|
|
2906
|
+
projectId: 'prj_funding',
|
|
2907
|
+
scopes: ['funding:read'],
|
|
2908
|
+
token: 'secret_funding_project',
|
|
2909
|
+
} satisfies TestApp.kvStore.Key
|
|
2910
|
+
|
|
2911
|
+
/** Key in a different org, used to prove owner isolation. */
|
|
2912
|
+
const fundingForeign = {
|
|
2913
|
+
id: 'key_funding_other',
|
|
2914
|
+
orgId: 'org_funding_other',
|
|
2915
|
+
scopes: ['funding:read'],
|
|
2916
|
+
token: 'secret_funding_other',
|
|
2917
|
+
} satisfies TestApp.kvStore.Key
|
|
2918
|
+
|
|
2919
|
+
/** Key with funding write access, owned by `org_funding`. */
|
|
2920
|
+
const fundingWriter = {
|
|
2921
|
+
id: 'key_funding_writer',
|
|
2922
|
+
orgId: 'org_funding',
|
|
2923
|
+
scopes: ['funding:read', 'funding:write'],
|
|
2924
|
+
token: 'secret_funding_writer',
|
|
2925
|
+
} satisfies TestApp.kvStore.Key
|
|
2926
|
+
|
|
2927
|
+
function transfersApp(options: TestApp.create.Options = {}) {
|
|
2928
|
+
return create({
|
|
2929
|
+
...options,
|
|
2930
|
+
auth: {
|
|
2931
|
+
keys: [TestApp.key, fundingReader, fundingProjectReader, fundingForeign, fundingWriter],
|
|
2932
|
+
},
|
|
2933
|
+
defaultChainId: options.defaultChainId ?? tempoMainnet.id,
|
|
2934
|
+
})
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/** RequestInit carrying a bearer token for the given key. */
|
|
2938
|
+
function as(key: { token: string }) {
|
|
2939
|
+
return { headers: { authorization: `Bearer ${key.token}` } } as const
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
let seedDepositAddressSecond = 0
|
|
2943
|
+
function seedDepositAddress(input: Partial<FundingDepositAddress.create.Input> = {}) {
|
|
2944
|
+
const index = seedDepositAddressSecond++
|
|
2945
|
+
const now = new Date(Date.UTC(2026, 0, 3, 0, 0, index))
|
|
2946
|
+
return FundingDepositAddress.create(db, {
|
|
2947
|
+
apiKeyId: fundingReader.id,
|
|
2948
|
+
deliveryStrategy: 'provider',
|
|
2949
|
+
environment: 'production',
|
|
2950
|
+
id: FundingDepositAddress.generateId(now),
|
|
2951
|
+
now,
|
|
2952
|
+
orgId: fundingReader.orgId,
|
|
2953
|
+
providerOutputToken: TestFunding.providerOutputToken(),
|
|
2954
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
2955
|
+
provider: { id: `relay_route_${index}`, name: 'Relay' },
|
|
2956
|
+
recipient: `0x${index.toString(16).padStart(40, '0')}`,
|
|
2957
|
+
}),
|
|
2958
|
+
...input,
|
|
2959
|
+
})
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
let seedDepositSecond = 0
|
|
2963
|
+
function seedDeposit(
|
|
2964
|
+
address: Awaited<ReturnType<typeof seedDepositAddress>>,
|
|
2965
|
+
input: Partial<FundingDeposit.create.Input> = {},
|
|
2966
|
+
) {
|
|
2967
|
+
const index = seedDepositSecond++
|
|
2968
|
+
const now = new Date(Date.UTC(2026, 0, 4, 0, 0, index))
|
|
2969
|
+
const sourceTransactionHash = index.toString(16).padStart(64, '0')
|
|
2970
|
+
return FundingDeposit.create(db, {
|
|
2971
|
+
now,
|
|
2972
|
+
providerRequestId: `relay_request_${index}`,
|
|
2973
|
+
snapshot: TestFunding.depositSnapshot(address.id, {
|
|
2974
|
+
provider: address.snapshot.provider,
|
|
2975
|
+
sourceTransactionHashes: [sourceTransactionHash],
|
|
2976
|
+
}),
|
|
2977
|
+
sourceTransactionHash,
|
|
2978
|
+
sourceTransferIndex: 0,
|
|
2979
|
+
...input,
|
|
2980
|
+
})
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
let seedSecond = 0
|
|
2984
|
+
function seedTransfer(input: Partial<FundingTransfer.create.Input> = {}) {
|
|
2985
|
+
// Distinct create times keep ids strictly ordered for paging assertions.
|
|
2986
|
+
return FundingTransfer.create(db, {
|
|
2987
|
+
apiKeyId: fundingReader.id,
|
|
2988
|
+
environment: 'production',
|
|
2989
|
+
id: FundingTransfer.generateId(new Date(Date.UTC(2026, 0, 2, 0, 0, seedSecond++))),
|
|
2990
|
+
orgId: fundingReader.orgId,
|
|
2991
|
+
snapshot: TestFunding.transferSnapshot(),
|
|
2992
|
+
...input,
|
|
2993
|
+
})
|
|
503
2994
|
}
|