@arkiv-network/sdk 0.7.0 → 0.8.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +29 -16
  2. package/dist/attr/index.cjs +34 -0
  3. package/dist/attr/index.d.cts +2 -0
  4. package/dist/attr/index.d.ts +2 -0
  5. package/dist/attr/index.js +3 -0
  6. package/dist/attr-BDZ-APg5.js +809 -0
  7. package/dist/attr-BDZ-APg5.js.map +1 -0
  8. package/dist/attr-BvxHN99W.cjs +1029 -0
  9. package/dist/chains/index.cjs +2 -3
  10. package/dist/chains/index.d.cts +39 -77
  11. package/dist/chains/index.d.cts.map +1 -1
  12. package/dist/chains/index.d.ts +22 -60
  13. package/dist/chains/index.d.ts.map +1 -1
  14. package/dist/chains/index.js +2 -2
  15. package/dist/chains-BJrZs6sG.js +57 -0
  16. package/dist/chains-BJrZs6sG.js.map +1 -0
  17. package/dist/chains-BYRTTipa.cjs +73 -0
  18. package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
  19. package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
  20. package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
  21. package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
  22. package/dist/entity/index.cjs +19 -0
  23. package/dist/entity/index.d.cts +4 -0
  24. package/dist/entity/index.d.ts +4 -0
  25. package/dist/entity/index.js +6 -0
  26. package/dist/entity-B0t6sN7F.cjs +71 -0
  27. package/dist/entity-BIp_df9u.js +67 -0
  28. package/dist/entity-BIp_df9u.js.map +1 -0
  29. package/dist/entity-CFhbAv0R.js +70 -0
  30. package/dist/entity-CFhbAv0R.js.map +1 -0
  31. package/dist/entity-wfBnZd91.cjs +98 -0
  32. package/dist/errors-D-eJkGVc.cjs +56 -0
  33. package/dist/errors-DGB8AgT2.js +33 -0
  34. package/dist/errors-DGB8AgT2.js.map +1 -0
  35. package/dist/expiry-BUYIXTGb.cjs +135 -0
  36. package/dist/expiry-BxOlvVWC.d.ts +79 -0
  37. package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
  38. package/dist/expiry-DGRzrSan.js +95 -0
  39. package/dist/expiry-DGRzrSan.js.map +1 -0
  40. package/dist/expiry-DRV29F1r.d.cts +79 -0
  41. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  42. package/dist/flags-0Vl5rrHq.d.cts +41 -0
  43. package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
  44. package/dist/flags-Ct8k0gpB.cjs +43 -0
  45. package/dist/flags-ZtlvOIIk.js +33 -0
  46. package/dist/flags-ZtlvOIIk.js.map +1 -0
  47. package/dist/flags-wmcMoAnN.d.ts +41 -0
  48. package/dist/flags-wmcMoAnN.d.ts.map +1 -0
  49. package/dist/index-Cgw1u5aY.d.cts +278 -0
  50. package/dist/index-Cgw1u5aY.d.cts.map +1 -0
  51. package/dist/index-ClgIuxJ8.d.ts +159 -0
  52. package/dist/index-ClgIuxJ8.d.ts.map +1 -0
  53. package/dist/index-D0CmtiWT.d.cts +159 -0
  54. package/dist/index-D0CmtiWT.d.cts.map +1 -0
  55. package/dist/index-D8LZt-3J.d.cts +417 -0
  56. package/dist/index-D8LZt-3J.d.cts.map +1 -0
  57. package/dist/index-DVYFaXNV.d.ts +417 -0
  58. package/dist/index-DVYFaXNV.d.ts.map +1 -0
  59. package/dist/index-VCqgn29D.d.ts +278 -0
  60. package/dist/index-VCqgn29D.d.ts.map +1 -0
  61. package/dist/index.cjs +675 -357
  62. package/dist/index.d.cts +251 -203
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +251 -203
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -355
  67. package/dist/index.js.map +1 -1
  68. package/dist/payload-Bnw1hrvb.cjs +83 -0
  69. package/dist/payload-CIxSn_DW.js +67 -0
  70. package/dist/payload-CIxSn_DW.js.map +1 -0
  71. package/dist/payload-Dgx3PQeK.d.ts +94 -0
  72. package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
  73. package/dist/payload-P0nWyjLg.d.cts +94 -0
  74. package/dist/payload-P0nWyjLg.d.cts.map +1 -0
  75. package/dist/query/index.cjs +24 -149
  76. package/dist/query/index.d.cts +5 -3
  77. package/dist/query/index.d.ts +5 -3
  78. package/dist/query/index.js +6 -137
  79. package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
  80. package/dist/queryBuilder-CbLL4pG5.js +963 -0
  81. package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
  82. package/dist/selection-BvK6yX0g.d.cts +433 -0
  83. package/dist/selection-BvK6yX0g.d.cts.map +1 -0
  84. package/dist/selection-fpmI3ADQ.d.ts +433 -0
  85. package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
  86. package/dist/types/index.cjs +4 -1
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +4 -1
  90. package/dist/utils/index.cjs +4 -17
  91. package/dist/utils/index.d.cts +3 -17
  92. package/dist/utils/index.d.ts +3 -17
  93. package/dist/utils/index.js +4 -18
  94. package/package.json +26 -5
  95. package/src/actions/public/getEntity.ts +27 -43
  96. package/src/actions/public/getEntityNonce.ts +30 -0
  97. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  98. package/src/actions/public/predictEntityKeys.ts +138 -0
  99. package/src/actions/public/query.ts +33 -69
  100. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  101. package/src/actions/public/watchEntityEvents.ts +225 -0
  102. package/src/actions/wallet/changeOwnership.ts +8 -8
  103. package/src/actions/wallet/createEntity.ts +104 -26
  104. package/src/actions/wallet/deleteEntity.ts +2 -3
  105. package/src/actions/wallet/extendEntity.ts +39 -16
  106. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  107. package/src/actions/wallet/mutateEntities.ts +29 -74
  108. package/src/actions/wallet/patchEntity.ts +90 -0
  109. package/src/attr/attributes.test.ts +120 -0
  110. package/src/attr/attributes.ts +190 -0
  111. package/src/attr/codec.test.ts +162 -0
  112. package/src/attr/codec.ts +220 -0
  113. package/src/attr/decimal.ts +86 -0
  114. package/src/attr/errors.ts +145 -0
  115. package/src/attr/index.ts +50 -0
  116. package/src/attr/names.ts +106 -0
  117. package/src/attr/types.ts +150 -0
  118. package/src/attr/values.test.ts +271 -0
  119. package/src/attr/values.ts +502 -0
  120. package/src/chains/cheesecake.ts +22 -0
  121. package/src/chains/index.ts +1 -2
  122. package/src/clients/createPublicClient.test.ts +3 -5
  123. package/src/clients/createPublicClient.ts +5 -4
  124. package/src/clients/createWalletClient.test.ts +2 -2
  125. package/src/clients/createWalletClient.ts +13 -7
  126. package/src/clients/decorators/arkivPublic.ts +160 -139
  127. package/src/clients/decorators/arkivWallet.ts +91 -95
  128. package/src/consts.ts +2 -1
  129. package/src/entity/create.test.ts +184 -0
  130. package/src/entity/errors.ts +38 -0
  131. package/src/entity/events.ts +16 -0
  132. package/src/entity/expiry.test.ts +195 -0
  133. package/src/entity/expiry.ts +181 -0
  134. package/src/entity/flags.ts +65 -0
  135. package/src/entity/index.ts +14 -0
  136. package/src/entity/key.ts +68 -0
  137. package/src/entity/operations.ts +134 -0
  138. package/src/entity/params.ts +15 -0
  139. package/src/errors.ts +33 -28
  140. package/src/index.ts +6 -0
  141. package/src/query/engine.ts +59 -134
  142. package/src/query/errors.ts +151 -0
  143. package/src/query/expression.test.ts +286 -0
  144. package/src/query/expression.ts +493 -0
  145. package/src/query/index.ts +40 -4
  146. package/src/query/injection.test.ts +151 -0
  147. package/src/query/queryBuilder.test.ts +322 -637
  148. package/src/query/queryBuilder.ts +126 -359
  149. package/src/query/queryResult.ts +54 -51
  150. package/src/query/selection.test.ts +48 -98
  151. package/src/query/selection.ts +77 -65
  152. package/src/types/entity.test.ts +44 -155
  153. package/src/types/entity.ts +62 -44
  154. package/src/types/events.ts +64 -23
  155. package/src/types/index.ts +20 -16
  156. package/src/types/rpcSchema.ts +70 -33
  157. package/src/utils/arkivTransactions.test.ts +681 -38
  158. package/src/utils/arkivTransactions.ts +249 -104
  159. package/src/utils/chains.test.ts +21 -24
  160. package/src/utils/compression.ts +1 -1
  161. package/src/utils/entities.test.ts +62 -0
  162. package/src/utils/entities.ts +111 -29
  163. package/src/utils/expirationTime.ts +114 -10
  164. package/src/utils/logger.ts +2 -2
  165. package/src/utils/payload.ts +4 -2
  166. package/src/utils/revert.test.ts +99 -0
  167. package/src/utils/revert.ts +145 -0
  168. package/dist/chains-BifaY04V.cjs +0 -108
  169. package/dist/chains-CrtkVbBh.js +0 -86
  170. package/dist/chains-CrtkVbBh.js.map +0 -1
  171. package/dist/consts-AUoL-v42.cjs +0 -18
  172. package/dist/consts-BV4PR-de.js +0 -7
  173. package/dist/consts-BV4PR-de.js.map +0 -1
  174. package/dist/entity-B6g_EPm1.d.cts +0 -382
  175. package/dist/entity-B6g_EPm1.d.cts.map +0 -1
  176. package/dist/entity-qoLMAuuk.d.ts +0 -382
  177. package/dist/entity-qoLMAuuk.d.ts.map +0 -1
  178. package/dist/index-DdspxNAA.d.ts +0 -481
  179. package/dist/index-DdspxNAA.d.ts.map +0 -1
  180. package/dist/index-Ds9dwqb5.d.cts +0 -481
  181. package/dist/index-Ds9dwqb5.d.cts.map +0 -1
  182. package/dist/payload-B2kFJyah.d.ts +0 -6
  183. package/dist/payload-B2kFJyah.d.ts.map +0 -1
  184. package/dist/payload-CJN8fTvX.d.cts +0 -6
  185. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  186. package/dist/payload-Claf2jUO.js +0 -13
  187. package/dist/payload-Claf2jUO.js.map +0 -1
  188. package/dist/payload-DpA5PA-w.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-D2uwP9jI.js +0 -704
  191. package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
  192. package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
  193. package/dist/utils/index.d.cts.map +0 -1
  194. package/dist/utils/index.d.ts.map +0 -1
  195. package/dist/utils/index.js.map +0 -1
  196. package/src/actions/public/buildQuery.ts +0 -10
  197. package/src/actions/public/subscribeEntityEvents.ts +0 -119
  198. package/src/actions/wallet/updateEntity.ts +0 -60
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -526
  202. package/src/query/predicate.test.ts +0 -461
  203. package/src/query/predicate.ts +0 -144
  204. package/src/query/queryResult.test.ts +0 -270
  205. package/src/query/selectQueryBuilder.test.ts +0 -210
  206. package/src/types/attributes.ts +0 -16
  207. package/src/utils/validation.test.ts +0 -60
  208. package/src/utils/validation.ts +0 -32
  209. /package/dist/{types-H-TSmRpm.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-o3zWarRp.cjs → types-CKPddlfS.cjs} +0 -0
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { ExpirationTime } from "../utils/expirationTime"
3
+ import { InvalidExpiryError } from "./errors"
4
+ import { type Expiry, type ExpiryContext, resolveExpiry, toBlocks } from "./expiry"
5
+ import { MAX_EXPIRES_AT } from "./params"
6
+
7
+ const CURRENT_BLOCK = 1000n
8
+ const ctx: ExpiryContext = { currentBlock: CURRENT_BLOCK }
9
+
10
+ describe("what ExpirationTime builds", () => {
11
+ it("is the wire pair itself, so what you write is what is sent", () => {
12
+ expect(ExpirationTime.fromDays(30)).toEqual({ minLifetime: 1_296_000n, expiresAt: 0n } as never)
13
+ expect(ExpirationTime.atBlock(1_200_000n)).toEqual({
14
+ minLifetime: 0n,
15
+ expiresAt: 1_200_000n,
16
+ } as never)
17
+ })
18
+
19
+ it("is plain data — no prototype, no methods to lose", () => {
20
+ const expiry = ExpirationTime.fromDays(1)
21
+ expect(Object.getPrototypeOf(expiry)).toBe(Object.prototype)
22
+ expect(structuredClone(expiry)).toEqual(expiry as never)
23
+ })
24
+
25
+ it("puts a duration in minLifetime and a deadline in expiresAt, never the other way round", () => {
26
+ // Which slot a value lands in is what decides the rules that govern it.
27
+ for (const lifetime of [
28
+ ExpirationTime.fromSeconds(120),
29
+ ExpirationTime.fromMinutes(1),
30
+ ExpirationTime.fromHours(1),
31
+ ExpirationTime.fromDays(1),
32
+ ExpirationTime.fromWeeks(1),
33
+ ExpirationTime.fromMonths(1),
34
+ ExpirationTime.fromYears(1),
35
+ ExpirationTime.fromBlocks(3),
36
+ ]) {
37
+ expect(lifetime.expiresAt).toBe(0n)
38
+ expect(lifetime.minLifetime > 0n).toBe(true)
39
+ }
40
+ expect(ExpirationTime.atBlock(5n).minLifetime).toBe(0n)
41
+ expect(ExpirationTime.atDate(new Date(Date.now() + 60_000)).minLifetime).toBe(0n)
42
+ })
43
+ })
44
+
45
+ describe("durations — the everyday case", () => {
46
+ it("turns a duration into a lifetime, with no block arithmetic at the call site", () => {
47
+ const resolved = resolveExpiry(ExpirationTime.fromDays(30), ctx)
48
+ // 30 days at 2s/block.
49
+ expect(resolved.minLifetime).toBe(1_296_000n)
50
+ expect(resolved.expiresAt).toBe(0n)
51
+ expect(resolved.target).toBe(CURRENT_BLOCK + 1_296_000n)
52
+ })
53
+
54
+ it("counts blocks exactly, with no conversion to be approximate about", () => {
55
+ expect(resolveExpiry(ExpirationTime.fromBlocks(7), ctx).minLifetime).toBe(7n)
56
+ expect(() => ExpirationTime.fromBlocks(0)).toThrow(/positive whole number of blocks/)
57
+ expect(() => ExpirationTime.fromBlocks(1.5)).toThrow(InvalidExpiryError)
58
+ })
59
+
60
+ it("refuses a duration that is not a whole number of blocks, at the call site", () => {
61
+ // 3s is a block and a half. Rounding it would silently grant 4s, so it is rejected instead —
62
+ // and it throws where it is written, not later where it is sent.
63
+ expect(() => ExpirationTime.fromSeconds(3)).toThrow(/multiple of the 2s block time/)
64
+ expect(() => ExpirationTime.fromSeconds(999)).toThrow(/Try 998 or 1000/)
65
+ expect(() => ExpirationTime.fromSeconds(0)).toThrow(/positive whole number of seconds/)
66
+ expect(() => ExpirationTime.fromSeconds(-2)).toThrow(InvalidExpiryError)
67
+ expect(() => ExpirationTime.fromSeconds(1.5)).toThrow(InvalidExpiryError)
68
+ })
69
+ })
70
+
71
+ describe("deadlines — the absolute case", () => {
72
+ it("takes a block height as given", () => {
73
+ const resolved = resolveExpiry(ExpirationTime.atBlock(5_000n), ctx)
74
+ expect(resolved.expiresAt).toBe(5_000n)
75
+ expect(resolved.minLifetime).toBe(0n)
76
+ expect(resolved.target).toBe(5_000n)
77
+ })
78
+
79
+ it("places a Date against the block the transaction is built on", () => {
80
+ const inAnHour = ExpirationTime.atDate(new Date(Date.now() + 3600_000))
81
+ const resolved = resolveExpiry(inAnHour, ctx)
82
+ // 3600s at 2s/block, counted from the current block.
83
+ expect(resolved.expiresAt).toBe(CURRENT_BLOCK + 1800n)
84
+ expect(resolved.target).toBe(CURRENT_BLOCK + 1800n)
85
+ })
86
+
87
+ it("rounds a Date up rather than rejecting it, unlike a duration", () => {
88
+ // A wall-clock instant becomes a block HEIGHT, which is whole by construction — there is no
89
+ // divisibility question to fail. `fromSeconds(3)` would have thrown; this cannot.
90
+ const odd = ExpirationTime.atDate(new Date(Date.now() + 3_000))
91
+ expect(resolveExpiry(odd, ctx).target).toBe(CURRENT_BLOCK + 2n)
92
+ })
93
+
94
+ it("holds the Date until it is resolved, so the value does not depend on when it was built", () => {
95
+ const date = new Date(Date.now() + 3600_000)
96
+ const expiry = ExpirationTime.atDate(date)
97
+ expect(expiry.expiresAt).toEqual(date)
98
+ // The same value resolves against whatever block it is later sent with.
99
+ expect(resolveExpiry(expiry, { ...ctx, currentBlock: 9_000n }).target).toBe(9_000n + 1800n)
100
+ })
101
+
102
+ it("rejects a deadline already gone", () => {
103
+ expect(() => resolveExpiry(ExpirationTime.atDate(new Date(Date.now() - 1000)), ctx)).toThrow(
104
+ /in the past/,
105
+ )
106
+ expect(() => resolveExpiry(ExpirationTime.atBlock(CURRENT_BLOCK - 1n), ctx)).toThrow(
107
+ /dead on arrival/,
108
+ )
109
+ expect(() => resolveExpiry(ExpirationTime.atBlock(CURRENT_BLOCK), ctx)).toThrow(
110
+ /dead on arrival/,
111
+ )
112
+ })
113
+
114
+ it("rejects something that is neither a block height nor a Date", () => {
115
+ expect(() => ExpirationTime.atBlock(-1n)).toThrow(/non-negative block height/)
116
+ expect(() => ExpirationTime.atDate(new Date("nonsense"))).toThrow(/valid Date/)
117
+ expect(() => ExpirationTime.atBlock(5 as never)).toThrow(/non-negative block height/)
118
+ })
119
+ })
120
+
121
+ describe("a deadline with a floor", () => {
122
+ it("lives until whichever is later", () => {
123
+ // The deadline is further out than the floor.
124
+ const deadlineWins = ExpirationTime.atBlock(5_000n, {
125
+ atLeast: ExpirationTime.fromSeconds(400),
126
+ })
127
+ expect(resolveExpiry(deadlineWins, ctx).target).toBe(5_000n)
128
+
129
+ // The floor is further out than the deadline.
130
+ const floorWins = ExpirationTime.atBlock(1_100n, { atLeast: ExpirationTime.fromDays(1) })
131
+ expect(resolveExpiry(floorWins, ctx).target).toBe(CURRENT_BLOCK + 43_200n)
132
+ })
133
+
134
+ it("puts both on the wire, leaving the engine to resolve them the same way", () => {
135
+ const resolved = resolveExpiry(
136
+ ExpirationTime.atBlock(5_000n, { atLeast: ExpirationTime.fromSeconds(200) }),
137
+ ctx,
138
+ )
139
+ expect(resolved.expiresAt).toBe(5_000n)
140
+ expect(resolved.minLifetime).toBe(100n)
141
+ })
142
+
143
+ it("works the same on a date deadline", () => {
144
+ const expiry = ExpirationTime.atDate(new Date(Date.now() + 2_000), {
145
+ atLeast: ExpirationTime.fromDays(1),
146
+ })
147
+ // The date is only a second or two out, so the floor is what keeps the entity alive.
148
+ expect(resolveExpiry(expiry, ctx).target).toBe(CURRENT_BLOCK + 43_200n)
149
+ })
150
+
151
+ it("treats an omitted floor as no floor", () => {
152
+ expect(ExpirationTime.atBlock(5_000n, {}).minLifetime).toBe(0n)
153
+ expect(ExpirationTime.atBlock(5_000n, { atLeast: undefined }).minLifetime).toBe(0n)
154
+ })
155
+ })
156
+
157
+ describe("bounds and misuse", () => {
158
+ it("refuses an expiry that says nothing", () => {
159
+ // Only reachable from JS, or by hand-building the pair — the type requires ExpirationTime.
160
+ const nothing = { minLifetime: 0n, expiresAt: 0n } as unknown as Expiry
161
+ expect(() => resolveExpiry(nothing, ctx)).toThrow(/no expiry given/)
162
+ })
163
+
164
+ it("names ExpirationTime when handed something it did not build", () => {
165
+ expect(() => resolveExpiry(2_592_000 as unknown as Expiry, ctx)).toThrow(
166
+ /must be built with ExpirationTime/,
167
+ )
168
+ expect(() => resolveExpiry(undefined as unknown as Expiry, ctx)).toThrow(
169
+ /must be built with ExpirationTime/,
170
+ )
171
+ })
172
+
173
+ it("bounds an expiry only by the width of the wire field", () => {
174
+ expect(() => resolveExpiry(ExpirationTime.atBlock(MAX_EXPIRES_AT + 1n), ctx)).toThrow(
175
+ /does not fit the uint64/,
176
+ )
177
+ expect(() => resolveExpiry(ExpirationTime.atBlock(MAX_EXPIRES_AT + 1n), ctx)).toThrow(
178
+ /ExpirationTime\.permanent\(\)/,
179
+ )
180
+ })
181
+ })
182
+
183
+ describe("toBlocks", () => {
184
+ it("converts whole blocks' worth of seconds", () => {
185
+ expect(toBlocks(2)).toBe(1n)
186
+ expect(toBlocks(4)).toBe(2n)
187
+ expect(toBlocks(86_400)).toBe(43_200n)
188
+ })
189
+
190
+ it("rejects anything that is not a positive multiple of the block time", () => {
191
+ for (const bad of [1, 3, 999, 0, -2, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) {
192
+ expect(() => toBlocks(bad)).toThrow(InvalidExpiryError)
193
+ }
194
+ })
195
+ })
@@ -0,0 +1,181 @@
1
+ import { BLOCK_TIME } from "../consts"
2
+ import { InvalidExpiryError } from "./errors"
3
+ import { MAX_EXPIRES_AT } from "./params"
4
+
5
+ /**
6
+ * The brand. Module-private and never exported, so the only way to obtain an {@link Expiry} is to
7
+ * build one with {@link ExpirationTime} — a hand-written `{ minLifetime, expiresAt }` literal will
8
+ * not typecheck, and neither will a bare number of seconds.
9
+ */
10
+ declare const validated: unique symbol
11
+
12
+ /**
13
+ * How long an entity should live.
14
+ *
15
+ * This is the pair the ABI carries, and the engine resolves it as
16
+ * `max(expiresAt, currentBlock + minLifetime)` — whichever is later. Build one with
17
+ * {@link ExpirationTime}; there is no other way to make one.
18
+ *
19
+ * Which slot a value lands in also decides which rules govern it. `minLifetime` is a duration, so
20
+ * it must come to a whole number of blocks. `expiresAt` is a block height, which is a whole number
21
+ * by construction, so a wall-clock date rounds into it rather than being rejected.
22
+ */
23
+ export type Expiry = {
24
+ /** Lifetime floor in blocks, counted from the block the transaction lands in. `0n` if unset. */
25
+ readonly minLifetime: bigint
26
+ /**
27
+ * Absolute deadline: a block height, or a `Date` still to be placed against the current block.
28
+ * `0n` if unset.
29
+ */
30
+ readonly expiresAt: bigint | Date
31
+ readonly [validated]: true
32
+ }
33
+
34
+ /**
35
+ * An expiry that is nothing but a duration — what the `ExpirationTime.from*` helpers return.
36
+ *
37
+ * Distinguished from a full {@link Expiry} by its `expiresAt` being the literal `0n`, which is what
38
+ * keeps `atBlock(x, { atLeast: atBlock(y) })` from compiling: a deadline is not a floor.
39
+ */
40
+ export type Lifetime = Expiry & { readonly expiresAt: 0n }
41
+
42
+ /**
43
+ * Builds a branded {@link Expiry}.
44
+ *
45
+ * Internal seam for {@link ExpirationTime}, which is the only thing that should call it — it is not
46
+ * part of the package's public surface.
47
+ */
48
+ export function makeExpiry(minLifetime: bigint, expiresAt: bigint | Date): Expiry {
49
+ return { minLifetime, expiresAt } as Expiry
50
+ }
51
+
52
+ /** The two `uint256` values the ABI carries, with every `Date` already placed. */
53
+ export type EncodedExpiry = {
54
+ /** Absolute target block height, or `0n` for a purely relative lifetime. */
55
+ expiresAt: bigint
56
+ /** Relative floor in blocks, or `0n` for a purely absolute expiry. */
57
+ minLifetime: bigint
58
+ }
59
+
60
+ /** What an expiry resolves to, given the chain's current block. */
61
+ export type ResolvedExpiry = EncodedExpiry & {
62
+ /** The block the entity will expire at — `max(expiresAt, currentBlock + minLifetime)`. */
63
+ target: bigint
64
+ }
65
+
66
+ /** What the SDK needs to know about the chain to resolve an expiry. */
67
+ export type ExpiryContext = {
68
+ currentBlock: bigint
69
+ }
70
+
71
+ /**
72
+ * Resolves an expiry into the wire pair and the block the entity will expire at.
73
+ *
74
+ * Durations are converted to blocks at the nominal `BLOCK_TIME` (2 seconds), which makes them
75
+ * **approximate**: block production is not a clock, so a lifetime asked for in days drifts if the
76
+ * chain runs fast or slow. The block it was converted to comes back as
77
+ * {@link ResolvedExpiry.target}, so the approximation is always observable rather than assumed.
78
+ *
79
+ * @throws {InvalidExpiryError} If the expiry is malformed, does not fit the `uint64` the wire
80
+ * carries, or would leave the entity dead on arrival.
81
+ */
82
+ export function resolveExpiry(expiry: Expiry, context: ExpiryContext): ResolvedExpiry {
83
+ const { currentBlock } = context
84
+
85
+ if (typeof expiry?.minLifetime !== "bigint" || expiry.expiresAt === undefined) {
86
+ throw new InvalidExpiryError(
87
+ "expires must be built with ExpirationTime, e.g. ExpirationTime.fromDays(30) or " +
88
+ "ExpirationTime.atBlock(1_200_000n)",
89
+ )
90
+ }
91
+
92
+ const { minLifetime } = expiry
93
+ const absolute =
94
+ typeof expiry.expiresAt === "bigint"
95
+ ? expiry.expiresAt
96
+ : blockAtDate(expiry.expiresAt, currentBlock)
97
+
98
+ if (minLifetime === 0n && absolute === 0n) {
99
+ throw new InvalidExpiryError(
100
+ "no expiry given. Pass ExpirationTime.fromDays(30) for a lifetime, " +
101
+ "ExpirationTime.atBlock(n) for an absolute deadline, or a deadline with " +
102
+ "{ atLeast } to take whichever is later.",
103
+ )
104
+ }
105
+
106
+ if (absolute > MAX_EXPIRES_AT) {
107
+ throw new InvalidExpiryError(
108
+ `expiresAt resolves to block ${absolute}, which does not fit the uint64 the wire carries ` +
109
+ `(max ${MAX_EXPIRES_AT}). Use ExpirationTime.permanent() for an entity that should not ` +
110
+ "expire.",
111
+ )
112
+ }
113
+ if (minLifetime > MAX_EXPIRES_AT) {
114
+ throw new InvalidExpiryError(
115
+ `the lifetime is ${minLifetime} blocks, which does not fit the uint64 the wire carries ` +
116
+ `(max ${MAX_EXPIRES_AT})`,
117
+ )
118
+ }
119
+
120
+ const relative = minLifetime === 0n ? 0n : currentBlock + minLifetime
121
+ const target = absolute > relative ? absolute : relative
122
+
123
+ if (target <= currentBlock) {
124
+ throw new InvalidExpiryError(
125
+ `the entity would expire at block ${target}, which is not after the current block ` +
126
+ `${currentBlock}, so it would be dead on arrival`,
127
+ )
128
+ }
129
+ if (target > MAX_EXPIRES_AT) {
130
+ throw new InvalidExpiryError(
131
+ `a lifetime of ${minLifetime} blocks from block ${currentBlock} lands at ${target}, past ` +
132
+ `the largest expiry the wire carries (${MAX_EXPIRES_AT})`,
133
+ )
134
+ }
135
+
136
+ return { expiresAt: absolute, minLifetime, target }
137
+ }
138
+
139
+ /**
140
+ * A duration in seconds as a whole number of blocks.
141
+ *
142
+ * Arkiv counts lifetimes in blocks, so a duration that is not a whole number of them does not
143
+ * describe anything the chain can store. Rather than silently round a caller's `3` up to two blocks
144
+ * — four seconds, not what they asked for — this rejects it: a duration must be a positive multiple
145
+ * of `BLOCK_TIME` (2 seconds). The {@link ExpirationTime} helpers all produce such multiples.
146
+ *
147
+ * @throws {InvalidExpiryError} If the duration is not a positive whole number of blocks' seconds.
148
+ */
149
+ export function toBlocks(seconds: number): bigint {
150
+ if (!Number.isInteger(seconds) || seconds <= 0) {
151
+ throw new InvalidExpiryError(
152
+ `a lifetime must be a positive whole number of seconds, got ${seconds}. Use the ` +
153
+ "ExpirationTime helpers, e.g. ExpirationTime.fromDays(30).",
154
+ )
155
+ }
156
+ if (seconds % BLOCK_TIME !== 0) {
157
+ throw new InvalidExpiryError(
158
+ `a lifetime must be a multiple of the ${BLOCK_TIME}s block time, got ${seconds}, which is ` +
159
+ `${seconds / BLOCK_TIME} blocks. Try ${seconds - (seconds % BLOCK_TIME)} or ` +
160
+ `${seconds + BLOCK_TIME - (seconds % BLOCK_TIME)}, or ExpirationTime.fromBlocks(n).`,
161
+ )
162
+ }
163
+ return BigInt(seconds / BLOCK_TIME)
164
+ }
165
+
166
+ /**
167
+ * The block a wall-clock instant lands on.
168
+ *
169
+ * Rounds **up**: a date almost never falls on a block boundary, and unlike a duration — which the
170
+ * caller states and can state exactly — there is nothing here to reject, so the entity lives until
171
+ * at least the instant asked for.
172
+ */
173
+ function blockAtDate(date: Date, currentBlock: bigint): bigint {
174
+ const secondsAway = (date.getTime() - Date.now()) / 1000
175
+ if (secondsAway <= 0) {
176
+ throw new InvalidExpiryError(
177
+ `the deadline ${date.toISOString()} is in the past, so the entity would be dead on arrival`,
178
+ )
179
+ }
180
+ return currentBlock + BigInt(Math.ceil(secondsAway / BLOCK_TIME))
181
+ }
@@ -0,0 +1,65 @@
1
+ import { InvalidCreationFlagsError } from "./errors"
2
+
3
+ /** Bit positions in the `creationFlags` byte. Bits 2-7 are reserved and must be zero. */
4
+ const READONLY_BIT = 0b0000_0001
5
+ const PERMISSIONLESS_EXTENSION_BIT = 0b0000_0010
6
+
7
+ /**
8
+ * The immutable properties an entity is created with. Both default to `false`.
9
+ *
10
+ * Creation flags are fixed at creation: there is no operation that changes them, so an entity is
11
+ * whatever it was created as for its whole life.
12
+ */
13
+ export type CreationFlags = {
14
+ /**
15
+ * The entity's attributes and payload can never be changed — only its expiry extended, its
16
+ * ownership transferred, and the entity deleted.
17
+ */
18
+ readonly?: boolean
19
+ /**
20
+ * Anyone, not just the owner, may extend the entity's expiry. Lets a third party keep something
21
+ * alive without being able to change it.
22
+ */
23
+ permissionlessExtension?: boolean
24
+ }
25
+
26
+ /** {@link CreationFlags} as read back, with every flag resolved and the raw byte alongside. */
27
+ export type ResolvedCreationFlags = {
28
+ readonly: boolean
29
+ permissionlessExtension: boolean
30
+ raw: number
31
+ }
32
+
33
+ /**
34
+ * Packs creation flags into the `creationFlags` byte.
35
+ *
36
+ * @throws {InvalidCreationFlagsError} If a value is not a boolean.
37
+ */
38
+ export function encodeCreationFlags(flags: CreationFlags = {}): number {
39
+ for (const [name, value] of Object.entries(flags)) {
40
+ if (value !== undefined && typeof value !== "boolean") {
41
+ throw new InvalidCreationFlagsError(`"${name}" must be a boolean, got ${typeof value}`)
42
+ }
43
+ }
44
+ return (
45
+ (flags.readonly ? READONLY_BIT : 0) |
46
+ (flags.permissionlessExtension ? PERMISSIONLESS_EXTENSION_BIT : 0)
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Reads the `creationFlags` byte back.
52
+ *
53
+ * @param raw - The `creationFlags` byte.
54
+ * @throws {InvalidCreationFlagsError} Malformed input
55
+ */
56
+ export function decodeCreationFlags(raw: number): ResolvedCreationFlags {
57
+ if (!Number.isInteger(raw) || raw < 0 || raw > 0xff) {
58
+ throw new InvalidCreationFlagsError(`${raw} is not a byte`)
59
+ }
60
+ return {
61
+ readonly: (raw & READONLY_BIT) !== 0,
62
+ permissionlessExtension: (raw & PERMISSIONLESS_EXTENSION_BIT) !== 0,
63
+ raw,
64
+ }
65
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ InvalidCreationFlagsError,
3
+ InvalidExpiryError,
4
+ InvalidSaltError,
5
+ NoRandomSourceError,
6
+ } from "./errors"
7
+ export type { EntityEventName } from "./events"
8
+ export { ENTITY_EVENTS_ABI } from "./events"
9
+ export type { EncodedExpiry, Expiry, ExpiryContext, Lifetime, ResolvedExpiry } from "./expiry"
10
+ export { resolveExpiry, toBlocks } from "./expiry"
11
+ export type { CreationFlags, ResolvedCreationFlags } from "./flags"
12
+ export { decodeCreationFlags, encodeCreationFlags } from "./flags"
13
+ export { MAX_SALT, predictEntityKey, randomSalt } from "./key"
14
+ export { MAX_EXPIRES_AT, OperationType } from "./params"
@@ -0,0 +1,68 @@
1
+ import { type Address, encodePacked, type Hex, keccak256 } from "viem"
2
+ import { ARKIV_ADDRESS } from "../consts"
3
+ import { InvalidSaltError, NoRandomSourceError } from "./errors"
4
+
5
+ /** The widest salt the `uint128` wire field carries. */
6
+ export const MAX_SALT = 2n ** 128n - 1n
7
+
8
+ /**
9
+ * A cryptographically random 128-bit salt — what {@link CreateEntityParameters.salt} defaults to.
10
+ *
11
+ * The salt exists for unpredictability, not uniqueness: the per-owner nonce already guarantees
12
+ * distinct keys. Random bits mean only the creator can predict the key of an entity they are about
13
+ * to create, so nobody else can reference or squat it in advance.
14
+ *
15
+ * @throws {NoRandomSourceError} If the runtime has no `crypto.getRandomValues`.
16
+ */
17
+ export function randomSalt(): bigint {
18
+ const source = globalThis.crypto
19
+ if (typeof source?.getRandomValues !== "function") {
20
+ throw new NoRandomSourceError()
21
+ }
22
+ const bytes = source.getRandomValues(new Uint8Array(16))
23
+ let salt = 0n
24
+ for (const byte of bytes) salt = (salt << 8n) | BigInt(byte)
25
+ return salt
26
+ }
27
+
28
+ /** Validates a caller-supplied salt against the `uint128` wire field. */
29
+ export function validateSalt(salt: bigint): bigint {
30
+ if (typeof salt !== "bigint" || salt < 0n || salt > MAX_SALT) {
31
+ throw new InvalidSaltError(salt)
32
+ }
33
+ return salt
34
+ }
35
+
36
+ /**
37
+ * The key an entity will be created with, derived exactly as the engine derives it:
38
+ * `keccak256(domain ++ owner ++ nonce ++ salt)`.
39
+ *
40
+ * Deriving it client-side is what lets a batch compose: predict the key of a create, then have a
41
+ * later operation in the same transaction reference it, or store it as a `key` attribute on a
42
+ * sibling entity.
43
+ */
44
+ export function predictEntityKey({
45
+ owner,
46
+ nonce,
47
+ salt,
48
+ chainId,
49
+ }: {
50
+ /** The account creating the entity. */
51
+ owner: Address
52
+ /**
53
+ * The owner's entity-nonce at creation time. Each create in a batch consumes one, so the *n*th
54
+ * create in a batch uses `nonce + n`.
55
+ */
56
+ nonce: bigint
57
+ /** The salt the create will carry. */
58
+ salt: bigint
59
+ /** The chain the entity is being created on — half of the derivation domain. */
60
+ chainId: number
61
+ }): Hex {
62
+ return keccak256(
63
+ encodePacked(
64
+ ["uint256", "address", "address", "uint64", "uint128"],
65
+ [BigInt(chainId), ARKIV_ADDRESS, owner, nonce, validateSalt(salt)],
66
+ ),
67
+ )
68
+ }
@@ -0,0 +1,134 @@
1
+ import { type Address, encodeAbiParameters, type Hex, parseAbi, parseAbiParameters } from "viem"
2
+ import type { AbiAttribute } from "../attr/codec"
3
+ import type { EncodedExpiry } from "./expiry"
4
+ import { OperationType } from "./params"
5
+
6
+ /**
7
+ * The one external entry point. A transaction is an ordered, **atomic** batch of entity operations:
8
+ * all of them apply or none do, operation *n+1* observes operation *n*'s effects, and each applied
9
+ * operation emits one event.
10
+ */
11
+ export const EXECUTE_ABI = parseAbi([
12
+ "function execute((uint8 operation, bytes operationData)[] ops) external returns (bytes32[] keys)",
13
+ ])
14
+
15
+ /**
16
+ * The engine's one read-only view: an owner's entity-minting nonce, which is the input
17
+ * {@link predictEntityKey} needs and the only part of the derivation the caller cannot know.
18
+ *
19
+ * It is a `view` on the same address `execute` is called on, so it answers over `eth_call` without
20
+ * a transaction.
21
+ */
22
+ export const ENTITY_NONCE_ABI = parseAbi([
23
+ "function entityNonce(address owner) external view returns (uint64)",
24
+ ])
25
+
26
+ /**
27
+ * The shared attribute shape, used by `create` (attributes) and `patch` (mutations). Written out
28
+ * once and reused by every payload that carries attributes.
29
+ */
30
+ const ATTRIBUTE_TUPLE = "(bytes32 name, uint8 typeId, bytes value)[]"
31
+
32
+ /**
33
+ * The payload struct for each operation tag. `operationData` is `abi.encode` of the struct the tag
34
+ * selects — a tagged union, so a new operation is additive rather than a change to `execute`.
35
+ */
36
+ const PAYLOAD_PARAMS = {
37
+ [OperationType.Create]: parseAbiParameters(
38
+ `(uint128 salt, uint64 expiresAt, uint64 minLifetime, uint8 creationFlags, ${ATTRIBUTE_TUPLE} attributes)`,
39
+ ),
40
+ [OperationType.Patch]: parseAbiParameters(`(bytes32 entityKey, ${ATTRIBUTE_TUPLE} mutations)`),
41
+ [OperationType.ExtendExpiry]: parseAbiParameters(
42
+ "(bytes32 entityKey, uint64 expiresAt, uint64 minLifetime)",
43
+ ),
44
+ [OperationType.TransferOwnership]: parseAbiParameters("(bytes32 entityKey, address newOwner)"),
45
+ [OperationType.Delete]: parseAbiParameters("(bytes32 entityKey)"),
46
+ } as const
47
+
48
+ /** One entry of the `execute` batch: the operation tag and its ABI-encoded payload. */
49
+ export type Operation = {
50
+ operation: number
51
+ operationData: Hex
52
+ }
53
+
54
+ /** Creates an entity. The key is derived by the engine; predict it with `predictEntityKey`. */
55
+ export function createOperation({
56
+ salt,
57
+ expiry,
58
+ creationFlags,
59
+ attributes,
60
+ }: {
61
+ salt: bigint
62
+ expiry: EncodedExpiry
63
+ creationFlags: number
64
+ attributes: AbiAttribute[]
65
+ }): Operation {
66
+ return {
67
+ operation: OperationType.Create,
68
+ operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Create], [
69
+ {
70
+ salt,
71
+ expiresAt: expiry.expiresAt,
72
+ minLifetime: expiry.minLifetime,
73
+ creationFlags,
74
+ attributes,
75
+ },
76
+ ]),
77
+ }
78
+ }
79
+
80
+ /** Applies a set of mutations to an existing entity. Tombstones unset attributes. */
81
+ export function patchOperation({
82
+ entityKey,
83
+ mutations,
84
+ }: {
85
+ entityKey: Hex
86
+ mutations: AbiAttribute[]
87
+ }): Operation {
88
+ return {
89
+ operation: OperationType.Patch,
90
+ operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Patch], [
91
+ { entityKey, mutations },
92
+ ]),
93
+ }
94
+ }
95
+
96
+ /** Pushes an entity's expiry further out. Extending is allowed; shortening reverts. */
97
+ export function extendExpiryOperation({
98
+ entityKey,
99
+ expiry,
100
+ }: {
101
+ entityKey: Hex
102
+ expiry: EncodedExpiry
103
+ }): Operation {
104
+ return {
105
+ operation: OperationType.ExtendExpiry,
106
+ operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.ExtendExpiry], [
107
+ { entityKey, expiresAt: expiry.expiresAt, minLifetime: expiry.minLifetime },
108
+ ]),
109
+ }
110
+ }
111
+
112
+ /** Hands an entity to a new owner. */
113
+ export function transferOwnershipOperation({
114
+ entityKey,
115
+ newOwner,
116
+ }: {
117
+ entityKey: Hex
118
+ newOwner: Address
119
+ }): Operation {
120
+ return {
121
+ operation: OperationType.TransferOwnership,
122
+ operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.TransferOwnership], [
123
+ { entityKey, newOwner },
124
+ ]),
125
+ }
126
+ }
127
+
128
+ /** Deletes an entity before its expiry. */
129
+ export function deleteOperation({ entityKey }: { entityKey: Hex }): Operation {
130
+ return {
131
+ operation: OperationType.Delete,
132
+ operationData: encodeAbiParameters(PAYLOAD_PARAMS[OperationType.Delete], [{ entityKey }]),
133
+ }
134
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Max uint64 value
3
+ */
4
+ export const MAX_EXPIRES_AT = 2n ** 64n - 1n
5
+
6
+ /** The operation tags of the `execute` ABI's tagged union. */
7
+ export const OperationType = {
8
+ Create: 1,
9
+ Patch: 2,
10
+ ExtendExpiry: 3,
11
+ TransferOwnership: 4,
12
+ Delete: 5,
13
+ } as const
14
+
15
+ export type OperationType = (typeof OperationType)[keyof typeof OperationType]