@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,120 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { encodeAttributes, encodeMutations, MAX_ATTRIBUTES, resolveAttributes } from "./attributes"
3
+ import { InvalidAttributeNameError, TooManyAttributesError } from "./errors"
4
+ import { isValidAttributeName, validateAttributeName } from "./names"
5
+ import { dec, i32, str, u256 } from "./values"
6
+
7
+ describe("resolveAttributes", () => {
8
+ it("types bare values and leaves tagged ones alone", () => {
9
+ const resolved = resolveAttributes({
10
+ level: 10,
11
+ balance: 1_000_000n,
12
+ name: "Bob",
13
+ flagged: true,
14
+ score: dec("3.5"),
15
+ })
16
+ expect(resolved.level).toEqual(i32(10))
17
+ expect(resolved.balance).toEqual(u256(1_000_000n))
18
+ expect(resolved.name).toEqual(str("Bob"))
19
+ expect(resolved.flagged.type).toBe("bool")
20
+ expect(resolved.score).toEqual(dec("3.5"))
21
+ })
22
+
23
+ it("reports the attribute count when there are too many", () => {
24
+ const tooMany = Object.fromEntries(
25
+ Array.from({ length: MAX_ATTRIBUTES + 1 }, (_, i) => [`a${i}`, i]),
26
+ )
27
+ expect(() => resolveAttributes(tooMany)).toThrow(TooManyAttributesError)
28
+ expect(() => resolveAttributes(tooMany)).toThrow(/at most 32 attributes, got 33/)
29
+ })
30
+ })
31
+
32
+ describe("the attribute cap", () => {
33
+ const attributes = (count: number) =>
34
+ Object.fromEntries(Array.from({ length: count }, (_, i) => [`a${i}`, i]))
35
+
36
+ it("counts the payload and content-type cells, which the engine also counts", () => {
37
+ // Under the cap on its own, over it once the two system cells are appended. Letting this
38
+ // through would spend gas on a transaction the engine answers with TooManyAttributes(34, 32).
39
+ expect(() =>
40
+ encodeAttributes(attributes(MAX_ATTRIBUTES), {
41
+ payload: new Uint8Array([1]),
42
+ contentType: "application/json",
43
+ }),
44
+ ).toThrow(/at most 32 attributes, got 34/)
45
+ })
46
+
47
+ it("leaves room for them rather than rejecting a full-but-legal operation", () => {
48
+ expect(
49
+ encodeAttributes(attributes(MAX_ATTRIBUTES - 2), {
50
+ payload: new Uint8Array([1]),
51
+ contentType: "application/json",
52
+ }),
53
+ ).toHaveLength(MAX_ATTRIBUTES)
54
+ })
55
+
56
+ it("counts tombstones in a patch — a removal occupies a cell like any other", () => {
57
+ expect(() =>
58
+ encodeMutations({
59
+ set: attributes(MAX_ATTRIBUTES - 1),
60
+ unset: ["gone_a", "gone_b"],
61
+ }),
62
+ ).toThrow(/at most 32 attributes, got 33/)
63
+ })
64
+ })
65
+
66
+ describe("encodeAttributes", () => {
67
+ it("sorts strictly ascending by the encoded name, whatever order they were written in", () => {
68
+ const encoded = encodeAttributes({ zebra: 1, apple: 2, mango: 3 })
69
+ const names = encoded.map((attribute) => attribute.name)
70
+ expect(names).toEqual([...names].sort())
71
+ expect(encoded).toHaveLength(3)
72
+ })
73
+
74
+ it("carries each value's typeId", () => {
75
+ const ids = encodeAttributes({ level: i32(1), score: dec("1"), tag: "x" }).map((a) => a.typeId)
76
+ expect([...ids].sort()).toEqual([2, 5, 8]) // i32, dec, str
77
+ })
78
+
79
+ it("accepts an empty attribute set", () => {
80
+ expect(encodeAttributes({})).toEqual([])
81
+ })
82
+ })
83
+
84
+ describe("validateAttributeName", () => {
85
+ it("accepts the Ident32 grammar, in either case", () => {
86
+ for (const name of ["a", "tag", "Tag", "projectId", "num_attr", "a.b-c_D9", "a".repeat(32)]) {
87
+ expect(isValidAttributeName(name)).toBe(true)
88
+ }
89
+ })
90
+
91
+ it("treats names as case-sensitive, so two casings are two attributes", () => {
92
+ const resolved = resolveAttributes({ level: 1, Level: 2 })
93
+ expect(resolved.level).toEqual(i32(1))
94
+ expect(resolved.Level).toEqual(i32(2))
95
+ expect(encodeAttributes({ level: 1, Level: 2 })).toHaveLength(2)
96
+ })
97
+
98
+ it("rejects names outside the grammar, pointing at the offending character", () => {
99
+ expect(() => validateAttributeName("")).toThrow(InvalidAttributeNameError)
100
+ expect(() => validateAttributeName("1tag")).toThrow(/starts with "1"/)
101
+ expect(() => validateAttributeName("_tag")).toThrow(/starts with "_"/)
102
+ expect(() => validateAttributeName("has space")).toThrow(/" " at position 3/)
103
+ expect(() => validateAttributeName("héllo")).toThrow(/"é" at position 1/)
104
+ expect(() => validateAttributeName("a:b")).toThrow(/":" at position 1/)
105
+ expect(() => validateAttributeName("a".repeat(33))).toThrow(/33 bytes long/)
106
+ })
107
+
108
+ it("reserves $ for the system attributes the engine sets", () => {
109
+ expect(() => validateAttributeName("$owner")).toThrow(/reserved for system attributes/)
110
+ })
111
+
112
+ it("rejects names the query language could never parse back, whatever their casing", () => {
113
+ for (const reserved of ["and", "OR", "Not", "key", "str", "u256", "EXISTS", "Bytes32"]) {
114
+ expect(() => validateAttributeName(reserved)).toThrow(/reserved by the query language/)
115
+ }
116
+ // Only the exact word is reserved — a name containing it is fine.
117
+ expect(isValidAttributeName("android")).toBe(true)
118
+ expect(isValidAttributeName("sortKey")).toBe(true)
119
+ })
120
+ })
@@ -0,0 +1,190 @@
1
+ import { type AbiAttribute, encodeAbiAttribute, encodeTombstone } from "./codec"
2
+ import { ConflictingMutationError, InvalidValueError, TooManyAttributesError } from "./errors"
3
+ import { validateAttributeName } from "./names"
4
+ import type { AnyArkivValue, ArkivValue, TypeTag } from "./types"
5
+ import { makeValue } from "./types"
6
+ import { str, toValue, type ValueInput } from "./values"
7
+
8
+ /**
9
+ * The most attribute cells one operation may carry.
10
+ */
11
+ export const MAX_ATTRIBUTES = 32
12
+
13
+ export const MAX_PAYLOAD_BYTES = 128 * 1024
14
+
15
+ /** The system attribute holding an entity's opaque payload. */
16
+ const PAYLOAD_CELL = "$payload"
17
+ /** The system attribute holding an entity's content type. */
18
+ const CONTENT_TYPE_CELL = "$contentType"
19
+
20
+ /**
21
+ * The attributes written on an entity, as a plain object keyed by attribute name.
22
+ *
23
+ * Values may be tagged ({@link ValueInput}) or bare where the type is unambiguous — see
24
+ * {@link toValue} for the bare-form defaults.
25
+ *
26
+ * @example
27
+ * const attributes = {
28
+ * level: i32(10),
29
+ * balance: u256(1_000_000n),
30
+ * score: dec("3.5"),
31
+ * parent: key(parentKey),
32
+ * name: "Bob", // bare string -> str
33
+ * flagged: true, // bare boolean -> bool
34
+ * } satisfies AttributeInputs
35
+ */
36
+ export type AttributeInputs = Readonly<Record<string, ValueInput>>
37
+
38
+ /**
39
+ * The attributes read back from an entity: the same shape, with every value typed.
40
+ *
41
+ * A value read from here drops straight back into an {@link AttributeInputs} — the vocabulary is the
42
+ * same in both directions.
43
+ */
44
+ export type Attributes = Readonly<Record<string, AnyArkivValue>>
45
+
46
+ /**
47
+ * The names and types of an entity's attributes, with no values — what the `attributeSchema`
48
+ * projection returns.
49
+ *
50
+ * Useful for asking what shape the data has without paying to transfer it, and for telling apart
51
+ * two entities that use the same name with different types.
52
+ */
53
+ export type AttributeSchema = Readonly<Record<string, TypeTag>>
54
+
55
+ /**
56
+ * The typed form of an attribute map: names validated, bare values resolved to their default type.
57
+ *
58
+ * @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
59
+ * @throws {MissingValueError} If a value is `undefined` or `null` — omit the name instead.
60
+ * @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
61
+ * @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
62
+ *
63
+ * @example
64
+ * resolveAttributes({ level: 10, name: "Bob" })
65
+ * // { level: { type: "i32", value: 10 }, name: { type: "str", value: "Bob" } }
66
+ */
67
+ export function resolveAttributes(attributes: AttributeInputs): Record<string, ArkivValue> {
68
+ const entries = Object.entries(attributes)
69
+ if (entries.length > MAX_ATTRIBUTES) {
70
+ throw new TooManyAttributesError(entries.length, MAX_ATTRIBUTES)
71
+ }
72
+ const resolved: Record<string, ArkivValue> = {}
73
+ for (const [name, input] of entries) {
74
+ validateAttributeName(name)
75
+ resolved[name] = toValue(input, name)
76
+ }
77
+ return resolved
78
+ }
79
+
80
+ /**
81
+ * The payload and content type, which travel to the engine as system attributes.
82
+ *
83
+ * They are separate fields on the SDK's surface because that is what they are to an application —
84
+ * an entity's contents and its type — but on the wire an operation carries no dedicated field for
85
+ * either: `$payload` is the one `bytes`-typed cell, and `$contentType` is a `str`.
86
+ */
87
+ export type SystemCells = {
88
+ payload?: Uint8Array | undefined
89
+ contentType?: string | undefined
90
+ }
91
+
92
+ /**
93
+ * Encodes an attribute map, plus the payload and content-type system cells, into the ABI attribute
94
+ * array an operation carries.
95
+ *
96
+ * @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
97
+ * @throws {MissingValueError} If a value is `undefined` or `null`.
98
+ * @throws {InvalidValueError} If a value does not fit its type.
99
+ * @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
100
+ */
101
+ export function encodeAttributes(
102
+ attributes: AttributeInputs = {},
103
+ system: SystemCells = {},
104
+ ): AbiAttribute[] {
105
+ const cells: [string, AnyArkivValue][] = Object.entries(resolveAttributes(attributes))
106
+
107
+ // Written whenever the field is given, empty or not: an empty payload is a payload, and dropping
108
+ // it would mean the entity does not hold what the caller passed. Absence is for the operations
109
+ // that genuinely leave a cell alone, not for a value that happens to be empty.
110
+ if (system.payload !== undefined) {
111
+ if (system.payload.length > MAX_PAYLOAD_BYTES) {
112
+ throw new InvalidValueError(
113
+ "bytes",
114
+ `<${system.payload.length} bytes>`,
115
+ `${system.payload.length} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte payload limit`,
116
+ "Store the bulk elsewhere and keep a reference to it on the entity.",
117
+ )
118
+ }
119
+ cells.push([PAYLOAD_CELL, makeValue("bytes", toHexBytes(system.payload))])
120
+ }
121
+ if (system.contentType !== undefined) {
122
+ cells.push([CONTENT_TYPE_CELL, str(system.contentType)])
123
+ }
124
+
125
+ if (cells.length > MAX_ATTRIBUTES) {
126
+ throw new TooManyAttributesError(cells.length, MAX_ATTRIBUTES)
127
+ }
128
+
129
+ return cells.map(([name, value]) => encodeAbiAttribute(name, value)).sort(byName)
130
+ }
131
+
132
+ export type MutationInputs = SystemCells & {
133
+ /** Attributes to write. Existing ones are overwritten; new ones are added. */
134
+ set?: AttributeInputs | undefined
135
+ /** Attribute names to remove. Each becomes a tombstone on the wire. */
136
+ unset?: readonly string[] | undefined
137
+ }
138
+
139
+ /**
140
+ * Encodes a patch's mutations into the ABI attribute array the operation carries.
141
+ *
142
+ * @throws {ConflictingMutationError} If a name appears in both `set` and `unset`.
143
+ * @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
144
+ * @throws {InvalidValueError} If a value does not fit its type.
145
+ * @throws {TooManyAttributesError} If `set` names more than {@link MAX_ATTRIBUTES} attributes.
146
+ */
147
+ export function encodeMutations({
148
+ set,
149
+ unset,
150
+ payload,
151
+ contentType,
152
+ }: MutationInputs): AbiAttribute[] {
153
+ const written = encodeAttributes(set, { payload, contentType })
154
+ const tombstones = resolveUnset(unset, set).map(encodeTombstone)
155
+ const mutations = [...written, ...tombstones]
156
+ if (mutations.length > MAX_ATTRIBUTES) {
157
+ throw new TooManyAttributesError(mutations.length, MAX_ATTRIBUTES)
158
+ }
159
+ return mutations.sort(byName)
160
+ }
161
+
162
+ function resolveUnset(unset: readonly string[] = [], set: AttributeInputs = {}): string[] {
163
+ if (!Array.isArray(unset)) {
164
+ throw new TypeError(`"unset" must be an array of attribute names`)
165
+ }
166
+ const names = new Set(unset)
167
+ for (const name of names) {
168
+ validateAttributeName(name)
169
+ if (Object.hasOwn(set, name)) {
170
+ throw new ConflictingMutationError(name)
171
+ }
172
+ }
173
+ return [...names]
174
+ }
175
+
176
+ /**
177
+ * The engine requires attributes **strictly ascending by their `bytes32` name**: that ordering is
178
+ * what the canonical serialization hashes into the state root, and it doubles as the uniqueness
179
+ * check.
180
+ */
181
+ function byName(a: AbiAttribute, b: AbiAttribute): number {
182
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
183
+ }
184
+
185
+ /** Raw bytes as lowercase `0x` hex, without viem's hex-string ambiguity. */
186
+ function toHexBytes(bytes: Uint8Array): `0x${string}` {
187
+ let out = "0x"
188
+ for (const byte of bytes) out += byte.toString(16).padStart(2, "0")
189
+ return out as `0x${string}`
190
+ }
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { Hex } from "viem"
3
+ import {
4
+ decodeRpcValue,
5
+ decodeValueBytes,
6
+ encodeAbiAttribute,
7
+ encodeTombstone,
8
+ encodeValueBytes,
9
+ } from "./codec"
10
+ import { InvalidValueError, UnknownAttributeTypeError } from "./errors"
11
+ import type { ArkivValue } from "./types"
12
+ import { addr, bool, bytes32, dec, i32, key, str, u64, u256 } from "./values"
13
+
14
+ const ZERO = `0x${"00".repeat(32)}` as Hex
15
+ const KEY = `0x${"ab".repeat(32)}` as Hex
16
+ const VITALIK = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
17
+
18
+ /** One value of every user-settable type, including the awkward edges of each. */
19
+ const SAMPLES: ArkivValue[] = [
20
+ bool(true),
21
+ bool(false),
22
+ i32(0),
23
+ i32(-42),
24
+ i32(2147483647),
25
+ i32(-2147483648),
26
+ u64(0n),
27
+ u64(2n ** 64n - 1n),
28
+ u256(0n),
29
+ u256(2n ** 256n - 1n),
30
+ dec("0"),
31
+ dec("3.5"),
32
+ dec("-3.5"),
33
+ dec("-0.000000000000000001"),
34
+ bytes32(KEY),
35
+ key(KEY),
36
+ addr(VITALIK),
37
+ str(""),
38
+ str("hello"),
39
+ str("é".repeat(64)),
40
+ ]
41
+
42
+ describe("ABI value encoding", () => {
43
+ it("round-trips every type", () => {
44
+ for (const value of SAMPLES) {
45
+ const back = decodeValueBytes(value.type, encodeValueBytes(value))
46
+ expect(back.type).toBe(value.type)
47
+ expect(back.value).toEqual(value.value)
48
+ }
49
+ })
50
+
51
+ it("gives every word type exactly 32 bytes", () => {
52
+ for (const value of SAMPLES) {
53
+ if (value.type === "str") continue
54
+ expect(encodeValueBytes(value)).toHaveLength(2 + 64)
55
+ }
56
+ })
57
+
58
+ it("sign-extends a negative i32 across the whole word, as Solidity does", () => {
59
+ expect(encodeValueBytes(i32(-1))).toBe(`0x${"ff".repeat(32)}`)
60
+ expect(encodeValueBytes(i32(-42))).toBe(`0x${"ff".repeat(31)}d6`)
61
+ expect(encodeValueBytes(i32(42))).toBe(`0x${"00".repeat(31)}2a`)
62
+ })
63
+
64
+ it("stores a dec as its two's-complement scaled int256", () => {
65
+ const scaled = (value: string) => BigInt.asIntN(256, BigInt(encodeValueBytes(dec(value))))
66
+ expect(scaled("1.5")).toBe(1_500_000_000_000_000_000n)
67
+ expect(scaled("-1.5")).toBe(-1_500_000_000_000_000_000n)
68
+ expect(scaled("0")).toBe(0n)
69
+ })
70
+
71
+ it("right-aligns an address and a bool", () => {
72
+ expect(encodeValueBytes(addr(VITALIK))).toBe(
73
+ `0x${"00".repeat(12)}${VITALIK.slice(2).toLowerCase()}`,
74
+ )
75
+ expect(encodeValueBytes(bool(true))).toBe(`0x${"00".repeat(31)}01`)
76
+ expect(encodeValueBytes(bool(false))).toBe(ZERO)
77
+ })
78
+
79
+ it("gives a str its raw UTF-8 bytes, with no padding", () => {
80
+ expect(encodeValueBytes(str("hi"))).toBe("0x6869")
81
+ expect(encodeValueBytes(str(""))).toBe("0x")
82
+ // Two bytes per "é", so no relationship to a 32-byte word.
83
+ expect(encodeValueBytes(str("é"))).toBe("0xc3a9")
84
+ })
85
+
86
+ it("rejects a word a well-formed encoder would not have produced", () => {
87
+ expect(() => decodeValueBytes("bool", `0x${"00".repeat(31)}02`)).toThrow(/exactly 0 or 1/)
88
+ expect(() => decodeValueBytes("u256", "0x2a")).toThrow(/must be 32 bytes, got 1/)
89
+ // An i32 whose upper bytes contradict its sign bit.
90
+ expect(() => decodeValueBytes("i32", `0xff${"00".repeat(31)}`)).toThrow(/sign-extended/)
91
+ // An address with junk above its 20 bytes.
92
+ expect(() => decodeValueBytes("addr", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/)
93
+ // A u64 with junk above its 8 bytes — truncating it would invent a plausible block height.
94
+ expect(() => decodeValueBytes("u64", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/)
95
+ })
96
+ })
97
+
98
+ describe("encodeAbiAttribute", () => {
99
+ it("packs the name left-aligned into its bytes32 cell", () => {
100
+ const attribute = encodeAbiAttribute("level", i32(10))
101
+ // "level" is 6c6576656c, null-padded to 32 bytes.
102
+ expect(attribute.name).toBe(`0x6c6576656c${"00".repeat(27)}`)
103
+ expect(attribute.typeId).toBe(2)
104
+ })
105
+
106
+ it("encodes a tombstone as a zero-length value under typeId 0", () => {
107
+ expect(encodeTombstone("flagged")).toEqual({
108
+ name: `0x666c6167676564${"00".repeat(25)}`,
109
+ typeId: 0,
110
+ value: "0x",
111
+ })
112
+ })
113
+ })
114
+
115
+ describe("decodeRpcValue", () => {
116
+ it("reads the JSON encoding each type declares", () => {
117
+ expect(decodeRpcValue("bool", true).value).toBe(true)
118
+ expect(decodeRpcValue("i32", -42).value).toBe(-42)
119
+ expect(decodeRpcValue("u256", "0xf4240").value).toBe(1_000_000n)
120
+ expect(decodeRpcValue("dec", "3.5").value).toBe("3.5")
121
+ expect(decodeRpcValue("dec", "-0.25").value).toBe("-0.25")
122
+ expect(decodeRpcValue("bytes32", KEY).value).toBe(KEY)
123
+ expect(decodeRpcValue("str", "hello").value).toBe("hello")
124
+ expect(decodeRpcValue("addr", VITALIK.toLowerCase()).value).toBe(VITALIK)
125
+ expect(decodeRpcValue("key", KEY).value).toBe(KEY)
126
+ })
127
+
128
+ it("takes an integer however it is rendered", () => {
129
+ // QUANTITY is the spec's encoding; decimal strings and numbers are tolerated.
130
+ expect(decodeRpcValue("u256", "0x2a").value).toBe(42n)
131
+ expect(decodeRpcValue("u256", "42").value).toBe(42n)
132
+ expect(decodeRpcValue("u256", 42).value).toBe(42n)
133
+ expect(decodeRpcValue("i32", "-42").value).toBe(-42)
134
+ })
135
+
136
+ it("decodes the system-only bytes type, which only ever arrives as a payload", () => {
137
+ expect(decodeRpcValue("bytes", "0xDEADBEEF").value).toBe("0xdeadbeef")
138
+ })
139
+
140
+ it("names the type it does not recognise, and says to upgrade", () => {
141
+ expect(() => decodeRpcValue("int", "x")).toThrow(UnknownAttributeTypeError)
142
+ expect(() => decodeRpcValue("int", "x")).toThrow(/upgrade @arkiv-network\/sdk/)
143
+ // The pre-spec wire form: a typeId where the JSON-RPC surface names a tag.
144
+ expect(() => decodeRpcValue(3 as unknown as string, "0x2a")).toThrow(UnknownAttributeTypeError)
145
+ })
146
+
147
+ it("does not mistake an inherited Object key for a type tag", () => {
148
+ // `type in TYPE_IDS` walks the prototype chain, so these passed the check and then fell off
149
+ // the end of the exhaustive switch, returning `undefined` — the caller got a TypeError from
150
+ // `entity.attributes.foo.value` instead of "upgrade the SDK".
151
+ for (const inherited of ["constructor", "toString", "valueOf", "hasOwnProperty"]) {
152
+ expect(() => decodeRpcValue(inherited, 1)).toThrow(UnknownAttributeTypeError)
153
+ }
154
+ })
155
+
156
+ it("rejects a value whose JSON shape contradicts its declared type", () => {
157
+ expect(() => decodeRpcValue("u256", "not a number")).toThrow(InvalidValueError)
158
+ expect(() => decodeRpcValue("str", 42)).toThrow(/expected a string/)
159
+ expect(() => decodeRpcValue("bool", "yes")).toThrow(/not a boolean/)
160
+ expect(() => decodeRpcValue("i32", 1.5)).toThrow(/not an integer/)
161
+ })
162
+ })
@@ -0,0 +1,220 @@
1
+ import { bytesToString, type Hex, hexToBytes, pad, size, stringToBytes, toHex } from "viem"
2
+ import { unitsToDecimal } from "./decimal"
3
+ import { InvalidValueError, UnknownAttributeTypeError } from "./errors"
4
+ import { type AnyArkivValue, makeValue, type StrValue, TYPE_IDS, type TypeTag } from "./types"
5
+ import { addr, bool, bytes32, dec, decUnits, i32, key, u64, u256 } from "./values"
6
+
7
+ function strFromChain(value: string): StrValue {
8
+ return makeValue("str", value)
9
+ }
10
+
11
+ /**
12
+ * The `typeId` marking a tombstone — an attribute being unset. It carries a zero-length value and
13
+ * is legal only in a patch's mutation list, never in a create.
14
+ */
15
+ export const TOMBSTONE_TYPE_ID = 0
16
+
17
+ /**
18
+ * An attribute in its ABI form: `(Ident32 name, uint8 typeId, bytes value)`.
19
+ *
20
+ * The `value` encoding is selected by `typeId`: an exact 32-byte word for the word types, the raw
21
+ * bytes for `str` and `bytes`, and zero-length for a tombstone.
22
+ */
23
+ export type AbiAttribute = {
24
+ name: Hex
25
+ typeId: number
26
+ value: Hex
27
+ }
28
+
29
+ /**
30
+ * Encodes a typed value into the `bytes` the ABI carries for it.
31
+ *
32
+ * This and {@link decodeValueBytes} are the only place the value/wire mapping is written down.
33
+ *
34
+ * - Word types (`bool`, `i32`, `u64`, `u256`, `dec`, `bytes32`, `addr`, `key`) become exactly 32
35
+ * bytes, in the encoding Solidity would use — right-aligned, and sign-extended for `i32`.
36
+ * - `str` becomes its raw UTF-8 bytes, and `bytes` its raw bytes: variable length, no padding.
37
+ */
38
+ export function encodeValueBytes(value: AnyArkivValue): Hex {
39
+ switch (value.type) {
40
+ case "bool":
41
+ return pad(value.value ? "0x01" : "0x00", { size: 32 })
42
+ case "i32":
43
+ // int32 is sign-extended across the whole word, as Solidity encodes it.
44
+ return toHex(BigInt.asUintN(256, BigInt(value.value)), { size: 32 })
45
+ case "u64":
46
+ case "u256":
47
+ return toHex(value.value, { size: 32 })
48
+ case "dec":
49
+ return toHex(BigInt.asUintN(256, decUnits(value)), { size: 32 })
50
+ case "bytes32":
51
+ case "key":
52
+ return value.value
53
+ case "addr":
54
+ return pad(value.value.toLowerCase() as Hex, { size: 32 })
55
+ case "str":
56
+ return toHex(stringToBytes(value.value))
57
+ case "bytes":
58
+ return value.value
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Decodes the ABI `bytes` of an attribute of type `tag`.
64
+ *
65
+ * Rejects anything a well-formed encoder would not have produced — a word type whose value is not
66
+ * 32 bytes, an `i32` that is not sign-extended, a `bool` that is neither 0 nor 1 — rather than
67
+ * silently truncating it.
68
+ */
69
+ export function decodeValueBytes(tag: TypeTag, value: Hex): AnyArkivValue {
70
+ if (tag === "str") return strFromChain(bytesToString(hexToBytes(value)))
71
+ if (tag === "bytes") return makeValue("bytes", value.toLowerCase() as Hex)
72
+
73
+ const width = size(value)
74
+ if (width !== 32) {
75
+ throw new InvalidValueError(tag, value, `a ${tag} value must be 32 bytes, got ${width}`)
76
+ }
77
+ const raw = BigInt(value)
78
+
79
+ switch (tag) {
80
+ case "bool":
81
+ if (raw !== 0n && raw !== 1n) {
82
+ throw new InvalidValueError("bool", value, "a bool word must hold exactly 0 or 1")
83
+ }
84
+ return bool(raw === 1n)
85
+ case "i32": {
86
+ const narrowed = BigInt.asIntN(32, raw)
87
+ if (BigInt.asUintN(256, narrowed) !== raw) {
88
+ throw new InvalidValueError("i32", value, "an i32 word must be sign-extended")
89
+ }
90
+ return i32(narrowed)
91
+ }
92
+ case "u64":
93
+ if (raw >> 64n !== 0n) {
94
+ throw new InvalidValueError("u64", value, "a u64 word must be zero-padded to 8 bytes")
95
+ }
96
+ return u64(raw)
97
+ case "u256":
98
+ return u256(raw)
99
+ case "dec":
100
+ return dec(unitsToDecimal(BigInt.asIntN(256, raw)))
101
+ case "bytes32":
102
+ return bytes32(value)
103
+ case "key":
104
+ return key(value)
105
+ case "addr": {
106
+ if (raw >> 160n !== 0n) {
107
+ throw new InvalidValueError(
108
+ "addr",
109
+ value,
110
+ "an address word must be zero-padded to 20 bytes",
111
+ )
112
+ }
113
+ return addr(`0x${value.slice(-40)}`)
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Encodes a named, typed attribute into its ABI form.
120
+ *
121
+ * @param name - The attribute name. Assumed already validated, or a system (`$`) name the SDK sets
122
+ * itself.
123
+ */
124
+ export function encodeAbiAttribute(name: string, value: AnyArkivValue): AbiAttribute {
125
+ return {
126
+ // Ident32 is left-aligned and null-padded, which is what `toHex` does for a string.
127
+ name: toHex(name, { size: 32 }),
128
+ typeId: TYPE_IDS[value.type],
129
+ value: encodeValueBytes(value),
130
+ }
131
+ }
132
+
133
+ /** Encodes a tombstone — the mutation that unsets `name`. Legal only in a patch. */
134
+ export function encodeTombstone(name: string): AbiAttribute {
135
+ return { name: toHex(name, { size: 32 }), typeId: TOMBSTONE_TYPE_ID, value: "0x" }
136
+ }
137
+
138
+ /**
139
+ * Decodes an attribute value as it comes back over JSON-RPC.
140
+ *
141
+ * The JSON encoding follows the value's **declared type**, never its magnitude: `bool` is a JSON
142
+ * boolean, `i32` a JSON number, `u64`/`u256` a `0x` QUANTITY, `dec` a decimal string, `str` a
143
+ * string, and
144
+ * the byte-shaped types `0x` DATA. Decimal strings are also accepted for the integer types, so this
145
+ * keeps working against a node that renders them that way.
146
+ *
147
+ * @param type - The type tag the response declared, e.g. `"u256"`.
148
+ * @param value - The JSON value.
149
+ * @throws {UnknownAttributeTypeError} If the tag names no known type.
150
+ * @throws {InvalidValueError} If the value does not parse as that type.
151
+ */
152
+ export function decodeRpcValue(type: string, value: unknown): AnyArkivValue {
153
+ const tag = asTypeTag(type)
154
+ switch (tag) {
155
+ case "bool":
156
+ return bool(asBoolean(value))
157
+ case "i32":
158
+ return i32(Number(asIntegerish("i32", value)))
159
+ case "u64":
160
+ return u64(asIntegerish("u64", value))
161
+ case "u256":
162
+ return u256(asIntegerish("u256", value))
163
+ case "dec":
164
+ return dec(asString("dec", value))
165
+ case "bytes32":
166
+ return bytes32(asString("bytes32", value) as Hex)
167
+ case "key":
168
+ return key(asString("key", value) as Hex)
169
+ case "addr":
170
+ return addr(asString("addr", value))
171
+ case "str":
172
+ return strFromChain(asString("str", value))
173
+ case "bytes":
174
+ return makeValue("bytes", asString("bytes", value).toLowerCase() as Hex)
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Narrows a type tag as the node spelled it.
180
+ *
181
+ * @throws {UnknownAttributeTypeError} If it names no type this SDK knows.
182
+ */
183
+ export function asTypeTag(type: string): TypeTag {
184
+ // `Object.hasOwn`, not `in`: `in` walks the prototype chain, so "constructor" and "toString"
185
+ // would pass as type tags and then fall off the end of the exhaustive switch below as `undefined`.
186
+ if (typeof type !== "string" || !Object.hasOwn(TYPE_IDS, type)) {
187
+ throw new UnknownAttributeTypeError(type)
188
+ }
189
+ return type as TypeTag
190
+ }
191
+
192
+ function asBoolean(value: unknown): boolean {
193
+ if (typeof value === "boolean") return value
194
+ // Tolerated because a node may render it as text; anything else is a protocol error.
195
+ if (value === "true" || value === "false") return value === "true"
196
+ throw new InvalidValueError("bool", value, "not a boolean")
197
+ }
198
+
199
+ function asString(tag: TypeTag, value: unknown): string {
200
+ if (typeof value !== "string") {
201
+ throw new InvalidValueError(tag, value, `expected a string, got ${typeof value}`)
202
+ }
203
+ return value
204
+ }
205
+
206
+ /** A `0x` QUANTITY, a JSON number, or a decimal string — all of which name one integer. */
207
+ function asIntegerish(tag: TypeTag, value: unknown): bigint {
208
+ if (typeof value === "number") {
209
+ if (!Number.isInteger(value)) {
210
+ throw new InvalidValueError(tag, value, "not an integer")
211
+ }
212
+ return BigInt(value)
213
+ }
214
+ if (typeof value === "bigint") return value
215
+ if (typeof value === "string") {
216
+ if (/^0x[0-9a-fA-F]+$/.test(value)) return BigInt(value)
217
+ if (/^-?\d+$/.test(value)) return BigInt(value)
218
+ }
219
+ throw new InvalidValueError(tag, value, "not an integer")
220
+ }