@arkiv-network/sdk 0.7.0 → 0.8.0-advanced.3

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 (240) 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-Ds2XRflQ.cjs +656 -0
  7. package/dist/advanced-FZy7WoYR.js +598 -0
  8. package/dist/advanced-FZy7WoYR.js.map +1 -0
  9. package/dist/arkivTransactions-BpARZOsH.d.cts +584 -0
  10. package/dist/arkivTransactions-BpARZOsH.d.cts.map +1 -0
  11. package/dist/arkivTransactions-DfZ_yVDD.d.ts +584 -0
  12. package/dist/arkivTransactions-DfZ_yVDD.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-BJGuhsqp.js +819 -0
  18. package/dist/attributes-BJGuhsqp.js.map +1 -0
  19. package/dist/attributes-CMJlp1wn.cjs +1039 -0
  20. package/dist/chains/index.cjs +2 -3
  21. package/dist/chains/index.d.cts +41 -76
  22. package/dist/chains/index.d.cts.map +1 -1
  23. package/dist/chains/index.d.ts +24 -59
  24. package/dist/chains/index.d.ts.map +1 -1
  25. package/dist/chains/index.js +2 -2
  26. package/dist/chains-BfNQliHU.cjs +76 -0
  27. package/dist/chains-Du8tql7I.js +60 -0
  28. package/dist/chains-Du8tql7I.js.map +1 -0
  29. package/dist/codec-M74UR1_0.d.ts +101 -0
  30. package/dist/codec-M74UR1_0.d.ts.map +1 -0
  31. package/dist/entity/index.cjs +21 -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-51znTSB1.cjs +56 -0
  41. package/dist/errors-CwPJ16bn.js +33 -0
  42. package/dist/errors-CwPJ16bn.js.map +1 -0
  43. package/dist/expiry-CZErShX6.js +95 -0
  44. package/dist/expiry-CZErShX6.js.map +1 -0
  45. package/dist/expiry-DRV29F1r.d.cts +79 -0
  46. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  47. package/dist/expiry-nDbCgZX4.d.ts +79 -0
  48. package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
  49. package/dist/expiry-xr6mx4f7.cjs +135 -0
  50. package/dist/flags-5dTNRnow.d.ts +41 -0
  51. package/dist/flags-5dTNRnow.d.ts.map +1 -0
  52. package/dist/flags-Bh0HJldY.cjs +43 -0
  53. package/dist/flags-CRjPpoh3.d.cts +41 -0
  54. package/dist/flags-CRjPpoh3.d.cts.map +1 -0
  55. package/dist/flags-Cr76ldwF.js +33 -0
  56. package/dist/flags-Cr76ldwF.js.map +1 -0
  57. package/dist/index-CMeD8uEq.d.cts +280 -0
  58. package/dist/index-CMeD8uEq.d.cts.map +1 -0
  59. package/dist/index-CTDI5pXZ.d.ts +184 -0
  60. package/dist/index-CTDI5pXZ.d.ts.map +1 -0
  61. package/dist/index-CekGjkRx.d.cts +184 -0
  62. package/dist/index-CekGjkRx.d.cts.map +1 -0
  63. package/dist/index-D0IzFwMX.d.ts +328 -0
  64. package/dist/index-D0IzFwMX.d.ts.map +1 -0
  65. package/dist/index-DZxwz2Ky.d.cts +177 -0
  66. package/dist/index-DZxwz2Ky.d.cts.map +1 -0
  67. package/dist/index-Dz4SkQWg.d.ts +280 -0
  68. package/dist/index-Dz4SkQWg.d.ts.map +1 -0
  69. package/dist/index-JP31fjyM.d.ts +177 -0
  70. package/dist/index-JP31fjyM.d.ts.map +1 -0
  71. package/dist/index-NbfnXxgr.d.cts +424 -0
  72. package/dist/index-NbfnXxgr.d.cts.map +1 -0
  73. package/dist/index.cjs +434 -412
  74. package/dist/index.d.cts +401 -235
  75. package/dist/index.d.cts.map +1 -1
  76. package/dist/index.d.ts +402 -235
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +370 -404
  79. package/dist/index.js.map +1 -1
  80. package/dist/key-E7mv0BYf.cjs +116 -0
  81. package/dist/key-XQkpw2xh.js +82 -0
  82. package/dist/key-XQkpw2xh.js.map +1 -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-Cvrir3Q-.cjs +108 -0
  87. package/dist/payload-D53ms-Mg.d.ts +153 -0
  88. package/dist/payload-D53ms-Mg.d.ts.map +1 -0
  89. package/dist/payload-DaPzUvEh.js +92 -0
  90. package/dist/payload-DaPzUvEh.js.map +1 -0
  91. package/dist/payload-hluX1rc0.d.cts +153 -0
  92. package/dist/payload-hluX1rc0.d.cts.map +1 -0
  93. package/dist/query/index.cjs +26 -149
  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 -137
  97. package/dist/queryBuilder-BXR-HxD9.cjs +947 -0
  98. package/dist/queryBuilder-BwauPaGO.js +823 -0
  99. package/dist/queryBuilder-BwauPaGO.js.map +1 -0
  100. package/dist/selection-C6KeWJ7g.d.cts +441 -0
  101. package/dist/selection-C6KeWJ7g.d.cts.map +1 -0
  102. package/dist/selection-mjiiAGDO.d.ts +442 -0
  103. package/dist/selection-mjiiAGDO.d.ts.map +1 -0
  104. package/dist/types/index.cjs +4 -1
  105. package/dist/types/index.d.cts +7 -2
  106. package/dist/types/index.d.ts +8 -2
  107. package/dist/types/index.js +4 -1
  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 -9
  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 +28 -43
  122. package/src/actions/public/getEntityNonce.ts +30 -0
  123. package/src/actions/public/predictEntityKeys.test.ts +159 -0
  124. package/src/actions/public/predictEntityKeys.ts +152 -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 +8 -8
  129. package/src/actions/wallet/createEntity.ts +105 -26
  130. package/src/actions/wallet/deleteEntity.ts +2 -3
  131. package/src/actions/wallet/executeBatch.test.ts +76 -0
  132. package/src/actions/wallet/executeBatch.ts +51 -0
  133. package/src/actions/wallet/extendEntity.ts +40 -16
  134. package/src/actions/wallet/patchEntity.ts +90 -0
  135. package/src/attr/attributes.test.ts +120 -0
  136. package/src/attr/attributes.ts +196 -0
  137. package/src/attr/codec.test.ts +162 -0
  138. package/src/attr/codec.ts +220 -0
  139. package/src/attr/decimal.ts +86 -0
  140. package/src/attr/errors.ts +145 -0
  141. package/src/attr/index.ts +50 -0
  142. package/src/attr/names.ts +106 -0
  143. package/src/attr/types.ts +150 -0
  144. package/src/attr/values.test.ts +271 -0
  145. package/src/attr/values.ts +506 -0
  146. package/src/chains/cheesecake.ts +22 -0
  147. package/src/chains/index.ts +1 -2
  148. package/src/chains/localhost.ts +3 -0
  149. package/src/clients/createPublicClient.test.ts +4 -6
  150. package/src/clients/createPublicClient.ts +9 -8
  151. package/src/clients/createWalletClient.test.ts +4 -4
  152. package/src/clients/createWalletClient.ts +13 -9
  153. package/src/clients/decorators/arkivAdvanced.ts +144 -0
  154. package/src/clients/decorators/arkivPublic.ts +181 -147
  155. package/src/clients/decorators/arkivWallet.ts +141 -122
  156. package/src/consts.ts +2 -1
  157. package/src/entity/create.test.ts +203 -0
  158. package/src/entity/errors.ts +39 -0
  159. package/src/entity/events.ts +16 -0
  160. package/src/entity/expiry.test.ts +195 -0
  161. package/src/entity/expiry.ts +182 -0
  162. package/src/entity/flags.ts +65 -0
  163. package/src/entity/index.ts +16 -0
  164. package/src/entity/key.ts +88 -0
  165. package/src/entity/operations.ts +135 -0
  166. package/src/entity/params.ts +15 -0
  167. package/src/errors.ts +33 -28
  168. package/src/index.ts +19 -0
  169. package/src/query/engine.ts +68 -132
  170. package/src/query/errors.ts +151 -0
  171. package/src/query/expression.test.ts +286 -0
  172. package/src/query/expression.ts +500 -0
  173. package/src/query/index.ts +40 -4
  174. package/src/query/injection.test.ts +151 -0
  175. package/src/query/queryBuilder.test.ts +342 -637
  176. package/src/query/queryBuilder.ts +133 -359
  177. package/src/query/queryResult.ts +54 -51
  178. package/src/query/selection.test.ts +48 -98
  179. package/src/query/selection.ts +77 -65
  180. package/src/types/entity.test.ts +44 -155
  181. package/src/types/entity.ts +62 -44
  182. package/src/types/events.ts +64 -23
  183. package/src/types/index.ts +24 -20
  184. package/src/types/rpcSchema.ts +71 -33
  185. package/src/types/txParams.ts +8 -0
  186. package/src/utils/arkivTransactions.test.ts +687 -38
  187. package/src/utils/arkivTransactions.ts +400 -115
  188. package/src/utils/chains.test.ts +21 -24
  189. package/src/utils/compression.ts +1 -1
  190. package/src/utils/entities.test.ts +62 -0
  191. package/src/utils/entities.ts +111 -29
  192. package/src/utils/expirationTime.ts +153 -10
  193. package/src/utils/logger.ts +2 -2
  194. package/src/utils/payload.ts +29 -2
  195. package/src/utils/revert.test.ts +99 -0
  196. package/src/utils/revert.ts +145 -0
  197. package/dist/chains-BifaY04V.cjs +0 -108
  198. package/dist/chains-CrtkVbBh.js +0 -86
  199. package/dist/chains-CrtkVbBh.js.map +0 -1
  200. package/dist/consts-AUoL-v42.cjs +0 -18
  201. package/dist/consts-BV4PR-de.js +0 -7
  202. package/dist/consts-BV4PR-de.js.map +0 -1
  203. package/dist/entity-B6g_EPm1.d.cts +0 -382
  204. package/dist/entity-B6g_EPm1.d.cts.map +0 -1
  205. package/dist/entity-qoLMAuuk.d.ts +0 -382
  206. package/dist/entity-qoLMAuuk.d.ts.map +0 -1
  207. package/dist/index-DdspxNAA.d.ts +0 -481
  208. package/dist/index-DdspxNAA.d.ts.map +0 -1
  209. package/dist/index-Ds9dwqb5.d.cts +0 -481
  210. package/dist/index-Ds9dwqb5.d.cts.map +0 -1
  211. package/dist/payload-B2kFJyah.d.ts +0 -6
  212. package/dist/payload-B2kFJyah.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-Claf2jUO.js +0 -13
  216. package/dist/payload-Claf2jUO.js.map +0 -1
  217. package/dist/payload-DpA5PA-w.cjs +0 -23
  218. package/dist/query/index.js.map +0 -1
  219. package/dist/queryBuilder-D2uwP9jI.js +0 -704
  220. package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
  221. package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
  222. package/dist/utils/index.d.cts.map +0 -1
  223. package/dist/utils/index.d.ts.map +0 -1
  224. package/dist/utils/index.js.map +0 -1
  225. package/src/actions/public/buildQuery.ts +0 -10
  226. package/src/actions/public/subscribeEntityEvents.ts +0 -119
  227. package/src/actions/wallet/mutateEntities.ts +0 -111
  228. package/src/actions/wallet/updateEntity.ts +0 -60
  229. package/src/chains/braga.ts +0 -26
  230. package/src/chains/kaolin.ts +0 -29
  231. package/src/query/engine.test.ts +0 -526
  232. package/src/query/predicate.test.ts +0 -461
  233. package/src/query/predicate.ts +0 -144
  234. package/src/query/queryResult.test.ts +0 -270
  235. package/src/query/selectQueryBuilder.test.ts +0 -210
  236. package/src/types/attributes.ts +0 -16
  237. package/src/utils/validation.test.ts +0 -60
  238. package/src/utils/validation.ts +0 -32
  239. /package/dist/{types-H-TSmRpm.js → attr-DlmErkvR.cjs} +0 -0
  240. /package/dist/{types-o3zWarRp.cjs → attr-GcQKatub.js} +0 -0
