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
@@ -1,8 +1,11 @@
1
- import { sql, type Selectable } from 'kysely'
1
+ import { sql, type ExpressionBuilder, type Selectable } from 'kysely'
2
2
 
3
3
  import * as Id from '../../internal/Id.js'
4
+ import * as Viem from '../../internal/Viem.js'
5
+ import * as SponsoredTransactions from './sponsoredTransactions.js'
4
6
  import type * as Db from '../Db.js'
5
7
  import type * as db_Schema from '../Schema.js'
8
+ import type * as EnabledBillingSources from './enabledBillingSources.js'
6
9
  import type * as Memberships from './memberships.js'
7
10
 
8
11
  /** Columns of the `organizations` table, derived from `Schema.Organization`. */
@@ -50,8 +53,8 @@ export declare namespace create {
50
53
  }
51
54
 
52
55
  /**
53
- * Inserts an organization owned by `userId` the org row plus its `owner`
54
- * membership, atomically.
56
+ * Inserts an organization, its owner membership, and initial billing sources
57
+ * atomically.
55
58
  *
56
59
  * @param db - The database.
57
60
  * @param input - The organization to insert.
@@ -82,6 +85,18 @@ export function createOwned(db: Db.Db, input: createOwned.Input): Promise<Record
82
85
  userId: input.userId,
83
86
  })
84
87
  .execute()
88
+ if (input.enabledBillingSources?.length)
89
+ await trx
90
+ .insertInto('enabled_billing_sources')
91
+ .values(
92
+ input.enabledBillingSources.map((source) => ({
93
+ createdAt: now,
94
+ createdBy: input.userId,
95
+ orgId: record.id,
96
+ source,
97
+ })),
98
+ )
99
+ .execute()
85
100
  return record
86
101
  })
87
102
  }
@@ -89,6 +104,8 @@ export function createOwned(db: Db.Db, input: createOwned.Input): Promise<Record
89
104
  export declare namespace createOwned {
90
105
  /** Fields accepted when inserting a user-owned organization. */
