tapimo 0.6.0 → 0.7.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 (288) hide show
  1. package/dist/App.d.ts +8641 -8263
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +9 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +4 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +5 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +112 -11
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +2 -1
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/early-access.d.ts +2 -2
  14. package/dist/admin/apps/early-access.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.js +5 -5
  16. package/dist/admin/apps/early-access.js.map +1 -1
  17. package/dist/admin/apps/organizations.d.ts +124 -1
  18. package/dist/admin/apps/organizations.d.ts.map +1 -1
  19. package/dist/admin/apps/organizations.js +97 -6
  20. package/dist/admin/apps/organizations.js.map +1 -1
  21. package/dist/admin/ui.generated.d.ts +1 -1
  22. package/dist/admin/ui.generated.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.js +47 -39
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +20 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  29. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  30. package/dist/analytics/tables/requestEvents.js +36 -7
  31. package/dist/analytics/tables/requestEvents.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +8050 -8019
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +2 -15
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +1 -1
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +1 -1
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +27 -27
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +21 -1
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +58 -58
  45. package/dist/apps/data/routes/blocks.d.ts +39 -39
  46. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  47. package/dist/apps/data/routes/exchanges.d.ts +669 -669
  48. package/dist/apps/data/routes/fee-amm.d.ts +157 -143
  49. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  50. package/dist/apps/data/routes/fee-amm.js +76 -91
  51. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  52. package/dist/apps/data/routes/funding.d.ts +72 -34
  53. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  54. package/dist/apps/data/routes/funding.js +34 -26
  55. package/dist/apps/data/routes/funding.js.map +1 -1
  56. package/dist/apps/data/routes/indexer.d.ts +30 -30
  57. package/dist/apps/data/routes/receipts.d.ts +8 -8
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  60. package/dist/apps/data/routes/tokenlist.js +12 -3
  61. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  62. package/dist/apps/data/routes/tokens.d.ts +4 -4
  63. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  64. package/dist/apps/data/routes/tokens.js +5 -1
  65. package/dist/apps/data/routes/tokens.js.map +1 -1
  66. package/dist/apps/data/routes/transactions.d.ts +6 -6
  67. package/dist/apps/data/routes/transfers.d.ts +101 -90
  68. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transfers.js +226 -141
  70. package/dist/apps/data/routes/transfers.js.map +1 -1
  71. package/dist/apps/data/routes/valuation.d.ts +2 -2
  72. package/dist/apps/data/routes/valuation.js +3 -3
  73. package/dist/apps/data/routes/valuation.js.map +1 -1
  74. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  75. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  76. package/dist/apps/data/routes/verified-tokens.js +34 -9
  77. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  78. package/dist/apps/data/routes/webhooks.d.ts +2 -2
  79. package/dist/apps/data/routes/webhooks.js +14 -6
  80. package/dist/apps/data/routes/webhooks.js.map +1 -1
  81. package/dist/apps/index.d.ts +1 -0
  82. package/dist/apps/index.d.ts.map +1 -1
  83. package/dist/apps/index.js +1 -0
  84. package/dist/apps/index.js.map +1 -1
  85. package/dist/apps/management/App.d.ts +460 -523
  86. package/dist/apps/management/App.d.ts.map +1 -1
  87. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  88. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  89. package/dist/apps/management/routes/api-keys.js +20 -8
  90. package/dist/apps/management/routes/api-keys.js.map +1 -1
  91. package/dist/apps/management/routes/billing.d.ts +106 -151
  92. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  93. package/dist/apps/management/routes/billing.js +28 -28
  94. package/dist/apps/management/routes/billing.js.map +1 -1
  95. package/dist/apps/management/routes/faucet.d.ts +48 -34
  96. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  97. package/dist/apps/management/routes/faucet.js +33 -18
  98. package/dist/apps/management/routes/faucet.js.map +1 -1
  99. package/dist/apps/management/routes/invitations.d.ts +131 -131
  100. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  101. package/dist/apps/management/routes/invitations.js +0 -3
  102. package/dist/apps/management/routes/invitations.js.map +1 -1
  103. package/dist/apps/management/routes/me.d.ts +6 -8
  104. package/dist/apps/management/routes/me.d.ts.map +1 -1
  105. package/dist/apps/management/routes/me.js +0 -4
  106. package/dist/apps/management/routes/me.js.map +1 -1
  107. package/dist/apps/management/routes/members.d.ts +73 -73
  108. package/dist/apps/management/routes/orgs.d.ts +94 -94
  109. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  110. package/dist/apps/management/routes/orgs.js +5 -0
  111. package/dist/apps/management/routes/orgs.js.map +1 -1
  112. package/dist/apps/management/routes/usage.d.ts +72 -72
  113. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  114. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  115. package/dist/apps/mpp/App.d.ts +363 -0
  116. package/dist/apps/mpp/App.d.ts.map +1 -0
  117. package/dist/apps/mpp/App.js +72 -0
  118. package/dist/apps/mpp/App.js.map +1 -0
  119. package/dist/apps/mpp/Mpp.d.ts +178 -0
  120. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  121. package/dist/apps/mpp/Mpp.js +183 -0
  122. package/dist/apps/mpp/Mpp.js.map +1 -0
  123. package/dist/apps/mpp/index.d.ts +3 -0
  124. package/dist/apps/mpp/index.d.ts.map +1 -0
  125. package/dist/apps/mpp/index.js +3 -0
  126. package/dist/apps/mpp/index.js.map +1 -0
  127. package/dist/apps/relay/App.d.ts.map +1 -1
  128. package/dist/apps/relay/App.js +57 -10
  129. package/dist/apps/relay/App.js.map +1 -1
  130. package/dist/apps/relay/Sponsorships.js +1 -1
  131. package/dist/apps/relay/Sponsorships.js.map +1 -1
  132. package/dist/db/Db.d.ts +2 -0
  133. package/dist/db/Db.d.ts.map +1 -1
  134. package/dist/db/Db.js +27 -0
  135. package/dist/db/Db.js.map +1 -1
  136. package/dist/db/Schema.d.ts +12 -0
  137. package/dist/db/Schema.d.ts.map +1 -1
  138. package/dist/db/Schema.js +11 -0
  139. package/dist/db/Schema.js.map +1 -1
  140. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  141. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  142. package/dist/db/tables/enabledBillingSources.js +69 -0
  143. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  144. package/dist/db/tables/invitations.d.ts +1 -12
  145. package/dist/db/tables/invitations.d.ts.map +1 -1
  146. package/dist/db/tables/invitations.js +3 -23
  147. package/dist/db/tables/invitations.js.map +1 -1
  148. package/dist/db/tables/organizations.d.ts +5 -2
  149. package/dist/db/tables/organizations.d.ts.map +1 -1
  150. package/dist/db/tables/organizations.js +32 -2
  151. package/dist/db/tables/organizations.js.map +1 -1
  152. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  153. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  154. package/dist/db/tables/sponsoredTransactions.js +13 -3
  155. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  156. package/dist/handlers/internal/utils.d.ts +1 -16
  157. package/dist/handlers/internal/utils.d.ts.map +1 -1
  158. package/dist/handlers/internal/utils.js +17 -6
  159. package/dist/handlers/internal/utils.js.map +1 -1
  160. package/dist/handlers/relay.d.ts.map +1 -1
  161. package/dist/handlers/relay.js +37 -5
  162. package/dist/handlers/relay.js.map +1 -1
  163. package/dist/internal/Auth.d.ts +0 -32
  164. package/dist/internal/Auth.d.ts.map +1 -1
  165. package/dist/internal/Auth.js +3 -51
  166. package/dist/internal/Auth.js.map +1 -1
  167. package/dist/internal/EdgeCache.d.ts +1 -1
  168. package/dist/internal/EdgeCache.d.ts.map +1 -1
  169. package/dist/internal/EdgeCache.js +3 -2
  170. package/dist/internal/EdgeCache.js.map +1 -1
  171. package/dist/internal/FundingQuotes.d.ts +78 -32
  172. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  173. package/dist/internal/FundingQuotes.js +45 -8
  174. package/dist/internal/FundingQuotes.js.map +1 -1
  175. package/dist/internal/Log.d.ts +18 -4
  176. package/dist/internal/Log.d.ts.map +1 -1
  177. package/dist/internal/Log.js +50 -3
  178. package/dist/internal/Log.js.map +1 -1
  179. package/dist/internal/MetricSink.d.ts.map +1 -1
  180. package/dist/internal/MetricSink.js +15 -0
  181. package/dist/internal/MetricSink.js.map +1 -1
  182. package/dist/internal/Response.d.ts +2 -2
  183. package/dist/internal/Response.d.ts.map +1 -1
  184. package/dist/internal/Response.js +12 -4
  185. package/dist/internal/Response.js.map +1 -1
  186. package/dist/internal/Schema.d.ts +2 -2
  187. package/dist/internal/Schema.d.ts.map +1 -1
  188. package/dist/internal/Schema.js +4 -2
  189. package/dist/internal/Schema.js.map +1 -1
  190. package/dist/internal/Viem.d.ts +1 -1
  191. package/package.json +7 -2
  192. package/src/App.test.ts +9 -4
  193. package/src/App.ts +14 -2
  194. package/src/Client.test-d.ts +109 -28
  195. package/src/Scope.ts +5 -0
  196. package/src/admin/App.test.ts +4 -0
  197. package/src/admin/apps/early-access.ts +6 -5
  198. package/src/admin/apps/organizations.test.ts +86 -2
  199. package/src/admin/apps/organizations.ts +155 -31
  200. package/src/admin/apps/scopes.test.ts +5 -0
  201. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  202. package/src/admin/ui/src/lib/organizations.ts +92 -0
  203. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  204. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  206. package/src/admin/ui.generated.ts +49 -39
  207. package/src/analytics/Analytics.test.ts +83 -0
  208. package/src/analytics/Analytics.ts +20 -0
  209. package/src/analytics/tables/requestEvents.ts +60 -7
  210. package/src/apps/composition.test.ts +11 -2
  211. package/src/apps/data/App.test.ts +14 -2
  212. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  213. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  214. package/src/apps/data/FxOracle.test.ts +2 -2
  215. package/src/apps/data/FxOracle.ts +2 -2
  216. package/src/apps/data/routes/activities.test.ts +6 -2
  217. package/src/apps/data/routes/activities.ts +39 -1
  218. package/src/apps/data/routes/balances.test.ts +6 -2
  219. package/src/apps/data/routes/exchanges.test.ts +7 -3
  220. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  221. package/src/apps/data/routes/fee-amm.ts +98 -119
  222. package/src/apps/data/routes/funding.test.ts +29 -8
  223. package/src/apps/data/routes/funding.ts +42 -48
  224. package/src/apps/data/routes/receipts.test.ts +6 -2
  225. package/src/apps/data/routes/tokenlist.test.ts +15 -0
  226. package/src/apps/data/routes/tokenlist.ts +18 -5
  227. package/src/apps/data/routes/tokens.ts +5 -1
  228. package/src/apps/data/routes/transfers.test.ts +203 -35
  229. package/src/apps/data/routes/transfers.ts +294 -172
  230. package/src/apps/data/routes/valuation.test.ts +18 -3
  231. package/src/apps/data/routes/valuation.ts +4 -4
  232. package/src/apps/data/routes/verified-tokens.test.ts +51 -0
  233. package/src/apps/data/routes/verified-tokens.ts +52 -15
  234. package/src/apps/data/routes/webhooks.test.ts +56 -0
  235. package/src/apps/data/routes/webhooks.ts +17 -8
  236. package/src/apps/index.ts +1 -0
  237. package/src/apps/management/routes/api-keys.test.ts +2 -75
  238. package/src/apps/management/routes/api-keys.ts +30 -7
  239. package/src/apps/management/routes/billing.test.ts +106 -85
  240. package/src/apps/management/routes/billing.ts +32 -21
  241. package/src/apps/management/routes/faucet.test.ts +11 -3
  242. package/src/apps/management/routes/faucet.ts +46 -30
  243. package/src/apps/management/routes/invitations.test.ts +0 -172
  244. package/src/apps/management/routes/invitations.ts +0 -3
  245. package/src/apps/management/routes/me.test.ts +3 -145
  246. package/src/apps/management/routes/me.ts +0 -6
  247. package/src/apps/management/routes/orgs.test.ts +45 -2
  248. package/src/apps/management/routes/orgs.ts +6 -0
  249. package/src/apps/management/routes/scopes.test.ts +1 -0
  250. package/src/apps/mpp/App.ts +86 -0
  251. package/src/apps/mpp/Mpp.test.ts +193 -0
  252. package/src/apps/mpp/Mpp.ts +353 -0
  253. package/src/apps/mpp/index.ts +2 -0
  254. package/src/apps/mpp/internal/Adapter.ts +11 -0
  255. package/src/apps/mpp/internal/Error.ts +9 -0
  256. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  257. package/src/apps/mpp/internal/Screening.ts +10 -0
  258. package/src/apps/relay/App.test.ts +90 -8
  259. package/src/apps/relay/App.ts +62 -10
  260. package/src/apps/relay/Sponsorships.test.ts +2 -0
  261. package/src/apps/relay/Sponsorships.ts +1 -1
  262. package/src/db/Db.test.ts +2 -2
  263. package/src/db/Db.ts +30 -0
  264. package/src/db/Schema.ts +15 -0
  265. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  266. package/src/db/tables/enabledBillingSources.ts +118 -0
  267. package/src/db/tables/invitations.ts +4 -26
  268. package/src/db/tables/organizations.test.ts +191 -0
  269. package/src/db/tables/organizations.ts +44 -3
  270. package/src/db/tables/sponsoredTransactions.test.ts +41 -2
  271. package/src/db/tables/sponsoredTransactions.ts +15 -3
  272. package/src/handlers/internal/utils.ts +22 -6
  273. package/src/handlers/relay.errors.test.ts +153 -0
  274. package/src/handlers/relay.ts +42 -7
  275. package/src/internal/Auth.test.ts +3 -3
  276. package/src/internal/Auth.ts +3 -63
  277. package/src/internal/EdgeCache.test.ts +18 -5
  278. package/src/internal/EdgeCache.ts +4 -2
  279. package/src/internal/FundingQuotes.test.ts +18 -5
  280. package/src/internal/FundingQuotes.ts +46 -11
  281. package/src/internal/Log.test.ts +91 -0
  282. package/src/internal/Log.ts +69 -5
  283. package/src/internal/MetricSink.test.ts +55 -0
  284. package/src/internal/MetricSink.ts +14 -0
  285. package/src/internal/Response.test.ts +28 -0
  286. package/src/internal/Response.ts +12 -4
  287. package/src/internal/Schema.test.ts +13 -0
  288. package/src/internal/Schema.ts +14 -7
