tapimo 0.8.0 → 0.10.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 (223) hide show
  1. package/dist/App.d.ts +7410 -7613
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +0 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.d.ts +21 -20
  6. package/dist/Webhooks.d.ts.map +1 -1
  7. package/dist/Webhooks.js +66 -23
  8. package/dist/Webhooks.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  10. package/dist/apps/Zones.d.ts +34 -22
  11. package/dist/apps/Zones.d.ts.map +1 -1
  12. package/dist/apps/Zones.js +9 -8
  13. package/dist/apps/Zones.js.map +1 -1
  14. package/dist/apps/data/App.d.ts +3120 -2782
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.d.ts +4 -5
  17. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  18. package/dist/apps/data/FxOracle.js +19 -4
  19. package/dist/apps/data/FxOracle.js.map +1 -1
  20. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  21. package/dist/apps/data/routes/activities.js +32 -27
  22. package/dist/apps/data/routes/activities.js.map +1 -1
  23. package/dist/apps/data/routes/addresses.d.ts +283 -52
  24. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.d.ts +291 -21
  26. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  27. package/dist/apps/data/routes/balances.js +232 -66
  28. package/dist/apps/data/routes/balances.js.map +1 -1
  29. package/dist/apps/data/routes/blocks.d.ts +4 -4
  30. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  31. package/dist/apps/data/routes/earn.d.ts +642 -126
  32. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  33. package/dist/apps/data/routes/earn.js +627 -58
  34. package/dist/apps/data/routes/earn.js.map +1 -1
  35. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  36. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  37. package/dist/apps/data/routes/exchanges.js +114 -118
  38. package/dist/apps/data/routes/exchanges.js.map +1 -1
  39. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  40. package/dist/apps/data/routes/receipts.js +68 -47
  41. package/dist/apps/data/routes/receipts.js.map +1 -1
  42. package/dist/apps/data/routes/tokens.d.ts +2 -0
  43. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  44. package/dist/apps/data/routes/transactions.d.ts +59 -51
  45. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  46. package/dist/apps/data/routes/transactions.js +25 -23
  47. package/dist/apps/data/routes/transactions.js.map +1 -1
  48. package/dist/apps/data/routes/transfers.d.ts +40 -18
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +618 -105
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/valuation.d.ts +21 -21
  53. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  54. package/dist/apps/data/routes/valuation.js +2 -2
  55. package/dist/apps/data/routes/valuation.js.map +1 -1
  56. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  57. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +484 -342
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +12 -12
  62. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  63. package/dist/apps/data/routes/zones.js +3 -1
  64. package/dist/apps/data/routes/zones.js.map +1 -1
  65. package/dist/apps/funding/App.d.ts +127 -386
  66. package/dist/apps/funding/App.d.ts.map +1 -1
  67. package/dist/apps/funding/App.js +20 -5
  68. package/dist/apps/funding/App.js.map +1 -1
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
  70. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js +21 -103
  72. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  73. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  74. package/dist/apps/funding/routes/transfers.js +5 -5
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +429 -727
  77. package/dist/apps/management/App.d.ts.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  79. package/dist/apps/management/routes/billing.d.ts +72 -72
  80. package/dist/apps/management/routes/faucet.d.ts +6 -6
  81. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  82. package/dist/apps/management/routes/me.d.ts +6 -6
  83. package/dist/apps/management/routes/members.d.ts +30 -30
  84. package/dist/apps/management/routes/projects.d.ts +20 -20
  85. package/dist/apps/management/routes/usage.d.ts +24 -24
  86. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  87. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/mpp/App.d.ts +12 -0
  89. package/dist/apps/mpp/App.d.ts.map +1 -1
  90. package/dist/apps/mpp/Mpp.d.ts +3 -0
  91. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  92. package/dist/apps/mpp/Mpp.js +3 -0
  93. package/dist/apps/mpp/Mpp.js.map +1 -1
  94. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  95. package/dist/apps/mpp/internal/Adapter.js +9 -2
  96. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  97. package/dist/db/Db.d.ts +2 -2
  98. package/dist/db/Db.d.ts.map +1 -1
  99. package/dist/db/Db.js +25 -15
  100. package/dist/db/Db.js.map +1 -1
  101. package/dist/db/tables/fundingDeposits.d.ts +18 -1
  102. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  103. package/dist/db/tables/fundingDeposits.js +46 -2
  104. package/dist/db/tables/fundingDeposits.js.map +1 -1
  105. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  106. package/dist/db/tables/sponsoredTransactions.js +4 -1
  107. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  108. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  109. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  110. package/dist/db/tables/webhookSubscriptions.js +76 -3
  111. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  112. package/dist/internal/Auth.d.ts +4 -0
  113. package/dist/internal/Auth.d.ts.map +1 -1
  114. package/dist/internal/Auth.js +11 -2
  115. package/dist/internal/Auth.js.map +1 -1
  116. package/dist/internal/MetricSink.d.ts +112 -3
  117. package/dist/internal/MetricSink.d.ts.map +1 -1
  118. package/dist/internal/MetricSink.js +116 -3
  119. package/dist/internal/MetricSink.js.map +1 -1
  120. package/dist/internal/Schema.js +1 -1
  121. package/dist/internal/Schema.js.map +1 -1
  122. package/dist/internal/Tidx.d.ts +1 -3
  123. package/dist/internal/Tidx.d.ts.map +1 -1
  124. package/dist/internal/Tidx.js +1 -3
  125. package/dist/internal/Tidx.js.map +1 -1
  126. package/dist/internal/Viem.d.ts.map +1 -1
  127. package/dist/internal/Viem.js +2 -1
  128. package/dist/internal/Viem.js.map +1 -1
  129. package/dist/internal/WebhookDestination.d.ts +2 -0
  130. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  131. package/dist/internal/WebhookDestination.js +11 -0
  132. package/dist/internal/WebhookDestination.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +55 -0
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  135. package/dist/internal/WebhookTransfer.js +296 -0
  136. package/dist/internal/WebhookTransfer.js.map +1 -0
  137. package/dist/internal/Webhooks.d.ts +37 -0
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +87 -13
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/Provider.d.ts +10 -6
  142. package/dist/internal/funding/Provider.d.ts.map +1 -1
  143. package/dist/internal/funding/Provider.js +11 -7
  144. package/dist/internal/funding/Provider.js.map +1 -1
  145. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  146. package/dist/internal/funding/Reconciliation.js +229 -16
  147. package/dist/internal/funding/Reconciliation.js.map +1 -1
  148. package/dist/internal/funding/providers/relay.d.ts +3 -0
  149. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  150. package/dist/internal/funding/providers/relay.js +18 -3
  151. package/dist/internal/funding/providers/relay.js.map +1 -1
  152. package/package.json +3 -3
  153. package/src/App.test.ts +32 -0
  154. package/src/App.ts +4 -1
  155. package/src/Client.test-d.ts +116 -38
  156. package/src/Webhooks.bench.ts +714 -0
  157. package/src/Webhooks.memory.bench.ts +250 -0
  158. package/src/Webhooks.test.ts +77 -6
  159. package/src/Webhooks.ts +83 -29
  160. package/src/apps/Zones.test.ts +5 -4
  161. package/src/apps/Zones.ts +9 -8
  162. package/src/apps/composition.test.ts +39 -10
  163. package/src/apps/data/FxOracle.test.ts +18 -0
  164. package/src/apps/data/FxOracle.ts +28 -5
  165. package/src/apps/data/routes/activities.test.ts +19 -0
  166. package/src/apps/data/routes/activities.ts +34 -30
  167. package/src/apps/data/routes/balances.test.ts +161 -10
  168. package/src/apps/data/routes/balances.ts +339 -147
  169. package/src/apps/data/routes/blocks.test.ts +12 -6
  170. package/src/apps/data/routes/earn.test.ts +286 -9
  171. package/src/apps/data/routes/earn.ts +841 -58
  172. package/src/apps/data/routes/exchanges.test.ts +7 -6
  173. package/src/apps/data/routes/exchanges.ts +124 -117
  174. package/src/apps/data/routes/receipts.test.ts +24 -7
  175. package/src/apps/data/routes/receipts.ts +67 -50
  176. package/src/apps/data/routes/tokens.test.ts +51 -0
  177. package/src/apps/data/routes/transactions.test.ts +42 -0
  178. package/src/apps/data/routes/transactions.ts +44 -39
  179. package/src/apps/data/routes/transfers.test.ts +402 -24
  180. package/src/apps/data/routes/transfers.ts +708 -101
  181. package/src/apps/data/routes/valuation.test.ts +26 -0
  182. package/src/apps/data/routes/valuation.ts +6 -2
  183. package/src/apps/data/routes/webhooks.test.ts +414 -130
  184. package/src/apps/data/routes/webhooks.ts +603 -419
  185. package/src/apps/data/routes/zones.ts +6 -1
  186. package/src/apps/funding/App.ts +20 -5
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
  188. package/src/apps/funding/routes/deposit-addresses.ts +34 -117
  189. package/src/apps/funding/routes/quotes.test.ts +1 -1
  190. package/src/apps/funding/routes/transfers.test.ts +6 -25
  191. package/src/apps/funding/routes/transfers.ts +5 -5
  192. package/src/apps/mpp/Mpp.test.ts +13 -1
  193. package/src/apps/mpp/Mpp.ts +8 -0
  194. package/src/apps/mpp/internal/Adapter.ts +10 -2
  195. package/src/db/Db.test.ts +40 -6
  196. package/src/db/Db.ts +42 -19
  197. package/src/db/tables/fundingDeposits.test.ts +101 -0
  198. package/src/db/tables/fundingDeposits.ts +71 -2
  199. package/src/db/tables/sponsoredTransactions.ts +9 -2
  200. package/src/db/tables/webhookSubscriptions.ts +116 -4
  201. package/src/internal/Auth.test.ts +28 -0
  202. package/src/internal/Auth.ts +15 -1
  203. package/src/internal/EarnVaults.test.ts +2 -2
  204. package/src/internal/MetricSink.test.ts +518 -0
  205. package/src/internal/MetricSink.ts +266 -9
  206. package/src/internal/Schema.ts +1 -1
  207. package/src/internal/Tidx.test.ts +4 -4
  208. package/src/internal/Tidx.ts +1 -3
  209. package/src/internal/Viem.test.ts +8 -0
  210. package/src/internal/Viem.ts +2 -1
  211. package/src/internal/WebhookDestination.test.ts +20 -0
  212. package/src/internal/WebhookDestination.ts +10 -0
  213. package/src/internal/WebhookTransfer.ts +426 -0
  214. package/src/internal/Webhooks.test.ts +137 -0
  215. package/src/internal/Webhooks.ts +131 -17
  216. package/src/internal/funding/Provider.test.ts +47 -7
  217. package/src/internal/funding/Provider.ts +16 -11
  218. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  219. package/src/internal/funding/Reconciliation.test.ts +1532 -54
  220. package/src/internal/funding/Reconciliation.ts +296 -53
  221. package/src/internal/funding/SourceObservation.test.ts +3 -2
  222. package/src/internal/funding/providers/relay.test.ts +1 -4
  223. package/src/internal/funding/providers/relay.ts +23 -3