@@ -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,37 +1,119 @@
1
- import { toBytes } from "viem"
2
- 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"
3
11
  import type { RpcEntity } from "../types/rpcSchema"
4
12
  import { getLogger } from "./logger"
5
13
 
6
14
  const logger = getLogger("utils:entities")
7
15
 
8
- export async function entityFromRpcResult(rpcEntity: RpcEntity) {
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)
26
+ }
27
+ return attributes
28
+ }
29
+
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
+ }
38
+
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 {
9
46
  logger("entityFromRpcResult %o", rpcEntity)
10
47
 
11
- return new Entity(
12
- rpcEntity.key,
13
- rpcEntity.contentType,
14
- rpcEntity.owner,
15
- rpcEntity.creator,
16
- rpcEntity.expiresAt !== undefined ? BigInt(rpcEntity.expiresAt) : undefined,
17
- rpcEntity.createdAtBlock !== undefined ? BigInt(rpcEntity.createdAtBlock) : undefined,
18
- rpcEntity.lastModifiedAtBlock !== undefined ? BigInt(rpcEntity.lastModifiedAtBlock) : undefined,
19
- rpcEntity.transactionIndexInBlock !== undefined
20
- ? BigInt(rpcEntity.transactionIndexInBlock)
21
- : undefined,
22
- rpcEntity.operationIndexInTransaction !== undefined
23
- ? BigInt(rpcEntity.operationIndexInTransaction)
24
- : undefined,
25
- rpcEntity.value !== undefined ? toBytes(rpcEntity.value) : undefined,
26
- [
27
- ...(rpcEntity.stringAttributes ?? []).map(({ key, value }) => ({
28
- key,
29
- value,
30
- })),
31
- ...(rpcEntity.numericAttributes ?? []).map(({ key, value }) => ({
32
- key,
33
- value: Number(value),
34
- })),
35
- ],
36
- )
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
37
119
  }