@@ -5,11 +5,10 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
5
5
  import * as ApiKeys from '../../../ApiKeys.js'
6
6
  import * as TestApp from '../../../../test/App.js'
7
7
  import * as TestStripe from '../../../../test/Stripe.js'
8
- import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
8
+ import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
9
9
  import * as Memberships from '../../../db/tables/memberships.js'
10
10
  import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
11
11
  import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
12
- import * as Users from '../../../db/tables/users.js'
13
12
  import * as Viem from '../../../internal/Viem.js'
14
13
  import * as core_Billing from '../Billing.js'
15
14
  import type * as App from '../../../App.js'
@@ -66,7 +65,6 @@ function createApp(
66
65
  billing?: App.Billing | undefined
67
66
  db?: TestApp.create.Options['db']
68
67
  kv?: { store: ReturnType<typeof TestApp.kvStore> }
69
- tmp_earlyAccess?: boolean
70
68
  } = {},
71
69
  ) {
72
70
  // prettier-ignore
@@ -76,7 +74,6 @@ function createApp(
76
74
  db: options.db ?? TestApp.database(),
77
75
  ...(options.kv ? { kv: options.kv } : {}),
78
76
  session: { wallet: { origin } },
79
- ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
80
77
  })
81
78
  }
82
79
 
@@ -108,6 +105,20 @@ async function seeded(app: ReturnType<typeof createApp>) {
108
105
  return { cookie, orgId: id }
109
106
  }