@@ -28,6 +28,7 @@ type OpenApiDocument = {
28
28
  string,
29
29
  {
30
30
  get?: {
31
+ description?: string
31
32
  parameters?: readonly {
32
33
  description?: string
33
34
  name?: string
@@ -87,6 +88,33 @@ describe('schema.getEarnVaults.Query', () => {
87
88
  })
88
89
  })
89
90
 
91
+ describe('schema.getEarnAddressPositions.Query', () => {
92
+ test('parses collection filters', () => {
93
+ const query = Earn.schema.getEarnAddressPositions.Query.parse({
94
+ limit: '5',
95
+ 'valuation.currency': 'usd',
96
+ verified: 'true',
97
+ })
98
+
99
+ expect(query.limit).toBe(5)
100
+ expect(query['valuation.currency']).toBe('USD')
101
+ expect(query.verified).toBe(true)
102
+ })
103
+
104
+ test('defaults the page size', () => {
105
+ expect(Earn.schema.getEarnAddressPositions.Query.parse({}).limit).toBe(10)
106
+ })
107
+
108
+ test('rejects vault-collection filters', () => {
109
+ expect(() =>
110
+ Earn.schema.getEarnAddressPositions.Query.parse({ capability: 'deposit' }),
111
+ ).toThrow()
112
+ expect(() =>
113
+ Earn.schema.getEarnAddressPositions.Query.parse({ asset: `0x${'aa'.repeat(20)}` }),
114
+ ).toThrow()
115
+ })
116
+ })
117
+
90
118
  describe('schema.getEarnVaultEarnings.Response', () => {
91
119
  test('enforces status-specific earnings fields', () => {
92
120
  const response = {
@@ -109,12 +137,6 @@ describe('schema.getEarnVaultEarnings.Response', () => {
109
137
  period: 'active',
110
138
  status: 'complete',
111
139
  }).success,
112
- lifetime: Earn.schema.getEarnVaultEarnings.Response.safeParse({
113
- ...response,
114
- lifetimeEarnings: '0',
115
- period: 'lifetime',
116
- status: 'complete',
117
- }).success,
118
140
  completeWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
119
141
  ...response,
120
142
  period: 'lifetime',
@@ -125,12 +147,23 @@ describe('schema.getEarnVaultEarnings.Response', () => {
125
147
  period: 'active',
126
148
  status: 'incomplete_cost_basis',
127
149
  }).success,
150
+ incompleteWindow: Earn.schema.getEarnVaultEarnings.Response.safeParse({
151
+ ...response,
152
+ period: '30d',
153
+ status: 'incomplete_cost_basis',
154
+ }).success,
128
155
  incompleteWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
129
156
  ...response,
130
157
  lifetimeEarnings: '0',
131
158
  period: 'lifetime',
132
159
  status: 'incomplete_cost_basis',
133
160
  }).success,
161
+ lifetime: Earn.schema.getEarnVaultEarnings.Response.safeParse({
162
+ ...response,
163
+ lifetimeEarnings: '0',
164
+ period: 'lifetime',
165
+ status: 'complete',
166
+ }).success,
134
167
  pending: Earn.schema.getEarnVaultEarnings.Response.safeParse({
135
168
  ...response,
136
169
  period: 'lifetime',
@@ -141,34 +174,77 @@ describe('schema.getEarnVaultEarnings.Response', () => {
141
174
  period: 'active',
142
175
  status: 'pending_redemption',
143
176
  }).success,
177
+ pendingWindow: Earn.schema.getEarnVaultEarnings.Response.safeParse({
178
+ ...response,
179
+ period: '30d',
180
+ status: 'pending_redemption',
181
+ }).success,
144
182
  pendingWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
145
183
  ...response,
146
184
  lifetimeEarnings: '0',
147
185
  period: 'lifetime',
148
186
  status: 'pending_redemption',
149
187
  }).success,
188
+ window: Earn.schema.getEarnVaultEarnings.Response.safeParse({
189
+ ...response,
190
+ period: '30d',
191
+ status: 'complete',
192
+ windowEarnings: '0',
193
+ }).success,
194
+ windowWithActiveEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
195
+ ...response,
196
+ activeEarnings: '0',
197
+ period: '30d',
198
+ status: 'complete',
199
+ }).success,
200
+ windowWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
201
+ ...response,
202
+ period: '30d',
203
+ status: 'complete',
204
+ }).success,
150
205
  }).toMatchInlineSnapshot(`
151
206
  {
152
207
  "active": true,
153
208
  "activeWithLifetimeEarnings": false,
154
209
  "completeWithoutEarnings": false,
155
210
  "incomplete": true,
211
+ "incompleteWindow": true,
156
212
  "incompleteWithEarnings": false,
157
213
  "lifetime": true,
158
214
  "pending": true,
159
215
  "pendingActive": false,
216
+ "pendingWindow": true,
160
217
  "pendingWithEarnings": false,
218
+ "window": true,
219
+ "windowWithActiveEarnings": false,
220
+ "windowWithoutEarnings": false,
161
221
  }
162
222
  `)
