tapimo 0.4.4 → 0.5.1

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.
Files changed (229) hide show
  1. package/dist/App.d.ts +4701 -3416
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +24 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.js +3 -3
  6. package/dist/Assets.js.map +1 -1
  7. package/dist/Scope.d.ts +18 -1
  8. package/dist/Scope.d.ts.map +1 -1
  9. package/dist/Scope.js +23 -3
  10. package/dist/Scope.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +5 -10
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +6 -34
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +3 -3
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +4 -1
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/scopes.d.ts +6 -34
  22. package/dist/admin/apps/scopes.d.ts.map +1 -1
  23. package/dist/admin/apps/scopes.js +2 -2
  24. package/dist/admin/apps/scopes.js.map +1 -1
  25. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  26. package/dist/admin/ui.generated.d.ts +1 -1
  27. package/dist/admin/ui.generated.js +33 -33
  28. package/dist/admin/ui.generated.js.map +1 -1
  29. package/dist/apps/data/App.d.ts +4574 -4277
  30. package/dist/apps/data/App.d.ts.map +1 -1
  31. package/dist/apps/data/App.js +155 -77
  32. package/dist/apps/data/App.js.map +1 -1
  33. package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
  34. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  35. package/dist/apps/data/FundingQuoteProviders.js +464 -0
  36. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  37. package/dist/apps/data/index.d.ts +1 -1
  38. package/dist/apps/data/index.js +1 -1
  39. package/dist/apps/data/routes/activities.d.ts +26 -26
  40. package/dist/apps/data/routes/addresses.d.ts +38 -38
  41. package/dist/apps/data/routes/balances.d.ts +45 -45
  42. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  43. package/dist/apps/data/routes/balances.js +67 -149
  44. package/dist/apps/data/routes/balances.js.map +1 -1
  45. package/dist/apps/data/routes/blocks.d.ts +2 -3
  46. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/blocks.js +3 -5
  48. package/dist/apps/data/routes/blocks.js.map +1 -1
  49. package/dist/apps/data/routes/coingecko.d.ts +177 -176
  50. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  51. package/dist/apps/data/routes/coingecko.js +3 -8
  52. package/dist/apps/data/routes/coingecko.js.map +1 -1
  53. package/dist/apps/data/routes/funding.d.ts +131 -228
  54. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  55. package/dist/apps/data/routes/funding.js +152 -219
  56. package/dist/apps/data/routes/funding.js.map +1 -1
  57. package/dist/apps/data/routes/indexer.d.ts +34 -34
  58. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  59. package/dist/apps/data/routes/indexer.js +9 -2
  60. package/dist/apps/data/routes/indexer.js.map +1 -1
  61. package/dist/apps/data/routes/receipts.d.ts +228 -228
  62. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  63. package/dist/apps/data/routes/rpc.js +86 -16
  64. package/dist/apps/data/routes/rpc.js.map +1 -1
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  67. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  68. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  69. package/dist/apps/data/routes/webhooks.js +13 -1
  70. package/dist/apps/data/routes/webhooks.js.map +1 -1
  71. package/dist/apps/index.d.ts +1 -1
  72. package/dist/apps/index.js +1 -1
  73. package/dist/apps/management/App.d.ts +1833 -851
  74. package/dist/apps/management/App.d.ts.map +1 -1
  75. package/dist/apps/management/App.js +4 -0
  76. package/dist/apps/management/App.js.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +41 -41
  78. package/dist/apps/management/routes/api-keys.js +2 -2
  79. package/dist/apps/management/routes/api-keys.js.map +1 -1
  80. package/dist/apps/management/routes/billing.d.ts +48 -48
  81. package/dist/apps/management/routes/invitations.d.ts +132 -132
  82. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  83. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  84. package/dist/apps/management/routes/invite-links.js +411 -0
  85. package/dist/apps/management/routes/invite-links.js.map +1 -0
  86. package/dist/apps/management/routes/members.d.ts +41 -41
  87. package/dist/apps/management/routes/orgs.d.ts +92 -92
  88. package/dist/apps/management/routes/projects.d.ts +9 -9
  89. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  90. package/dist/apps/management/routes/scopes.js +2 -2
  91. package/dist/apps/management/routes/scopes.js.map +1 -1
  92. package/dist/apps/management/routes/usage.d.ts +24 -24
  93. package/dist/apps/mcp.js +2 -2
  94. package/dist/apps/mcp.js.map +1 -1
  95. package/dist/apps/relay/App.d.ts +2 -2
  96. package/dist/apps/relay/App.d.ts.map +1 -1
  97. package/dist/apps/relay/App.js +44 -6
  98. package/dist/apps/relay/App.js.map +1 -1
  99. package/dist/db/Db.d.ts +3 -0
  100. package/dist/db/Db.d.ts.map +1 -1
  101. package/dist/db/Db.js +57 -0
  102. package/dist/db/Db.js.map +1 -1
  103. package/dist/db/Schema.d.ts +31 -0
  104. package/dist/db/Schema.d.ts.map +1 -1
  105. package/dist/db/Schema.js +31 -0
  106. package/dist/db/Schema.js.map +1 -1
  107. package/dist/db/tables/inviteLinks.d.ts +42 -0
  108. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  109. package/dist/db/tables/inviteLinks.js +148 -0
  110. package/dist/db/tables/inviteLinks.js.map +1 -0
  111. package/dist/db/tables/organizations.d.ts.map +1 -1
  112. package/dist/db/tables/organizations.js +2 -0
  113. package/dist/db/tables/organizations.js.map +1 -1
  114. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  115. package/dist/db/tables/sponsoredTransactions.js +6 -2
  116. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  117. package/dist/handlers/relay.d.ts.map +1 -1
  118. package/dist/handlers/relay.js +13 -3
  119. package/dist/handlers/relay.js.map +1 -1
  120. package/dist/internal/Auth.d.ts +9 -1
  121. package/dist/internal/Auth.d.ts.map +1 -1
  122. package/dist/internal/Auth.js +138 -10
  123. package/dist/internal/Auth.js.map +1 -1
  124. package/dist/internal/EdgeCache.d.ts +3 -1
  125. package/dist/internal/EdgeCache.d.ts.map +1 -1
  126. package/dist/internal/EdgeCache.js +17 -2
  127. package/dist/internal/EdgeCache.js.map +1 -1
  128. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
  129. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  130. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
  131. package/dist/internal/FundingQuotes.js.map +1 -0
  132. package/dist/internal/Log.d.ts +18 -12
  133. package/dist/internal/Log.d.ts.map +1 -1
  134. package/dist/internal/Log.js +2 -0
  135. package/dist/internal/Log.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts +9 -3
  137. package/dist/internal/Tidx.d.ts.map +1 -1
  138. package/dist/internal/Tidx.js +13 -7
  139. package/dist/internal/Tidx.js.map +1 -1
  140. package/dist/internal/Viem.d.ts +17 -2
  141. package/dist/internal/Viem.d.ts.map +1 -1
  142. package/dist/internal/Viem.js +30 -3
  143. package/dist/internal/Viem.js.map +1 -1
  144. package/dist/internal/index.d.ts +1 -1
  145. package/dist/internal/index.js +1 -1
  146. package/package.json +2 -2
  147. package/src/ApiKeys.test.ts +25 -0
  148. package/src/App.test-d.ts +2 -0
  149. package/src/App.test.ts +13 -2
  150. package/src/App.ts +37 -5
  151. package/src/Assets.ts +3 -3
  152. package/src/Client.test-d.ts +21 -11
  153. package/src/Scope.test.ts +1 -0
  154. package/src/Scope.ts +32 -4
  155. package/src/Webhooks.ts +5 -10
  156. package/src/admin/apps/api-keys.test.ts +27 -0
  157. package/src/admin/apps/api-keys.ts +8 -1
  158. package/src/admin/apps/scopes.test.ts +7 -2
  159. package/src/admin/apps/scopes.ts +2 -2
  160. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  161. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  162. package/src/admin/ui/src/lib/scopes.ts +12 -0
  163. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  164. package/src/admin/ui.generated.ts +33 -33
  165. package/src/apps/composition.test.ts +77 -0
  166. package/src/apps/data/App.test.ts +354 -0
  167. package/src/apps/data/App.ts +178 -91
  168. package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
  169. package/src/apps/data/FundingQuoteProviders.ts +633 -0
  170. package/src/apps/data/index.ts +1 -1
  171. package/src/apps/data/routes/balances.test.ts +64 -18
  172. package/src/apps/data/routes/balances.ts +76 -177
  173. package/src/apps/data/routes/blocks.ts +3 -5
  174. package/src/apps/data/routes/coingecko.ts +3 -8
  175. package/src/apps/data/routes/funding.test.ts +304 -192
  176. package/src/apps/data/routes/funding.ts +166 -286
  177. package/src/apps/data/routes/indexer.test.ts +173 -15
  178. package/src/apps/data/routes/indexer.ts +8 -2
  179. package/src/apps/data/routes/rpc.test.ts +219 -0
  180. package/src/apps/data/routes/rpc.ts +92 -17
  181. package/src/apps/data/routes/webhooks.test.ts +21 -0
  182. package/src/apps/data/routes/webhooks.ts +13 -1
  183. package/src/apps/index.ts +1 -1
  184. package/src/apps/management/App.ts +4 -0
  185. package/src/apps/management/Billing.test.ts +2 -2
  186. package/src/apps/management/routes/api-keys.test.ts +47 -3
  187. package/src/apps/management/routes/api-keys.ts +2 -2
  188. package/src/apps/management/routes/invite-links.test.ts +385 -0
  189. package/src/apps/management/routes/invite-links.ts +570 -0
  190. package/src/apps/management/routes/scopes.test.ts +3 -2
  191. package/src/apps/management/routes/scopes.ts +4 -2
  192. package/src/apps/mcp.test.ts +18 -0
  193. package/src/apps/mcp.ts +2 -2
  194. package/src/apps/relay/App.test.ts +328 -7
  195. package/src/apps/relay/App.ts +42 -6
  196. package/src/apps/relay/Sponsorships.test.ts +2 -2
  197. package/src/db/Db.test.ts +5 -5
  198. package/src/db/Db.ts +65 -0
  199. package/src/db/Schema.ts +39 -0
  200. package/src/db/tables/billingSettings.test.ts +2 -2
  201. package/src/db/tables/inviteLinks.ts +187 -0
  202. package/src/db/tables/organizations.test.ts +2 -2
  203. package/src/db/tables/organizations.ts +2 -0
  204. package/src/db/tables/sponsoredTransactions.test.ts +19 -2
  205. package/src/db/tables/sponsoredTransactions.ts +21 -14
  206. package/src/db/tables/stripeCustomers.test.ts +2 -2
  207. package/src/handlers/relay.ts +16 -4
  208. package/src/internal/Auth.test.ts +132 -22
  209. package/src/internal/Auth.ts +181 -14
  210. package/src/internal/EdgeCache.test.ts +24 -3
  211. package/src/internal/EdgeCache.ts +18 -2
  212. package/src/internal/FundingQuotes.test.ts +268 -0
  213. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
  214. package/src/internal/Log.ts +21 -14
  215. package/src/internal/Tidx.test.ts +48 -0
  216. package/src/internal/Tidx.ts +21 -7
  217. package/src/internal/Viem.test.ts +46 -0
  218. package/src/internal/Viem.ts +48 -6
  219. package/src/internal/index.test.ts +1 -1
  220. package/src/internal/index.ts +1 -1
  221. package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
  222. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  223. package/dist/apps/data/FundingRouteProviders.js +0 -245
  224. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  225. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  226. package/dist/internal/FundingRoutes.js.map +0 -1
  227. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  228. package/src/apps/data/FundingRouteProviders.ts +0 -312
  229. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -1,235 +1,256 @@
