tapimo 0.6.0 → 0.7.0

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 (250) 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 +95 -87
  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 +5016 -4985
  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 +113 -113
  48. package/dist/apps/data/routes/fee-amm.d.ts +105 -91
  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 +5 -5
  57. package/dist/apps/data/routes/receipts.d.ts +6 -6
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokens.d.ts +2 -2
  60. package/dist/apps/data/routes/tokens.js +1 -1
  61. package/dist/apps/data/routes/transactions.d.ts +4 -4
  62. package/dist/apps/data/routes/transfers.d.ts +99 -88
  63. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  64. package/dist/apps/data/routes/transfers.js +226 -141
  65. package/dist/apps/data/routes/transfers.js.map +1 -1
  66. package/dist/apps/data/routes/valuation.d.ts +34 -34
  67. package/dist/apps/data/routes/valuation.js +3 -3
  68. package/dist/apps/data/routes/valuation.js.map +1 -1
  69. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  70. package/dist/apps/data/routes/webhooks.d.ts +366 -366
  71. package/dist/apps/index.d.ts +1 -0
  72. package/dist/apps/index.d.ts.map +1 -1
  73. package/dist/apps/index.js +1 -0
  74. package/dist/apps/index.js.map +1 -1
  75. package/dist/apps/management/App.d.ts +460 -523
  76. package/dist/apps/management/App.d.ts.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  78. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  79. package/dist/apps/management/routes/api-keys.js +20 -8
  80. package/dist/apps/management/routes/api-keys.js.map +1 -1
  81. package/dist/apps/management/routes/billing.d.ts +106 -151
  82. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  83. package/dist/apps/management/routes/billing.js +28 -28
  84. package/dist/apps/management/routes/billing.js.map +1 -1
  85. package/dist/apps/management/routes/faucet.d.ts +40 -26
  86. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  87. package/dist/apps/management/routes/faucet.js +33 -18
  88. package/dist/apps/management/routes/faucet.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +131 -131
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +0 -3
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +6 -8
  94. package/dist/apps/management/routes/me.d.ts.map +1 -1
  95. package/dist/apps/management/routes/me.js +0 -4
  96. package/dist/apps/management/routes/me.js.map +1 -1
  97. package/dist/apps/management/routes/members.d.ts +45 -45
  98. package/dist/apps/management/routes/orgs.d.ts +94 -94
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +5 -0
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +32 -32
  103. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  104. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  105. package/dist/apps/mpp/App.d.ts +363 -0
  106. package/dist/apps/mpp/App.d.ts.map +1 -0
  107. package/dist/apps/mpp/App.js +72 -0
  108. package/dist/apps/mpp/App.js.map +1 -0
  109. package/dist/apps/mpp/Mpp.d.ts +178 -0
  110. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  111. package/dist/apps/mpp/Mpp.js +183 -0
  112. package/dist/apps/mpp/Mpp.js.map +1 -0
  113. package/dist/apps/mpp/index.d.ts +3 -0
  114. package/dist/apps/mpp/index.d.ts.map +1 -0
  115. package/dist/apps/mpp/index.js +3 -0
  116. package/dist/apps/mpp/index.js.map +1 -0
  117. package/dist/apps/relay/App.d.ts.map +1 -1
  118. package/dist/apps/relay/App.js +56 -9
  119. package/dist/apps/relay/App.js.map +1 -1
  120. package/dist/db/Db.d.ts +2 -0
  121. package/dist/db/Db.d.ts.map +1 -1
  122. package/dist/db/Db.js +27 -0
  123. package/dist/db/Db.js.map +1 -1
  124. package/dist/db/Schema.d.ts +12 -0
  125. package/dist/db/Schema.d.ts.map +1 -1
  126. package/dist/db/Schema.js +11 -0
  127. package/dist/db/Schema.js.map +1 -1
  128. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  129. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  130. package/dist/db/tables/enabledBillingSources.js +69 -0
  131. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  132. package/dist/db/tables/invitations.d.ts +1 -12
  133. package/dist/db/tables/invitations.d.ts.map +1 -1
  134. package/dist/db/tables/invitations.js +3 -23
  135. package/dist/db/tables/invitations.js.map +1 -1
  136. package/dist/db/tables/organizations.d.ts +5 -2
  137. package/dist/db/tables/organizations.d.ts.map +1 -1
  138. package/dist/db/tables/organizations.js +13 -2
  139. package/dist/db/tables/organizations.js.map +1 -1
  140. package/dist/handlers/internal/utils.d.ts +1 -16
  141. package/dist/handlers/internal/utils.d.ts.map +1 -1
  142. package/dist/handlers/internal/utils.js +17 -6
  143. package/dist/handlers/internal/utils.js.map +1 -1
  144. package/dist/handlers/relay.d.ts.map +1 -1
  145. package/dist/handlers/relay.js +37 -5
  146. package/dist/handlers/relay.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +0 -32
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +3 -51
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/FundingQuotes.d.ts +78 -32
  152. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  153. package/dist/internal/FundingQuotes.js +45 -8
  154. package/dist/internal/FundingQuotes.js.map +1 -1
  155. package/dist/internal/Log.d.ts +18 -4
  156. package/dist/internal/Log.d.ts.map +1 -1
  157. package/dist/internal/Log.js +50 -3
  158. package/dist/internal/Log.js.map +1 -1
  159. package/dist/internal/MetricSink.d.ts.map +1 -1
  160. package/dist/internal/MetricSink.js +15 -0
  161. package/dist/internal/MetricSink.js.map +1 -1
  162. package/dist/internal/Response.d.ts +2 -2
  163. package/dist/internal/Response.d.ts.map +1 -1
  164. package/dist/internal/Response.js +12 -4
  165. package/dist/internal/Response.js.map +1 -1
  166. package/dist/internal/Viem.d.ts +1 -1
  167. package/package.json +7 -2
  168. package/src/App.test.ts +9 -4
  169. package/src/App.ts +14 -2
  170. package/src/Client.test-d.ts +109 -28
  171. package/src/Scope.ts +5 -0
  172. package/src/admin/App.test.ts +4 -0
  173. package/src/admin/apps/early-access.ts +6 -5
  174. package/src/admin/apps/organizations.test.ts +86 -2
  175. package/src/admin/apps/organizations.ts +155 -31
  176. package/src/admin/apps/scopes.test.ts +5 -0
  177. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  178. package/src/admin/ui/src/lib/organizations.ts +92 -0
  179. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  180. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  181. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  182. package/src/admin/ui.generated.ts +95 -109
  183. package/src/analytics/Analytics.test.ts +83 -0
  184. package/src/analytics/Analytics.ts +20 -0
  185. package/src/analytics/tables/requestEvents.ts +60 -7
  186. package/src/apps/composition.test.ts +11 -2
  187. package/src/apps/data/App.test.ts +1 -1
  188. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  189. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  190. package/src/apps/data/FxOracle.test.ts +2 -2
  191. package/src/apps/data/FxOracle.ts +2 -2
  192. package/src/apps/data/routes/activities.test.ts +6 -2
  193. package/src/apps/data/routes/activities.ts +39 -1
  194. package/src/apps/data/routes/balances.test.ts +6 -2
  195. package/src/apps/data/routes/exchanges.test.ts +7 -3
  196. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  197. package/src/apps/data/routes/fee-amm.ts +98 -119
  198. package/src/apps/data/routes/funding.test.ts +29 -8
  199. package/src/apps/data/routes/funding.ts +42 -48
  200. package/src/apps/data/routes/receipts.test.ts +6 -2
  201. package/src/apps/data/routes/tokens.ts +1 -1
  202. package/src/apps/data/routes/transfers.test.ts +203 -35
  203. package/src/apps/data/routes/transfers.ts +294 -172
  204. package/src/apps/data/routes/valuation.test.ts +18 -3
  205. package/src/apps/data/routes/valuation.ts +4 -4
  206. package/src/apps/index.ts +1 -0
  207. package/src/apps/management/routes/api-keys.test.ts +2 -75
  208. package/src/apps/management/routes/api-keys.ts +30 -7
  209. package/src/apps/management/routes/billing.test.ts +106 -85
  210. package/src/apps/management/routes/billing.ts +32 -21
  211. package/src/apps/management/routes/faucet.test.ts +11 -3
  212. package/src/apps/management/routes/faucet.ts +46 -30
  213. package/src/apps/management/routes/invitations.test.ts +0 -172
  214. package/src/apps/management/routes/invitations.ts +0 -3
  215. package/src/apps/management/routes/me.test.ts +3 -145
  216. package/src/apps/management/routes/me.ts +0 -6
  217. package/src/apps/management/routes/orgs.test.ts +45 -2
  218. package/src/apps/management/routes/orgs.ts +6 -0
  219. package/src/apps/management/routes/scopes.test.ts +1 -0
  220. package/src/apps/mpp/App.ts +86 -0
  221. package/src/apps/mpp/Mpp.test.ts +193 -0
  222. package/src/apps/mpp/Mpp.ts +353 -0
  223. package/src/apps/mpp/index.ts +2 -0
  224. package/src/apps/mpp/internal/Adapter.ts +11 -0
  225. package/src/apps/mpp/internal/Error.ts +9 -0
  226. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  227. package/src/apps/mpp/internal/Screening.ts +10 -0
  228. package/src/apps/relay/App.test.ts +88 -6
  229. package/src/apps/relay/App.ts +61 -9
  230. package/src/db/Db.test.ts +2 -2
  231. package/src/db/Db.ts +30 -0
  232. package/src/db/Schema.ts +15 -0
  233. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  234. package/src/db/tables/enabledBillingSources.ts +118 -0
  235. package/src/db/tables/invitations.ts +4 -26
  236. package/src/db/tables/organizations.test.ts +103 -0
  237. package/src/db/tables/organizations.ts +18 -2
  238. package/src/handlers/internal/utils.ts +22 -6
  239. package/src/handlers/relay.errors.test.ts +153 -0
  240. package/src/handlers/relay.ts +42 -7
  241. package/src/internal/Auth.test.ts +3 -3
  242. package/src/internal/Auth.ts +3 -63
  243. package/src/internal/FundingQuotes.test.ts +18 -5
  244. package/src/internal/FundingQuotes.ts +46 -11
  245. package/src/internal/Log.test.ts +91 -0
  246. package/src/internal/Log.ts +69 -5
  247. package/src/internal/MetricSink.test.ts +55 -0
  248. package/src/internal/MetricSink.ts +14 -0
  249. package/src/internal/Response.test.ts +28 -0
  250. package/src/internal/Response.ts +12 -4
