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

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 (237) hide show
  1. package/README.md +71 -16
  2. package/dist/actions/advanced/index.cjs +13 -0
  3. package/dist/actions/advanced/index.d.cts +8 -0
  4. package/dist/actions/advanced/index.d.ts +9 -0
  5. package/dist/actions/advanced/index.js +9 -0
  6. package/dist/advanced-8ZPTmmaN.js +598 -0
  7. package/dist/advanced-8ZPTmmaN.js.map +1 -0
  8. package/dist/advanced-fPImL6lW.cjs +656 -0
  9. package/dist/arkivTransactions-D74gERHG.d.cts +571 -0
  10. package/dist/arkivTransactions-D74gERHG.d.cts.map +1 -0
  11. package/dist/arkivTransactions-NtGD6uk5.d.ts +571 -0
  12. package/dist/arkivTransactions-NtGD6uk5.d.ts.map +1 -0
  13. package/dist/attr/index.cjs +35 -0
  14. package/dist/attr/index.d.cts +2 -0
  15. package/dist/attr/index.d.ts +3 -0
  16. package/dist/attr/index.js +4 -0
  17. package/dist/attributes-BhY5DwJp.js +809 -0
  18. package/dist/attributes-BhY5DwJp.js.map +1 -0
  19. package/dist/attributes-Df9IsCVs.cjs +1029 -0
  20. package/dist/chains/index.cjs +2 -3
  21. package/dist/chains/index.d.cts +22 -60
  22. package/dist/chains/index.d.cts.map +1 -1
  23. package/dist/chains/index.d.ts +22 -60
  24. package/dist/chains/index.d.ts.map +1 -1
  25. package/dist/chains/index.js +2 -2
  26. package/dist/chains-BJrZs6sG.js +57 -0
  27. package/dist/chains-BJrZs6sG.js.map +1 -0
  28. package/dist/chains-BYRTTipa.cjs +73 -0
  29. package/dist/codec-Car3KkFF.d.ts +102 -0
  30. package/dist/codec-Car3KkFF.d.ts.map +1 -0
  31. package/dist/entity/index.cjs +20 -0
  32. package/dist/entity/index.d.cts +4 -0
  33. package/dist/entity/index.d.ts +5 -0
  34. package/dist/entity/index.js +7 -0
  35. package/dist/entity-B00Li04j.cjs +71 -0
  36. package/dist/entity-B7G8q_Eo.cjs +0 -0
  37. package/dist/entity-BYQR1uoe.js +67 -0
  38. package/dist/entity-BYQR1uoe.js.map +1 -0
  39. package/dist/entity-CD_qFF2M.js +0 -0
  40. package/dist/errors-BVbjKVMN.cjs +56 -0
  41. package/dist/errors-LTe7bOYS.js +33 -0
  42. package/dist/errors-LTe7bOYS.js.map +1 -0
  43. package/dist/expiry-BELY2Kpw.cjs +135 -0
  44. package/dist/expiry-BjpxZZQU.js +95 -0
  45. package/dist/expiry-BjpxZZQU.js.map +1 -0
  46. package/dist/expiry-DRV29F1r.d.cts +79 -0
  47. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  48. package/dist/expiry-nDbCgZX4.d.ts +79 -0
  49. package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
  50. package/dist/flags-5dTNRnow.d.ts +41 -0
  51. package/dist/flags-5dTNRnow.d.ts.map +1 -0
  52. package/dist/flags-CRjPpoh3.d.cts +41 -0
  53. package/dist/flags-CRjPpoh3.d.cts.map +1 -0
  54. package/dist/flags-DFBs2J94.js +33 -0
  55. package/dist/flags-DFBs2J94.js.map +1 -0
  56. package/dist/flags-kr276q6i.cjs +43 -0
  57. package/dist/index-B5j76711.d.ts +184 -0
  58. package/dist/index-B5j76711.d.ts.map +1 -0
  59. package/dist/index-BF7Mq9Is.d.cts +280 -0
  60. package/dist/index-BF7Mq9Is.d.cts.map +1 -0
  61. package/dist/index-CA4V7Pdh.d.cts +167 -0
  62. package/dist/index-CA4V7Pdh.d.cts.map +1 -0
  63. package/dist/index-CV5zCdpa.d.cts +417 -0
  64. package/dist/index-CV5zCdpa.d.cts.map +1 -0
  65. package/dist/index-DHDud2hC.d.ts +280 -0
  66. package/dist/index-DHDud2hC.d.ts.map +1 -0
  67. package/dist/index-DvYvrkdk.d.ts +320 -0
  68. package/dist/index-DvYvrkdk.d.ts.map +1 -0
  69. package/dist/index-EeUO0nnJ.d.cts +184 -0
  70. package/dist/index-EeUO0nnJ.d.cts.map +1 -0
  71. package/dist/index-fX_XyqLp.d.ts +167 -0
  72. package/dist/index-fX_XyqLp.d.ts.map +1 -0
  73. package/dist/index.cjs +411 -405
  74. package/dist/index.d.cts +360 -209
  75. package/dist/index.d.cts.map +1 -1
  76. package/dist/index.d.ts +361 -209
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +342 -387
  79. package/dist/index.js.map +1 -1
  80. package/dist/key-D1Bh7MF0.js +70 -0
  81. package/dist/key-D1Bh7MF0.js.map +1 -0
  82. package/dist/key-WKhXnXuU.cjs +98 -0
  83. package/dist/logger-CVpvU8ik.js +153 -0
  84. package/dist/logger-CVpvU8ik.js.map +1 -0
  85. package/dist/logger-Cr15lPAQ.cjs +188 -0
  86. package/dist/payload-BNoud2KZ.d.cts +94 -0
  87. package/dist/payload-BNoud2KZ.d.cts.map +1 -0
  88. package/dist/payload-Bz6651ax.js +67 -0
  89. package/dist/payload-Bz6651ax.js.map +1 -0
  90. package/dist/payload-CcUj85ys.d.ts +94 -0
  91. package/dist/payload-CcUj85ys.d.ts.map +1 -0
  92. package/dist/payload-KC14SDlN.cjs +83 -0
  93. package/dist/query/index.cjs +26 -150
  94. package/dist/query/index.d.cts +5 -3
  95. package/dist/query/index.d.ts +6 -3
  96. package/dist/query/index.js +8 -138
  97. package/dist/queryBuilder-DM6AWCBa.js +820 -0
  98. package/dist/queryBuilder-DM6AWCBa.js.map +1 -0
  99. package/dist/queryBuilder-DbUMEZln.cjs +944 -0
  100. package/dist/selection-C4UhsMOO.d.ts +434 -0
  101. package/dist/selection-C4UhsMOO.d.ts.map +1 -0
  102. package/dist/selection-DPg0uyJf.d.cts +433 -0
  103. package/dist/selection-DPg0uyJf.d.cts.map +1 -0
  104. package/dist/types/index.cjs +3 -3
  105. package/dist/types/index.d.cts +7 -2
  106. package/dist/types/index.d.ts +8 -2
  107. package/dist/types/index.js +3 -3
  108. package/dist/types-Bu_ZwMbZ.js +0 -0
  109. package/dist/types-NRqymi6S.cjs +0 -0
  110. package/dist/utils/index.cjs +4 -17
  111. package/dist/utils/index.d.cts +3 -17
  112. package/dist/utils/index.d.ts +3 -17
  113. package/dist/utils/index.js +4 -18
  114. package/package.json +26 -5
  115. package/src/actions/advanced/advanced.test.ts +258 -0
  116. package/src/actions/advanced/buildMutation.ts +104 -0
  117. package/src/actions/advanced/getMutationResult.ts +112 -0
  118. package/src/actions/advanced/index.ts +21 -0
  119. package/src/actions/advanced/pingTransaction.ts +43 -0
  120. package/src/actions/advanced/sendMutation.ts +59 -0
  121. package/src/actions/public/getEntity.ts +27 -43
  122. package/src/actions/public/getEntityNonce.ts +30 -0
  123. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  124. package/src/actions/public/predictEntityKeys.ts +138 -0
  125. package/src/actions/public/query.ts +35 -67
  126. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  127. package/src/actions/public/watchEntityEvents.ts +225 -0
  128. package/src/actions/wallet/changeOwnership.ts +2 -2
  129. package/src/actions/wallet/createEntity.ts +99 -24
  130. package/src/actions/wallet/extendEntity.ts +37 -13
  131. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  132. package/src/actions/wallet/mutateEntities.ts +16 -33
  133. package/src/actions/wallet/patchEntity.ts +90 -0
  134. package/src/attr/attributes.test.ts +120 -0
  135. package/src/attr/attributes.ts +190 -0
  136. package/src/attr/codec.test.ts +162 -0
  137. package/src/attr/codec.ts +220 -0
  138. package/src/attr/decimal.ts +86 -0
  139. package/src/attr/errors.ts +145 -0
  140. package/src/attr/index.ts +50 -0
  141. package/src/attr/names.ts +106 -0
  142. package/src/attr/types.ts +150 -0
  143. package/src/attr/values.test.ts +271 -0
  144. package/src/attr/values.ts +502 -0
  145. package/src/chains/cheesecake.ts +22 -0
  146. package/src/chains/index.ts +1 -2
  147. package/src/clients/createPublicClient.test.ts +3 -5
  148. package/src/clients/createPublicClient.ts +5 -4
  149. package/src/clients/createWalletClient.test.ts +2 -2
  150. package/src/clients/createWalletClient.ts +13 -7
  151. package/src/clients/decorators/arkivAdvanced.ts +144 -0
  152. package/src/clients/decorators/arkivPublic.ts +172 -139
  153. package/src/clients/decorators/arkivWallet.ts +101 -94
  154. package/src/entity/create.test.ts +184 -0
  155. package/src/entity/errors.ts +38 -0
  156. package/src/entity/events.ts +16 -0
  157. package/src/entity/expiry.test.ts +195 -0
  158. package/src/entity/expiry.ts +181 -0
  159. package/src/entity/flags.ts +65 -0
  160. package/src/entity/index.ts +15 -0
  161. package/src/entity/key.ts +68 -0
  162. package/src/entity/operations.ts +134 -0
  163. package/src/entity/params.ts +15 -0
  164. package/src/errors.ts +26 -49
  165. package/src/index.ts +19 -0
  166. package/src/query/engine.ts +68 -137
  167. package/src/query/errors.ts +151 -0
  168. package/src/query/expression.test.ts +286 -0
  169. package/src/query/expression.ts +493 -0
  170. package/src/query/index.ts +40 -4
  171. package/src/query/injection.test.ts +151 -0
  172. package/src/query/queryBuilder.test.ts +342 -637
  173. package/src/query/queryBuilder.ts +133 -359
  174. package/src/query/queryResult.ts +54 -51
  175. package/src/query/selection.test.ts +48 -98
  176. package/src/query/selection.ts +77 -65
  177. package/src/types/entity.test.ts +44 -155
  178. package/src/types/entity.ts +60 -52
  179. package/src/types/events.ts +59 -16
  180. package/src/types/index.ts +20 -17
  181. package/src/types/rpcSchema.ts +69 -37
  182. package/src/utils/arkivTransactions.test.ts +600 -155
  183. package/src/utils/arkivTransactions.ts +350 -219
  184. package/src/utils/chains.test.ts +21 -24
  185. package/src/utils/compression.ts +1 -1
  186. package/src/utils/entities.test.ts +62 -0
  187. package/src/utils/entities.ts +109 -37
  188. package/src/utils/expirationTime.ts +114 -10
  189. package/src/utils/logger.ts +2 -2
  190. package/src/utils/revert.test.ts +99 -0
  191. package/src/utils/revert.ts +145 -0
  192. package/dist/chains-BifaY04V.cjs +0 -108
  193. package/dist/chains-CrtkVbBh.js +0 -86
  194. package/dist/chains-CrtkVbBh.js.map +0 -1
  195. package/dist/consts-Bz0LZQZY.js +0 -7
  196. package/dist/consts-Bz0LZQZY.js.map +0 -1
  197. package/dist/consts-CxT1-RXU.cjs +0 -18
  198. package/dist/entity-BhSXaB23.cjs +0 -81
  199. package/dist/entity-CN41IOrq.d.cts +0 -401
  200. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  201. package/dist/entity-DGfVyB-a.js +0 -71
  202. package/dist/entity-DGfVyB-a.js.map +0 -1
  203. package/dist/entity-NnEVlePN.d.ts +0 -401
  204. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  205. package/dist/index-ClkpS6s_.d.ts +0 -481
  206. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  207. package/dist/index-w4qop0Pe.d.cts +0 -481
  208. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  209. package/dist/payload-BpNUPOnY.js +0 -13
  210. package/dist/payload-BpNUPOnY.js.map +0 -1
  211. package/dist/payload-C2KHOUoj.d.ts +0 -6
  212. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  213. package/dist/payload-CJN8fTvX.d.cts +0 -6
  214. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  215. package/dist/payload-D8YLKcJj.cjs +0 -23
  216. package/dist/query/index.js.map +0 -1
  217. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  218. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  219. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  220. package/dist/utils/index.d.cts.map +0 -1
  221. package/dist/utils/index.d.ts.map +0 -1
  222. package/dist/utils/index.js.map +0 -1
  223. package/src/actions/public/buildQuery.ts +0 -10
  224. package/src/actions/public/subscribeEntityEvents.ts +0 -93
  225. package/src/actions/wallet/updateEntity.ts +0 -59
  226. package/src/chains/braga.ts +0 -26
  227. package/src/chains/kaolin.ts +0 -29
  228. package/src/query/engine.test.ts +0 -571
  229. package/src/query/predicate.test.ts +0 -461
  230. package/src/query/predicate.ts +0 -144
  231. package/src/query/queryResult.test.ts +0 -270
  232. package/src/query/selectQueryBuilder.test.ts +0 -213
  233. package/src/types/attributes.ts +0 -28
  234. package/src/utils/validation.test.ts +0 -106
  235. package/src/utils/validation.ts +0 -80
  236. /package/dist/{types-Ci4BRClw.js → attr-DlmErkvR.cjs} +0 -0
  237. /package/dist/{types-a8gm_IaQ.cjs → attr-GcQKatub.js} +0 -0