1
1
  import { Schema } from 'tapimo'
2
2
 
3
+ import type * as FundingQuotes from '../../../internal/FundingQuotes.js'
4
+ import type * as Log from '../../../internal/Log.js'
3
5
  import * as TestApp from '../../../../test/App.js'
4
6
  import * as Funding from './funding.js'
5
7
 
6
- describe('GET /funding/routes', () => {
7
- test('returns static inventory-backed route candidates', async () => {
8
- const response = await TestApp.client().v1.funding.routes.$get(
9
- {
10
- query: {
11
- destinationToken: 'USDC.e',
12
- sourceAmount: '1000000',
13
- sourceChain: 'base',
14
- sourceToken: 'USDC',
15
- },
16
- },
17
- TestApp.auth,
18
- )
8
+ describe('GET /funding/quotes', () => {
9
+ test('returns successful live quotes with output amounts', async () => {
10
+ const app = TestApp.create({ funding: { adapters: { relay: relayAdapter() } } })
11
+ const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
19
12
  expect(response.status).toBe(200)
20
- const body = await TestApp.json(response, Funding.schema.listFundingRoutes.Response)
13
+ expect(response.headers.get('cache-control')).toBe('no-store')
14
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
21
15
 
22
- expect(body.data.map((route) => route.provider.id)).toEqual(['relay', 'bridge', 'bungee'])
16
+ expect(body.data).toHaveLength(1)
23
17
  expect(body.data[0]).toMatchObject({
24
- destinationChain: { id: 'eip155:4217', name: 'Tempo' },
25
- destinationToken: { symbol: 'USDC.e' },
26
18
  id: 'base-usdc-tempo-usdce-relay',
27
- quality: { liquiditySource: 'staticInventory', tier: 'unknown' },
19
+ provider: { id: 'relay' },
20
+ quality: { liquiditySource: 'providerQuote', tier: 'liquid' },
21
+ quote: {
22
+ inputAmount: '1000000',
23
+ outputAmount: '990000',
24
+ outputAmountFormatted: '0.99',
25
+ },
28
26
  sourceAmount: { amount: '1000000', formatted: '1' },
29
- sourceChain: { id: 'eip155:8453', name: 'Base' },
30
- sourceToken: { symbol: 'USDC' },
31
- status: 'unknown',
32
27
  })
33
28
  })
34
29
 
35
- test('enriches route candidates with configured provider adapters', async () => {
30
+ test('queries only the requested provider', async () => {
31
+ let acrossCalls = 0
32
+ let relayCalls = 0
36
33
  const app = TestApp.create({
37
- fundingRoutes: {
34
+ funding: {
38
35
  adapters: {
39
- relay: {
40
- async probeQuote(input, signal) {
41
- expect(signal.aborted).toBe(false)
42
- expect(input.cache.bypass).toBe(true)
43
- expect(input.include).toEqual(['quotes'])
44
- expect(input.route.id).toBe('base-usdc-tempo-usdce-relay')
45
- expect(input.sourceAmount.amount).toBe('1000000')
46
- expect(input.sourceChain.id).toBe('eip155:8453')
47
- expect(input.sourceToken.symbol).toBe('USDC')
48
- return {
49
- expiresAt: '2026-01-01T00:00:30Z',
50
- limits: {
51
- instantSourceAmount: '1000000000',
52
- maxSourceAmount: '100000000000',
53
- minSourceAmount: '500000',
54
- },
55
- minimumOutputAmount: '980000',
56
- outputAmount: '990000',
57
- outputAmountFormatted: '0.99',
58
- quality: {
59
- availableVolumeUsd: '1000.00',
60
- estimatedSeconds: 4,
61
- feeBps: 12,
62
- historicalVolumeUsd24h: '100000.00',
63
- instantVolumeUsd: '1000.00',
64
- liquiditySource: 'providerQuote',
65
- score: 91,
66
- sourceDetail: 'providerEndpoint',
67
- tier: 'liquid',
68
- },
69
- sampledAt: '2026-01-01T00:00:00Z',
70
- status: 'available',
71
- }
72
- },
73
- },
36
+ across: acrossAdapter(() => {
37
+ acrossCalls += 1
38
+ }),
39
+ relay: relayAdapter(() => {
40
+ relayCalls += 1
41
+ }),
74
42
  },
75
43
  },
76
44
  })
45
+ const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
46
+ headers: TestApp.auth.headers,
47
+ })
77
48
 
49
+ expect(response.status).toBe(200)
50
+ expect(acrossCalls).toBe(1)
51
+ expect(relayCalls).toBe(0)
52
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
53
+ expect(body.data[0]?.quote.expiresAt).toBe('2026-01-01T00:01:00Z')
54
+ })
55
+
56
+ test('returns non-EVM Rhino quotes with stable chain and token ids', async () => {
57
+ const app = TestApp.create({ funding: { adapters: { rhino: rhinoAdapter() } } })
78
58
  const response = await app.request(
79
- '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&provider=relay&include=quotes',
80
- { headers: { ...TestApp.auth.headers, 'Cache-Control': 'no-cache' } },
59
+ quoteUrl({
60
+ destinationToken: 'USDC.e',
61
+ provider: 'rhino',
62
+ sourceAmount: '1000000',
63
+ sourceChain: 'solana',
64
+ sourceToken: 'USDC',
65
+ }),
66
+ { headers: TestApp.auth.headers },
81
67
  )
82
- expect(response.status).toBe(200)
83
- const body = await TestApp.json(response, Funding.schema.listFundingRoutes.Response)
84
68
 
85
- expect(body.data).toHaveLength(1)
69
+ expect(response.status).toBe(200)
70
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
86
71
  expect(body.data[0]).toMatchObject({
87
- errorCode: null,
88
- id: 'base-usdc-tempo-usdce-relay',
89
- quality: {
90
- availableVolumeUsd: '1000.00',
91
- liquiditySource: 'providerQuote',
92
- sourceDetail: 'providerEndpoint',
93
- tier: 'liquid',
72
+ id: 'solana-usdc-tempo-usdce-rhino',
73
+ provider: { id: 'rhino' },
74
+ sourceChain: {
75
+ addressFormat: 'base58',
76
+ id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
77
+ kind: 'solana',
94
78
  },
95
- quote: {
96
- expiresAt: '2026-01-01T00:00:30Z',
97
- inputAmount: '1000000',
98
- limits: {
99
- instantSourceAmount: '1000000000',
100
- maxSourceAmount: '100000000000',
101
- minSourceAmount: '500000',
102
- },
103
- minimumOutputAmount: '980000',
104
- outputAmount: '990000',
105
- outputAmountFormatted: '0.99',
106
- sampledAt: '2026-01-01T00:00:00Z',
79
+ sourceToken: {
80
+ tokenKey:
81
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
107
82
  },
108
- status: 'available',
109
83
  })
110
84
  })
111
85
 
112
- test('converts decimal source amounts after resolving the source token', async () => {
113
- const response = await TestApp.client().v1.funding.routes.$get(
114
- {
115
- query: {
116
- provider: 'relay',
117
- sourceAmount: '1.25',
118
- sourceAmountUnits: 'decimal',
119
- sourceChain: 'eip155:8453',
120
- sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
121
- },
122
- },
123
- TestApp.auth,
124
- )
86
+ test('returns an empty list when no provider is configured', async () => {
87
+ const response = await TestApp.create().request(quoteUrl(), {
88
+ headers: TestApp.auth.headers,
89
+ })
125
90
  expect(response.status).toBe(200)
126
- const body = await TestApp.json(response, Funding.schema.listFundingRoutes.Response)
91
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
92
+ expect(body.data).toEqual([])
93
+ })
127
94
 
128
- expect(body.data).toHaveLength(1)
129
- expect(body.data[0]?.sourceAmount).toMatchObject({
130
- amount: '1250000',
131
- formatted: '1.25',
95
+ test('returns an empty list when the provider reports no quote', async () => {
96
+ const app = TestApp.create({
97
+ funding: {
98
+ adapters: {
99
+ relay: {
100
+ async probeQuote() {
101
+ return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
102
+ },
103
+ },
104
+ },
105
+ },
132
106
  })
107
+ const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
108
+ expect(response.status).toBe(200)
109
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
110
+ expect(body.data).toEqual([])
133
111
  })
134
112
 
135
- test('does not case-fold non-EVM source token identifiers', async () => {
136
- const solanaMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
137
- const exactResponse = await TestApp.client().v1.funding.routes.$get(
138
- {
139
- query: {
140
- sourceAmount: '1000000',
141
- sourceChain: 'solana',
142
- sourceToken: solanaMint,
113
+ test('returns 502 when every matching provider fails', async () => {
114
+ const app = TestApp.create({
115
+ funding: {
116
+ adapters: {
117
+ relay: {
118
+ async probeQuote() {
119
+ throw new Error('upstream unavailable')
120
+ },
121
+ },
143
122
  },
144
123
  },
145
- TestApp.auth,
146
- )
147
- expect(exactResponse.status).toBe(200)
148
- const exactBody = await TestApp.json(exactResponse, Funding.schema.listFundingRoutes.Response)
149
- expect(exactBody.data.map((route) => route.provider.id)).toEqual(['bridge'])
124
+ })
125
+ const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
126
+ expect(response.status).toBe(502)
127
+ const body = await TestApp.json(response, Schema.ErrorResponse)
128
+ expect(body.error.code).toBe('upstream_error')
129
+ })
150
130
 
151
- const lowercasedSolanaResponse = await TestApp.client().v1.funding.routes.$get(
152
- {
153
- query: {
154
- sourceAmount: '1000000',
155
- sourceChain: 'solana',
156
- sourceToken: solanaMint.toLowerCase(),
131
+ test('logs provider quote deadlines as timeouts', async () => {
132
+ const entries: Log.Entry[] = []
133
+ const app = TestApp.create({
134
+ funding: {
135
+ adapterTimeoutMs: 1,
136
+ adapters: {
137
+ across: {
138
+ probeQuote() {
139
+ return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
140
+ },
141
+ },
157
142
  },
158
143
  },
159
- TestApp.auth,
160
- )
161
- expect(lowercasedSolanaResponse.status).toBe(200)
162
- const lowercasedSolanaBody = await TestApp.json(
163
- lowercasedSolanaResponse,
164
- Funding.schema.listFundingRoutes.Response,
165
- )
166
- expect(lowercasedSolanaBody.data).toEqual([])
144
+ logger: (entry) => {
145
+ entries.push(entry)
146
+ },
147
+ })
167
148
 
168
- const lowercasedTronResponse = await TestApp.client().v1.funding.routes.$get(
169
- {
170
- query: {
171
- sourceAmount: '1000000',
172
- sourceChain: 'tron',
173
- sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'.toLowerCase(),
149
+ const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
150
+ headers: TestApp.auth.headers,
151
+ })
152
+
153
+ expect(response.status).toBe(502)
154
+ expect(entries[0]?.provider).toEqual({
155
+ failure: 'timeout',
156
+ id: 'across',
157
+ operation: 'swap/approval',
158
+ })
159
+ })
160
+
161
+ test('logs every failure when multiple providers fail', async () => {
162
+ const entries: Log.Entry[] = []
163
+ const app = TestApp.create({
164
+ funding: {
165
+ adapters: {
166
+ across: {
167
+ async probeQuote() {
168
+ throw new TypeError('Across network failure')
169
+ },
170
+ },
171
+ relay: {
172
+ async probeQuote() {
173
+ throw new Error('Relay failure')
174
+ },
175
+ },
174
176
  },
175
177
  },
176
- TestApp.auth,
177
- )
178
- expect(lowercasedTronResponse.status).toBe(200)
179
- const lowercasedTronBody = await TestApp.json(
180
- lowercasedTronResponse,
181
- Funding.schema.listFundingRoutes.Response,
182
- )
183
- expect(lowercasedTronBody.data).toEqual([])
178
+ logger: (entry) => {
179
+ entries.push(entry)
180
+ },
181
+ })
182
+
183
+ const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
184
+
185
+ expect(response.status).toBe(502)
186
+ expect(entries[0]?.provider).toEqual({
187
+ failure: 'network',
188
+ id: 'across',
189
+ operation: 'swap/approval',
190
+ })
191
+ expect(entries[0]?.providerFailures).toEqual([
192
+ { failure: 'network', id: 'across', operation: 'swap/approval' },
193
+ { failure: 'unknown', id: 'relay', operation: 'quote/v2' },
194
+ ])
184
195
  })
185
196
 
186
- test('returns no candidates for an unsupported source token', async () => {
187
- const response = await TestApp.client().v1.funding.routes.$get(
188
- {
189
- query: {
190
- sourceAmount: '1.234567890',
191
- sourceAmountUnits: 'decimal',
192
- sourceChain: 'base',
193
- sourceToken: 'DAI',
197
+ test('rejects successful provider results without output amounts', async () => {
198
+ const app = TestApp.create({
199
+ funding: {
200
+ adapters: {
201
+ relay: {
202
+ async probeQuote() {
203
+ return { sampledAt: '2026-01-01T00:00:00Z', status: 'available' }
204
+ },
205
+ },
194
206
  },
195
207
  },
196
- TestApp.auth,
208
+ })
209
+ const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
210
+ expect(response.status).toBe(502)
211
+ })
212
+
213
+ test('rejects invalid query parameters', async () => {
214
+ const response = await TestApp.create().request(
215
+ quoteUrl({ sourceAmount: '1000000', sourceChain: 'base' }),
216
+ { headers: TestApp.auth.headers },
197
217
  )
198
- expect(response.status).toBe(200)
199
- const body = await TestApp.json(response, Funding.schema.listFundingRoutes.Response)
200
- expect(body.data).toEqual([])
218
+ expect(response.status).toBe(400)
219
+ const body = await TestApp.json(response, Schema.ErrorResponse)
220
+ expect(body.error.code).toBe('query_invalid')
201
221
  })
202
222
 
203
- test('rejects amounts that cannot be represented in source decimals', async () => {
204
- const response = await TestApp.client().v1.funding.routes.$get(
205
- {
206
- query: {
207
- sourceAmount: '1.0000001',
208
- sourceAmountUnits: 'decimal',
209
- sourceChain: 'base',
210
- sourceToken: 'USDC',
211
- },
212
- },
213
- TestApp.auth,
223
+ test('rejects unsupported providers', async () => {
224
+ const response = await TestApp.create().request(
225
+ quoteUrl({ ...baseUsdcQuery, provider: 'bridge' }),
226
+ { headers: TestApp.auth.headers },
214
227
  )
215
228
  expect(response.status).toBe(400)
216
229
  const body = await TestApp.json(response, Schema.ErrorResponse)
217
230
  expect(body.error.code).toBe('query_invalid')
218
231
  })
219
232
 
220
- test('rejects unsupported include parameters', async () => {
233
+ test('rejects unsupported source chains', async () => {
221
234
  const response = await TestApp.create().request(
222
- '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&include=debug',
235
+ quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }),
223
236
  { headers: TestApp.auth.headers },
224
237
  )
225
238
  expect(response.status).toBe(400)
226
239
  const body = await TestApp.json(response, Schema.ErrorResponse)
227
240
  expect(body.error.code).toBe('query_invalid')
228
241
  })
242
+
243
+ test('does not expose the removed routes endpoint', async () => {
244
+ const response = await TestApp.create().request(
245
+ '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000',
246
+ { headers: TestApp.auth.headers },
247
+ )
248
+ expect(response.status).toBe(404)
249
+ })
229
250
  })
230
251
 
231
252
  describe('GET /funding/chains', () => {
232
- test('lists source chain and stablecoin inventory', async () => {
253
+ test('lists only inputs supported by quote providers', async () => {
233
254
  const response = await TestApp.client().v1.funding.chains.$get(undefined, TestApp.auth)
234
255
  expect(response.status).toBe(200)
235
256
  const body = await TestApp.json(response, Funding.schema.listFundingChains.Response)
@@ -238,42 +259,133 @@ describe('GET /funding/chains', () => {
238
259
  'eip155:1',
239
260
  'eip155:8453',
240
261
  'eip155:56',
241
- 'solana:mainnet',
242
- 'tron:mainnet',
262
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
263
+ 'tron:0x2b6653dc',
243
264
  ])
244
- expect(body.data.find((chain) => chain.id === 'solana:mainnet')?.tokens).toEqual(
245
- expect.arrayContaining([expect.objectContaining({ symbol: 'USDC', standard: 'SPL' })]),
265
+ expect(body.data.flatMap((chain) => chain.tokens.map((token) => token.symbol))).toEqual(
266
+ expect.arrayContaining(['USDC', 'USDT']),
246
267
  )
247
268
  })
248
269
  })
249
270
 
250
- describe('GET /providers', () => {
251
- test('lists providers filtered by capability', async () => {
252
- const response = await TestApp.client().v1.providers.$get(
253
- { query: { capability: 'fundingRoutes' } },
254
- TestApp.auth,
255
- )
271
+ describe('GET /funding/providers', () => {
272
+ test('lists quote providers and their configuration status', async () => {
273
+ const app = TestApp.create({ funding: { adapters: { relay: relayAdapter() } } })
274
+ const response = await app.request('/v1/funding/providers', {
275
+ headers: TestApp.auth.headers,
276
+ })
256
277
  expect(response.status).toBe(200)
257
278
  const body = await TestApp.json(response, Funding.schema.listProviders.Response)
258
279
 
259
- expect(body.data.map((provider) => provider.id)).toEqual(['relay', 'bridge', 'bungee', 'lifi'])
260
- expect(body.data.every((provider) => provider.capabilities.includes('fundingRoutes'))).toBe(
261
- true,
262
- )
263
- expect(body.data.map((provider) => provider.status)).toEqual([
264
- 'inventoryOnly',
265
- 'inventoryOnly',
266
- 'inventoryOnly',
267
- 'planned',
280
+ expect(body.data).toEqual([
281
+ {
282
+ id: 'across',
283
+ kind: 'intentBridge',
284
+ name: 'Across',
285
+ requiresApiKey: true,
286
+ status: 'unconfigured',
287
+ },
288
+ {
289
+ id: 'relay',
290
+ kind: 'solverNetwork',
291
+ name: 'Relay',
292
+ requiresApiKey: false,
293
+ status: 'configured',
294
+ },
295
+ {
296
+ id: 'rhino',
297
+ kind: 'bridge',
298
+ name: 'Rhino',
299
+ requiresApiKey: false,
300
+ status: 'unconfigured',
301
+ },
268
302
  ])
269
303
  })
270
304
 
271
- test('rejects unsupported provider capabilities', async () => {
272
- const response = await TestApp.create().request('/v1/providers?capability=settlement', {
305
+ test('does not expose the removed generic provider endpoint', async () => {
306
+ const response = await TestApp.create().request('/v1/providers', {
273
307
  headers: TestApp.auth.headers,
274
308
  })
275
- expect(response.status).toBe(400)
276
- const body = await TestApp.json(response, Schema.ErrorResponse)
277
- expect(body.error.code).toBe('query_invalid')
309
+ expect(response.status).toBe(404)
278
310
  })
279
311
  })
312
+
313
+ const baseUsdcQuery = {
314
+ sourceAmount: '1000000',
315
+ sourceChain: 'base',
316
+ sourceToken: 'USDC',
317
+ } as const
318
+
319
+ type QuoteQuery = Record<string, string>
320
+
321
+ function quoteUrl(query: QuoteQuery = baseUsdcQuery) {
322
+ return `/v1/funding/quotes?${new URLSearchParams(
323
+ Object.entries(query).map(([key, value]) => [key, String(value)]),
324
+ )}`
325
+ }
326
+
327
+ function relayAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
328
+ return {
329
+ async probeQuote(input, signal) {
330
+ onProbe()
331
+ expect(signal.aborted).toBe(false)
332
+ expect(input.candidate.id).toBe('base-usdc-tempo-usdce-relay')
333
+ return {
334
+ minimumOutputAmount: '980000',
335
+ outputAmount: '990000',
336
+ outputAmountFormatted: '0.99',
337
+ quality: {
338
+ estimatedSeconds: 4,
339
+ liquiditySource: 'providerQuote',
340
+ sourceDetail: 'relay:quote-v2',
341
+ tier: 'liquid',
342
+ },
343
+ sampledAt: '2026-01-01T00:00:00Z',
344
+ status: 'available',
345
+ }
346
+ },
347
+ }
348
+ }
349
+
350
+ function acrossAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
351
+ return {
352
+ async probeQuote(input, signal) {
353
+ onProbe()
354
+ expect(signal.aborted).toBe(false)
355
+ expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
356
+ return {
357
+ expiresAt: '2026-01-01T00:01:00Z',
358
+ minimumOutputAmount: '988000',
359
+ outputAmount: '990000',
360
+ outputAmountFormatted: '0.99',
361
+ quality: {
362
+ estimatedSeconds: 2,
363
+ liquiditySource: 'providerQuote',
364
+ sourceDetail: 'across:swap-approval',
365
+ tier: 'liquid',
366
+ },
367
+ sampledAt: '2026-01-01T00:00:00Z',
368
+ status: 'available',
369
+ }
370
+ },
371
+ }
372
+ }
373
+
374
+ function rhinoAdapter(): FundingQuotes.ProviderAdapter {
375
+ return {
376
+ async probeQuote(input, signal) {
377
+ expect(signal.aborted).toBe(false)
378
+ expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
379
+ return {
380
+ outputAmount: '983000',
381
+ quality: {
382
+ liquiditySource: 'providerQuote',
383
+ sourceDetail: 'rhino:bridge-swap-public',
384
+ tier: 'liquid',
385
+ },
386
+ sampledAt: '2026-01-01T00:00:00Z',
387
+ status: 'available',
388
+ }
389
+ },
390
+ }
391
+ }