163
223
  })
164
224
  })
165
225
 
166
226
  describe('schema.getEarnVaultEarnings.Query', () => {
167
- test('defaults to lifetime and accepts active earnings', () => {
227
+ test('defaults to lifetime and accepts active and trailing earnings', () => {
168
228
  expect([
169
229
  Earn.schema.getEarnVaultEarnings.Query.parse({}).period,
230
+ Earn.schema.getEarnVaultEarnings.Query.parse({ period: '30d' }).period,
170
231
  Earn.schema.getEarnVaultEarnings.Query.parse({ period: 'active' }).period,
171
- ]).toStrictEqual(['lifetime', 'active'])
232
+ ]).toStrictEqual(['lifetime', '30d', 'active'])
233
+ })
234
+ })
235
+
236
+ describe('calculateWindowEarnings', () => {
237
+ test('adjusts snapshot growth for deposits and realized assets', () => {
238
+ expect(
239
+ Earn.calculateWindowEarnings({
240
+ deposited: 50n,
241
+ endingValue: 130n,
242
+ finalized: 10n,
243
+ openingValue: 100n,
244
+ redeemed: 15n,
245
+ withdrewExact: 5n,
246
+ }),
247
+ ).toBe(10n)
172
248
  })
173
249
  })
174
250
 