91
106
  type Input = {
107
+ /** Billing sources enabled when the organization is created. */
108
+ enabledBillingSources?: readonly EnabledBillingSources.Source[] | undefined
92
109
  /** Human-readable organization name. */
93
110
  name: string
94
111
  /** Owning user id (`usr_…`), granted the `owner` membership. */
@@ -105,12 +122,17 @@ export declare namespace createOwned {
105
122
  */
106
123
  export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
107
124
  return db.kysely.transaction().execute(async (trx) => {
125
+ const failedSince = new Date(
126
+ Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs,
127
+ ).toISOString()
108
128
  const unreported = await trx
109
129
  .selectFrom('sponsored_transactions')
110
130
  .select('id')
111
131
  .where('orgId', '=', id)
112
132
  .where('billable', '=', true)
133
+ .where('chainId', '=', Viem.chainId.mainnet)
113
134
  .where('meterReportedAt', 'is', null)
135
+ .where(unreportedSponsorshipStatus(failedSince))
114
136
  .limit(1)
115
137
  .executeTakeFirst()
116
138
  if (unreported) throw new UnreportedSponsorshipsError()
@@ -121,6 +143,7 @@ export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
121
143
  .where('ownerId', '=', id)
122
144
  .execute()
123
145
  await trx.deleteFrom('billing_settings').where('orgId', '=', id).execute()
146
+ await trx.deleteFrom('enabled_billing_sources').where('orgId', '=', id).execute()
124
147
  await trx.deleteFrom('invitations').where('orgId', '=', id).execute()
125
148
  await trx.deleteFrom('invite_link_redemptions').where('orgId', '=', id).execute()
126
149
  await trx.deleteFrom('invite_links').where('orgId', '=', id).execute()
@@ -136,12 +159,17 @@ export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
136
159
 
137
160
  /** Whether an organization has billable sponsorships awaiting settlement. */
138
161
  export async function hasUnreportedSponsorships(db: Db.Db, id: string): Promise<boolean> {
162
+ const failedSince = new Date(
163
+ Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs,
164
+ ).toISOString()
139
165
  const record = await db.kysely
140
166
  .selectFrom('sponsored_transactions')
141
167
  .select('id')
142
168
  .where('orgId', '=', id)
143
169
  .where('billable', '=', true)
170
+ .where('chainId', '=', Viem.chainId.mainnet)
144
171
  .where('meterReportedAt', 'is', null)
172
+ .where(unreportedSponsorshipStatus(failedSince))
145
173
  .limit(1)
146
174
  .executeTakeFirst()
147
175
  return record !== undefined
@@ -413,6 +441,19 @@ export declare namespace upsert {
413
441
  }
414
442
  }
415
443
 
444
+ /** Blocks rows that can still settle or recover into billable mainnet spend. */
445
+ function unreportedSponsorshipStatus(failedSince: string) {
446
+ return (eb: ExpressionBuilder<Db.Database, 'sponsored_transactions'>) =>
447
+ eb.or([
448
+ eb('status', '!=', 'failed'),
449
+ eb.and([
450
+ eb('status', '=', 'failed'),
451
+ eb('transactionHash', 'is', null),
452
+ eb('finalizedAt', '>=', failedSince),
453
+ ]),
454
+ ])
455
+ }
456
+
416
457
  /** Organization deletion is blocked until billable sponsorships settle. */
417
458
  export class UnreportedSponsorshipsError extends Error {
418
459
  override name = 'Organizations.UnreportedSponsorshipsError'
@@ -2,6 +2,7 @@ import { nanoid } from 'nanoid'
2
2
 
3
3
  import * as Runtime from '../../../test/runtime.js'
4
4
  import * as Db from '../Db.js'
5
+ import * as Organizations from './organizations.js'
5
6
  import * as SponsoredTransactions from './sponsoredTransactions.js'
6
7
 
7
8
  const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
@@ -160,7 +161,8 @@ describe('listPending', () => {
160
161
 
161
162
  const pending = await SponsoredTransactions.listPending(db)
162
163
  // Inserts can share a millisecond, so assert membership, not order.
163
- expect(pending.map((row) => row.transactionHash).sort()).toMatchInlineSnapshot(`
164
+ expect(pending.map((row) => row.transactionHash).sort((left, right) => String(left).localeCompare(String(right)))) // prettier-ignore
165
+ .toMatchInlineSnapshot(`
164
166
  [
165
167
  "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
166
168
  "0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3",
@@ -175,9 +177,10 @@ describe('assignTransactionHash', () => {
175
177
  test('behavior: reopens a failed intent when reconciliation finds its hash', async () => {
176
178
  const db = create()
177
179
  await db.migrate()
180
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
178
181
  const { transactionHash: _, ...intent } = input
179
182
  const record = await SponsoredTransactions.upsert(db, intent)
180
- await SponsoredTransactions.fail(db, record.id, '2026-01-01T00:00:00.000Z')
183
+ await SponsoredTransactions.fail(db, record.id, new Date().toISOString())
181
184
 
182
185
  await SponsoredTransactions.assignTransactionHash(db, record.id, input.transactionHash)
183
186
 
@@ -190,9 +193,45 @@ describe('assignTransactionHash', () => {
190
193
  await db.close()
191
194
  })
192
195
 
196
+ test('behavior: does not reopen failed intents after the recovery window', async () => {
197
+ const db = create()
198
+ await db.migrate()
199
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
200
+ const { transactionHash: _, ...intent } = input
201
+ const record = await SponsoredTransactions.upsert(db, intent)
202
+ await SponsoredTransactions.fail(
203
+ db,
204
+ record.id,
205
+ new Date(Date.now() - SponsoredTransactions.failedIntentRecoveryTtlMs - 1).toISOString(),
206
+ )
207
+
208
+ await SponsoredTransactions.assignTransactionHash(db, record.id, input.transactionHash)
209
+
210
+ const assigned = await SponsoredTransactions.get(db, record.id)
211
+ expect(assigned?.status).toMatchInlineSnapshot(`"failed"`)
212
+ expect(assigned?.transactionHash).toBeNull()
213
+ await db.close()
214
+ })
215
+
216
+ test('behavior: does not reopen failed intents for deleted orgs', async () => {
217
+ const db = create()
218
+ await db.migrate()
219
+ const { transactionHash: _, ...intent } = input
220
+ const record = await SponsoredTransactions.upsert(db, intent)
221
+ await SponsoredTransactions.fail(db, record.id, new Date().toISOString())
222
+
223
+ await SponsoredTransactions.assignTransactionHash(db, record.id, input.transactionHash)
224
+
225
+ const assigned = await SponsoredTransactions.get(db, record.id)
226
+ expect(assigned?.status).toMatchInlineSnapshot(`"failed"`)
227
+ expect(assigned?.transactionHash).toBeNull()
228
+ await db.close()
229
+ })
230
+
193
231
  test('behavior: keeps an over-limit failed intent retryable across a period rollover', async () => {
194
232
  const db = create()
195
233
  await db.migrate()
234
+ await Organizations.create(db, { id: 'org_1', name: 'One' })
196
235
  const { transactionHash: _, ...intent } = input
197
236
  vi.useFakeTimers({ now: new Date('2026-01-31T23:59:00.000Z') })
198
237
  try {
@@ -1,4 +1,4 @@
1
- import type { ExpressionBuilder, Selectable } from 'kysely'
1
+ import type { ExpressionBuilder, Selectable, SqlBool } from 'kysely'
2
2
  import { sql } from 'kysely'
3
3
 
4
4
  import * as Id from '../../internal/Id.js'
@@ -11,6 +11,9 @@ export type Table = db_Schema.SponsoredTransaction
11
11
  /** A stored sponsored-transaction row. */
12
12
  export type Record = Selectable<Table>
13
13
 
14
+ /** How long failed fill intents remain eligible for on-chain reconciliation. */
15
+ export const failedIntentRecoveryTtlMs = 86_400_000
16
+
14
17
  /**
15
18
  * Records one sponsorship commitment as `pending`, keyed by its fee-payer
16
19
  * sign payload. Written after fee-payer signing and before any broadcast, so
@@ -199,9 +202,15 @@ export async function assignTransactionHash(
199
202
  !['failed', 'pending'].includes(record.status)
200
203
  )
201
204
  return
205
+ const failedSince = new Date(Date.now() - failedIntentRecoveryTtlMs).toISOString()
206
+ if (
207
+ record.status === 'failed' &&
208
+ (record.finalizedAt === null || record.finalizedAt < failedSince)
209
+ )
210
+ return
202
211
  if (record.status === 'failed' && record.billable && limit)
203
212
  await sql`select pg_advisory_xact_lock(${billingLockClass}, hashtext(${record.orgId}))`.execute(tx.kysely) // prettier-ignore
204
- await tx.kysely
213
+ let query = tx.kysely
205
214
  .updateTable('sponsored_transactions')
206
215
  .set({
207
216
  ...(record.status === 'failed' ? { createdAt: new Date().toISOString() } : {}),
@@ -212,7 +221,10 @@ export async function assignTransactionHash(
212
221
  .where('id', '=', id)
213
222
  .where('status', '=', record.status)
214
223
  .where('transactionHash', 'is', null)
215
- .execute()
224
+ if (record.status === 'failed')
225
+ query = query.where(sql<SqlBool>`exists (select 1 from organizations where organizations.id = sponsored_transactions.org_id)`) // prettier-ignore
226
+ const updated = await query.executeTakeFirst()
227
+ if (updated.numUpdatedRows === 0n) return
216
228
  if (record.status === 'failed' && record.billable && limit) {
217
229
  const committed = await spend(tx, {
218
230
  chainIds: limit.chainIds,
@@ -3,6 +3,8 @@ import { KeyAuthorization, Transaction as core_Transaction, TxEnvelopeTempo } fr
3
3
  import { type Client, BaseError } from 'viem'
4
4
  import * as z from 'zod/mini'
5
5
 
6
+ import * as ApiResponse from '../../internal/Response.js'
7
+
6
8
  export function resolveChainId(value: unknown) {
7
9
  if (typeof value === 'number') return value
8
10
  if (typeof value === 'bigint') return Number(value)
@@ -97,24 +99,38 @@ export function rpcErrorJson(request: RpcRequest.RpcRequest, error: unknown) {
97
99
  { request },
98
100
  )
99
101
 
100
- if ((error as { name?: string | undefined }).name === 'ZodError')
102
+ if (error instanceof z.core.$ZodError)
101
103
  return RpcResponse.from(
102
104
  {
103
105
  error: {
104
106
  code: RpcResponse.InvalidParamsError.code,
105
- message: (error as Error).message,
107
+ data: {
108
+ code: 'invalid_params',
109
+ issues: ApiResponse.validationDetails(error.issues.slice(0, 10)),
110
+ },
111
+ message: 'Invalid params.',
106
112
  },
107
113
  },
108
114
  { request },
109
115
  )
110
116
 
111
117
  const inner = resolveError(error)
112
- const message = inner.message ?? (error as Error).message
113
- const code = inner.code ?? -32603
114
- const data = inner.data
118
+ if (inner.code === undefined || inner.message === undefined)
119
+ return RpcResponse.from(
120
+ {
121
+ error: {
122
+ code: RpcResponse.InternalError.code,
123
+ data: { code: 'internal_error' },
124
+ message: 'Internal error',
125
+ },
126
+ },
127
+ { request },
128
+ )
129
+
130
+ const { code, data, message } = inner
115
131
  return RpcResponse.from(
116
132
  {
117
- error: { code, message, ...(data ? { data } : {}) },
133
+ error: { code, message, ...(data === undefined ? {} : { data }) },
118
134
  },
119
135
  { request },
120
136
  )
@@ -0,0 +1,153 @@
1
+ import * as z from 'zod/mini'
2
+
3
+ import * as Relay from '../../test/Relay.js'
4
+
5
+ describe('request errors', () => {
6
+ test('returns Parse error for invalid JSON', async () => {
7
+ const server = await Relay.createServer((await Relay.relay()).listener)
8
+
9
+ try {
10
+ const response = await fetch(server.url, {
11
+ body: '{',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ method: 'POST',
14
+ })
15
+
16
+ expect(await response.json()).toMatchInlineSnapshot(`
17
+ {
18
+ "error": {
19
+ "code": -32700,
20
+ "message": "Parse error",
21
+ },
22
+ "id": null,
23
+ "jsonrpc": "2.0",
24
+ }
25
+ `)
26
+ } finally {
27
+ await server.closeAsync()
28
+ }
29
+ })
30
+
31
+ test.each([
32
+ ['non-object request', 'null'],
33
+ ['invalid version', JSON.stringify({ id: 1, jsonrpc: '1.0', method: 'eth_chainId' })],
34
+ ['empty batch', '[]'],
35
+ ])('returns Invalid Request for %s', async (_name, body) => {
36
+ const server = await Relay.createServer((await Relay.relay()).listener)
37
+
38
+ try {
39
+ const response = await fetch(server.url, {
40
+ body,
41
+ headers: { 'Content-Type': 'application/json' },
42
+ method: 'POST',
43
+ })
44
+
45
+ expect(await response.json()).toMatchInlineSnapshot(`
46
+ {
47
+ "error": {
48
+ "code": -32600,
49
+ "message": "Invalid Request",
50
+ },
51
+ "id": null,
52
+ "jsonrpc": "2.0",
53
+ }
54
+ `)
55
+ } finally {
56
+ await server.closeAsync()
57
+ }
58
+ })
59
+
60
+ test('returns one Invalid Request error for a malformed batch member', async () => {
61
+ const server = await Relay.createServer(
62
+ (
63
+ await Relay.relay({
64
+ getClient: () => {
65
+ throw new Error('transport credential must not leak')
66
+ },
67
+ })
68
+ ).listener,
69
+ )
70
+
71
+ try {
72
+ const response = await fetch(server.url, {
73
+ body: JSON.stringify([
74
+ { id: 1, jsonrpc: '2.0', method: 'eth_chainId' },
75
+ { id: 2, jsonrpc: '1.0', method: 'eth_chainId' },
76
+ ]),
77
+ headers: { 'Content-Type': 'application/json' },
78
+ method: 'POST',
79
+ })
80
+
81
+ expect(await response.json()).toMatchInlineSnapshot(`
82
+ [
83
+ {
84
+ "error": {
85
+ "code": -32603,
86
+ "data": {
87
+ "code": "internal_error",
88
+ },
89
+ "message": "Internal error",
90
+ },
91
+ "id": 1,
92
+ "jsonrpc": "2.0",
93
+ },
94
+ {
95
+ "error": {
96
+ "code": -32600,
97
+ "message": "Invalid Request",
98
+ },
99
+ "id": null,
100
+ "jsonrpc": "2.0",
101
+ },
102
+ ]
103
+ `)
104
+ } finally {
105
+ await server.closeAsync()
106
+ }
107
+ })
108
+
109
+ test('returns bounded structured issues for invalid params', async () => {
110
+ const schema = z.object({ value: z.string() })
111
+ const server = await Relay.createServer(
112
+ (
113
+ await Relay.relay({
114
+ onRequest: async () => {
115
+ schema.parse({ value: 1 })
116
+ },
117
+ })
118
+ ).listener,
119
+ )
120
+
121
+ try {
122
+ const response = await fetch(server.url, {
123
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
124
+ headers: { 'Content-Type': 'application/json' },
125
+ method: 'POST',
126
+ })
127
+
128
+ expect(await response.json()).toMatchInlineSnapshot(`
129
+ {
130
+ "error": {
131
+ "code": -32602,
132
+ "data": {
133
+ "code": "invalid_params",
134
+ "issues": [
135
+ {
136
+ "message": "Invalid input",
137
+ "path": [
138
+ "value",
139
+ ],
140
+ },
141
+ ],
142
+ },
143
+ "message": "Invalid params.",
144
+ },
145
+ "id": 1,
146
+ "jsonrpc": "2.0",
147
+ }
148
+ `)
149
+ } finally {
150
+ await server.closeAsync()
151
+ }
152
+ })
153
+ })
@@ -639,20 +639,30 @@ export function relay(options: relay.Options = {}): Handler {
639
639
 
640
640
  async function handlePost(c: { req: { raw: Request; param: (key: string) => string } }) {
641
641
  const chainId = Utils.resolveChainId(c.req.param('chainId'))
642
- const body = await c.req.raw.json()
642
+ const body = await c.req.raw.json().catch(() => undefined)
643
+ if (body === undefined) return Response.json(rpcProtocolError('parse'))
644
+
643
645
  const isBatch = Array.isArray(body)
644
646
 
645
647
  if (!isBatch) {
648
+ if (!isRpcRequest(body)) return Response.json(rpcProtocolError('invalid_request'))
646
649
  const request = RpcRequest.from(body as never) as RpcRequest.RpcRequest<RpcSchema.Ox>
647
- return Response.json(await handleRequest(request, { chainId }))
650
+ const response = await handleRequest(request, { chainId }).catch((error) =>
651
+ Utils.rpcErrorJson(request, error),
652
+ )
653
+ return Response.json(response)
648
654
  }
649
655
 
656
+ if (body.length === 0) return Response.json(rpcProtocolError('invalid_request'))
657
+
650
658
  const responses = await Promise.all(
651
- (body as unknown[]).map((item) =>
652
- handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<RpcSchema.Ox>, {
653
- chainId,
654
- }),
655
- ),
659
+ body.map((item) => {
660
+ if (!isRpcRequest(item)) return Promise.resolve(rpcProtocolError('invalid_request'))
661
+ return handleRequest(
662
+ RpcRequest.from(item as never) as RpcRequest.RpcRequest<RpcSchema.Ox>,
663
+ { chainId },
664
+ ).catch((error) => Utils.rpcErrorJson(item as RpcRequest.RpcRequest, error))
665
+ }),
656
666
  )
657
667
  return Response.json(responses)
658
668
  }
@@ -663,6 +673,31 @@ export function relay(options: relay.Options = {}): Handler {
663
673
  return router
664
674
  }
665
675
 
676
+ function isRpcRequest(value: unknown): value is Record<string, unknown> {
677
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
678
+ const request = value as Record<string, unknown>
679
+ if (request['jsonrpc'] !== '2.0' || typeof request['method'] !== 'string') return false
680
+
681
+ const id = request['id']
682
+ if (id !== undefined && id !== null && typeof id !== 'number' && typeof id !== 'string')
683
+ return false
684
+
685
+ const params = request['params']
686
+ if (params === undefined || Array.isArray(params)) return true
687
+ return Boolean(params) && typeof params === 'object'
688
+ }
689
+
690
+ function rpcProtocolError(type: 'invalid_request' | 'parse') {
691
+ return {
692
+ error:
693
+ type === 'parse'
694
+ ? { code: RpcResponse.ParseError.code, message: 'Parse error' }
695
+ : { code: RpcResponse.InvalidRequestError.code, message: 'Invalid Request' },
696
+ id: null,
697
+ jsonrpc: '2.0',
698
+ }
699
+ }
700
+
666
701
  function defaultGetClient(options: { apiKey?: string | undefined }) {
667
702
  const clients = new Map<number, Client>()
668
703
  for (const chain of defaultChains) {
@@ -671,7 +671,7 @@ describe('require', () => {
671
671
  headers: { 'tempo-api-key': sandboxToken },
672
672
  })
673
673
  expect(response.status).toBe(200)
674
- // The public ceiling (60), not the API-key default (100).
674
+ // The public ceiling (60), not the API-key default (10,000).
675
675
  expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"60"`)
676
676
  })
677
677
 
@@ -684,8 +684,8 @@ describe('require', () => {
684
684
  headers: { 'tempo-api-key': sandboxToken },
685
685
  })
686
686
  expect(response.status).toBe(200)
687
- // The API-key default (100), not the public ceiling (60).
688
- expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"100"`)
687
+ // The API-key default (10,000), not the public ceiling (60).
688
+ expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"10000"`)
689
689
  })
690
690
 
691
691
  test('throttles a sandbox key to the public quota when billing is unknown', async () => {