@@ -1,21 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test"
2
- import { braga, kaolin, localhost } from "../chains"
2
+ import { cheesecake, localhost } from "../chains"
3
3
  import { chainFromName } from "./chains"
4
4
 
5
5
  describe("chainFromName", () => {
6
6
  describe("valid chain names", () => {
7
- test("returns braga chain for 'braga'", () => {
8
- const chain = chainFromName("braga")
9
- expect(chain).toBe(braga)
10
- expect(chain.id).toBe(60138453102)
11
- expect(chain.name).toBe("Braga")
12
- })
13
-
14
- test("returns kaolin chain for 'kaolin'", () => {
15
- const chain = chainFromName("kaolin")
16
- expect(chain).toBe(kaolin)
17
- expect(chain.id).toBe(60138453025)
18
- expect(chain.name).toBe("Kaolin")
7
+ test("returns cheesecake chain for 'cheesecake'", () => {
8
+ const chain = chainFromName("cheesecake")
9
+ expect(chain).toBe(cheesecake)
10
+ expect(chain.id).toBe(7733102)
11
+ expect(chain.name).toBe("Cheesecake")
19
12
  })
20
13
 
21
14
  test("returns localhost chain for 'localhost'", () => {
@@ -28,17 +21,14 @@ describe("chainFromName", () => {
28
21
 
29
22
  describe("case insensitivity", () => {
30
23
  test("handles uppercase chain names", () => {
31
- expect(chainFromName("BRAGA")).toBe(braga)
32
- expect(chainFromName("KAOLIN")).toBe(kaolin)
24
+ expect(chainFromName("CHEESECAKE")).toBe(cheesecake)
33
25
  expect(chainFromName("LOCALHOST")).toBe(localhost)
34
26
  })
35
27
 
36
28
  test("handles mixed case chain names", () => {
37
- expect(chainFromName("Braga")).toBe(braga)
38
- expect(chainFromName("Kaolin")).toBe(kaolin)
29
+ expect(chainFromName("Cheesecake")).toBe(cheesecake)
39
30
  expect(chainFromName("Localhost")).toBe(localhost)
40
- expect(chainFromName("bRaGa")).toBe(braga)
41
- expect(chainFromName("kAoLiN")).toBe(kaolin)
31
+ expect(chainFromName("cHeEsEcAkE")).toBe(cheesecake)
42
32
  })
43
33
  })
44
34
 
@@ -52,22 +42,28 @@ describe("chainFromName", () => {
52
42
  })
53
43
 
54
44
  test("throws error for chain name with whitespace", () => {
55
- expect(() => chainFromName("kaolin ")).toThrow("Unknown chain: kaolin ")
56
- expect(() => chainFromName(" kaolin")).toThrow("Unknown chain: kaolin")
45
+ expect(() => chainFromName("cheesecake ")).toThrow("Unknown chain: cheesecake ")
46
+ expect(() => chainFromName(" cheesecake")).toThrow("Unknown chain: cheesecake")
57
47
  })
58
48
 
59
49
  test("throws error for similar but incorrect chain names", () => {
60
- expect(() => chainFromName("kaolinn")).toThrow("Unknown chain: kaolinn")
50
+ expect(() => chainFromName("cheesecakee")).toThrow("Unknown chain: cheesecakee")
51
+ expect(() => chainFromName("cheesecak")).toThrow("Unknown chain: cheesecak")
61
52
  expect(() => chainFromName("mendoza")).toThrow("Unknown chain: mendoza")
62
53
  expect(() => chainFromName("marketplace")).toThrow("Unknown chain: marketplace")
63
54
  expect(() => chainFromName("rosario")).toThrow("Unknown chain: rosario")
64
55
  expect(() => chainFromName("local")).toThrow("Unknown chain: local")
65
56
  })
57
+
58
+ test("throws error for the retired chains", () => {
59
+ expect(() => chainFromName("braga")).toThrow("Unknown chain: braga")
60
+ expect(() => chainFromName("kaolin")).toThrow("Unknown chain: kaolin")
61
+ })
66
62
  })
67
63
 
68
64
  describe("chain properties", () => {
69
65
  test("returned chain has required viem Chain properties", () => {
70
- const chain = chainFromName("kaolin")
66
+ const chain = chainFromName("cheesecake")
71
67
 
72
68
  expect(chain).toHaveProperty("id")
73
69
  expect(chain).toHaveProperty("name")
@@ -79,12 +75,13 @@ describe("chainFromName", () => {
79
75
  })
80
76
 
81
77
  test("returned chain has rpcUrls configured", () => {
82
- const chain = chainFromName("braga")
78
+ const chain = chainFromName("cheesecake")
83
79
 
84
80
  expect(chain.rpcUrls).toBeDefined()
85
81
  expect(chain.rpcUrls.default).toBeDefined()
86
82
  expect(chain.rpcUrls.default.http).toBeInstanceOf(Array)
87
83
  expect(chain.rpcUrls.default.http.length).toBeGreaterThan(0)
84
+ expect(chain.rpcUrls.default.webSocket?.length).toBeGreaterThan(0)
88
85
  })
89
86
  })
90
87
  })
@@ -17,7 +17,7 @@ async function getZlib(): Promise<typeof import("zlib") | null> {
17
17
 
18
18
  if (!zlib) {
19
19
  try {
20
- zlib = await import("zlib")
20
+ zlib = await import("node:zlib")
21
21
  } catch (error) {
22
22
  console.error("Error importing zlib", error)
23
23
  return null
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { RpcEntity } from "../types/rpcSchema"
3
+ import { entityFromRpcResult } from "./entities"
4
+
5
+ const KEY = `0x${"aa".repeat(32)}` as const
6
+
7
+ /** A minimal row, with only the field under test varying. */
8
+ function rowWith(creationFlags: RpcEntity["creationFlags"]): RpcEntity {
9
+ return { key: KEY, creationFlags }
10
+ }
11
+
12
+ describe("the creationFlags projection", () => {
13
+ it("decodes the bare byte a pre-spec node sends", () => {
14
+ expect(entityFromRpcResult(rowWith(0b11)).creationFlags).toEqual({
15
+ readonly: true,
16
+ permissionlessExtension: true,
17
+ raw: 0b11,
18
+ })
19
+ })
20
+
21
+ it("prefers raw over the booleans sent alongside it", () => {
22
+ // `raw` is the byte the engine stored, so it is the one reading that cannot disagree with what
23
+ // is on chain — and it keeps bits this SDK has no name for visible.
24
+ const flags = entityFromRpcResult(
25
+ rowWith({ readonly: false, permissionlessExtension: false, raw: 0b101 }),
26
+ ).creationFlags
27
+ expect(flags).toEqual({ readonly: true, permissionlessExtension: false, raw: 0b101 })
28
+ })
29
+
30
+ it("reads raw as the 0x quantity every other numeric field already uses", () => {
31
+ // createdAt, updatedAt and expiresAt are all hex on this surface, so a node rendering the flags
32
+ // byte the same way is the expected spelling, not a malformed response.
33
+ expect(
34
+ entityFromRpcResult(rowWith({ raw: "0x03" } as unknown as RpcEntity["creationFlags"]))
35
+ .creationFlags,
36
+ ).toEqual({ readonly: true, permissionlessExtension: true, raw: 3 })
37
+ })
38
+
39
+ it("falls back to the booleans when no byte arrives", () => {
40
+ expect(
41
+ entityFromRpcResult(
42
+ rowWith({ readonly: true, permissionlessExtension: false } as RpcEntity["creationFlags"]),
43
+ ).creationFlags,
44
+ ).toEqual({ readonly: true, permissionlessExtension: false, raw: 0b1 })
45
+ })
46
+
47
+ it("drops an unreadable encoding instead of failing the whole read", () => {
48
+ // Nothing on the decode path catches, so a throw here would not fail this field — it would
49
+ // fail the entire getEntity/fetch and take every other entity on the page with it.
50
+ for (const unreadable of [{}, { raw: "nonsense" }, { raw: 999 }, "flags"]) {
51
+ const entity = entityFromRpcResult(
52
+ rowWith(unreadable as unknown as RpcEntity["creationFlags"]),
53
+ )
54
+ expect(entity.creationFlags).toBeUndefined()
55
+ expect(entity.key).toBe(KEY)
56
+ }
57
+ })
58
+
59
+ it("stays undefined when the projection did not ask for it", () => {
60
+ expect(entityFromRpcResult({ key: KEY }).creationFlags).toBeUndefined()
61
+ })
62
+ })
@@ -1,47 +1,119 @@
1
- import { type Hex, toBytes } from "viem"
2
- import type { Attribute } from "../types/attributes"
3
- import { Entity } from "../types/entity"
1
+ import { checksumAddress, type Hex, hexToBytes } from "viem"
2
+ import type { AttributeSchema, Attributes } from "../attr"
3
+ // Internal seam: the JSON-RPC decoder is not part of the package's public surface.
4
+ import { asTypeTag, decodeRpcValue } from "../attr/codec"
5
+ import {
6
+ decodeCreationFlags,
7
+ encodeCreationFlags,
8
+ type ResolvedCreationFlags,
9
+ } from "../entity/flags"
10
+ import { Entity, type EntityFields } from "../types/entity"
4
11
  import type { RpcEntity } from "../types/rpcSchema"
5
- import { RpcAttributeValueType } from "../types/rpcSchema"
6
12
  import { getLogger } from "./logger"
7
13
 
8
- function decodeAttributeValue(value: string, valueType: RpcAttributeValueType): Attribute["value"] {
9
- switch (valueType) {
10
- case RpcAttributeValueType.Uint:
11
- return Number(BigInt(value as Hex))
12
- case RpcAttributeValueType.EntityKey:
13
- return value as Hex
14
- case RpcAttributeValueType.String:
15
- default:
16
- return value
14
+ const logger = getLogger("utils:entities")
15
+
16
+ /**
17
+ * Turns the attribute array a query returns into a name-keyed map of typed values.
18
+ *
19
+ * Attribute names are unique per entity, so the map loses nothing and gives callers
20
+ * `entity.attributes.level.value` instead of a linear search.
21
+ */
22
+ function decodeAttributes(rpcAttributes: NonNullable<RpcEntity["attributes"]>): Attributes {
23
+ const attributes: Record<string, ReturnType<typeof decodeRpcValue>> = {}
24
+ for (const { name, type, value } of rpcAttributes) {
25
+ attributes[name] = decodeRpcValue(type, value)
17
26
  }
27
+ return attributes
18
28
  }
19
29
 
20
- const logger = getLogger("utils:entities")
30
+ /** The same, for the values-free `attributeSchema` projection. */
31
+ function decodeSchema(entries: NonNullable<RpcEntity["attributeSchema"]>): AttributeSchema {
32
+ const schema: Record<string, ReturnType<typeof asTypeTag>> = {}
33
+ for (const { name, type } of entries) {
34
+ schema[name] = asTypeTag(type)
35
+ }
36
+ return schema
37
+ }
21
38
 
22
- export async function entityFromRpcResult(rpcEntity: RpcEntity) {
39
+ /**
40
+ * Builds an {@link Entity} from a query result row.
41
+ *
42
+ * A field the query did not select is absent from the response and stays `undefined` here — the
43
+ * decoder never substitutes an empty value for one that was not asked for.
44
+ */
45
+ export function entityFromRpcResult(rpcEntity: RpcEntity): Entity {
23
46
  logger("entityFromRpcResult %o", rpcEntity)
24
47
 
25
- return new Entity(
26
- rpcEntity.key,
27
- rpcEntity.contentType,
28
- rpcEntity.owner,
29
- rpcEntity.creator,
30
- rpcEntity.expiresAt !== undefined ? BigInt(rpcEntity.expiresAt) : undefined,
31
- rpcEntity.createdAtBlock !== undefined ? BigInt(rpcEntity.createdAtBlock) : undefined,
32
- rpcEntity.lastModifiedAtBlock !== undefined ? BigInt(rpcEntity.lastModifiedAtBlock) : undefined,
33
- rpcEntity.transactionIndexInBlock !== undefined
34
- ? BigInt(rpcEntity.transactionIndexInBlock)
35
- : undefined,
36
- rpcEntity.operationIndexInTransaction !== undefined
37
- ? BigInt(rpcEntity.operationIndexInTransaction)
38
- : undefined,
39
- rpcEntity.value !== undefined ? toBytes(rpcEntity.value) : undefined,
40
- [
41
- ...(rpcEntity.attributes ?? []).map(({ key, value, valueType }) => ({
42
- key,
43
- value: decodeAttributeValue(value, valueType),
44
- })),
45
- ],
46
- )
48
+ const fields: EntityFields = {
49
+ key: rpcEntity.key,
50
+ owner: toAddress(rpcEntity.owner),
51
+ creator: toAddress(rpcEntity.creator),
52
+ createdAt: toBlock(rpcEntity.createdAt),
53
+ updatedAt: toBlock(rpcEntity.updatedAt),
54
+ expiresAt: toBlock(rpcEntity.expiresAt),
55
+ creationFlags: decodeFlags(rpcEntity.creationFlags),
56
+ contentType: rpcEntity.contentType,
57
+ payload: rpcEntity.payload !== undefined ? hexToBytes(rpcEntity.payload) : undefined,
58
+ attributeSchema:
59
+ rpcEntity.attributeSchema !== undefined ? decodeSchema(rpcEntity.attributeSchema) : undefined,
60
+ attributes:
61
+ rpcEntity.attributes !== undefined ? decodeAttributes(rpcEntity.attributes) : undefined,
62
+ }
63
+
64
+ return new Entity(fields)
65
+ }
66
+
67
+ function toBlock(value: string | undefined): bigint | undefined {
68
+ return value === undefined ? undefined : BigInt(value)
69
+ }
70
+
71
+ function toAddress(value: Hex | undefined): Hex | undefined {
72
+ if (value === undefined) return undefined
73
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
74
+ logger("unreadable address %o", value)
75
+ return value
76
+ }
77
+ return checksumAddress(value)
78
+ }
79
+
80
+ /**
81
+ * Resolves the `creationFlags` projection, preferring the byte.
82
+ */
83
+ function decodeFlags(
84
+ value: NonNullable<RpcEntity["creationFlags"]> | undefined,
85
+ ): ResolvedCreationFlags | undefined {
86
+ if (value === undefined) return undefined
87
+
88
+ const raw = toFlagsByte(typeof value === "number" ? value : value.raw)
89
+ if (raw !== undefined) return decodeCreationFlags(raw)
90
+
91
+ if (
92
+ typeof value === "object" &&
93
+ (value.readonly ?? value.permissionlessExtension) !== undefined
94
+ ) {
95
+ return decodeCreationFlags(
96
+ encodeCreationFlags({
97
+ readonly: value.readonly ?? false,
98
+ permissionlessExtension: value.permissionlessExtension ?? false,
99
+ }),
100
+ )
101
+ }
102
+
103
+ logger("unreadable creationFlags %o", value)
104
+ return undefined
105
+ }
106
+
107
+ /**
108
+ * A flags byte in whichever spelling arrived: a JSON number, or the `0x` QUANTITY that every other
109
+ * numeric field on {@link RpcEntity} already uses. `undefined` for anything that is not a byte.
110
+ */
111
+ function toFlagsByte(value: unknown): number | undefined {
112
+ const parsed =
113
+ typeof value === "number"
114
+ ? value
115
+ : typeof value === "string" && /^(0x[0-9a-fA-F]+|\d+)$/.test(value)
116
+ ? Number(value)
117
+ : Number.NaN
118
+ return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0xff ? parsed : undefined
47
119
  }
@@ -1,13 +1,117 @@
1
- import { BLOCK_TIME } from "../consts"
1
+ import { InvalidExpiryError } from "../entity/errors"
2
+ import { type Expiry, type Lifetime, makeExpiry, toBlocks } from "../entity/expiry"
3
+ import { MAX_EXPIRES_AT } from "../entity/params"
2
4
 
5
+ /**
6
+ * The optional floor a deadline can carry.
7
+ *
8
+ * `atLeast` takes a duration — one of the `from*` helpers — and never another deadline, so
9
+ * `atBlock(x, { atLeast: atBlock(y) })` does not compile. Two deadlines are not a thing the engine
10
+ * can resolve.
11
+ */
12
+ export type DeadlineOptions = {
13
+ /** Live at least this long from now, even if the deadline is sooner. */
14
+ atLeast?: Lifetime | undefined
15
+ }
16
+
17
+ function lifetimeOf(seconds: number): Lifetime {
18
+ return makeExpiry(toBlocks(seconds), 0n) as Lifetime
19
+ }
20
+
21
+ function deadlineOf(expiresAt: bigint | Date, options?: DeadlineOptions): Expiry {
22
+ return makeExpiry(options?.atLeast?.minLifetime ?? 0n, expiresAt)
23
+ }
24
+
25
+ /**
26
+ * Builds the `expires` of a create or an extension.
27
+ *
28
+ * Every helper returns the `{ minLifetime, expiresAt }` pair the ABI carries, so what you write is
29
+ * what goes on the wire. The `from*` helpers fill `minLifetime` and leave `expiresAt` at `0n`;
30
+ * {@link ExpirationTime.atBlock} and {@link ExpirationTime.atDate} do the reverse. The engine takes
31
+ * whichever comes later, which is why a deadline can also carry a floor.
32
+ *
33
+ * Values are plain data — no methods, no prototype — so they log, clone and compare like any other
34
+ * object.
35
+ *
36
+ * @example The 90% case: a duration, and nothing else to think about.
37
+ * await client.createEntity({ payload, contentType, expires: ExpirationTime.fromDays(30) })
38
+ *
39
+ * @example An exact block, or a wall-clock date.
40
+ * expires: ExpirationTime.atBlock(1_200_000n)
41
+ * expires: ExpirationTime.atDate(new Date("2027-01-01"))
42
+ *
43
+ * @example A deadline that still guarantees a minimum life.
44
+ * expires: ExpirationTime.atBlock(1_200_000n, { atLeast: ExpirationTime.fromDays(1) })
45
+ */
3
46
  export const ExpirationTime = {
4
- fromSeconds: (seconds: number) => seconds,
5
- fromMinutes: (minutes: number) => minutes * 60,
6
- fromHours: (hours: number) => hours * 60 * 60,
7
- fromDays: (days: number) => days * 24 * 60 * 60,
8
- fromWeeks: (weeks: number) => weeks * 7 * 24 * 60 * 60,
9
- fromMonths: (months: number) => months * 30 * 24 * 60 * 60,
10
- fromYears: (years: number) => years * 365 * 24 * 60 * 60,
11
- fromBlocks: (blocks: number) => blocks * BLOCK_TIME,
12
- fromDate: (date: Date) => Math.floor(date.getTime() / 1000) - Math.floor(Date.now() / 1000),
47
+ /**
48
+ * A raw number of seconds. Must be a whole number of blocks' worth — see the other helpers, which
49
+ * all produce one by construction.
50
+ *
51
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
52
+ */
53
+ fromSeconds: (seconds: number): Lifetime => lifetimeOf(seconds),
54
+ fromMinutes: (minutes: number): Lifetime => lifetimeOf(minutes * 60),
55
+ fromHours: (hours: number): Lifetime => lifetimeOf(hours * 60 * 60),
56
+ fromDays: (days: number): Lifetime => lifetimeOf(days * 24 * 60 * 60),
57
+ fromWeeks: (weeks: number): Lifetime => lifetimeOf(weeks * 7 * 24 * 60 * 60),
58
+ fromMonths: (months: number): Lifetime => lifetimeOf(months * 30 * 24 * 60 * 60),
59
+ fromYears: (years: number): Lifetime => lifetimeOf(years * 365 * 24 * 60 * 60),
60
+
61
+ /**
62
+ * A number of blocks, for callers who think in the chain's own unit. Always exact — there is no
63
+ * conversion to be approximate about.
64
+ *
65
+ * @throws {InvalidExpiryError} If the count is not a positive whole number.
66
+ */
67
+ fromBlocks: (blocks: number): Lifetime => {
68
+ if (!Number.isInteger(blocks) || blocks <= 0) {
69
+ throw new InvalidExpiryError(
70
+ `a lifetime must be a positive whole number of blocks, got ${blocks}`,
71
+ )
72
+ }
73
+ return makeExpiry(BigInt(blocks), 0n) as Lifetime
74
+ },
75
+
76
+ /**
77
+ * An absolute block height.
78
+ *
79
+ * @param block - The block the entity expires at.
80
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
81
+ * @throws {InvalidExpiryError} If the height is negative.
82
+ */
83
+ atBlock: (block: bigint, options?: DeadlineOptions): Expiry => {
84
+ if (typeof block !== "bigint" || block < 0n) {
85
+ throw new InvalidExpiryError(`a deadline must be a non-negative block height, got ${block}`)
86
+ }
87
+ return deadlineOf(block, options)
88
+ },
89
+
90
+ /**
91
+ * A wall-clock instant, placed against the block the transaction is built on.
92
+ *
93
+ * A date cannot land on a block boundary in general, so it rounds **up** to the next block: the
94
+ * entity lives until at least the instant asked for. This is why a date is never rejected for
95
+ * "not being a whole number of blocks" the way a duration is — it becomes a block height, not a
96
+ * duration.
97
+ *
98
+ * @param date - When the entity should expire.
99
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
100
+ * @throws {InvalidExpiryError} If the value is not a valid `Date`.
101
+ */
102
+ atDate: (date: Date, options?: DeadlineOptions): Expiry => {
103
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
104
+ throw new InvalidExpiryError(`a deadline must be a valid Date, got ${String(date)}`)
105
+ }
106
+ return deadlineOf(date, options)
107
+ },
108
+
109
+ /**
110
+ * An entity that should never expire.
111
+ *
112
+ * This is `atBlock(MAX_EXPIRES_AT)` — the largest expiry the `uint64` field holds — and the
113
+ * engine treats it as an ordinary deadline rather than a special case. There is no permanence
114
+ * flag; a block height no chain will reach is what permanence is.
115
+ */
116
+ permanent: (): Expiry => deadlineOf(MAX_EXPIRES_AT),
13
117
  }
@@ -144,8 +144,8 @@ function getColorForNamespace(namespace: string): { css: string; ansi: string }
144
144
 
145
145
  // Use absolute value and modulo to get index
146
146
  const colorIndex = Math.abs(hash) % COLOR_PALETTE.length
147
- const cssColor = COLOR_PALETTE[colorIndex]
148
- const ansiColor = ANSI_COLORS[colorIndex % ANSI_COLORS.length]
147
+ const cssColor = COLOR_PALETTE[colorIndex] ?? "inherit"
148
+ const ansiColor = ANSI_COLORS[colorIndex % ANSI_COLORS.length] ?? ""
149
149
 
150
150
  const color = { css: cssColor, ansi: ansiColor }
151
151
 
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { BaseError, ContractFunctionRevertedError, type Hex, toHex } from "viem"
3
+ import { describeEntityRevert } from "./revert"
4
+
5
+ /**
6
+ * Wraps a decoded revert the way viem delivers one: the `ContractFunctionRevertedError` is nested
7
+ * in a cause chain, which is why `describeEntityRevert` walks rather than type-checks the top.
8
+ */
9
+ function reverted(errorName: string, args: readonly unknown[]): BaseError {
10
+ const inner = new ContractFunctionRevertedError({
11
+ abi: [],
12
+ functionName: "execute",
13
+ })
14
+ // The constructor cannot build `data` without a real ABI decode, so it is set directly — the
15
+ // shape is what matters here, not how viem arrived at it.
16
+ Object.assign(inner, { data: { errorName, args } })
17
+ return new BaseError('The contract function "execute" reverted.', { cause: inner })
18
+ }
19
+
20
+ /** An Ident32 name cell: the name, left-aligned and null-padded to 32 bytes. */
21
+ const ident = (name: string): Hex => toHex(name, { size: 32 })
22
+
23
+ const KEY = `0x${"ab".repeat(32)}` as Hex
24
+
25
+ describe("describeEntityRevert", () => {
26
+ it("ignores anything that is not a decoded engine revert", () => {
27
+ expect(describeEntityRevert(new Error("boom"))).toBeUndefined()
28
+ expect(describeEntityRevert("boom")).toBeUndefined()
29
+ expect(describeEntityRevert(undefined)).toBeUndefined()
30
+ expect(describeEntityRevert(new BaseError("no cause"))).toBeUndefined()
31
+ })
32
+
33
+ describe("attribute names", () => {
34
+ it("names the offending character rather than printing the byte alone", () => {
35
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [5n, "0x21"]))
36
+ expect(message).toContain('"!"')
37
+ expect(message).toContain("0x21")
38
+ expect(message).toContain("byte 5")
39
+ })
40
+
41
+ it("quotes the name charset the SDK itself validates against", () => {
42
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [0n, "0x2f"]))
43
+ expect(message).toContain('"A"-"Z"')
44
+ expect(message).toContain('"a"-"z"')
45
+ expect(message).toContain("with a letter first")
46
+ })
47
+
48
+ it("renders an unprintable byte as a byte", () => {
49
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [1n, "0x07"]))
50
+ expect(message).toContain("byte 0x07")
51
+ })
52
+ })
53
+
54
+ describe("names carried in a bytes32 cell", () => {
55
+ it("reads the name back out of its null padding", () => {
56
+ expect(
57
+ describeEntityRevert(reverted("SystemAttributeNotWritable", [ident("$owner")])),
58
+ ).toContain('"$owner"')
59
+ expect(describeEntityRevert(reverted("TombstoneInCreate", [ident("gone")]))).toContain(
60
+ '"gone"',
61
+ )
62
+ expect(describeEntityRevert(reverted("InvalidValueType", [ident("level"), 99]))).toContain(
63
+ '"level"',
64
+ )
65
+ })
66
+ })
67
+
68
+ describe("entity state", () => {
69
+ it("abbreviates the key so it does not bury the sentence", () => {
70
+ const message = describeEntityRevert(reverted("EntityNotFound", [KEY]))
71
+ expect(message).toContain("…")
72
+ expect(message).not.toContain(KEY)
73
+ })
74
+
75
+ it("explains a readonly entity in terms of what is still allowed", () => {
76
+ const message = describeEntityRevert(reverted("ReadOnlyEntity", [KEY]))
77
+ expect(message).toContain("readonly")
78
+ expect(message).toContain("extended")
79
+ })
80
+
81
+ it("renders reserved creation flags as the bits they are", () => {
82
+ const message = describeEntityRevert(reverted("ReservedCreationFlags", [0b100]))
83
+ expect(message).toContain("0b00000100")
84
+ })
85
+ })
86
+
87
+ describe("expiry", () => {
88
+ it("says which way the extension went wrong", () => {
89
+ const message = describeEntityRevert(reverted("ExpiryNotExtended", [KEY, 100n, 500n]))
90
+ expect(message).toContain("already expires at block 500")
91
+ expect(message).toContain("shorten")
92
+ })
93
+ })
94
+
95
+ it("falls back to naming the error when it has no tailored explanation", () => {
96
+ const message = describeEntityRevert(reverted("SomeFutureError", []))
97
+ expect(message).toBe("the engine rejected the batch with SomeFutureError")
98
+ })
99
+ })