@@ -1,13 +1,156 @@
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
+ /**
55
+ * A number of minutes.
56
+ *
57
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
58
+ */
59
+ fromMinutes: (minutes: number): Lifetime => lifetimeOf(minutes * 60),
60
+
61
+ /**
62
+ * A number of hours.
63
+ *
64
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
65
+ */
66
+ fromHours: (hours: number): Lifetime => lifetimeOf(hours * 60 * 60),
67
+
68
+ /**
69
+ * A number of days. The everyday form — `fromDays(30)` is what most creates want.
70
+ *
71
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
72
+ */
73
+ fromDays: (days: number): Lifetime => lifetimeOf(days * 24 * 60 * 60),
74
+
75
+ /**
76
+ * A number of weeks, each of 7 days.
77
+ *
78
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
79
+ */
80
+ fromWeeks: (weeks: number): Lifetime => lifetimeOf(weeks * 7 * 24 * 60 * 60),
81
+
82
+ /**
83
+ * A number of months, each of exactly **30 days** — calendar months are not all the same length,
84
+ * and a lifetime is a duration rather than a date. Use {@link ExpirationTime.atDate} when the
85
+ * expiry has to land on a particular calendar day.
86
+ *
87
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
88
+ */
89
+ fromMonths: (months: number): Lifetime => lifetimeOf(months * 30 * 24 * 60 * 60),
90
+
91
+ /**
92
+ * A number of years, each of exactly **365 days** — leap days are not counted, so `fromYears(4)`
93
+ * is a day short of four calendar years. Use {@link ExpirationTime.atDate} for a calendar
94
+ * deadline.
95
+ *
96
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
97
+ */
98
+ fromYears: (years: number): Lifetime => lifetimeOf(years * 365 * 24 * 60 * 60),
99
+
100
+ /**
101
+ * A number of blocks, for callers who think in the chain's own unit. Always exact — there is no
102
+ * conversion to be approximate about.
103
+ *
104
+ * @throws {InvalidExpiryError} If the count is not a positive whole number.
105
+ */
106
+ fromBlocks: (blocks: number): Lifetime => {
107
+ if (!Number.isInteger(blocks) || blocks <= 0) {
108
+ throw new InvalidExpiryError(
109
+ `a lifetime must be a positive whole number of blocks, got ${blocks}`,
110
+ )
111
+ }
112
+ return makeExpiry(BigInt(blocks), 0n) as Lifetime
113
+ },
114
+
115
+ /**
116
+ * An absolute block height.
117
+ *
118
+ * @param block - The block the entity expires at.
119
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
120
+ * @throws {InvalidExpiryError} If the height is negative.
121
+ */
122
+ atBlock: (block: bigint, options?: DeadlineOptions): Expiry => {
123
+ if (typeof block !== "bigint" || block < 0n) {
124
+ throw new InvalidExpiryError(`a deadline must be a non-negative block height, got ${block}`)
125
+ }
126
+ return deadlineOf(block, options)
127
+ },
128
+
129
+ /**
130
+ * A wall-clock instant, placed against the block the transaction is built on.
131
+ *
132
+ * A date cannot land on a block boundary in general, so it rounds **up** to the next block: the
133
+ * entity lives until at least the instant asked for. This is why a date is never rejected for
134
+ * "not being a whole number of blocks" the way a duration is — it becomes a block height, not a
135
+ * duration.
136
+ *
137
+ * @param date - When the entity should expire.
138
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
139
+ * @throws {InvalidExpiryError} If the value is not a valid `Date`.
140
+ */
141
+ atDate: (date: Date, options?: DeadlineOptions): Expiry => {
142
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
143
+ throw new InvalidExpiryError(`a deadline must be a valid Date, got ${String(date)}`)
144
+ }
145
+ return deadlineOf(date, options)
146
+ },
147
+
148
+ /**
149
+ * An entity that should never expire.
150
+ *
151
+ * This is `atBlock(MAX_EXPIRES_AT)` — the largest expiry the `uint64` field holds — and the
152
+ * engine treats it as an ordinary deadline rather than a special case. There is no permanence
153
+ * flag; a block height no chain will reach is what permanence is.
154
+ */
155
+ permanent: (): Expiry => deadlineOf(MAX_EXPIRES_AT),
13
156
  }
