@arkiv-network/sdk 0.7.0-dev.1 → 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 +22 -60
  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 +676 -375
  62. package/dist/index.d.cts +247 -208
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +247 -208
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -368
  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 -150
  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 -138
  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 +3 -3
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +3 -3
  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 +2 -2
  103. package/src/actions/wallet/createEntity.ts +99 -24
  104. package/src/actions/wallet/extendEntity.ts +37 -13
  105. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  106. package/src/actions/wallet/mutateEntities.ts +19 -21
  107. package/src/actions/wallet/patchEntity.ts +90 -0
  108. package/src/attr/attributes.test.ts +120 -0
  109. package/src/attr/attributes.ts +190 -0
  110. package/src/attr/codec.test.ts +162 -0
  111. package/src/attr/codec.ts +220 -0
  112. package/src/attr/decimal.ts +86 -0
  113. package/src/attr/errors.ts +145 -0
  114. package/src/attr/index.ts +50 -0
  115. package/src/attr/names.ts +106 -0
  116. package/src/attr/types.ts +150 -0
  117. package/src/attr/values.test.ts +271 -0
  118. package/src/attr/values.ts +502 -0
  119. package/src/chains/cheesecake.ts +22 -0
  120. package/src/chains/index.ts +1 -2
  121. package/src/clients/createPublicClient.test.ts +3 -5
  122. package/src/clients/createPublicClient.ts +5 -4
  123. package/src/clients/createWalletClient.test.ts +2 -2
  124. package/src/clients/createWalletClient.ts +13 -7
  125. package/src/clients/decorators/arkivPublic.ts +160 -139
  126. package/src/clients/decorators/arkivWallet.ts +86 -94
  127. package/src/entity/create.test.ts +184 -0
  128. package/src/entity/errors.ts +38 -0
  129. package/src/entity/events.ts +16 -0
  130. package/src/entity/expiry.test.ts +195 -0
  131. package/src/entity/expiry.ts +181 -0
  132. package/src/entity/flags.ts +65 -0
  133. package/src/entity/index.ts +14 -0
  134. package/src/entity/key.ts +68 -0
  135. package/src/entity/operations.ts +134 -0
  136. package/src/entity/params.ts +15 -0
  137. package/src/errors.ts +26 -49
  138. package/src/index.ts +6 -0
  139. package/src/query/engine.ts +59 -139
  140. package/src/query/errors.ts +151 -0
  141. package/src/query/expression.test.ts +286 -0
  142. package/src/query/expression.ts +493 -0
  143. package/src/query/index.ts +40 -4
  144. package/src/query/injection.test.ts +151 -0
  145. package/src/query/queryBuilder.test.ts +322 -637
  146. package/src/query/queryBuilder.ts +126 -359
  147. package/src/query/queryResult.ts +54 -51
  148. package/src/query/selection.test.ts +48 -98
  149. package/src/query/selection.ts +77 -65
  150. package/src/types/entity.test.ts +44 -155
  151. package/src/types/entity.ts +60 -52
  152. package/src/types/events.ts +59 -16
  153. package/src/types/index.ts +20 -17
  154. package/src/types/rpcSchema.ts +69 -37
  155. package/src/utils/arkivTransactions.test.ts +600 -155
  156. package/src/utils/arkivTransactions.ts +181 -190
  157. package/src/utils/chains.test.ts +21 -24
  158. package/src/utils/compression.ts +1 -1
  159. package/src/utils/entities.test.ts +62 -0
  160. package/src/utils/entities.ts +109 -37
  161. package/src/utils/expirationTime.ts +114 -10
  162. package/src/utils/logger.ts +2 -2
  163. package/src/utils/revert.test.ts +99 -0
  164. package/src/utils/revert.ts +145 -0
  165. package/dist/chains-BifaY04V.cjs +0 -108
  166. package/dist/chains-CrtkVbBh.js +0 -86
  167. package/dist/chains-CrtkVbBh.js.map +0 -1
  168. package/dist/consts-Bz0LZQZY.js +0 -7
  169. package/dist/consts-Bz0LZQZY.js.map +0 -1
  170. package/dist/consts-CxT1-RXU.cjs +0 -18
  171. package/dist/entity-BhSXaB23.cjs +0 -81
  172. package/dist/entity-CN41IOrq.d.cts +0 -401
  173. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  174. package/dist/entity-DGfVyB-a.js +0 -71
  175. package/dist/entity-DGfVyB-a.js.map +0 -1
  176. package/dist/entity-NnEVlePN.d.ts +0 -401
  177. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  178. package/dist/index-ClkpS6s_.d.ts +0 -481
  179. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  180. package/dist/index-w4qop0Pe.d.cts +0 -481
  181. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  182. package/dist/payload-BpNUPOnY.js +0 -13
  183. package/dist/payload-BpNUPOnY.js.map +0 -1
  184. package/dist/payload-C2KHOUoj.d.ts +0 -6
  185. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  186. package/dist/payload-CJN8fTvX.d.cts +0 -6
  187. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  188. package/dist/payload-D8YLKcJj.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  191. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  192. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  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 -93
  198. package/src/actions/wallet/updateEntity.ts +0 -59
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -571
  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 -213
  206. package/src/types/attributes.ts +0 -28
  207. package/src/utils/validation.test.ts +0 -106
  208. package/src/utils/validation.ts +0 -80
  209. /package/dist/{types-Ci4BRClw.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-a8gm_IaQ.cjs → types-CKPddlfS.cjs} +0 -0