@@ -33,37 +33,16 @@ export const signature = 'event Transfer(address indexed from, address indexed t
33
33
  const memoSignature =
34
34
  'event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo)'
35
35
 
36
+ const tokenFields = ['token.logoUri', 'token.verified'] as const
37
+
36
38
  /** Zod schemas owned by the transfer handlers. */
37
39
  export namespace schema {
38
- /**
39
- * The token moved by a transfer: its contract address and the transferred
40
- * amount, plus the trimmed token reference spread in when requested via
41
- * `include=token`. Reference fields are absent on the base (no-`include`)
42
- * response and best-effort when requested (a row whose metadata is
43
- * unavailable keeps just `address`/`amount`). Shared by every per-row
44
- * transfer response across the API.
45
- *
46
- * The optional metadata fields are inlined here (rather than spread from
47
- * `Tokens.schema.Token` via `z.partial`) so this module avoids referencing
48
- * the `tokens` schema namespace at module-init time — `transfers` and
49
- * `tokens` import each other for runtime helpers (`Transfers.query`,
50
- * `Tokens.resolveToken`), and the schema-evaluation back-edge would
51
- * otherwise deadlock the namespace.
52
- */
40
+ /** A transfer token reference with RPC metadata. Inlined to avoid the `transfers`/`tokens` module cycle. */
53
41
  export const SourceToken = z
54
42
  .object({
55
43
  address: Schema.TokenAddress.check(z.describe('The TIP-20 token contract address on Tempo.')),
56
- amount: z
57
- .string()
58
- .check(
59
- z.regex(/^\d+$/),
60
- z.describe(
61
- 'A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token.',
62
- ),
63
- z.meta({ examples: ['1000000'] }),
64
- ),
65
44
  currency: z
66
- .optional(z.string())
45
+ .string()
67
46
  .check(
68
47
  z.describe(
69
48
  'The currency label for this token, such as `USD` for USD-denominated stablecoins.',
@@ -71,7 +50,8 @@ export namespace schema {
71
50
  z.meta({ examples: ['USD'] }),
72
51
  ),
73
52
  decimals: z
74
- .optional(z.number().check(z.int(), z.nonnegative()))
53
+ .number()
54
+ .check(z.int(), z.nonnegative())
75
55
  .check(
76
56
  z.describe(
77
57
  'The number of decimal places the token uses; Tempo stablecoins typically use 6.',
@@ -79,7 +59,7 @@ export namespace schema {
79
59
  z.meta({ examples: [6] }),
80
60
  ),
81
61
  logoUri: z.optional(z.string()).check(
82
- z.describe('A URL for the token’s logo image, when one is available.'),
62
+ z.describe('A URL for the token’s logo image, returned with `include=token.logoUri`.'),
83
63
  z.meta({
84
64
  examples: [
85
65
  'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
@@ -87,35 +67,41 @@ export namespace schema {
87
67
  }),
88
68
  ),
89
69
  name: z
90
- .optional(z.string())
70
+ .string()
91
71
  .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['USD Coin'] })),
92
72
  symbol: z
93
- .optional(z.string())
73
+ .string()
94
74
  .check(
95
75
  z.describe('The short ticker symbol wallets and apps show for this token.'),
96
76
  z.meta({ examples: ['USDC'] }),
97
77
  ),
98
- valuation: z
99
- .optional(z.nullable(Valuation.schema.Value))
100
- .check(
101
- z.describe(
102
- 'The leg’s nominal value in the requested `denomination`. `null` when the token is ' +
103
- 'unverified, its display currency has no rate, or rates were unavailable.',
104
- ),
105
- ),
106
78
  verified: z
107
79
  .optional(z.boolean())
108
80
  .check(
109
- z.describe('Whether this token is in Tempo’s curated verified token list.'),
81
+ z.describe(
82
+ 'Whether this token is in Tempo’s curated verified list, returned with `include=token.verified`.',
83
+ ),
110
84
  z.meta({ examples: [true] }),
111
85
  ),
112
86
  })
113
87
  .check(
114
88
  z.describe(
115
- 'The token and amount that moved in this transfer, with token metadata added when you request `include=token`.',
89
+ 'The token that moved in this transfer, with RPC metadata and optional curated fields.',
116
90
  ),
117
91
  )
118
92
 
93
+ /** A token amount carrying its nominal value in the requested denomination. */
94
+ const ValuedAmount = z.extend(Schema.TokenAmount, {
95
+ valuation: z
96
+ .optional(z.nullable(Valuation.schema.Value))
97
+ .check(
98
+ z.describe(
99
+ 'The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
100
+ 'unverified, its display currency has no rate, or rates were unavailable.',
101
+ ),
102
+ ),
103
+ })
104
+
119
105
  /**
120
106
  * Filter predicates shared verbatim by the `GET /transfers` list query
121
107
  * ({@link getTransfers.Query}, which spreads `Predicates.shape`) and `transfer`
@@ -147,18 +133,17 @@ export namespace schema {
147
133
  export namespace getTransfers {
148
134
  /** Optional related resources that callers opt into via `include`. */
149
135
  export const Include = z
150
- .enum(['crossToken', 'memo', 'token', 'totalCount'])
136
+ .enum(['crossToken', 'memo', ...tokenFields, 'totalCount'])
151
137
  .check(z.describe('Related resources to include only when you request them with `include`.'))
152
138
 
153
139
  /**
154
140
  * Parses a comma-separated `include` query value into a list of optional
155
- * resources to embed. Extra lookups (token metadata, the `TransferWithMemo`
156
- * memo/attribution decode, the capped total count) each run only when their
157
- * resource is requested, keeping the base transfer page fast.
141
+ * resources to embed. Extra lookups run only when their resource is
142
+ * requested, keeping the base transfer page fast.
158
143
  */
159
144
  export const includeQuery = Schema.includeQuery(
160
145
  Include,
161
- 'Comma-separated related resources to include, such as `token,memo,crossToken,totalCount`.',
146
+ 'Comma-separated related resources to include, such as `token.logoUri,token.verified,memo,crossToken,totalCount`.',
162
147
  )
163
148
 
164
149
  /**
@@ -225,11 +210,18 @@ export namespace schema {
225
210
  z.describe('The block number where this transfer was recorded.'),
226
211
  z.meta({ examples: [23456789] }),
227
212
  ),
213
+ destinationAmount: z
214
+ .optional(ValuedAmount)
215
+ .check(
216
+ z.describe(
217
+ 'Amount delivered to the recipient for a cross-token transfer. Omitted with `destinationToken` for a same-token transfer.',
218
+ ),
219
+ ),
228
220
  destinationToken: z
229
221
  .optional(schema.SourceToken)
230
222
  .check(
231
223
  z.describe(
232
- 'The token and amount delivered to the recipient when it differs from `sourceToken` — a cross-token transfer routed through the DEX, where `sourceToken` is the token swapped in. Omitted for same-token transfers, and only returned when you request `include=crossToken`.',
224
+ 'The token delivered to the recipient when it differs from `sourceToken`. Omitted for same-token transfers and returned only with `include=crossToken`.',
233
225
  ),
234
226
  ),
235
227
  id: z.string().check(
@@ -260,7 +252,8 @@ export namespace schema {
260
252
  z.describe('The account address that received the tokens.'),
261
253
  ),
262
254
  sender: Schema.Address.check(z.describe('The account address that sent the tokens.')),
263
- sourceToken: schema.SourceToken.check(z.describe('The token and amount that moved.')),
255
+ sourceAmount: ValuedAmount.check(z.describe('Amount associated with `sourceToken`.')),
256
+ sourceToken: schema.SourceToken.check(z.describe('The token that moved.')),
264
257
  timestamp: z.iso
265
258
  .datetime()
266
259
  .check(
@@ -272,6 +265,14 @@ export namespace schema {
272
265
  ),
273
266
  })
274
267
  .check(
268
+ z.refine(
269
+ (data) =>
270
+ (data.destinationToken === undefined) === (data.destinationAmount === undefined),
271
+ {
272
+ error: '`destinationAmount` and `destinationToken` must be returned together.',
273
+ path: ['destinationAmount'],
274
+ },
275
+ ),
275
276
  z.describe(
276
277
  'One TIP-20 token transfer event, optionally including a payment memo for reconciliation or invoices.',
277
278
  ),
@@ -309,6 +310,14 @@ export namespace schema {
309
310
  }
310
311
  }
311
312
 
313
+ type UnenrichedTransfer = Omit<
314
+ z.output<typeof schema.getTransfers.Transfer>,
315
+ 'destinationAmount' | 'destinationToken' | 'sourceAmount' | 'sourceToken'
316
+ > & {
317
+ destinationToken?: CrossToken | undefined
318
+ sourceToken: CrossToken
319
+ }
320
+
312
321
  /** Creates transfer handlers. */
313
322
  export function transfers(options: transfers.Options = {}) {
314
323
  const oracle = options.fx?.oracle ?? FxOracle.ecb()
@@ -323,7 +332,7 @@ export function transfers(options: transfers.Options = {}) {
323
332
  description: 'List token transfer events across Tempo.',
324
333
  operationId: 'getTransfers',
325
334
  responses: OpenApi.responses({
326
- errors: { 502: 'The upstream indexer could not serve this request.' },
335
+ errors: { 502: 'The indexer or Tempo RPC could not serve this request.' },
327
336
  success: {
328
337
  description: 'A page of token transfer events.',
329
338
  schema: schema.getTransfers.Response,
@@ -334,7 +343,7 @@ export function transfers(options: transfers.Options = {}) {
334
343
  }),
335
344
  Cache.response<App.Environment>({
336
345
  cacheControl: Cache.policies.feed,
337
- name: 'tempo-api:transfers:v1',
346
+ name: 'tempo-api:transfers:v2',
338
347
  key: (c) => Cache.urlKey(c, schema.getTransfers.Query),
339
348
  }),
340
349
  async (c) => {
@@ -388,12 +397,7 @@ export function transfers(options: transfers.Options = {}) {
388
397
  timing: 'transfers',
389
398
  })
390
399
 
391
- // Token metadata is opt-in. When requested, resolve each page token's
392
- // metadata concurrently (sharing one verified-token fetch) and spread
393
- // it into the row's `sourceToken`. Resolution is best-effort: a token
394
- // whose metadata is unavailable keeps `sourceToken` as just
395
- // `address`/`amount` on that row rather than failing the whole page.
396
- let data: z.output<typeof schema.getTransfers.Transfer>[] = page.data.map((row) => ({
400
+ let data: UnenrichedTransfer[] = page.data.map((row) => ({
397
401
  blockNumber: row.blockNumber,
398
402
  id: `${row.transactionHash}-${row.logIndex}`,
399
403
  logIndex: row.logIndex,
@@ -460,43 +464,45 @@ export function transfers(options: transfers.Options = {}) {
460
464
  data.length > 0
461
465
  ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
462
466
  : undefined
463
- if (options.include.includes('token') && data.length > 0 && snapshot) {
464
- const uniqueTokens = Array.from(
465
- new Set(
466
- data.flatMap((transfer) => [
467
- transfer.sourceToken.address,
468
- ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
469
- ]),
470
- ),
471
- )
472
- const resolved = await Timing.time(c, 'tokens', () =>
467
+ const includeLogoUri = options.include.includes('token.logoUri')
468
+ const includeVerified = options.include.includes('token.verified')
469
+ const uniqueTokens = Array.from(
470
+ new Set(
471
+ data.flatMap((transfer) => [
472
+ transfer.sourceToken.address,
473
+ ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
474
+ ]),
475
+ ),
476
+ )
477
+ const tokensByAddress = new Map(
478
+ await Timing.time(c, 'tokens', () =>
473
479
  Promise.all(
474
- uniqueTokens.map((token) =>
475
- Tokens.resolveToken(c, {
476
- address: token,
477
- chainId,
478
- snapshot,
479
- })
480
- .then((metadata) => [token, metadata] as const)
481
- .catch(() => [token, undefined] as const),
482
- ),
480
+ uniqueTokens.map(async (token) => {
481
+ const [logoUri, metadata] = await Promise.all([
482
+ includeLogoUri
483
+ ? Timing.time(c, 'token_logo', () =>
484
+ Tokens.getTokenLogo(c, { address: token, chainId }),
485
+ ).catch(() => undefined)
486
+ : undefined,
487
+ Timing.time(c, 'token_metadata', () =>
488
+ Tokens.getTokenMetadata(c, { address: token, chainId }),
489
+ ),
490
+ ])
491
+ return [
492
+ token.toLowerCase(),
493
+ {
494
+ ...metadata,
495
+ logoUri: includeLogoUri
496
+ ? (logoUri ??
497
+ snapshot?.byAddress.get(token.toLowerCase())?.logoUri ??
498
+ metadata.logoUri)
499
+ : undefined,
500
+ },
501
+ ] as const
502
+ }),
483
503
  ),
484
- )
485
- const tokensByAddress = new Map(resolved)
486
- const withToken = <T extends { address: z.output<typeof Schema.TokenAddress> }>(
487
- entry: T,
488
- ) => {
489
- const token = tokensByAddress.get(entry.address)
490
- return token ? { ...entry, ...token } : entry
491
- }
492
- data = data.map((transfer) => ({
493
- ...transfer,
494
- sourceToken: withToken(transfer.sourceToken),
495
- ...(transfer.destinationToken
496
- ? { destinationToken: withToken(transfer.destinationToken) }
497
- : {}),
498
- }))
499
- }
504
+ ),
505
+ )
500
506
 
501
507
  // Value each leg via its curated display currency, in the requested
502
508
  // denomination.
@@ -513,22 +519,55 @@ export function transfers(options: transfers.Options = {}) {
513
519
  denomination,
514
520
  oracle,
515
521
  })
516
- const valued = <leg extends { address: string; amount: string }>(leg: leg) => ({
517
- ...leg,
518
- valuation: Valuation.valuationFor({
519
- amount: BigInt(leg.amount),
520
- denomination,
521
- rates,
522
- token: snapshot?.byAddress.get(leg.address.toLowerCase()),
523
- }),
524
- })
525
- data = data.map((transfer) => ({
526
- ...transfer,
527
- sourceToken: valued(transfer.sourceToken),
528
- ...(transfer.destinationToken
529
- ? { destinationToken: valued(transfer.destinationToken) }
530
- : {}),
531
- }))
522
+ const enrich = (leg: CrossToken) => {
523
+ const metadata = tokensByAddress.get(leg.address.toLowerCase())
524
+ if (!metadata) throw new Error(`Token metadata unavailable for ${leg.address}`)
525
+ return {
526
+ amount: {
527
+ ...Value.tokenAmount({
528
+ baseUnits: leg.amount,
529
+ currency: metadata.currency,
530
+ decimals: metadata.decimals,
531
+ }),
532
+ valuation: Valuation.valuationFor({
533
+ amount: BigInt(leg.amount),
534
+ denomination,
535
+ rates,
536
+ token: snapshot?.byAddress.get(leg.address.toLowerCase()),
537
+ }),
538
+ },
539
+ token: {
540
+ address: leg.address,
541
+ currency: metadata.currency,
542
+ decimals: metadata.decimals,
543
+ ...(metadata.logoUri !== undefined ? { logoUri: metadata.logoUri } : {}),
544
+ name: metadata.name,
545
+ symbol: metadata.symbol,
546
+ ...(includeVerified && snapshot
547
+ ? { verified: snapshot.byAddress.has(leg.address.toLowerCase()) }
548
+ : {}),
549
+ },
550
+ }
551
+ }
552
+ const responseData: z.output<typeof schema.getTransfers.Transfer>[] = data.map(
553
+ ({ destinationToken, sourceToken, ...transfer }) => {
554
+ const source = enrich(sourceToken)
555
+ if (!destinationToken)
556
+ return {
557
+ ...transfer,
558
+ sourceAmount: source.amount,
559
+ sourceToken: source.token,
560
+ }
561
+ const destination = enrich(destinationToken)
562
+ return {
563
+ ...transfer,
564
+ destinationAmount: destination.amount,
565
+ destinationToken: destination.token,
566
+ sourceAmount: source.amount,
567
+ sourceToken: source.token,
568
+ }
569
+ },
570
+ )
532
571
 
533
572
  const counts = countPromise ? await countPromise : undefined
534
573
  const meta = {
@@ -538,7 +577,7 @@ export function transfers(options: transfers.Options = {}) {
538
577
 
539
578
  return c.json(
540
579
  Response.validated(schema.getTransfers.Response, {
541
- data,
580
+ data: responseData,
542
581
  ...(Object.keys(meta).length > 0 ? { meta } : {}),
543
582
  nextCursor: page.nextCursor,
544
583
  }),
@@ -563,15 +602,13 @@ export declare namespace transfers {
563
602
 
564
603
  /**
565
604
  * Shared TIP-20 `Transfer` page query. Used by `GET /transfers` and the
566
- * webhook poller. Builds the signature-decoded `Transfer` CTE query, applies
605
+ * webhook poller. Builds signature-decoded `Transfer` CTE queries, applies
567
606
  * opaque keyset pagination on `(block_num, log_idx)`, and decodes rows into a
568
607
  * strongly-typed intermediate shape that each caller maps to its own public
569
608
  * response.
570
609
  *
571
- * `eitherSide` (the subject address must appear on _either_ side of the
572
- * transfer) is exposed alongside the one-sided `sender`/`recipient` filters
573
- * because composing it with the keyset cursor pre-WHERE in SQL is awkward at
574
- * the call site; the helper takes the knob directly.
610
+ * `eitherSide` is exposed alongside one-sided `sender`/`recipient` filters
611
+ * because it requires two upstream scans. The helper owns their ordered merge.
575
612
  *
576
613
  * Memoization lives inside the helper under one canonical `transfers:v1:…`
577
614
  * key derived from every filter — so equivalent filter combinations share a
@@ -661,11 +698,11 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
661
698
  // translate to a SQL OFFSET of `(page - 1) * limit` rows.
662
699
  const offset =
663
700
  options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined
701
+ const address = options.eitherSide
664
702
 
665
- // Transfer events filtered by any combination of contract / subject /
666
- // sender / recipient / block / timestamp. Ordered by block then log
667
- // index so transfers within a block stay stable. The keyset cursor clause is
668
- // page-only and added on top of the shared filters below.
703
+ // Transfer events filtered by any combination of contract / sender /
704
+ // recipient / block / timestamp. The address shortcut is split below so
705
+ // each upstream query can use one indexed side.
669
706
  const filters = filterClauses(options)
670
707
  if (cursor !== undefined)
671
708
  filters.push(
@@ -674,24 +711,41 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
674
711
  { literal: Cursor.literal(cursor[1]!, 'int'), name: 'log_idx', order },
675
712
  ]),
676
713
  )
677
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
678
-
679
- // Fetch one extra row to detect `hasMore` without a separate count query.
680
- const result = await tidx.fetch({
681
- chainId,
682
- ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
683
- query: `
684
- SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
685
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
686
- ${where}
687
- ORDER BY block_num ${direction}, log_idx ${direction}
688
- LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
689
- ` as string,
690
- })
714
+ const addressScoped = address !== undefined
715
+ const window = addressScoped ? (offset ?? 0) + limit + 1 : limit + 1
716
+ const fetchRows = async (side?: 'from' | 'to') => {
717
+ const sideFilters =
718
+ side !== undefined && address !== undefined
719
+ ? [`"${side}" = '${address}'`, ...filters]
720
+ : filters
721
+ const where = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : ''
722
+ const result = await tidx.fetch({
723
+ chainId,
724
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
725
+ query: `
726
+ SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
727
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
728
+ ${where}
729
+ ORDER BY block_num ${direction}, log_idx ${direction}
730
+ LIMIT ${window}${!addressScoped && offset !== undefined ? ` OFFSET ${offset}` : ''}
731
+ ` as string,
732
+ })
733
+ return result.rows
734
+ }
735
+
736
+ // TIDX cannot plan `(from = X OR to = X)` over the decoded Transfer table.
737
+ // Merge two bounded, server-filtered streams while preserving global order.
738
+ const rows = addressScoped
739
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
740
+ limit: limit + 1,
741
+ offset: offset ?? 0,
742
+ order,
743
+ })
744
+ : await fetchRows()
691
745
 
692
746
  // The next page anchors below the last fetched row's `(block, log_idx)`.
693
747
  const page = Cursor.paginate({
694
- rows: result.rows,
748
+ rows,
695
749
  limit,
696
750
  key: (row) => {
697
751
  const block = Value.toNumber(row['block_num'])
@@ -738,6 +792,49 @@ export async function scan(deps: scan.Deps, options: scan.Options): Promise<scan
738
792
  return { hasMore: page.hasMore, items, limit, nextCursor: page.nextCursor }
739
793
  }
740
794
 
795
+ /** Deduplicates and globally orders bounded `from`/`to` transfer streams. */
796
+ function mergeTransferRows(
797
+ rows: readonly Record<string, unknown>[],
798
+ options: mergeTransferRows.Options,
799
+ ): Record<string, unknown>[] {
800
+ const positioned: mergeTransferRows.PositionedRow[] = []
801
+ const seen = new Set<string>()
802
+ for (const row of rows) {
803
+ const blockNumber = Value.toNumber(row['block_num'])
804
+ const logIndex = Value.toNumber(row['log_idx'])
805
+ const transactionHash = Value.toText(row['tx_hash'])
806
+ if (blockNumber === undefined || logIndex === undefined || !transactionHash) continue
807
+ const key = eventKey(transactionHash, logIndex)
808
+ if (seen.has(key)) continue
809
+ seen.add(key)
810
+ positioned.push({ blockNumber, logIndex, row })
811
+ }
812
+ positioned.sort((a, b) => {
813
+ const comparison = a.blockNumber - b.blockNumber || a.logIndex - b.logIndex
814
+ return options.order === 'asc' ? comparison : -comparison
815
+ })
816
+ return positioned.slice(options.offset, options.offset + options.limit).map(({ row }) => row)
817
+ }
818
+
819
+ declare namespace mergeTransferRows {
820
+ type Options = {
821
+ /** Maximum merged rows to return. */
822
+ limit: number
823
+ /** Number of globally ordered rows to skip. */
824
+ offset: number
825
+ /** Global transfer ordering. */
826
+ order: 'asc' | 'desc'
827
+ }
828
+ type PositionedRow = {
829
+ /** Transfer block number. */
830
+ blockNumber: number
831
+ /** Transfer log index. */
832
+ logIndex: number
833
+ /** Raw TIDX row. */
834
+ row: Record<string, unknown>
835
+ }
836
+ }
837
+
741
838
  export declare namespace scan {
742
839
  /** Dependencies for {@link scan}. */
743
840
  type Deps = {
@@ -767,11 +864,9 @@ export declare namespace scan {
767
864
  * physical `token` column; unscoped queries omit it and use the `Transfer` CTE.
768
865
  */
769
866
  function filterClauses(options: filterClauses.Options): string[] {
770
- const { eitherSide, fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } =
771
- options
867
+ const { fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } = options
772
868
  const filters: string[] = []
773
869
  if (token !== undefined) filters.push(`token = '${token}'`)
774
- if (eitherSide !== undefined) filters.push(`("from" = '${eitherSide}' OR "to" = '${eitherSide}')`)
775
870
  if (sender !== undefined) filters.push(`"from" = '${sender}'`)
776
871
  if (recipient !== undefined) filters.push(`"to" = '${recipient}'`)
777
872
  if (fromBlock !== undefined) filters.push(`block_num >= ${fromBlock}`)
@@ -785,14 +880,7 @@ declare namespace filterClauses {
785
880
  /** The non-pagination filter fields of {@link scan.Options}. */
786
881
  type Options = Pick<
787
882
  scan.Options,
788
- | 'eitherSide'
789
- | 'fromBlock'
790
- | 'fromTimestamp'
791
- | 'recipient'
792
- | 'sender'
793
- | 'toBlock'
794
- | 'toTimestamp'
795
- | 'token'
883
+ 'fromBlock' | 'fromTimestamp' | 'recipient' | 'sender' | 'toBlock' | 'toTimestamp' | 'token'
796
884
  >
797
885
  }
798
886
 
@@ -841,7 +929,7 @@ export declare namespace count {
841
929
  /**
842
930
  * Context-free core of {@link count}: counts token-scoped rows from
843
931
  * `token_transfers`, otherwise the decoded `Transfer` CTE. The capped inner
844
- * scan bounds work; the SQL aggregate runs on ClickHouse.
932
+ * scan bounds work while preserving each source's established query engine.
845
933
  */
846
934
  export async function countScan(
847
935
  deps: scan.Deps,
@@ -851,56 +939,90 @@ export async function countScan(
851
939
  const { tidx } = deps
852
940
  const filters = filterClauses(options)
853
941
  const tokenScoped = options.token !== undefined
854
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
942
+ const address = options.eitherSide
855
943
 
856
944
  // Folded count: cross-token transfers collapse several `Transfer` legs into
857
945
  // one row, so a raw `count(*)` would overcount. Scan the capped `Transfer`
858
946
  // rows, classify each transaction, and subtract the folded-away legs. Bounded
859
947
  // by `countCap` like the raw count, so it stays within the indexer's budget.
860
948
  if (options.crossToken) {
949
+ const fetchRows = async (side?: 'from' | 'to') => {
950
+ const sideFilters =
951
+ side !== undefined && address !== undefined
952
+ ? [`"${side}" = '${address}'`, ...filters]
953
+ : filters
954
+ const sideWhere = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : ''
955
+ const result = await tidx.fetch({
956
+ chainId,
957
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
958
+ query: `
959
+ SELECT tx_hash, block_num, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
960
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
961
+ ${sideWhere}
962
+ ORDER BY block_num, log_idx
963
+ LIMIT ${Schema.countCap}
964
+ ` as string,
965
+ })
966
+ return result.rows
967
+ }
968
+ const rows =
969
+ address !== undefined
970
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
971
+ limit: Schema.countCap,
972
+ offset: 0,
973
+ order: 'asc',
974
+ })
975
+ : await fetchRows()
976
+ const rawCount = rows.length
977
+ let suppressed = 0
978
+ for (const legs of legsByTransaction(rows).values()) {
979
+ const bundle = classifyCrossToken(legs)
980
+ if (bundle) suppressed += bundle.suppressLogIndexes.length
981
+ }
982
+ return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed }
983
+ }
984
+
985
+ const countRows = async (additionalFilters: readonly string[] = []) => {
986
+ const countFilters = [...additionalFilters, ...filters]
987
+ const countWhere = countFilters.length > 0 ? `WHERE ${countFilters.join(' AND ')}` : ''
861
988
  const result = await tidx.fetch({
862
989
  chainId,
863
990
  ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
864
991
  query: `
865
- SELECT tx_hash, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
866
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
867
- ${where}
868
- ORDER BY block_num, log_idx
869
- LIMIT ${Schema.countCap}
992
+ SELECT count(*) AS total
993
+ FROM (
994
+ SELECT 1
995
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
996
+ ${countWhere}
997
+ LIMIT ${Schema.countCap}
998
+ ) AS capped
870
999
  ` as string,
871
1000
  })
872
- const rawCount = result.rows.length
873
- let suppressed = 0
874
- for (const legs of legsByTransaction(result.rows).values()) {
875
- const bundle = classifyCrossToken(legs)
876
- if (bundle) suppressed += bundle.suppressLogIndexes.length
877
- }
878
- return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed }
1001
+ return Value.toNumber(result.rows[0]?.['total']) ?? 0
879
1002
  }
880
1003
 
881
- const result = await tidx.fetch({
882
- chainId,
883
- engine: 'clickhouse',
884
- ...(tokenScoped ? {} : { signatures: [signature] }),
885
- query: `
886
- SELECT count(*) AS total
887
- FROM (
888
- SELECT 1
889
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
890
- ${where}
891
- LIMIT ${Schema.countCap}
892
- ) AS capped
893
- ` as string,
894
- })
1004
+ if (address !== undefined) {
1005
+ // Keep branches disjoint so self-transfers count once.
1006
+ const [sent, received] = await Promise.all([
1007
+ countRows([`"from" = '${address}'`]),
1008
+ countRows([`"to" = '${address}'`, `"from" != '${address}'`]),
1009
+ ])
1010
+ const rawCount = sent + received
1011
+ return {
1012
+ totalCount: Math.min(rawCount, Schema.countCap),
1013
+ totalCountCapped:
1014
+ sent >= Schema.countCap || received >= Schema.countCap || rawCount >= Schema.countCap,
1015
+ }
1016
+ }
895
1017
 
896
- const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0
1018
+ const totalCount = await countRows()
897
1019
  return { totalCountCapped: totalCount >= Schema.countCap, totalCount }
898
1020
  }
899
1021
 
900
1022
  export declare namespace countScan {
901
1023
  /** Options for {@link countScan}: the shared filters plus the target chain. */
902
1024
  type Options = filterClauses.Options &
903
- Pick<scan.Options, 'chainId'> & {
1025
+ Pick<scan.Options, 'chainId' | 'eitherSide'> & {
904
1026
  /** Count folded cross-token transfers instead of raw `Transfer` logs. */
905
1027
  crossToken?: boolean | undefined
906
1028
  }
@@ -984,7 +1106,7 @@ declare namespace memoMap {
984
1106
  }
985
1107
  }
986
1108
 
987
- /** A token's contract address and moved amount, matching `SourceToken`'s core. */
1109
+ /** A transfer leg before amount and token metadata are separated. */
988
1110
  type CrossToken = { address: z.output<typeof Schema.Address>; amount: string }
989
1111
 
990
1112
  /** A decoded `Transfer` leg used for cross-token classification. */