@@ -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
 
@@ -1,9 +1,36 @@
1
- import { toBytes } from "viem"
1
+ import { stringToBytes, toBytes } from "viem"
2
2
 
3
+ /**
4
+ * A JSON-serialisable value as an entity payload — `JSON.stringify` then UTF-8.
5
+ *
6
+ * Pair it with `contentType: "application/json"`, which is what lets `entity.toJson()` read it back.
7
+ *
8
+ * @example
9
+ * await client.createEntity({
10
+ * payload: jsonToPayload({ hello: "world" }),
11
+ * contentType: "application/json",
12
+ * expires: ExpirationTime.fromDays(30),
13
+ * })
14
+ */
3
15
  export function jsonToPayload(json: object): Uint8Array {
4
16
  return toBytes(JSON.stringify(json))
5
17
  }
6
18
 
19
+ /**
20
+ * A string as an entity payload, encoded as UTF-8.
21
+ *
22
+ * Hex-looking text is encoded as text: `stringToPayload("0xab")` gives the four bytes of that
23
+ * string, not the single byte `0xab`. Read it back with `entity.toText()`.
24
+ *
25
+ * @example
26
+ * await client.createEntity({
27
+ * payload: stringToPayload("hello"),
28
+ * contentType: "text/plain",
29
+ * expires: ExpirationTime.fromDays(30),
30
+ * })
31
+ */
7
32
  export function stringToPayload(data: string): Uint8Array {
8
- return toBytes(data)
33
+ // stringToBytes, not toBytes: toBytes hex-decodes hex-looking strings, which
34
+ // would corrupt payloads like "0xab" instead of UTF-8 encoding them
35
+ return stringToBytes(data)
9
36
  }