110
107
 
108
+ /** Seeds an owned organization with Stripe setup enabled. */
109
+ async function seededWithStripe(
110
+ app: ReturnType<typeof createApp>,
111
+ db: ReturnType<typeof TestApp.database>,
112
+ ) {
113
+ const result = await seeded(app)
114
+ await EnabledBillingSources.add(db, {
115
+ createdBy: 'test',
116
+ orgId: result.orgId,
117
+ source: 'stripe',
118
+ })
119
+ return result
120
+ }
121
+
111
122
  describe('GET /orgs/:orgId/billing', () => {
112
123
  test('behavior: synthesizes none for orgs without a billing source', async () => {
113
124
  const app = createApp()
@@ -116,6 +127,7 @@ describe('GET /orgs/:orgId/billing', () => {
116
127
  expect(response.status).toBe(200)
117
128
  expect(await response.json()).toMatchInlineSnapshot(`
118
129
  {
130
+ "enabledSources": [],
119
131
  "spend": {
120
132
  "amount": "0",
121
133
  "currency": "usd",
@@ -139,6 +151,28 @@ describe('GET /orgs/:orgId/billing', () => {
139
151
  expect(body.updatedAt).toBeDefined()
140
152
  })
141
153
 
154
+ test('behavior: lists organization-wide sources in canonical order', async () => {
155
+ const db = TestApp.database()
156
+ const app = createApp({ billing: billingWithSandbox(), db })
157
+ const { cookie, orgId } = await seeded(app)
158
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'tempo' })
159
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
160
+
161
+ const production = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
162
+ const sandbox = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
163
+ headers: { cookie },
164
+ })
165
+
166
+ expect(await production.json()).toMatchObject({
167
+ enabledSources: ['stripe', 'tempo'],
168
+ status: 'none',
169
+ })
170
+ expect(await sandbox.json()).toMatchObject({
171
+ enabledSources: ['stripe', 'tempo'],
172
+ status: 'none',
173
+ })
174
+ })
175
+
142
176
  test('behavior: the super admin reads any org', async () => {
143
177
  const db = TestApp.database()
144
178
  const app = createApp({ db })
@@ -165,7 +199,7 @@ describe('GET /orgs/:orgId/billing', () => {
165
199
  expect(response.status).toBe(401)
166
200
  })
167
201
 
168
- test('behavior: 501 when billing is unconfigured', async () => {
202
+ test('behavior: reads database-backed state when Stripe is unconfigured', async () => {
169
203
  const app = TestApp.create({
170
204
  auth: { superAdmin: { secret } },
171
205
  db: TestApp.database(),
@@ -173,9 +207,18 @@ describe('GET /orgs/:orgId/billing', () => {
173
207
  })
174
208
  const { cookie, orgId } = await seeded(app)
175
209
  const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
176
- expect(response.status).toBe(501)
177
- const body = (await response.json()) as { error: { code: string } }
178
- expect(body.error.code).toMatchInlineSnapshot(`"billing_unconfigured"`)
210
+ expect(response.status).toBe(200)
211
+ expect(await response.json()).toMatchInlineSnapshot(`
212
+ {
213
+ "enabledSources": [],
214
+ "spend": {
215
+ "amount": "0",
216
+ "currency": "usd",
217
+ "period": "month",
218
+ },
219
+ "status": "none",
220
+ }
221
+ `)
179
222
  })
180
223
  })
181
224
 
@@ -202,6 +245,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
202
245
  expect(response.status).toBe(200)
203
246
  expect(await response.json()).toMatchInlineSnapshot(`
204
247
  {
248
+ "enabledSources": [],
205
249
  "spend": {
206
250
  "amount": "0",
207
251
  "currency": "usd",
@@ -294,7 +338,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
294
338
  expect(response.status).toBe(404)
295
339
  })
296
340
 
297
- test('behavior: 501 when billing is unconfigured', async () => {
341
+ test('behavior: updates database-backed state when Stripe is unconfigured', async () => {
298
342
  const app = TestApp.create({
299
343
  auth: { superAdmin: { secret } },
300
344
  db: TestApp.database(),
@@ -303,9 +347,12 @@ describe('PATCH /orgs/:orgId/billing', () => {
303
347
  const { cookie, orgId } = await seeded(app)
304
348
  const response = await app.request(
305
349
  `/v1/orgs/${orgId}/billing`,
306
- patch(cookie, { spendLimit: null }),
350
+ patch(cookie, { spendLimit: { amount: '250' } }),
307
351
  )
308
- expect(response.status).toBe(501)
352
+ expect(response.status).toBe(200)
353
+ expect(
354
+ ((await response.json()) as { spendLimit?: { amount: string } }).spendLimit?.amount,
355
+ ).toBe('250')
309
356
  })
310
357
  })
311
358
 
@@ -355,8 +402,9 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
355
402
  })
356
403
 
357
404
  test('behavior: an unreachable Stripe maps to the upstream error envelope', async () => {
358
- const app = createApp()
359
- const { cookie, orgId } = await seeded(app)
405
+ const db = TestApp.database()
406
+ const app = createApp({ db })
407
+ const { cookie, orgId } = await seededWithStripe(app, db)
360
408
  // First Stripe call the route makes is the customer create; the connection
361
409
  // is refused for real (no transport fake).
362
410
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
@@ -366,9 +414,13 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
366
414
  })
367
415
  })
368
416
 
369
- describe('early access', () => {
370
- test('behavior: checkout requires early access', async () => {
371
- const app = createApp({ tmp_earlyAccess: true })
417
+ describe('billing source access', () => {
418
+ test('behavior: checkout requires Stripe to be enabled', async () => {
419
+ const app = TestApp.create({
420
+ auth: { superAdmin: { secret } },
421
+ db: TestApp.database(),
422
+ session: { wallet: { origin } },
423
+ })
372
424
  const { cookie, orgId } = await seeded(app)
373
425
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
374
426
  expect(response.status).toBe(403)
@@ -377,75 +429,29 @@ describe('early access', () => {
377
429
  expect(body).toMatchInlineSnapshot(`
378
430
  {
379
431
  "error": {
380
- "code": "early_access_required",
381
- "message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
432
+ "code": "billing_source_disabled",
433
+ "message": "Stripe billing is not enabled for this organization",
382
434
  },
383
435
  }
384
436
  `)
385
437
  })
386
438
 
387
- test('behavior: the gate precedes membership resolution', async () => {
388
- // The gate depends only on the caller, so it leaks nothing about the org;
389
- // early-access strangers still get the membership 404.
439
+ test('behavior: the source gate replaces temporary billing early access', async () => {
390
440
  const db = TestApp.database()
391
- const app = createApp({ db, tmp_earlyAccess: true })
392
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
393
- const { orgId } = await seeded(app)
394
-
395
- const other = await session(app)
396
- const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))
397
- expect(gated.status).toBe(403)
398
- expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
399
-
400
- const granted = await session(app)
401
- const me = await app.request('/v1/me', { headers: { cookie: granted } })
402
- const { id: userId } = (await me.json()) as { id: string }
403
- await Users.setEmail(db, userId, 'stranger@example.org')
404
- expect(
405
- (await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(granted))).status,
406
- ).toBe(404)
407
- })
408
-
409
- test('behavior: the gate precedes the owner-role check', async () => {
410
- const db = TestApp.database()
411
- const app = createApp({ db, tmp_earlyAccess: true })
412
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
413
- const { orgId } = await seeded(app)
414
- const member = await session(app)
415
- const me = await app.request('/v1/me', { headers: { cookie: member } })
416
- const { id: userId } = (await me.json()) as { id: string }
417
- await Memberships.create(db, { orgId, role: 'member', userId })
418
-
419
- const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
420
- expect(gated.status).toBe(403)
421
- expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
422
-
423
- // Early-access members still fail the owner-role check.
424
- await Users.setEmail(db, userId, 'member@example.org')
425
- const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
426
- expect(write.status).toBe(403)
427
- expect(((await write.json()) as { error: { code: string } }).error.code).toBe('forbidden')
428
- })
429
-
430
- test('behavior: early-access owners pass the gate', async () => {
431
- const db = TestApp.database()
432
- const app = createApp({ db, tmp_earlyAccess: true })
433
- await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
434
- const { cookie, orgId } = await seeded(app)
435
- const me = await app.request('/v1/me', { headers: { cookie } })
436
- const { id: userId } = (await me.json()) as { id: string }
437
- await Users.setEmail(db, userId, 'dev@example.org')
441
+ const app = createApp({ db })
442
+ const { cookie, orgId } = await seededWithStripe(app, db)
438
443
 
439
- // The unreachable Stripe upstream (502) proves the request passed the gate.
444
+ // The unreachable Stripe upstream proves source access, not user early access, is the gate.
440
445
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
441
446
  expect(response.status).toBe(502)
442
447
  expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
443
448
  })
444
449
 
445
- test('behavior: management keys pass the gate', async () => {
450
+ test('behavior: an enabled management key reaches Stripe', async () => {
451
+ const db = TestApp.database()
446
452
  const store = TestApp.kvStore({ keys: [] })
447
- const app = createApp({ tmp_earlyAccess: true, kv: { store } })
448
- const { orgId } = await seeded(app)
453
+ const app = createApp({ db, kv: { store } })
454
+ const { orgId } = await seededWithStripe(app, db)
449
455
  const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
450
456
 
451
457
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
@@ -455,9 +461,10 @@ describe('early access', () => {
455
461
  expect(response.status).toBe(502)
456
462
  })
457
463
 
458
- test('behavior: the super admin passes the gate', async () => {
459
- const app = createApp({ tmp_earlyAccess: true })
460
- const { orgId } = await seeded(app)
464
+ test('behavior: an enabled super admin reaches Stripe', async () => {
465
+ const db = TestApp.database()
466
+ const app = createApp({ db })
467
+ const { orgId } = await seededWithStripe(app, db)
461
468
 
462
469
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
463
470
  ...as(secret),
@@ -551,6 +558,7 @@ describe('DELETE /orgs/:orgId', () => {
551
558
  const db = TestApp.database()
552
559
  const app = createApp({ db })
553
560
  const { cookie, orgId } = await seeded(app)
561
+ await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
554
562
  await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_cascade' })
555
563
 
556
564
  const response = await app.request(`/v1/orgs/${orgId}`, {
@@ -558,19 +566,30 @@ describe('DELETE /orgs/:orgId', () => {
558
566
  method: 'DELETE',
559
567
  })
560
568
  expect(response.status).toBe(200)
569
+ expect(await EnabledBillingSources.listByOrg(db, orgId)).toEqual([])
561
570
  expect(await StripeCustomers.get(db, orgId)).toBeUndefined()
562
571
  })
563
572
  })
564
573
 
565
574
  describe('environment scoping', () => {
566
- test('behavior: sandbox is 501 when no sandbox source is configured', async () => {
575
+ test('behavior: sandbox billing state remains readable without a Stripe source', async () => {
567
576
  const app = createApp()
568
577
  const { cookie, orgId } = await seeded(app)
569
578
  const response = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
570
579
  headers: { cookie },
571
580
  })
572
- expect(response.status).toBe(501)
573
- expect(((await response.json()) as { error: { code: string } }).error.code).toBe('billing_unconfigured') // prettier-ignore
581
+ expect(response.status).toBe(200)
582
+ expect(await response.json()).toMatchInlineSnapshot(`
583
+ {
584
+ "enabledSources": [],
585
+ "spend": {
586
+ "amount": "0",
587
+ "currency": "usd",
588
+ "period": "month",
589
+ },
590
+ "status": "none",
591
+ }
592
+ `)
574
593
  })
575
594
 
576
595
  test('behavior: rejects an unknown environment', async () => {
@@ -756,7 +775,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
756
775
  test('checkout provisions distinct customers per environment', { timeout: 30_000 }, async () => {
757
776
  const db = TestApp.database()
758
777
  const app = createApp({ billing: sandboxBillingBoth(), db })
759
- const { cookie, orgId } = await seeded(app)
778
+ const { cookie, orgId } = await seededWithStripe(app, db)
760
779
 
761
780
  const prod = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
762
781
  const sbx = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout?environment=sandbox`, post(cookie)) // prettier-ignore
@@ -783,7 +802,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
783
802
  test('checkout provisions a real customer and setup session', { timeout: 30_000 }, async () => {
784
803
  const db = TestApp.database()
785
804
  const app = createApp({ billing: sandboxBilling(), db })
786
- const { cookie, orgId } = await seeded(app)
805
+ const { cookie, orgId } = await seededWithStripe(app, db)
787
806
 
788
807
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
789
808
  // Track for teardown before asserting: a failed assertion must not leak
@@ -831,7 +850,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
831
850
  // prettier-ignore
832
851
  const db = TestApp.database()
833
852
  const app = createApp({ billing: sandboxBilling(), db })
834
- const { cookie, orgId } = await seeded(app)
853
+ const { cookie, orgId } = await seededWithStripe(app, db)
835
854
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
836
855
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
837
856
  if (customerId) TestStripe.track(customerId)
@@ -844,8 +863,9 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
844
863
  invoice_settings: { default_payment_method: attached.paymentMethod },
845
864
  })
846
865
  await StripeCustomers.setStatus(db, orgId, 'active')
866
+ await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
847
867
 
848
- // The list reads live Stripe state: card details plus the default marker.
868
+ // Disabling setup does not block management of the existing Stripe source.
849
869
  const list = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, {
850
870
  headers: { cookie },
851
871
  })
@@ -900,7 +920,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
900
920
  // prettier-ignore
901
921
  const db = TestApp.database()
902
922
  const app = createApp({ billing: sandboxBilling(), db })
903
- const { cookie, orgId } = await seeded(app)
923
+ const { cookie, orgId } = await seededWithStripe(app, db)
904
924
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
905
925
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
906
926
  if (customerId) TestStripe.track(customerId)
@@ -930,11 +950,12 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
930
950
 
931
951
  const db = TestApp.database()
932
952
  const app = createApp({ billing: sandboxBilling(), db })
933
- const { cookie, orgId } = await seeded(app)
953
+ const { cookie, orgId } = await seededWithStripe(app, db)
934
954
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
935
955
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
936
956
  if (customerId) TestStripe.track(customerId)
937
957
  expect(checkout.status).toBe(200)
958
+ await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
938
959
 
939
960
  const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie))
940
961
  expect(response.status).toBe(200)
@@ -963,7 +984,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (live delivery)', () => {
963
984
  })
964
985
 
965
986
  test('attach → active, detach → none, delete → canceled', { timeout: 300_000 }, async () => {
966
- const { cookie, orgId } = await seeded(app)
987
+ const { cookie, orgId } = await seededWithStripe(app, db)
967
988
  const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
968
989
  const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
969
990
  if (customerId) TestStripe.track(customerId)
@@ -6,6 +6,7 @@ import * as ApiKeys from '../../../ApiKeys.js'
6
6
  import * as Auth from '../../../internal/Auth.js'
7
7
  import * as BillingSettings from '../../../db/tables/billingSettings.js'
8
8
  import * as Db from '../../../db/Db.js'
9
+ import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
9
10
  import * as OpenApi from '../../../internal/OpenApi.js'
10
11
  import * as Response from '../../../internal/Response.js'
11
12
  import * as Schema from '../../../internal/Schema.js'
@@ -113,6 +114,12 @@ export namespace schema {
113
114
  /** An organization's billing state. */
114
115
  export const Billing = Schema.describe(
115
116
  z.object({
117
+ enabledSources: z
118
+ .array(z.enum(EnabledBillingSources.sources))
119
+ .check(
120
+ z.describe('Billing sources this organization may set up.'),
121
+ z.meta({ examples: [['stripe']] }),
122
+ ),
116
123
  spend: z
117
124
  .optional(
118
125
  z.object({
@@ -234,7 +241,7 @@ export namespace schema {
234
241
  }
235
242
 
236
243
  /**
237
- * Mounts billing routes. Reads require membership or scoped API key access; organizations without Stripe state return `none`.
244
+ * Mounts billing routes. Reads require membership or scoped API key access; organizations without provider state return `none`.
238
245
  */
239
246
  export function billing() {
240
247
  return new Hono<Environment>()
@@ -255,7 +262,6 @@ export function billing() {
255
262
  responses: OpenApi.responses({
256
263
  errors: {
257
264
  404: 'No accessible organization was found for this id.',
258
- 501: 'Billing is not configured on this deployment.',
259
265
  },
260
266
  success: { description: "The organization's billing state.", schema: schema.Billing },
261
267
  }),
@@ -276,10 +282,7 @@ export function billing() {
276
282
  message: 'Check the query parameters and try again.',
277
283
  })
278
284
 
279
- const billing = c.get('billing')
280
- if (!billing) return billingUnconfigured(c)
281
285
  const environment = c.req.valid('query').environment
282
- if (!stripeFor(billing, environment)) return billingUnconfigured(c)
283
286
 
284
287
  try {
285
288
  return c.json(
@@ -313,7 +316,6 @@ export function billing() {
313
316
  errors: {
314
317
  403: 'Requires the owner role.',
315
318
  404: 'No accessible organization was found for this id.',
316
- 501: 'Billing is not configured on this deployment.',
317
319
  },
318
320
  success: {
319
321
  description: "The organization's updated billing state.",
@@ -342,10 +344,7 @@ export function billing() {
342
344
  message: 'Check the request body and try again.',
343
345
  })
344
346
 
345
- const billing = c.get('billing')
346
- if (!billing) return billingUnconfigured(c)
347
347
  const environment = c.req.valid('query').environment
348
- if (!stripeFor(billing, environment)) return billingUnconfigured(c)
349
348
 
350
349
  const body = c.req.valid('json')
351
350
  const org = Auth.org(c)
@@ -380,7 +379,7 @@ export function billing() {
380
379
  )
381
380
  .post(
382
381
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
383
- Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true, tmp_earlyAccess: true }), // prettier-ignore
382
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
384
383
  Auth.ensureOrg({ role: 'owner' }),
385
384
  OpenApi.validate('param', schema.Params, {
386
385
  code: 'param_invalid',
@@ -395,8 +394,8 @@ export function billing() {
395
394
  responses: OpenApi.responses({
396
395
  errors: {
397
396
  403: {
398
- codes: ['api_key_forbidden', 'early_access_required', 'forbidden'],
399
- description: 'Requires the owner role and early access.',
397
+ codes: ['api_key_forbidden', 'billing_source_disabled', 'forbidden'],
398
+ description: 'Requires the owner role and Stripe billing enabled.',
400
399
  },
401
400
  404: 'No accessible organization was found for this id.',
402
401
  501: 'Billing is not configured on this deployment.',
@@ -413,7 +412,6 @@ export function billing() {
413
412
  if (Auth.narrowAccess) return Auth.accessError(c)
414
413
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
415
414
  if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
416
- if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
417
415
  if (OpenApi.narrowValidation)
418
416
  return OpenApi.validationError(c, {
419
417
  code: 'param_invalid',
@@ -425,16 +423,19 @@ export function billing() {
425
423
  message: 'Check the query parameters and try again.',
426
424
  })
427
425
 
428
- const billing = c.get('billing')
429
- if (!billing) return billingUnconfigured(c)
430
- const environment = c.req.valid('query').environment
431
- const source = stripeFor(billing, environment)
432
- if (!source) return billingUnconfigured(c)
433
-
434
- const { client } = source
435
426
  const org = Auth.org(c)
436
427
  const db = Db.get(c.get('db'))
437
428
  try {
429
+ if (!(await EnabledBillingSources.isEnabled(db, { orgId: org.id, source: 'stripe' })))
430
+ return billingSourceDisabled(c)
431
+
432
+ const billing = c.get('billing')
433
+ if (!billing) return billingUnconfigured(c)
434
+ const environment = c.req.valid('query').environment
435
+ const source = stripeFor(billing, environment)
436
+ if (!source) return billingUnconfigured(c)
437
+
438
+ const { client } = source
438
439
  const existing = await StripeCustomers.get(db, org.id, environment)
439
440
  const customerId = existing
440
441
  ? existing.stripeCustomerId
@@ -885,7 +886,8 @@ async function billingBody(
885
886
  environment: ResourceEnvironment,
886
887
  ) {
887
888
  const db = Db.get(c.get('db'))
888
- const [record, settings] = await Promise.all([
889
+ const [enabledSources, record, settings] = await Promise.all([
890
+ EnabledBillingSources.listByOrg(db, orgId),
889
891
  StripeCustomers.get(db, orgId, environment),
890
892
  BillingSettings.get(db, orgId, environment),
891
893
  ])
@@ -900,6 +902,7 @@ async function billingBody(
900
902
  since: core_Billing.periodStart(period),
901
903
  })
902
904
  return {
905
+ enabledSources: enabledSources.map((record) => record.source),
903
906
  spend: { amount: core_Billing.fromBaseUnits(spend), currency, period },
904
907
  ...(settings?.spendLimit != null
905
908
  ? { spendLimit: { amount: settings.spendLimit, currency, period } }
@@ -959,6 +962,14 @@ function billingNotFound(c: Context<Environment>) {
959
962
  })
960
963
  }
961
964
 
965
+ function billingSourceDisabled(c: Context<Environment>) {
966
+ return Response.error(c, {
967
+ code: 'billing_source_disabled',
968
+ message: 'Stripe billing is not enabled for this organization',
969
+ status: 403,
970
+ })
971
+ }
972
+
962
973
  function billingUnconfigured(c: Context<Environment>) {
963
974
  return Response.error(c, {
964
975
  code: 'billing_unconfigured',
@@ -43,9 +43,17 @@ describe('faucet', () => {
43
43
  expect(funded.status).toBe(200)
44
44
  const result = await TestApp.json(funded, Faucet.schema.FundResponse)
45
45
  expect(result.tokens.length).toBeGreaterThan(0)
46
- expect(result.tokens.every((token) => token.symbol.length > 0)).toBe(true)
47
- expect(result.tokens.every((token) => BigInt(token.amount) > 0n)).toBe(true)
48
- expect(result.transactionHashes).toEqual(result.tokens.map((token) => token.transactionHash))
46
+ expect(
47
+ result.tokens.every(
48
+ (entry) =>
49
+ entry.token.symbol.length > 0 &&
50
+ BigInt(entry.amount.baseUnits) > 0n &&
51
+ entry.amount.currency === entry.token.currency &&
52
+ entry.amount.decimals === entry.token.decimals &&
53
+ /^\d+(\.\d+)?$/.test(entry.amount.formatted),
54
+ ),
55
+ ).toBe(true)
56
+ expect(result.transactionHashes).toEqual(result.tokens.map((entry) => entry.transactionHash))
49
57
 
50
58
  const repeated = await app.request(`/v1/orgs/${org.id}/faucet`, {
51
59
  body: JSON.stringify({ address: recipient }),
@@ -6,6 +6,7 @@ import * as OpenApi from '../../../internal/OpenApi.js'
6
6
  import * as RateLimit from '../../../internal/RateLimit.js'
7
7
  import * as Response from '../../../internal/Response.js'
8
8
  import * as Schema from '../../../internal/Schema.js'
9
+ import * as Value from '../../../internal/Value.js'
9
10
  import * as Viem from '../../../internal/Viem.js'
10
11
  import type { Environment } from '../App.js'
11
12
 
@@ -14,6 +15,37 @@ const userRateLimit = { perMinute: 3 } satisfies RateLimit.Limit
14
15
 
15
16
  /** OpenAPI schemas owned by the sandbox faucet. */
16
17
  export namespace schema {
18
+ const Token = Schema.describe(
19
+ z.object({
20
+ address: Schema.TokenAddress.check(z.describe('The test token contract that minted funds.')),
21
+ currency: z
22
+ .string()
23
+ .check(z.describe('The token’s currency label.'), z.meta({ examples: ['USD'] })),
24
+ decimals: z
25
+ .number()
26
+ .check(
27
+ z.int(),
28
+ z.nonnegative(),
29
+ z.describe('The decimal places used by the token.'),
30
+ z.meta({ examples: [6] }),
31
+ ),
32
+ name: z
33
+ .string()
34
+ .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['Test USD'] })),
35
+ symbol: z.string().check(z.describe('The token’s symbol.'), z.meta({ examples: ['TUSD'] })),
36
+ }),
37
+ 'A test token minted by the faucet.',
38
+ )
39
+
40
+ const FundedToken = Schema.describe(
41
+ z.object({
42
+ amount: Schema.TokenAmount.check(z.describe('Amount minted by the faucet.')),
43
+ token: Token,
44
+ transactionHash: Schema.Hash.check(z.describe('The transaction that minted this token.')),
45
+ }),
46
+ 'One token amount minted by the faucet.',
47
+ )
48
+
17
49
  /** Faucet request body. */
18
50
  export const FundBody = Schema.describe(
19
51
  z.object({ address: Schema.Address }),
@@ -24,29 +56,7 @@ export namespace schema {
24
56
  export const FundResponse = Schema.describe(
25
57
  z.object({
26
58
  tokens: z
27
- .array(
28
- z.object({
29
- address: Schema.TokenAddress.check(
30
- z.describe('The test token contract that minted funds.'),
31
- ),
32
- amount: Schema.DecimalString.check(
33
- z.describe('The minted amount in the token’s smallest units.'),
34
- ),
35
- currency: z.string().check(z.describe('The token’s currency label.')),
36
- decimals: z
37
- .number()
38
- .check(
39
- z.int(),
40
- z.nonnegative(),
41
- z.describe('The number of decimal places the token uses.'),
42
- ),
43
- name: z.string().check(z.describe('The token’s human-readable name.')),
44
- symbol: z.string().check(z.describe('The token’s symbol.')),
45
- transactionHash: Schema.Hash.check(
46
- z.describe('The transaction that minted this token.'),
47
- ),
48
- }),
49
- )
59
+ .array(FundedToken)
50
60
  .check(z.describe('The configured test tokens minted to the account.')),
51
61
  transactionHashes: z
52
62
  .array(Schema.Hash)
@@ -124,17 +134,23 @@ export function faucet() {
124
134
  const event = client.token.mint.extractEvent(receipt.logs)
125
135
  const metadata = await client.token.getMetadata({ token: event.address })
126
136
  return {
127
- address: event.address,
128
- amount: event.args.amount.toString(),
129
- currency: metadata.currency,
130
- decimals: metadata.decimals,
131
- name: metadata.name,
132
- symbol: metadata.symbol,
137
+ amount: Value.tokenAmount({
138
+ baseUnits: event.args.amount,
139
+ currency: metadata.currency,
140
+ decimals: metadata.decimals,
141
+ }),
142
+ token: {
143
+ address: event.address,
144
+ currency: metadata.currency,
145
+ decimals: metadata.decimals,
146
+ name: metadata.name,
147
+ symbol: metadata.symbol,
148
+ },
133
149
  transactionHash: receipt.transactionHash,
134
150
  }
135
151
  }),
136
152
  )
137
- const transactionHashes = tokens.map((token) => token.transactionHash)
153
+ const transactionHashes = tokens.map((entry) => entry.transactionHash)
138
154
  return c.json(Response.validated(schema.FundResponse, { tokens, transactionHashes }), 200)
139
155
  } catch (cause) {
140
156
  return Response.upstream(c, cause)