@@ -491,6 +567,144 @@ describe('GET /v1/earn/vaults/verified', () => {
491
567
  )
492
568
  })
493
569
 
570
+ describe('GET /v1/earn/addresses/:address/positions', () => {
571
+ test('returns chain selection errors', async () => {
572
+ const app = TestApp.create({ auth: false, cache })
573
+ const invalid = await app.request(
574
+ `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=abc`,
575
+ )
576
+ const unsupported = await app.request(
577
+ `/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=999999`,
578
+ )
579
+
580
+ expect([
581
+ (await TestApp.json(invalid, Schema.ErrorResponse)).error.code,
582
+ (await TestApp.json(unsupported, Schema.ErrorResponse)).error.code,
583
+ ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported'])
584
+ })
585
+
586
+ test('rejects a malformed account address', async () => {
587
+ const response = await TestApp.create({ auth: false, cache }).request(
588
+ '/v1/earn/addresses/not-an-address/positions',
589
+ )
590
+
591
+ expect(response.status).toBe(400)
592
+ expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
593
+ error: { code: 'positions_invalid' },
594
+ })
595
+ })
596
+
597
+ test('returns an empty verified page without curated vaults', async () => {
598
+ const app = TestApp.create({ auth: false, cache })
599
+ const response = await app.request(
600
+ `/v1/earn/addresses/${TestEarn.positionAddress}/positions?verified=true`,
601
+ )
602
+
603
+ expect(response.status).toBe(200)
604
+ expect(
605
+ await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response),
606
+ ).toStrictEqual({ data: [], nextCursor: null })
607
+ })
608
+
609
+ test.runIf(Runtime.get().mode === 'testnet')(
610
+ 'lists non-zero verified positions for an account',
611
+ async () => {
612
+ const db = TestApp.database()
613
+ // Valuation resolves through the curated verified list, so seed it.
614
+ await TestApp.verifiedSeed(db, TestEarn.chain.id)
615
+ await core_EarnVaults.upsert(db, TestEarn.verifiedVault)
616
+ await core_EarnVaults.upsert(db, {
617
+ ...TestEarn.verifiedVault,
618
+ description: 'Active-earnings Earn deployment.',
619
+ label: 'Active btPATHUSD Earn',
620
+ vaultAddress: TestEarn.activeEarningsVaultAddress,
621
+ })
622
+ const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] })
623
+ const response = await app.request(
624
+ `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&verified=true&valuation.currency=USD`,
625
+ )
626
+
627
+ expect(response.status).toBe(200)
628
+ const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response)
629
+ expect(body.data.every((position) => BigInt(position.shareAmount.amount) > 0n)).toBe(true)
630
+ const row = body.data.find(
631
+ (position) => position.vaultAddress === TestEarn.activeEarningsVaultAddress,
632
+ )
633
+ expect(row).toBeDefined()
634
+ if (!row) return
635
+ // Amounts render in their own token's display currency and precision.
636
+ expect(row.shareAmount.currency).toBe(row.shareToken.currency)
637
+ expect(row.shareAmount.decimals).toBe(row.shareToken.decimals)
638
+ expect(row.assetAmount.currency).toBe(row.assetToken.currency)
639
+ expect(row.assetAmount.decimals).toBe(row.assetToken.decimals)
640
+ // A USD-denominated asset values identically without consulting rates.
641
+ expect(row.valuation).toStrictEqual({ amount: row.assetAmount.formatted, currency: 'USD' })
642
+ expect(body.meta).toBeUndefined()
643
+ expect({
644
+ assetToken: row.assetToken.address,
645
+ formattedShape: /^\d+(\.\d+)?$/.test(row.assetAmount.formatted),
646
+ id: row.id,
647
+ sharesHeld: BigInt(row.shareAmount.amount) > 0n,
648
+ valued: BigInt(row.assetAmount.amount) > 0n,
649
+ vaultAddress: row.vaultAddress,
650
+ verified: row.verified,
651
+ }).toMatchInlineSnapshot(`
652
+ {
653
+ "assetToken": "0x20c0000000000000000000000000000000000001",
654
+ "formattedShape": true,
655
+ "id": "0x3e4405801a1bee1b58394228ea0b44b667e222fc",
656
+ "sharesHeld": true,
657
+ "valued": true,
658
+ "vaultAddress": "0x3e4405801a1bee1b58394228ea0b44b667e222fc",
659
+ "verified": true,
660
+ }
661
+ `)
662
+ },
663
+ 60_000,
664
+ )
665
+
666
+ test.runIf(Runtime.get().mode === 'testnet')(
667
+ 'discovers held vaults from indexed share balances',
668
+ async () => {
669
+ const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] })
670
+ const response = await app.request(
671
+ `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}`,
672
+ )
673
+
674
+ expect(response.status).toBe(200)
675
+ const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response)
676
+ expect(body.data.map((position) => position.vaultAddress)).toContain(
677
+ TestEarn.activeEarningsVaultAddress,
678
+ )
679
+ expect(
680
+ body.data.every(
681
+ (position) =>
682
+ position.id === position.vaultAddress &&
683
+ !position.verified &&
684
+ /^\d+$/.test(position.assetAmount.amount) &&
685
+ BigInt(position.shareAmount.amount) > 0n &&
686
+ position.assetToken.address.startsWith('0x20c0') &&
687
+ position.shareToken.address.startsWith('0x20c0'),
688
+ ),
689
+ ).toBe(true)
690
+
691
+ // An address-keyset cursor excludes every vault at or before it.
692
+ const cursor = Cursor.encode([TestEarn.activeEarningsVaultAddress])
693
+ const paged = await app.request(
694
+ `/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&cursor=${encodeURIComponent(cursor)}`,
695
+ )
696
+ expect(paged.status).toBe(200)
697
+ const pagedBody = await TestApp.json(paged, Earn.schema.getEarnAddressPositions.Response)
698
+ expect(
699
+ pagedBody.data.every(
700
+ (position) => position.vaultAddress > TestEarn.activeEarningsVaultAddress,
701
+ ),
702
+ ).toBe(true)
703
+ },
704
+ 90_000,
705
+ )
706
+ })
707
+
494
708
  describe('GET /v1/earn/vaults/:vaultId', () => {
495
709
  test('rejects a malformed vault address', async () => {
496
710
  const app = TestApp.create({ auth: false, cache })
@@ -1098,6 +1312,30 @@ describe('GET /v1/earn/vaults/:vaultId/earnings/:address', () => {
1098
1312
  60_000,
1099
1313
  )
1100
1314
 
1315
+ test.runIf(Runtime.get().mode === 'testnet')(
1316
+ 'returns cash-flow-adjusted trailing earnings',
1317
+ async () => {
1318
+ const app = TestApp.create({ auth: false, cache })
1319
+ const response = await app.request(
1320
+ `/v1/earn/vaults/${TestEarn.activeEarningsVaultAddress}/earnings/${TestEarn.activeEarningsAddress}?chainId=${TestEarn.chain.id}&period=30d`,
1321
+ )
1322
+
1323
+ expect(response.status).toBe(200)
1324
+ const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
1325
+ if (earnings.status !== 'complete' || earnings.period !== '30d')
1326
+ throw new Error('Expected complete trailing earnings history.')
1327
+ expect(earnings).toMatchObject({
1328
+ account: TestEarn.activeEarningsAddress,
1329
+ period: '30d',
1330
+ status: 'complete',
1331
+ windowEarnings: expect.stringMatching(/^-?\d+$/),
1332
+ })
1333
+ expect(earnings).not.toHaveProperty('activeEarnings')
1334
+ expect(earnings).not.toHaveProperty('lifetimeEarnings')
1335
+ },
1336
+ 60_000,
1337
+ )
1338
+
1101
1339
  test.runIf(Runtime.get().mode === 'testnet')(
1102
1340
  'omits lifetime earnings when venue shares make the cost basis incomplete',
1103
1341
  async () => {
@@ -1302,6 +1540,7 @@ describe('OpenAPI', () => {
1302
1540
 
1303
1541
  expect(Object.keys(spec.paths)).toEqual(
1304
1542
  expect.arrayContaining([
1543
+ '/v1/earn/addresses/{address}/positions',
1305
1544
  '/v1/earn/vaults',
1306
1545
  '/v1/earn/vaults/verified',
1307
1546
  '/v1/earn/vaults/{vaultId}',
@@ -1373,18 +1612,56 @@ describe('OpenAPI', () => {
1373
1612
  expect(vaultId?.schema?.examples).toStrictEqual([TestEarn.mainnetVaultAddress])
1374
1613
  })
1375
1614
 
1615
+ test('documents address-position inputs and responses', async () => {
1616
+ const app = TestApp.create({ auth: false, cache })
1617
+ const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
1618
+ const operation = spec.paths['/v1/earn/addresses/{address}/positions']?.get
1619
+ const address = operation?.parameters?.find((parameter) => parameter.name === 'address')
1620
+ const denomination = operation?.parameters?.find(
1621
+ (parameter) => parameter.name === 'valuation.currency',
1622
+ )
1623
+ const verified = operation?.parameters?.find((parameter) => parameter.name === 'verified')
1624
+ const response = JSON.stringify(operation?.responses)
1625
+
1626
+ expect(operation?.description).toContain('currently holds shares')
1627
+ expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress])
1628
+ expect(denomination?.description).toContain('nominal value')
1629
+ expect(verified?.description).toContain('registry-curated')
1630
+ expect(verified?.schema?.examples).toStrictEqual([true])
1631
+ expect(response).toContain('"chain_id_invalid"')
1632
+ expect(response).toContain('"chain_id_unsupported"')
1633
+ expect(response).toContain('"positions_invalid"')
1634
+ expect(response).toContain('"query_invalid"')
1635
+ expect(response).toContain('Stable resource ID for this position')
1636
+ expect(response).toContain('Current asset value of the held shares')
1637
+ expect(response).toContain('"assetAmount"')
1638
+ expect(response).toContain('"shareAmount"')
1639
+ expect(response).toContain('"valuation"')
1640
+ expect(response).toContain('"vaultAddress"')
1641
+ expect(response).toContain('"nextCursor"')
1642
+ // Token references embed the trimmed token object, not a bare address.
1643
+ expect(response).toContain('"symbol"')
1644
+ })
1645
+
1376
1646
  test('documents vault-earnings variants', async () => {
1377
1647
  const app = TestApp.create({ auth: false, cache })
1378
1648
  const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
1379
1649
  const operation = spec.paths['/v1/earn/vaults/{vaultId}/earnings/{address}']?.get
1650
+ const period = operation?.parameters?.find((parameter) => parameter.name === 'period')
1380
1651
  const response = JSON.stringify(operation?.responses)
1381
1652
 
1653
+ expect(operation?.description).toContain('the trailing 30 days')
1654
+ expect(period?.description).toContain('trailing 30 days')
1655
+ expect(period?.schema?.enum).toStrictEqual(['30d', 'active', 'lifetime'])
1656
+ expect(period?.schema?.examples).toStrictEqual(['lifetime'])
1382
1657
  expect(response).toContain('"earn_vault_not_found"')
1383
1658
  expect(response).toContain('Stable resource ID for these vault earnings')
1384
1659
  expect(response).toContain('Current share value plus completed redemptions')
1385
1660
  expect(response).toContain('weighted-average asset cost basis')
1386
- expect(response).toContain('cannot reconstruct the remaining asset cost basis')
1661
+ expect(response).toContain('Ending share value plus realized assets')
1662
+ expect(response).toContain('cannot reconstruct the asset cost basis for the requested period')
1387
1663
  expect(response).toContain('"active"')
1664
+ expect(response).toContain('"30d"')
1388
1665
  expect(response).toContain('"incomplete_cost_basis"')
1389
1666
  expect(response).toContain('"pending_redemption"')
1390
1667
  })