@@ -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
+ })
@@ -0,0 +1,145 @@
1
+ import { BaseError, ContractFunctionRevertedError, type Hex, hexToString } from "viem"
2
+
3
+ /**
4
+ * Turns a decoded engine revert into a sentence that says what to change.
5
+ *
6
+ * viem decodes the error and then renders it positionally — `Ident32InvalidByte(uint256 position,
7
+ * bytes1 value)` arrives as `(5, 0x41)`, which is the right data and no help at all. The engine's
8
+ * errors carry enough to name the actual problem, so this spends the args rather than printing
9
+ * them.
10
+ *
11
+ * @param error - Anything thrown by a write action.
12
+ * @returns The explanation, or `undefined` if this was not a decodable engine revert — in which
13
+ * case the caller should fall back to whatever the transport said.
14
+ */
15
+ export function describeEntityRevert(error: unknown): string | undefined {
16
+ if (!(error instanceof BaseError)) return undefined
17
+ const reverted = error.walk((e) => e instanceof ContractFunctionRevertedError)
18
+ if (!(reverted instanceof ContractFunctionRevertedError)) return undefined
19
+
20
+ const name = reverted.data?.errorName
21
+ if (name === undefined) return undefined
22
+ const args = (reverted.data?.args ?? []) as readonly unknown[]
23
+
24
+ return explain(name, args) ?? `the engine rejected the batch with ${name}`
25
+ }
26
+
27
+ /** The attribute-name charset, quoted the way an error message should read. */
28
+ const CHARSET = '"A"-"Z", "a"-"z", "0"-"9", ".", "-" and "_", with a letter first'
29
+
30
+ function explain(name: string, args: readonly unknown[]): string | undefined {
31
+ switch (name) {
32
+ // ── attribute names ──────────────────────────────────────────────────────
33
+ case "Ident32InvalidByte": {
34
+ const position = Number(args[0] ?? 0)
35
+ const char = printable(String(args[1] ?? "0x00"))
36
+ return (
37
+ `an attribute name holds ${char} at byte ${position}, which is outside the ` +
38
+ `name charset (${CHARSET})`
39
+ )
40
+ }
41
+ case "Ident32Empty":
42
+ return "an attribute name is empty"
43
+
44
+ // ── the attribute list ───────────────────────────────────────────────────
45
+ case "SystemAttributeNotWritable":
46
+ return (
47
+ `"${identName(args[0])}" is owned by the engine and cannot be written. Only $payload and ` +
48
+ "$contentType are yours to set"
49
+ )
50
+ case "TombstoneInCreate":
51
+ return (
52
+ `a create cannot unset "${identName(args[0])}" — nothing exists yet, so leave the name ` +
53
+ "out instead"
54
+ )
55
+ case "TombstoneValueNotEmpty":
56
+ return `the tombstone for "${identName(args[0])}" carries a value; unsetting takes no value`
57
+ case "InvalidValueType":
58
+ return `"${identName(args[0])}" names type id ${String(args[1])}, which is not a known type`
59
+ case "AttributesNotSorted":
60
+ return "attributes must be strictly ascending by name, which also makes each name unique"
61
+ case "TooManyAttributes":
62
+ return `${String(args[0])} attributes exceeds the limit of ${String(args[1])}`
63
+ case "EmptyMutations":
64
+ return `the patch of ${short(args[0])} has no mutations, so it would do nothing`
65
+
66
+ // ── entity state and authorisation ───────────────────────────────────────
67
+ case "EntityNotFound":
68
+ return `no entity ${short(args[0])} — it may have been deleted, or have expired`
69
+ case "EntityExpired":
70
+ return `entity ${short(args[0])} expired at block ${String(args[1])}`
71
+ case "NotOwner":
72
+ return `entity ${short(args[0])} is owned by ${String(args[2])}, not ${String(args[1])}`
73
+ case "ReadOnlyEntity":
74
+ return (
75
+ `entity ${short(args[0])} was created readonly, so its contents can never change. Its ` +
76
+ "expiry can still be extended, and it can be transferred or deleted"
77
+ )
78
+ case "ReservedCreationFlags":
79
+ return (
80
+ `creation flags 0b${Number(args[0] ?? 0)
81
+ .toString(2)
82
+ .padStart(8, "0")} set a reserved ` +
83
+ "bit; only readonly and permissionlessExtension are defined"
84
+ )
85
+
86
+ // ── expiry ───────────────────────────────────────────────────────────────
87
+ case "ExpiryDeadOnArrival":
88
+ return (
89
+ `the expiry resolves to block ${String(args[0])}, which is not after the current block ` +
90
+ `${String(args[1])}`
91
+ )
92
+ case "ExpiryNotExtended":
93
+ return (
94
+ `entity ${short(args[0])} already expires at block ${String(args[2])}, so extending it ` +
95
+ `to ${String(args[1])} would shorten its life`
96
+ )
97
+
98
+ // ── ownership ────────────────────────────────────────────────────────────
99
+ case "TransferToZeroAddress":
100
+ return `entity ${short(args[0])} cannot be transferred to the zero address`
101
+ case "TransferToSelf":
102
+ return `entity ${short(args[0])} is already owned by that account`
103
+
104
+ // ── batch framing ────────────────────────────────────────────────────────
105
+ case "EmptyBatch":
106
+ return "the batch has no operations"
107
+ case "InvalidOpType":
108
+ return `operation tag ${String(args[0])} is not one the engine knows`
109
+ case "NonCanonicalOperationData":
110
+ return (
111
+ `the payload for operation tag ${String(args[0])} is not its canonical encoding, so the ` +
112
+ "engine refused it rather than accept a second spelling of the same operation"
113
+ )
114
+
115
+ default:
116
+ return undefined
117
+ }
118
+ }
119
+
120
+ /** A `bytes32` Ident32 cell as the name it holds — left-aligned, null-padded. */
121
+ function identName(raw: unknown): string {
122
+ if (typeof raw !== "string") return String(raw)
123
+ try {
124
+ // Cut at the first null rather than trimming with a regex: the padding is whatever follows
125
+ // the name, so everything before it is the name and nothing after it is. Written as an escape
126
+ // because a literal NUL byte in source is invisible to every reader after you.
127
+ return hexToString(raw as Hex).split("\u0000")[0] ?? ""
128
+ } catch {
129
+ return raw
130
+ }
131
+ }
132
+
133
+ /** An entity key, abbreviated */
134
+ function short(raw: unknown): string {
135
+ const text = String(raw)
136
+ return text.length > 18 ? `${text.slice(0, 10)}…${text.slice(-6)}` : text
137
+ }
138
+
139
+ /** A `bytes1` as something readable: the character it is, plus the byte it was. */
140
+ function printable(byte: string): string {
141
+ const code = Number.parseInt(byte.replace(/^0x/, ""), 16)
142
+ if (!Number.isFinite(code)) return `byte ${byte}`
143
+ const char = String.fromCharCode(code)
144
+ return code >= 0x21 && code <= 0x7e ? `"${char}" (${byte})` : `byte ${byte}`
145
+ }