zklicensing 0.1.0-beta.2 → 0.1.0-beta.21

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 (317) hide show
  1. package/README.md +417 -201
  2. package/build/agent.d.ts +371 -0
  3. package/build/agent.d.ts.map +1 -0
  4. package/build/agent.js +553 -0
  5. package/build/agent.js.map +1 -0
  6. package/build/agentActions.d.ts +841 -0
  7. package/build/agentActions.d.ts.map +1 -0
  8. package/build/agentActions.js +581 -0
  9. package/build/agentActions.js.map +1 -0
  10. package/build/agentRecord.d.ts +83 -0
  11. package/build/agentRecord.d.ts.map +1 -0
  12. package/build/agentRecord.js +69 -0
  13. package/build/agentRecord.js.map +1 -0
  14. package/build/agentVendor.d.ts +220 -0
  15. package/build/agentVendor.d.ts.map +1 -0
  16. package/build/agentVendor.js +306 -0
  17. package/build/agentVendor.js.map +1 -0
  18. package/build/appsLookup.d.ts +14 -0
  19. package/build/appsLookup.d.ts.map +1 -0
  20. package/build/appsLookup.js +12 -0
  21. package/build/appsLookup.js.map +1 -0
  22. package/build/archiveBootstrap.d.ts +31 -5
  23. package/build/archiveBootstrap.d.ts.map +1 -0
  24. package/build/archiveBootstrap.js +140 -47
  25. package/build/archiveBootstrap.js.map +1 -0
  26. package/build/atomicJson.d.ts +5 -0
  27. package/build/atomicJson.d.ts.map +1 -0
  28. package/build/atomicJson.js +55 -0
  29. package/build/atomicJson.js.map +1 -0
  30. package/build/bindClient.d.ts +85 -0
  31. package/build/bindClient.d.ts.map +1 -0
  32. package/build/bindClient.js +187 -0
  33. package/build/bindClient.js.map +1 -0
  34. package/build/buyClient.d.ts +27 -4
  35. package/build/buyClient.d.ts.map +1 -0
  36. package/build/buyClient.js +72 -26
  37. package/build/buyClient.js.map +1 -0
  38. package/build/chainHead.d.ts +61 -0
  39. package/build/chainHead.d.ts.map +1 -0
  40. package/build/chainHead.js +88 -0
  41. package/build/chainHead.js.map +1 -0
  42. package/build/compactMerkleMap.d.ts +72 -0
  43. package/build/compactMerkleMap.d.ts.map +1 -0
  44. package/build/compactMerkleMap.js +224 -0
  45. package/build/compactMerkleMap.js.map +1 -0
  46. package/build/contractInterface.d.ts +191 -51
  47. package/build/contractInterface.d.ts.map +1 -0
  48. package/build/contractInterface.js +234 -43
  49. package/build/contractInterface.js.map +1 -0
  50. package/build/contractTypes.d.ts +9 -0
  51. package/build/contractTypes.d.ts.map +1 -0
  52. package/build/contractTypes.js +28 -0
  53. package/build/contractTypes.js.map +1 -0
  54. package/build/countryStats.d.ts +12 -3
  55. package/build/countryStats.d.ts.map +1 -0
  56. package/build/countryStats.js +87 -23
  57. package/build/countryStats.js.map +1 -0
  58. package/build/deployClient.d.ts +138 -24
  59. package/build/deployClient.d.ts.map +1 -0
  60. package/build/deployClient.js +91 -31
  61. package/build/deployClient.js.map +1 -0
  62. package/build/deviceIdentity.d.ts +68 -0
  63. package/build/deviceIdentity.d.ts.map +1 -0
  64. package/build/deviceIdentity.js +209 -0
  65. package/build/deviceIdentity.js.map +1 -0
  66. package/build/freezeClient.d.ts +26 -6
  67. package/build/freezeClient.d.ts.map +1 -0
  68. package/build/freezeClient.js +30 -6
  69. package/build/freezeClient.js.map +1 -0
  70. package/build/generationClient.d.ts +6 -3
  71. package/build/generationClient.d.ts.map +1 -0
  72. package/build/generationClient.js +25 -38
  73. package/build/generationClient.js.map +1 -0
  74. package/build/holderSecret.d.ts +70 -0
  75. package/build/holderSecret.d.ts.map +1 -0
  76. package/build/holderSecret.js +76 -0
  77. package/build/holderSecret.js.map +1 -0
  78. package/build/index.d.ts +38 -38
  79. package/build/index.d.ts.map +1 -0
  80. package/build/index.js +97 -121
  81. package/build/index.js.map +1 -0
  82. package/build/keysClient.d.ts +142 -0
  83. package/build/keysClient.d.ts.map +1 -0
  84. package/build/keysClient.js +109 -0
  85. package/build/keysClient.js.map +1 -0
  86. package/build/licenseLeaf.d.ts +76 -0
  87. package/build/licenseLeaf.d.ts.map +1 -0
  88. package/build/licenseLeaf.js +189 -0
  89. package/build/licenseLeaf.js.map +1 -0
  90. package/build/licenseRecord.d.ts +68 -5
  91. package/build/licenseRecord.d.ts.map +1 -0
  92. package/build/licenseRecord.js +26 -1
  93. package/build/licenseRecord.js.map +1 -0
  94. package/build/licenseStore.d.ts +81 -4
  95. package/build/licenseStore.d.ts.map +1 -0
  96. package/build/licenseStore.js +400 -30
  97. package/build/licenseStore.js.map +1 -0
  98. package/build/licenseTrustless.d.ts +43 -0
  99. package/build/licenseTrustless.d.ts.map +1 -0
  100. package/build/licenseTrustless.js +180 -0
  101. package/build/licenseTrustless.js.map +1 -0
  102. package/build/manifestVerify.d.ts +29 -1
  103. package/build/manifestVerify.d.ts.map +1 -0
  104. package/build/manifestVerify.js +46 -4
  105. package/build/manifestVerify.js.map +1 -0
  106. package/build/minaPriceHistory.d.ts +9 -0
  107. package/build/minaPriceHistory.d.ts.map +1 -0
  108. package/build/minaPriceHistory.js +152 -0
  109. package/build/minaPriceHistory.js.map +1 -0
  110. package/build/onChainRoot.d.ts +60 -0
  111. package/build/onChainRoot.d.ts.map +1 -0
  112. package/build/onChainRoot.js +74 -0
  113. package/build/onChainRoot.js.map +1 -0
  114. package/build/ownershipLicenseHash.d.ts +25 -0
  115. package/build/ownershipLicenseHash.d.ts.map +1 -0
  116. package/build/ownershipLicenseHash.js +29 -0
  117. package/build/ownershipLicenseHash.js.map +1 -0
  118. package/build/ownershipSignature.d.ts +153 -0
  119. package/build/ownershipSignature.d.ts.map +1 -0
  120. package/build/ownershipSignature.js +297 -0
  121. package/build/ownershipSignature.js.map +1 -0
  122. package/build/permitCheckFlow.d.ts +155 -0
  123. package/build/permitCheckFlow.d.ts.map +1 -0
  124. package/build/permitCheckFlow.js +196 -0
  125. package/build/permitCheckFlow.js.map +1 -0
  126. package/build/permitDescribe.d.ts +124 -0
  127. package/build/permitDescribe.d.ts.map +1 -0
  128. package/build/permitDescribe.js +324 -0
  129. package/build/permitDescribe.js.map +1 -0
  130. package/build/permitInterface.d.ts +85 -0
  131. package/build/permitInterface.d.ts.map +1 -0
  132. package/build/permitInterface.js +121 -0
  133. package/build/permitInterface.js.map +1 -0
  134. package/build/permitMessages.d.ts +313 -0
  135. package/build/permitMessages.d.ts.map +1 -0
  136. package/build/permitMessages.js +462 -0
  137. package/build/permitMessages.js.map +1 -0
  138. package/build/permitProof.d.ts +108 -0
  139. package/build/permitProof.d.ts.map +1 -0
  140. package/build/permitProof.js +202 -0
  141. package/build/permitProof.js.map +1 -0
  142. package/build/permitRegistryRebuild.d.ts +166 -0
  143. package/build/permitRegistryRebuild.d.ts.map +1 -0
  144. package/build/permitRegistryRebuild.js +347 -0
  145. package/build/permitRegistryRebuild.js.map +1 -0
  146. package/build/permitTree.d.ts +88 -0
  147. package/build/permitTree.d.ts.map +1 -0
  148. package/build/permitTree.js +230 -0
  149. package/build/permitTree.js.map +1 -0
  150. package/build/permitsClient.d.ts +680 -0
  151. package/build/permitsClient.d.ts.map +1 -0
  152. package/build/permitsClient.js +597 -0
  153. package/build/permitsClient.js.map +1 -0
  154. package/build/proveErrors.d.ts +50 -0
  155. package/build/proveErrors.d.ts.map +1 -0
  156. package/build/proveErrors.js +76 -0
  157. package/build/proveErrors.js.map +1 -0
  158. package/build/receipt.d.ts +95 -0
  159. package/build/receipt.d.ts.map +1 -0
  160. package/build/receipt.js +84 -0
  161. package/build/receipt.js.map +1 -0
  162. package/build/refundClient.d.ts +7 -12
  163. package/build/refundClient.d.ts.map +1 -0
  164. package/build/refundClient.js +52 -21
  165. package/build/refundClient.js.map +1 -0
  166. package/build/registryTrustVerify.d.ts +78 -0
  167. package/build/registryTrustVerify.d.ts.map +1 -0
  168. package/build/registryTrustVerify.js +129 -0
  169. package/build/registryTrustVerify.js.map +1 -0
  170. package/build/releaseClient.d.ts +26 -0
  171. package/build/releaseClient.d.ts.map +1 -0
  172. package/build/releaseClient.js +65 -0
  173. package/build/releaseClient.js.map +1 -0
  174. package/build/renewClient.d.ts +28 -2
  175. package/build/renewClient.d.ts.map +1 -0
  176. package/build/renewClient.js +31 -27
  177. package/build/renewClient.js.map +1 -0
  178. package/build/seatName.d.ts +8 -0
  179. package/build/seatName.d.ts.map +1 -0
  180. package/build/seatName.js +136 -0
  181. package/build/seatName.js.map +1 -0
  182. package/build/slotConstants.d.ts +15 -0
  183. package/build/slotConstants.d.ts.map +1 -0
  184. package/build/slotConstants.js +17 -0
  185. package/build/slotConstants.js.map +1 -0
  186. package/build/statsClient.d.ts +61 -7
  187. package/build/statsClient.d.ts.map +1 -0
  188. package/build/statsClient.js +44 -5
  189. package/build/statsClient.js.map +1 -0
  190. package/build/successorChain.d.ts +35 -0
  191. package/build/successorChain.d.ts.map +1 -0
  192. package/build/successorChain.js +69 -0
  193. package/build/successorChain.js.map +1 -0
  194. package/build/taxId.d.ts +44 -0
  195. package/build/taxId.d.ts.map +1 -0
  196. package/build/taxId.js +212 -0
  197. package/build/taxId.js.map +1 -0
  198. package/build/trustAnchorVerify.d.ts +49 -0
  199. package/build/trustAnchorVerify.d.ts.map +1 -0
  200. package/build/trustAnchorVerify.js +432 -0
  201. package/build/trustAnchorVerify.js.map +1 -0
  202. package/build/trustAnchorVerifyCli.d.ts +45 -0
  203. package/build/trustAnchorVerifyCli.d.ts.map +1 -0
  204. package/build/trustAnchorVerifyCli.js +235 -0
  205. package/build/trustAnchorVerifyCli.js.map +1 -0
  206. package/build/txBroadcastClient.d.ts +26 -0
  207. package/build/txBroadcastClient.d.ts.map +1 -0
  208. package/build/txBroadcastClient.js +20 -0
  209. package/build/txBroadcastClient.js.map +1 -0
  210. package/build/unbindClient.d.ts +77 -0
  211. package/build/unbindClient.d.ts.map +1 -0
  212. package/build/unbindClient.js +169 -0
  213. package/build/unbindClient.js.map +1 -0
  214. package/build/units.d.ts +28 -0
  215. package/build/units.d.ts.map +1 -0
  216. package/build/units.js +65 -0
  217. package/build/units.js.map +1 -0
  218. package/build/vatRates.d.ts +54 -0
  219. package/build/vatRates.d.ts.map +1 -0
  220. package/build/vatRates.js +82 -0
  221. package/build/vatRates.js.map +1 -0
  222. package/build/vatRatesData.d.ts +5 -0
  223. package/build/vatRatesData.d.ts.map +1 -0
  224. package/build/vatRatesData.js +42 -0
  225. package/build/vatRatesData.js.map +1 -0
  226. package/build/vendorAccountClient.d.ts +66 -0
  227. package/build/vendorAccountClient.d.ts.map +1 -0
  228. package/build/vendorAccountClient.js +47 -0
  229. package/build/vendorAccountClient.js.map +1 -0
  230. package/build/verifyCore.d.ts +4 -1
  231. package/build/verifyCore.d.ts.map +1 -0
  232. package/build/verifyCore.js +21 -10
  233. package/build/verifyCore.js.map +1 -0
  234. package/build/verifyOffline.d.ts +85 -0
  235. package/build/verifyOffline.d.ts.map +1 -0
  236. package/build/verifyOffline.js +139 -0
  237. package/build/verifyOffline.js.map +1 -0
  238. package/build/verifyService.d.ts.map +1 -0
  239. package/build/verifyService.js +571 -501
  240. package/build/verifyService.js.map +1 -0
  241. package/build/verifyWithAncestors.d.ts +19 -0
  242. package/build/verifyWithAncestors.d.ts.map +1 -0
  243. package/build/verifyWithAncestors.js +127 -0
  244. package/build/verifyWithAncestors.js.map +1 -0
  245. package/build/zipToState.d.ts +2 -0
  246. package/build/zipToState.d.ts.map +1 -0
  247. package/build/zipToState.js +94 -0
  248. package/build/zipToState.js.map +1 -0
  249. package/package.json +141 -16
  250. package/src/agent.ts +682 -0
  251. package/src/agentActions.ts +637 -0
  252. package/src/agentRecord.ts +121 -0
  253. package/src/agentVendor.ts +356 -0
  254. package/src/appsLookup.ts +48 -0
  255. package/src/archiveBootstrap.ts +386 -0
  256. package/src/atomicJson.ts +53 -0
  257. package/src/bindClient.ts +297 -0
  258. package/src/buyClient.ts +344 -0
  259. package/src/chainHead.ts +106 -0
  260. package/src/compactMerkleMap.ts +222 -0
  261. package/src/contractInterface.ts +465 -0
  262. package/src/contractTypes.ts +31 -0
  263. package/src/countryStats.ts +200 -0
  264. package/src/deployClient.ts +517 -0
  265. package/src/deviceIdentity.ts +249 -0
  266. package/src/freezeClient.ts +162 -0
  267. package/src/generationClient.ts +145 -0
  268. package/src/holderSecret.ts +91 -0
  269. package/src/index.ts +362 -0
  270. package/src/keysClient.ts +135 -0
  271. package/src/licenseLeaf.ts +221 -0
  272. package/src/licenseRecord.ts +190 -0
  273. package/src/licenseStore.ts +767 -0
  274. package/src/licenseTrustless.ts +207 -0
  275. package/src/manifestVerify.ts +164 -0
  276. package/src/minaPriceHistory.ts +166 -0
  277. package/src/onChainRoot.ts +90 -0
  278. package/src/ownershipLicenseHash.ts +31 -0
  279. package/src/ownershipSignature.ts +421 -0
  280. package/src/permitCheckFlow.ts +227 -0
  281. package/src/permitDescribe.ts +362 -0
  282. package/src/permitInterface.ts +146 -0
  283. package/src/permitMessages.ts +572 -0
  284. package/src/permitProof.ts +230 -0
  285. package/src/permitRegistryRebuild.ts +423 -0
  286. package/src/permitTree.ts +232 -0
  287. package/src/permitsClient.ts +746 -0
  288. package/src/proveErrors.ts +98 -0
  289. package/src/receipt.ts +156 -0
  290. package/src/refundClient.ts +177 -0
  291. package/src/registryTrustVerify.ts +192 -0
  292. package/src/releaseClient.ts +101 -0
  293. package/src/renewClient.ts +234 -0
  294. package/src/seatName.ts +165 -0
  295. package/src/slotConstants.ts +18 -0
  296. package/src/statsClient.ts +249 -0
  297. package/src/successorChain.ts +87 -0
  298. package/src/taxId.ts +227 -0
  299. package/src/trustAnchorVerify.ts +550 -0
  300. package/src/trustAnchorVerifyCli.ts +234 -0
  301. package/src/txBroadcastClient.ts +48 -0
  302. package/src/unbindClient.ts +233 -0
  303. package/src/units.ts +74 -0
  304. package/src/vatRates.ts +100 -0
  305. package/src/vatRatesData.ts +43 -0
  306. package/src/vendorAccountClient.ts +120 -0
  307. package/src/verifyCore.ts +100 -0
  308. package/src/verifyOffline.ts +212 -0
  309. package/src/verifyService.ts +982 -0
  310. package/src/verifyWithAncestors.ts +163 -0
  311. package/src/zipToState.ts +94 -0
  312. package/build/licenseProof.d.ts +0 -26
  313. package/build/licenseProof.js +0 -41
  314. package/build/migrateClient.d.ts +0 -43
  315. package/build/migrateClient.js +0 -47
  316. package/build/ownership.d.ts +0 -34
  317. package/build/ownership.js +0 -147
