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
@@ -26,56 +26,44 @@ export const signature = 'event Transfer(address indexed from, address indexed t
26
26
  // text memo is decoded into `memo`; an MPP attribution fingerprint memo is
27
27
  // resolved into `attribution` instead.
28
28
  const memoSignature = 'event TransferWithMemo(address indexed from, address indexed to, uint256 amount, bytes32 indexed memo)';
29
+ const tokenFields = ['token.logoUri', 'token.verified'];
29
30
  /** Zod schemas owned by the transfer handlers. */
30
31
  export var schema;
31
32
  (function (schema) {
32
- /**
33
- * The token moved by a transfer: its contract address and the transferred
34
- * amount, plus the trimmed token reference spread in when requested via
35
- * `include=token`. Reference fields are absent on the base (no-`include`)
36
- * response and best-effort when requested (a row whose metadata is
37
- * unavailable keeps just `address`/`amount`). Shared by every per-row
38
- * transfer response across the API.
39
- *
40
- * The optional metadata fields are inlined here (rather than spread from
41
- * `Tokens.schema.Token` via `z.partial`) so this module avoids referencing
42
- * the `tokens` schema namespace at module-init time — `transfers` and
43
- * `tokens` import each other for runtime helpers (`Transfers.query`,
44
- * `Tokens.resolveToken`), and the schema-evaluation back-edge would
45
- * otherwise deadlock the namespace.
46
- */
33
+ /** A transfer token reference with RPC metadata. Inlined to avoid the `transfers`/`tokens` module cycle. */
47
34
  schema.SourceToken = z
48
35
  .object({
49
36
  address: Schema.TokenAddress.check(z.describe('The TIP-20 token contract address on Tempo.')),
50
- amount: z
51
- .string()
52
- .check(z.regex(/^\d+$/), z.describe('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.'), z.meta({ examples: ['1000000'] })),
53
37
  currency: z
54
- .optional(z.string())
38
+ .string()
55
39
  .check(z.describe('The currency label for this token, such as `USD` for USD-denominated stablecoins.'), z.meta({ examples: ['USD'] })),
56
40
  decimals: z
57
- .optional(z.number().check(z.int(), z.nonnegative()))
41
+ .number()
42
+ .check(z.int(), z.nonnegative())
58
43
  .check(z.describe('The number of decimal places the token uses; Tempo stablecoins typically use 6.'), z.meta({ examples: [6] })),
59
- logoUri: z.optional(z.string()).check(z.describe('A URL for the token’s logo image, when one is available.'), z.meta({
44
+ logoUri: z.optional(z.string()).check(z.describe('A URL for the token’s logo image, returned with `include=token.logoUri`.'), z.meta({
60
45
  examples: [
61
46
  'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
62
47
  ],
63
48
  })),
64
49
  name: z
65
- .optional(z.string())
50
+ .string()
66
51
  .check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['USD Coin'] })),
67
52
  symbol: z
68
- .optional(z.string())
53
+ .string()
69
54
  .check(z.describe('The short ticker symbol wallets and apps show for this token.'), z.meta({ examples: ['USDC'] })),
70
- valuation: z
71
- .optional(z.nullable(Valuation.schema.Value))
72
- .check(z.describe('The leg’s nominal value in the requested `denomination`. `null` when the token is ' +
73
- 'unverified, its display currency has no rate, or rates were unavailable.')),
74
55
  verified: z
75
56
  .optional(z.boolean())
76
- .check(z.describe('Whether this token is in Tempo’s curated verified token list.'), z.meta({ examples: [true] })),
57
+ .check(z.describe('Whether this token is in Tempo’s curated verified list, returned with `include=token.verified`.'), z.meta({ examples: [true] })),
77
58
  })
78
- .check(z.describe('The token and amount that moved in this transfer, with token metadata added when you request `include=token`.'));
59
+ .check(z.describe('The token that moved in this transfer, with RPC metadata and optional curated fields.'));
60
+ /** A token amount carrying its nominal value in the requested denomination. */
61
+ const ValuedAmount = z.extend(Schema.TokenAmount, {
62
+ valuation: z
63
+ .optional(z.nullable(Valuation.schema.Value))
64
+ .check(z.describe('The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
65
+ 'unverified, its display currency has no rate, or rates were unavailable.')),
66
+ });
79
67
  /**
80
68
  * Filter predicates shared verbatim by the `GET /transfers` list query
81
69
  * ({@link getTransfers.Query}, which spreads `Predicates.shape`) and `transfer`
@@ -105,15 +93,14 @@ export var schema;
105
93
  (function (getTransfers) {
106
94
  /** Optional related resources that callers opt into via `include`. */
107
95
  getTransfers.Include = z
108
- .enum(['crossToken', 'memo', 'token', 'totalCount'])
96
+ .enum(['crossToken', 'memo', ...tokenFields, 'totalCount'])
109
97
  .check(z.describe('Related resources to include only when you request them with `include`.'));
110
98
  /**
111
99
  * Parses a comma-separated `include` query value into a list of optional
112
- * resources to embed. Extra lookups (token metadata, the `TransferWithMemo`
113
- * memo/attribution decode, the capped total count) each run only when their
114
- * resource is requested, keeping the base transfer page fast.
100
+ * resources to embed. Extra lookups run only when their resource is
101
+ * requested, keeping the base transfer page fast.
115
102
  */
116
- getTransfers.includeQuery = Schema.includeQuery(getTransfers.Include, 'Comma-separated related resources to include, such as `token,memo,crossToken,totalCount`.');
103
+ getTransfers.includeQuery = Schema.includeQuery(getTransfers.Include, 'Comma-separated related resources to include, such as `token.logoUri,token.verified,memo,crossToken,totalCount`.');
117
104
  /**
118
105
  * Query parameters for top-level transfer list requests. `address` is a
119
106
  * convenience for "either side" — passing it alongside an explicit
@@ -159,9 +146,12 @@ export var schema;
159
146
  blockNumber: z
160
147
  .number()
161
148
  .check(z.int(), z.nonnegative(), z.describe('The block number where this transfer was recorded.'), z.meta({ examples: [23456789] })),
149
+ destinationAmount: z
150
+ .optional(ValuedAmount)
151
+ .check(z.describe('Amount delivered to the recipient for a cross-token transfer. Omitted with `destinationToken` for a same-token transfer.')),
162
152
  destinationToken: z
163
153
  .optional(schema.SourceToken)
164
- .check(z.describe('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`.')),
154
+ .check(z.describe('The token delivered to the recipient when it differs from `sourceToken`. Omitted for same-token transfers and returned only with `include=crossToken`.')),
165
155
  id: z.string().check(z.describe('A stable resource ID for this transfer, built from `${transactionHash}-${logIndex}`.'), z.meta({
166
156
  examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-0'],
167
157
  })),
@@ -173,13 +163,17 @@ export var schema;
173
163
  .check(z.describe('The decoded text memo from the matching `TransferWithMemo` event, when one is present. Returned when you request `include=memo`.'), z.meta({ examples: ['Payment for invoice 123'] })),
174
164
  recipient: Schema.Address.check(z.describe('The account address that received the tokens.')),
175
165
  sender: Schema.Address.check(z.describe('The account address that sent the tokens.')),
176
- sourceToken: schema.SourceToken.check(z.describe('The token and amount that moved.')),
166
+ sourceAmount: ValuedAmount.check(z.describe('Amount associated with `sourceToken`.')),
167
+ sourceToken: schema.SourceToken.check(z.describe('The token that moved.')),
177
168
  timestamp: z.iso
178
169
  .datetime()
179
170
  .check(z.describe('The block timestamp when this transfer was recorded.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
180
171
  transactionHash: Schema.Hash.check(z.describe('The hash of the transaction that emitted this transfer event.')),
181
172
  })
182
- .check(z.describe('One TIP-20 token transfer event, optionally including a payment memo for reconciliation or invoices.'));
173
+ .check(z.refine((data) => (data.destinationToken === undefined) === (data.destinationAmount === undefined), {
174
+ error: '`destinationAmount` and `destinationToken` must be returned together.',
175
+ path: ['destinationAmount'],
176
+ }), z.describe('One TIP-20 token transfer event, optionally including a payment memo for reconciliation or invoices.'));
183
177
  /** Page-level resources: opt-in counts plus valuation rate provenance. */
184
178
  getTransfers.Meta = z
185
179
  .object({
@@ -216,7 +210,7 @@ export function transfers(options = {}) {
216
210
  description: 'List token transfer events across Tempo.',
217
211
  operationId: 'getTransfers',
218
212
  responses: OpenApi.responses({
219
- errors: { 502: 'The upstream indexer could not serve this request.' },
213
+ errors: { 502: 'The indexer or Tempo RPC could not serve this request.' },
220
214
  success: {
221
215
  description: 'A page of token transfer events.',
222
216
  schema: schema.getTransfers.Response,
@@ -226,7 +220,7 @@ export function transfers(options = {}) {
226
220
  tags: ['Transfers'],
227
221
  }), Cache.response({
228
222
  cacheControl: Cache.policies.feed,
229
- name: 'tempo-api:transfers:v1',
223
+ name: 'tempo-api:transfers:v2',
230
224
  key: (c) => Cache.urlKey(c, schema.getTransfers.Query),
231
225
  }), async (c) => {
232
226
  if (Auth.narrowAccess)
@@ -275,11 +269,6 @@ export function transfers(options = {}) {
275
269
  page: options.page,
276
270
  timing: 'transfers',
277
271
  });
278
- // Token metadata is opt-in. When requested, resolve each page token's
279
- // metadata concurrently (sharing one verified-token fetch) and spread
280
- // it into the row's `sourceToken`. Resolution is best-effort: a token
281
- // whose metadata is unavailable keeps `sourceToken` as just
282
- // `address`/`amount` on that row rather than failing the whole page.
283
272
  let data = page.data.map((row) => ({
284
273
  blockNumber: row.blockNumber,
285
274
  id: `${row.transactionHash}-${row.logIndex}`,
@@ -338,31 +327,31 @@ export function transfers(options = {}) {
338
327
  const snapshot = data.length > 0
339
328
  ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
340
329
  : undefined;
341
- if (options.include.includes('token') && data.length > 0 && snapshot) {
342
- const uniqueTokens = Array.from(new Set(data.flatMap((transfer) => [
343
- transfer.sourceToken.address,
344
- ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
345
- ])));
346
- const resolved = await Timing.time(c, 'tokens', () => Promise.all(uniqueTokens.map((token) => Tokens.resolveToken(c, {
347
- address: token,
348
- chainId,
349
- snapshot,
350
- })
351
- .then((metadata) => [token, metadata])
352
- .catch(() => [token, undefined]))));
353
- const tokensByAddress = new Map(resolved);
354
- const withToken = (entry) => {
355
- const token = tokensByAddress.get(entry.address);
356
- return token ? { ...entry, ...token } : entry;
357
- };
358
- data = data.map((transfer) => ({
359
- ...transfer,
360
- sourceToken: withToken(transfer.sourceToken),
361
- ...(transfer.destinationToken
362
- ? { destinationToken: withToken(transfer.destinationToken) }
363
- : {}),
364
- }));
365
- }
330
+ const includeLogoUri = options.include.includes('token.logoUri');
331
+ const includeVerified = options.include.includes('token.verified');
332
+ const uniqueTokens = Array.from(new Set(data.flatMap((transfer) => [
333
+ transfer.sourceToken.address,
334
+ ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
335
+ ])));
336
+ const tokensByAddress = new Map(await Timing.time(c, 'tokens', () => Promise.all(uniqueTokens.map(async (token) => {
337
+ const [logoUri, metadata] = await Promise.all([
338
+ includeLogoUri
339
+ ? Timing.time(c, 'token_logo', () => Tokens.getTokenLogo(c, { address: token, chainId })).catch(() => undefined)
340
+ : undefined,
341
+ Timing.time(c, 'token_metadata', () => Tokens.getTokenMetadata(c, { address: token, chainId })),
342
+ ]);
343
+ return [
344
+ token.toLowerCase(),
345
+ {
346
+ ...metadata,
347
+ logoUri: includeLogoUri
348
+ ? (logoUri ??
349
+ snapshot?.byAddress.get(token.toLowerCase())?.logoUri ??
350
+ metadata.logoUri)
351
+ : undefined,
352
+ },
353
+ ];
354
+ }))));
366
355
  // Value each leg via its curated display currency, in the requested
367
356
  // denomination.
368
357
  const rates = await Valuation.ratesFor(c, {
@@ -376,29 +365,61 @@ export function transfers(options = {}) {
376
365
  denomination,
377
366
  oracle,
378
367
  });
379
- const valued = (leg) => ({
380
- ...leg,
381
- valuation: Valuation.valuationFor({
382
- amount: BigInt(leg.amount),
383
- denomination,
384
- rates,
385
- token: snapshot?.byAddress.get(leg.address.toLowerCase()),
386
- }),
368
+ const enrich = (leg) => {
369
+ const metadata = tokensByAddress.get(leg.address.toLowerCase());
370
+ if (!metadata)
371
+ throw new Error(`Token metadata unavailable for ${leg.address}`);
372
+ return {
373
+ amount: {
374
+ ...Value.tokenAmount({
375
+ baseUnits: leg.amount,
376
+ currency: metadata.currency,
377
+ decimals: metadata.decimals,
378
+ }),
379
+ valuation: Valuation.valuationFor({
380
+ amount: BigInt(leg.amount),
381
+ denomination,
382
+ rates,
383
+ token: snapshot?.byAddress.get(leg.address.toLowerCase()),
384
+ }),
385
+ },
386
+ token: {
387
+ address: leg.address,
388
+ currency: metadata.currency,
389
+ decimals: metadata.decimals,
390
+ ...(metadata.logoUri !== undefined ? { logoUri: metadata.logoUri } : {}),
391
+ name: metadata.name,
392
+ symbol: metadata.symbol,
393
+ ...(includeVerified && snapshot
394
+ ? { verified: snapshot.byAddress.has(leg.address.toLowerCase()) }
395
+ : {}),
396
+ },
397
+ };
398
+ };
399
+ const responseData = data.map(({ destinationToken, sourceToken, ...transfer }) => {
400
+ const source = enrich(sourceToken);
401
+ if (!destinationToken)
402
+ return {
403
+ ...transfer,
404
+ sourceAmount: source.amount,
405
+ sourceToken: source.token,
406
+ };
407
+ const destination = enrich(destinationToken);
408
+ return {
409
+ ...transfer,
410
+ destinationAmount: destination.amount,
411
+ destinationToken: destination.token,
412
+ sourceAmount: source.amount,
413
+ sourceToken: source.token,
414
+ };
387
415
  });
388
- data = data.map((transfer) => ({
389
- ...transfer,
390
- sourceToken: valued(transfer.sourceToken),
391
- ...(transfer.destinationToken
392
- ? { destinationToken: valued(transfer.destinationToken) }
393
- : {}),
394
- }));
395
416
  const counts = countPromise ? await countPromise : undefined;
396
417
  const meta = {
397
418
  ...(rates ? { pricing: Valuation.pricing(rates, oracle) } : {}),
398
419
  ...counts,
399
420
  };
400
421
  return c.json(Response.validated(schema.getTransfers.Response, {
401
- data,
422
+ data: responseData,
402
423
  ...(Object.keys(meta).length > 0 ? { meta } : {}),
403
424
  nextCursor: page.nextCursor,
404
425
  }), 200);
@@ -412,15 +433,13 @@ export function transfers(options = {}) {
412
433
  }
413
434
  /**
414
435
  * Shared TIP-20 `Transfer` page query. Used by `GET /transfers` and the
415
- * webhook poller. Builds the signature-decoded `Transfer` CTE query, applies
436
+ * webhook poller. Builds signature-decoded `Transfer` CTE queries, applies
416
437
  * opaque keyset pagination on `(block_num, log_idx)`, and decodes rows into a
417
438
  * strongly-typed intermediate shape that each caller maps to its own public
418
439
  * response.
419
440
  *
420
- * `eitherSide` (the subject address must appear on _either_ side of the
421
- * transfer) is exposed alongside the one-sided `sender`/`recipient` filters
422
- * because composing it with the keyset cursor pre-WHERE in SQL is awkward at
423
- * the call site; the helper takes the knob directly.
441
+ * `eitherSide` is exposed alongside one-sided `sender`/`recipient` filters
442
+ * because it requires two upstream scans. The helper owns their ordered merge.
424
443
  *
425
444
  * Memoization lives inside the helper under one canonical `transfers:v1:…`
426
445
  * key derived from every filter — so equivalent filter combinations share a
@@ -461,32 +480,48 @@ export async function scan(deps, options) {
461
480
  // the head page, so it shares the head's SQL and cache entries; deeper pages
462
481
  // translate to a SQL OFFSET of `(page - 1) * limit` rows.
463
482
  const offset = options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : undefined;
464
- // Transfer events filtered by any combination of contract / subject /
465
- // sender / recipient / block / timestamp. Ordered by block then log
466
- // index so transfers within a block stay stable. The keyset cursor clause is
467
- // page-only and added on top of the shared filters below.
483
+ const address = options.eitherSide;
484
+ // Transfer events filtered by any combination of contract / sender /
485
+ // recipient / block / timestamp. The address shortcut is split below so
486
+ // each upstream query can use one indexed side.
468
487
  const filters = filterClauses(options);
469
488
  if (cursor !== undefined)
470
489
  filters.push(Cursor.keyset([
471
490
  { literal: Cursor.literal(cursor[0], 'int'), name: 'block_num', order },
472
491
  { literal: Cursor.literal(cursor[1], 'int'), name: 'log_idx', order },
473
492
  ]));
474
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
475
- // Fetch one extra row to detect `hasMore` without a separate count query.
476
- const result = await tidx.fetch({
477
- chainId,
478
- ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
479
- query: `
480
- SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
481
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
482
- ${where}
483
- ORDER BY block_num ${direction}, log_idx ${direction}
484
- LIMIT ${limit + 1}${offset !== undefined ? ` OFFSET ${offset}` : ''}
485
- `,
486
- });
493
+ const addressScoped = address !== undefined;
494
+ const window = addressScoped ? (offset ?? 0) + limit + 1 : limit + 1;
495
+ const fetchRows = async (side) => {
496
+ const sideFilters = side !== undefined && address !== undefined
497
+ ? [`"${side}" = '${address}'`, ...filters]
498
+ : filters;
499
+ const where = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : '';
500
+ const result = await tidx.fetch({
501
+ chainId,
502
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
503
+ query: `
504
+ SELECT "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}, tx_hash, block_num, log_idx, block_timestamp
505
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
506
+ ${where}
507
+ ORDER BY block_num ${direction}, log_idx ${direction}
508
+ LIMIT ${window}${!addressScoped && offset !== undefined ? ` OFFSET ${offset}` : ''}
509
+ `,
510
+ });
511
+ return result.rows;
512
+ };
513
+ // TIDX cannot plan `(from = X OR to = X)` over the decoded Transfer table.
514
+ // Merge two bounded, server-filtered streams while preserving global order.
515
+ const rows = addressScoped
516
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
517
+ limit: limit + 1,
518
+ offset: offset ?? 0,
519
+ order,
520
+ })
521
+ : await fetchRows();
487
522
  // The next page anchors below the last fetched row's `(block, log_idx)`.
488
523
  const page = Cursor.paginate({
489
- rows: result.rows,
524
+ rows,
490
525
  limit,
491
526
  key: (row) => {
492
527
  const block = Value.toNumber(row['block_num']);
@@ -528,17 +563,37 @@ export async function scan(deps, options) {
528
563
  }
529
564
  return { hasMore: page.hasMore, items, limit, nextCursor: page.nextCursor };
530
565
  }
566
+ /** Deduplicates and globally orders bounded `from`/`to` transfer streams. */
567
+ function mergeTransferRows(rows, options) {
568
+ const positioned = [];
569
+ const seen = new Set();
570
+ for (const row of rows) {
571
+ const blockNumber = Value.toNumber(row['block_num']);
572
+ const logIndex = Value.toNumber(row['log_idx']);
573
+ const transactionHash = Value.toText(row['tx_hash']);
574
+ if (blockNumber === undefined || logIndex === undefined || !transactionHash)
575
+ continue;
576
+ const key = eventKey(transactionHash, logIndex);
577
+ if (seen.has(key))
578
+ continue;
579
+ seen.add(key);
580
+ positioned.push({ blockNumber, logIndex, row });
581
+ }
582
+ positioned.sort((a, b) => {
583
+ const comparison = a.blockNumber - b.blockNumber || a.logIndex - b.logIndex;
584
+ return options.order === 'asc' ? comparison : -comparison;
585
+ });
586
+ return positioned.slice(options.offset, options.offset + options.limit).map(({ row }) => row);
587
+ }
531
588
  /**
532
589
  * Builds shared transfer filters. A token predicate targets `token_transfers`'s
533
590
  * physical `token` column; unscoped queries omit it and use the `Transfer` CTE.
534
591
  */
535
592
  function filterClauses(options) {
536
- const { eitherSide, fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } = options;
593
+ const { fromBlock, fromTimestamp, recipient, sender, toBlock, toTimestamp, token } = options;
537
594
  const filters = [];
538
595
  if (token !== undefined)
539
596
  filters.push(`token = '${token}'`);
540
- if (eitherSide !== undefined)
541
- filters.push(`("from" = '${eitherSide}' OR "to" = '${eitherSide}')`);
542
597
  if (sender !== undefined)
543
598
  filters.push(`"from" = '${sender}'`);
544
599
  if (recipient !== undefined)
@@ -572,54 +627,84 @@ export function count(c, options) {
572
627
  /**
573
628
  * Context-free core of {@link count}: counts token-scoped rows from
574
629
  * `token_transfers`, otherwise the decoded `Transfer` CTE. The capped inner
575
- * scan bounds work; the SQL aggregate runs on ClickHouse.
630
+ * scan bounds work while preserving each source's established query engine.
576
631
  */
577
632
  export async function countScan(deps, options) {
578
633
  const { chainId } = options;
579
634
  const { tidx } = deps;
580
635
  const filters = filterClauses(options);
581
636
  const tokenScoped = options.token !== undefined;
582
- const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
637
+ const address = options.eitherSide;
583
638
  // Folded count: cross-token transfers collapse several `Transfer` legs into
584
639
  // one row, so a raw `count(*)` would overcount. Scan the capped `Transfer`
585
640
  // rows, classify each transaction, and subtract the folded-away legs. Bounded
586
641
  // by `countCap` like the raw count, so it stays within the indexer's budget.
587
642
  if (options.crossToken) {
588
- const result = await tidx.fetch({
589
- chainId,
590
- ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
591
- query: `
592
- SELECT tx_hash, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
593
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
594
- ${where}
595
- ORDER BY block_num, log_idx
596
- LIMIT ${Schema.countCap}
597
- `,
598
- });
599
- const rawCount = result.rows.length;
643
+ const fetchRows = async (side) => {
644
+ const sideFilters = side !== undefined && address !== undefined
645
+ ? [`"${side}" = '${address}'`, ...filters]
646
+ : filters;
647
+ const sideWhere = sideFilters.length > 0 ? `WHERE ${sideFilters.join(' AND ')}` : '';
648
+ const result = await tidx.fetch({
649
+ chainId,
650
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
651
+ query: `
652
+ SELECT tx_hash, block_num, log_idx, "from", "to", ${tokenScoped ? 'token AS address, amount AS value' : 'address, value'}
653
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
654
+ ${sideWhere}
655
+ ORDER BY block_num, log_idx
656
+ LIMIT ${Schema.countCap}
657
+ `,
658
+ });
659
+ return result.rows;
660
+ };
661
+ const rows = address !== undefined
662
+ ? mergeTransferRows((await Promise.all([fetchRows('from'), fetchRows('to')])).flat(), {
663
+ limit: Schema.countCap,
664
+ offset: 0,
665
+ order: 'asc',
666
+ })
667
+ : await fetchRows();
668
+ const rawCount = rows.length;
600
669
  let suppressed = 0;
601
- for (const legs of legsByTransaction(result.rows).values()) {
670
+ for (const legs of legsByTransaction(rows).values()) {
602
671
  const bundle = classifyCrossToken(legs);
603
672
  if (bundle)
604
673
  suppressed += bundle.suppressLogIndexes.length;
605
674
  }
606
675
  return { totalCountCapped: rawCount >= Schema.countCap, totalCount: rawCount - suppressed };
607
676
  }
608
- const result = await tidx.fetch({
609
- chainId,
610
- engine: 'clickhouse',
611
- ...(tokenScoped ? {} : { signatures: [signature] }),
612
- query: `
613
- SELECT count(*) AS total
614
- FROM (
615
- SELECT 1
616
- FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
617
- ${where}
618
- LIMIT ${Schema.countCap}
619
- ) AS capped
620
- `,
621
- });
622
- const totalCount = Value.toNumber(result.rows[0]?.['total']) ?? 0;
677
+ const countRows = async (additionalFilters = []) => {
678
+ const countFilters = [...additionalFilters, ...filters];
679
+ const countWhere = countFilters.length > 0 ? `WHERE ${countFilters.join(' AND ')}` : '';
680
+ const result = await tidx.fetch({
681
+ chainId,
682
+ ...(tokenScoped ? { engine: 'clickhouse' } : { signatures: [signature] }),
683
+ query: `
684
+ SELECT count(*) AS total
685
+ FROM (
686
+ SELECT 1
687
+ FROM ${tokenScoped ? 'token_transfers' : 'Transfer'}
688
+ ${countWhere}
689
+ LIMIT ${Schema.countCap}
690
+ ) AS capped
691
+ `,
692
+ });
693
+ return Value.toNumber(result.rows[0]?.['total']) ?? 0;
694
+ };
695
+ if (address !== undefined) {
696
+ // Keep branches disjoint so self-transfers count once.
697
+ const [sent, received] = await Promise.all([
698
+ countRows([`"from" = '${address}'`]),
699
+ countRows([`"to" = '${address}'`, `"from" != '${address}'`]),
700
+ ]);
701
+ const rawCount = sent + received;
702
+ return {
703
+ totalCount: Math.min(rawCount, Schema.countCap),
704
+ totalCountCapped: sent >= Schema.countCap || received >= Schema.countCap || rawCount >= Schema.countCap,
705
+ };
706
+ }
707
+ const totalCount = await countRows();
623
708
  return { totalCountCapped: totalCount >= Schema.countCap, totalCount };
624
709
  }
625
710
  /** `'tx1', 'tx2', …` SQL `IN` list of the page's distinct transaction hashes. */