@@ -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
+ }
@@ -0,0 +1,86 @@
1
+ import { InvalidValueError } from "./errors"
2
+
3
+ /**
4
+ * Decimal places a `dec` value is scaled by. Fixed by the protocol, so any two decimals are
5
+ * directly comparable — and range-indexable — as plain integers.
6
+ */
7
+ export const DECIMAL_SCALE = 18
8
+
9
+ /** 10 ** {@link DECIMAL_SCALE} — the scaling factor between a `dec` and its integer units. */
10
+ export const DECIMAL_UNIT = 10n ** BigInt(DECIMAL_SCALE)
11
+
12
+ /** The widest scaled value an `int256` holds. */
13
+ const INT256_MAX = 2n ** 255n - 1n
14
+ /** The narrowest scaled value an `int256` holds. */
15
+ const INT256_MIN = -(2n ** 255n)
16
+
17
+ /**
18
+ * A decimal literal: an optional sign, at least one integer digit, and an optional fraction of at
19
+ * most {@link DECIMAL_SCALE} digits. Exponents are not part of the grammar — `1e18` is a parse
20
+ * error rather than a silent reinterpretation.
21
+ */
22
+ const DECIMAL_RE = /^([+-]?)(\d+)(?:\.(\d+))?$/
23
+
24
+ /**
25
+ * Parses a decimal literal into its scaled integer units.
26
+ *
27
+ * Excess fractional precision is an **error, never a rounding**: the protocol stores exactly 18
28
+ * decimal places, and silently dropping the 19th digit would make a stored value differ from the
29
+ * one that was written.
30
+ *
31
+ * @param input - The decimal literal, e.g. `"3.5"` or `"-0.000001"`.
32
+ * @returns The value scaled by 10 ** 18.
33
+ * @throws {InvalidValueError} If the literal is malformed, too precise, or out of `int256` range.
34
+ */
35
+ export function decimalToUnits(input: string): bigint {
36
+ const match = DECIMAL_RE.exec(input)
37
+ if (!match) {
38
+ throw new InvalidValueError(
39
+ "dec",
40
+ input,
41
+ "not a decimal literal",
42
+ 'Write it as sign, digits, and an optional "." with up to 18 fractional digits — e.g. ' +
43
+ 'dec("3.5") or dec("-0.25"). Exponents such as "1e18" are not accepted.',
44
+ )
45
+ }
46
+
47
+ const [, sign, whole, fraction = ""] = match
48
+ if (fraction.length > DECIMAL_SCALE) {
49
+ throw new InvalidValueError(
50
+ "dec",
51
+ input,
52
+ `${fraction.length} fractional digits exceeds the ${DECIMAL_SCALE} the protocol stores`,
53
+ "Round it yourself before storing — a dec is never rounded for you, so that what you " +
54
+ "write is exactly what is stored.",
55
+ )
56
+ }
57
+
58
+ const digits = BigInt(whole + fraction.padEnd(DECIMAL_SCALE, "0"))
59
+ const units = sign === "-" ? -digits : digits
60
+ if (units > INT256_MAX || units < INT256_MIN) {
61
+ throw new InvalidValueError("dec", input, "exceeds the int256 range a dec is stored in")
62
+ }
63
+ return units
64
+ }
65
+
66
+ /**
67
+ * Renders scaled integer units back as a canonical decimal string: no leading zeros, no trailing
68
+ * fractional zeros, no `-0`. Parsing the result yields the same units, so this is the inverse of
69
+ * {@link decimalToUnits} on every value it accepts.
70
+ *
71
+ * @param units - The value scaled by 10 ** 18.
72
+ * @returns The canonical decimal string, e.g. `"3.5"`.
73
+ * @throws {InvalidValueError} If the units are outside the `int256` range.
74
+ */
75
+ export function unitsToDecimal(units: bigint): string {
76
+ if (units > INT256_MAX || units < INT256_MIN) {
77
+ throw new InvalidValueError("dec", units, "exceeds the int256 range a dec is stored in")
78
+ }
79
+ const negative = units < 0n
80
+ const magnitude = negative ? -units : units
81
+ const whole = magnitude / DECIMAL_UNIT
82
+ const fraction = (magnitude % DECIMAL_UNIT).toString().padStart(DECIMAL_SCALE, "0")
83
+ const trimmed = fraction.replace(/0+$/, "")
84
+ const sign = negative ? "-" : ""
85
+ return trimmed ? `${sign}${whole}.${trimmed}` : `${sign}${whole}`
86
+ }