@@ -0,0 +1,200 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * countryStats.ts
6
+ *
7
+ * Buyer-country breakdown of a vendor's revenue, bucketed by UTC calendar year.
8
+ * Structured for tax reporting: one row per (year, appId, countryCode) with
9
+ * license count + the vendor's cut in MINA. Both the backend (source of
10
+ * truth for /stats) and the dashboard UI import from here so the numbers can
11
+ * never drift between them.
12
+ *
13
+ * Purchases attribute to the year the buyer confirmed on-chain (LicenseRecord
14
+ * `createdAt` ISO timestamp). Legacy records that predate `createdAt` fall
15
+ * back to slot-derived time. Renewals attribute to the renewal year via
16
+ * `renewals[i].slot → time`. Refunded purchases contribute zero revenue but
17
+ * still count the license (a purchase happened even if the money was
18
+ * returned) — remove `count` from refunded rows client-side if you want a
19
+ * strictly net view.
20
+ *
21
+ * Browser-safe: pure computation, no fs, no fetch.
22
+ */
23
+
24
+ import { vendorShareNano, type LicenseRecord } from './licenseRecord.js';
25
+ import { standardVatRate, vatIncludedNano } from './vatRates.js';
26
+ import { zipToState } from './zipToState.js';
27
+
28
+ export { zipToState } from './zipToState.js';
29
+
30
+ // LicensingApp charges a 2 % platform fee (200 basis points); the vendor
31
+ // receives the remaining 9 800 bps. Keep this in sync with the on-chain
32
+ // constant in LicensingApp.
33
+
34
+ // 90 s/slot post-Mesa. Used only for slot→time fallback attribution.
35
+ const SLOT_DURATION_MS = 90_000;
36
+
37
+ export type CountryBucket = {
38
+ count: number; // licenses purchased by buyers from this country (renewals don't bump)
39
+ revenueMina: number; // vendor cut in MINA — purchase + renewals combined
40
+ // What buyers paid (the VAT-inclusive price), before the platform fee —
41
+ // the VAT base. Refunded purchases count zero, as for revenue.
42
+ grossMina: number;
43
+ // Estimated VAT included in grossMina, at the buyer country's standard
44
+ // rate on each sale's date (zklicensing/vat). Absent for countries
45
+ // without VAT in the table. An estimate for consumer sales, not tax
46
+ // advice: reverse-charged business sales and thresholds aren't applied.
47
+ estimatedVatMina?: number;
48
+ // The distinct standard rates (percent) applied, sorted; more than one
49
+ // when the rate changed during the year.
50
+ vatRates?: number[];
51
+ // State sub-buckets — populated only for US buyers, derived from
52
+ // `buyerZip` via zipToState. States are the jurisdictional unit that US
53
+ // sales-tax filings care about; the raw ZIP stays on the LicenseRecord
54
+ // for audit trail but isn't rolled up here. Missing entirely on non-US
55
+ // rows; may be missing on US rows for legacy records that predate ZIP
56
+ // capture or whose ZIP prefix doesn't resolve to a state.
57
+ states?: Record<string, { count: number; revenueMina: number; grossMina: number }>;
58
+ };
59
+ export type CountryYearBreakdown = {
60
+ aggregate: Record<string, CountryBucket>; // keyed by ISO-alpha-2 (or '??' when missing)
61
+ byApp: Record<string, Record<string, CountryBucket>>; // appId → country → bucket
62
+ };
63
+ export type CountryBreakdown = {
64
+ years: number[]; // UTC calendar years with activity, most-recent first
65
+ byYear: Record<number, CountryYearBreakdown>;
66
+ };
67
+
68
+ // tzOffsetMinutes: signed minutes east of UTC (e.g. +60 for CET, -480 for PST).
69
+ // Purchase/renewal timestamps are shifted by this offset before extracting the
70
+ // calendar year, so the "tax year" cut-over aligns with the operator's local
71
+ // clock instead of hardcoded UTC. Defaults to 0 (UTC) for callers who don't
72
+ // pass it — preserves the pre-tz behavior.
73
+ export function computeCountryBreakdown(
74
+ apps: { appId: string; zkAppAddress: string }[],
75
+ records: LicenseRecord[],
76
+ currentSlot: number,
77
+ nowMs: number,
78
+ tzOffsetMinutes: number = 0,
79
+ ): CountryBreakdown {
80
+ const appByAddress = new Map(apps.map(a => [a.zkAppAddress, a.appId] as const));
81
+ const byYear: Record<number, CountryYearBreakdown> = {};
82
+ const yearSet = new Set<number>();
83
+ const tzShiftMs = tzOffsetMinutes * 60_000;
84
+
85
+ const ensureYear = (year: number): CountryYearBreakdown => {
86
+ let yb = byYear[year];
87
+ if (!yb) {
88
+ yb = { aggregate: {}, byApp: {} };
89
+ byYear[year] = yb;
90
+ yearSet.add(year);
91
+ }
92
+ return yb;
93
+ };
94
+ const ensureAppBucket = (yb: CountryYearBreakdown, appId: string): Record<string, CountryBucket> => {
95
+ let ab = yb.byApp[appId];
96
+ if (!ab) {
97
+ ab = {};
98
+ yb.byApp[appId] = ab;
99
+ }
100
+ return ab;
101
+ };
102
+ const add = (
103
+ bucket: Record<string, CountryBucket>,
104
+ code: string,
105
+ count: number,
106
+ revenueNano: bigint,
107
+ grossNano: bigint,
108
+ saleMs: number | null,
109
+ state?: string,
110
+ ): void => {
111
+ let b = bucket[code];
112
+ if (!b) {
113
+ b = { count: 0, revenueMina: 0, grossMina: 0 };
114
+ bucket[code] = b;
115
+ }
116
+ b.count += count;
117
+ b.revenueMina += Number(revenueNano) / 1e9;
118
+ b.grossMina += Number(grossNano) / 1e9;
119
+ // VAT included at the rate in force on the sale date.
120
+ const rate = code !== '??' && saleMs !== null ? standardVatRate(code, saleMs) : null;
121
+ if (rate !== null) {
122
+ b.estimatedVatMina = (b.estimatedVatMina ?? 0) + Number(vatIncludedNano(grossNano, rate).vatNano) / 1e9;
123
+ if (grossNano > 0n) {
124
+ const rates = (b.vatRates ??= []);
125
+ if (!rates.includes(rate)) { rates.push(rate); rates.sort((x, y) => x - y); }
126
+ }
127
+ }
128
+ // State sub-bucket only for US rows whose ZIP resolved to a state.
129
+ // Non-US or unresolvable rows just add to the country total.
130
+ if (code === 'US' && state) {
131
+ const states = b.states ?? (b.states = {});
132
+ let sb = states[state];
133
+ if (!sb) {
134
+ sb = { count: 0, revenueMina: 0, grossMina: 0 };
135
+ states[state] = sb;
136
+ }
137
+ sb.count += count;
138
+ sb.revenueMina += Number(revenueNano) / 1e9;
139
+ sb.grossMina += Number(grossNano) / 1e9;
140
+ }
141
+ };
142
+ const slotToMs = (slot: number): number | null => {
143
+ if (currentSlot <= 0) return null;
144
+ const ms = nowMs - (currentSlot - slot) * SLOT_DURATION_MS;
145
+ return Number.isFinite(ms) ? ms : null;
146
+ };
147
+ const msToYear = (ms: number | null): number | null => {
148
+ if (ms === null) return null;
149
+ const y = new Date(ms + tzShiftMs).getUTCFullYear();
150
+ return Number.isFinite(y) ? y : null;
151
+ };
152
+
153
+ for (const r of records) {
154
+ const appId = appByAddress.get(r.zkAppAddress);
155
+ if (!appId) continue;
156
+ const country = r.buyerCountry && /^[A-Z]{2}$/.test(r.buyerCountry) ? r.buyerCountry : '??';
157
+ // Only US rows carry a meaningful ZIP for sales-tax purposes; derive
158
+ // the state jurisdiction here and let `add` roll it up. Non-US ZIPs
159
+ // and unresolvable prefixes fall through to country-total only.
160
+ const state = country === 'US' && r.buyerZip ? zipToState(r.buyerZip) : undefined;
161
+
162
+ // Purchase — attribute to the year of confirm-on-chain (createdAt).
163
+ // Fallback: slot-derived time for legacy records that lack createdAt.
164
+ let purchaseMs: number | null = null;
165
+ if (r.createdAt) {
166
+ const t = new Date(r.createdAt).getTime();
167
+ if (Number.isFinite(t)) purchaseMs = t;
168
+ }
169
+ if (purchaseMs === null && r.purchaseSlot > 0) {
170
+ purchaseMs = slotToMs(r.purchaseSlot);
171
+ }
172
+ const purchaseYear = msToYear(purchaseMs);
173
+ if (purchaseYear !== null) {
174
+ const yb = ensureYear(purchaseYear);
175
+ const ab = ensureAppBucket(yb, appId);
176
+ const purchaseGross = (!r.refunded && r.purchaseAmount && r.purchaseAmount !== '0') ? BigInt(r.purchaseAmount) : 0n;
177
+ const purchaseCut = purchaseGross > 0n ? vendorShareNano(purchaseGross) : 0n;
178
+ add(ab, country, 1, purchaseCut, purchaseGross, purchaseMs, state);
179
+ add(yb.aggregate, country, 1, purchaseCut, purchaseGross, purchaseMs, state);
180
+ }
181
+
182
+ // Renewals — revenue only (no license count), attributed to renewal year.
183
+ for (const rn of r.renewals ?? []) {
184
+ const ms = slotToMs(rn.slot);
185
+ const y = msToYear(ms);
186
+ if (y === null) continue;
187
+ const yb = ensureYear(y);
188
+ const ab = ensureAppBucket(yb, appId);
189
+ const gross = BigInt(rn.amount);
190
+ const cut = vendorShareNano(gross);
191
+ add(ab, country, 0, cut, gross, ms, state);
192
+ add(yb.aggregate, country, 0, cut, gross, ms, state);
193
+ }
194
+ }
195
+
196
+ return {
197
+ years: Array.from(yearSet).sort((a, b) => b - a),
198
+ byYear,
199
+ };
200
+ }