@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
@@ -1,50 +1,35 @@
1
- import { bytesToHex, type Hex, hexToBytes } from "viem"
1
+ import type { Hex } from "viem"
2
+ import { key } from "../../attr"
2
3
  import type { ArkivClient } from "../../clients/baseClient"
3
4
  import { NoEntityFoundError } from "../../errors"
4
- import { entityFromRpcResult } from "../../utils/entities"
5
+ import { runQuery } from "../../query/engine"
6
+ import { eq } from "../../query/expression"
7
+ import { type FullEntity, toRpcSelect } from "../../query/selection"
8
+ import type { Entity } from "../../types/entity"
5
9
 
6
- export async function getEntity(client: ArkivClient, key: Hex) {
7
- // Normalize the key into bytes
8
- let bytes: Uint8Array
9
- try {
10
- bytes = hexToBytes(key)
11
- } catch {
12
- throw new Error(`Invalid key format: ${key}. Key must be a valid hex string.`)
13
- }
14
-
15
- if (bytes.length !== 32) {
16
- throw new Error(
17
- `Invalid key length: ${bytes.length} bytes. Key must be 32 bytes (64 hex characters) long.`,
18
- )
19
- }
20
-
21
- const hexKey = bytesToHex(bytes)
22
-
23
- const result = await client.request({
24
- method: "arkiv_query",
25
- params: [
26
- `$key = ${hexKey}`,
27
- {
28
- includeData: {
29
- key: true,
30
- attributes: true,
31
- payload: true,
32
- contentType: true,
33
- expiration: true,
34
- owner: true,
35
- creator: true,
36
- createdAtBlock: true,
37
- lastModifiedAtBlock: true,
38
- transactionIndexInBlock: true,
39
- operationIndexInTransaction: true,
40
- },
41
- },
42
- ],
10
+ /**
11
+ * Reads one entity by key, with every field selected.
12
+ *
13
+ * Returns a {@link FullEntity} rather than an {@link Entity}: the selection is fixed at `"*"`, so
14
+ * every field is populated and none of them needs a `?` at the call site. The bare `Entity` type
15
+ * has every field optional because a `select()` result carries only what it asked for.
16
+ *
17
+ * @throws {InvalidValueError} If the key is not 32 bytes.
18
+ * @throws {NoEntityFoundError} If no live entity has that key — it never existed, or it was
19
+ * deleted or has expired.
20
+ */
21
+ export async function getEntity(client: ArkivClient, entityKey: Hex): Promise<FullEntity> {
22
+ const { entities } = await runQuery(client, {
23
+ query: eq("$key", key(entityKey)).toString(),
24
+ select: toRpcSelect("*"),
25
+ limit: 1,
43
26
  })
44
27
 
45
- if (!result.data.length) {
46
- throw new NoEntityFoundError()
28
+ const entity = entities[0]
29
+ if (entity === undefined) {
30
+ throw new NoEntityFoundError(entityKey)
47
31
  }
48
-
49
- return await entityFromRpcResult(result.data[0])
32
+ // Every field was selected, so the runtime Entity carries them all; the cast is the same
33
+ // narrowing `client.select("*")` gets from its own inference.
34
+ return entity as unknown as FullEntity
50
35
  }
@@ -0,0 +1,30 @@
1
+ import type { Address } from "viem"
2
+ import { readContract } from "viem/actions"
3
+ import type { ArkivClient } from "../../clients/baseClient"
4
+ import { ARKIV_ADDRESS } from "../../consts"
5
+ import { ENTITY_NONCE_ABI } from "../../entity/operations"
6
+ import { getLogger } from "../../utils/logger"
7
+
8
+ const logger = getLogger("actions:public:get-entity-nonce")
9
+
10
+ /**
11
+ * Reads an owner's entity-minting nonce — how many entities that account has created.
12
+ *
13
+ * This is not the account's transaction nonce. The engine keeps a separate counter per creator and
14
+ * mixes it into every key it mints, which is what makes two identical creates from one account land
15
+ * on different keys.
16
+ *
17
+ * @param owner - The account whose nonce to read.
18
+ * @returns The number of entities `owner` has created so far, which is the nonce its next create
19
+ * will use.
20
+ */
21
+ export async function getEntityNonce(client: ArkivClient, owner: Address): Promise<bigint> {
22
+ const nonce = await readContract(client, {
23
+ address: ARKIV_ADDRESS,
24
+ abi: ENTITY_NONCE_ABI,
25
+ functionName: "entityNonce",
26
+ args: [owner],
27
+ })
28
+ logger("entity nonce of %s is %s", owner, nonce)
29
+ return nonce
30
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it, vi } from "bun:test"
2
+ import { encodeFunctionResult, type Hex } from "viem"
3
+ import type { ArkivClient } from "../../clients/baseClient"
4
+ import { ARKIV_ADDRESS } from "../../consts"
5
+ import { NO_SALT, predictEntityKey } from "../../entity/key"
6
+ import { ENTITY_NONCE_ABI } from "../../entity/operations"
7
+ import { getEntityNonce } from "./getEntityNonce"
8
+ import { predictEntityKeys } from "./predictEntityKeys"
9
+
10
+ const OWNER = "0x1111111111111111111111111111111111111111" as const
11
+ const CHAIN_ID = 1337
12
+
13
+ /** A client whose `eth_call` answers one nonce, and records what it was asked. */
14
+ function makeClient(nonce: bigint, options: { chain?: boolean } = {}) {
15
+ const request = vi.fn(async ({ method }: { method: string }) => {
16
+ if (method === "eth_call") {
17
+ return encodeFunctionResult({
18
+ abi: ENTITY_NONCE_ABI,
19
+ functionName: "entityNonce",
20
+ result: nonce,
21
+ })
22
+ }
23
+ if (method === "eth_chainId") return `0x${CHAIN_ID.toString(16)}`
24
+ throw new Error(`unexpected request: ${method}`)
25
+ })
26
+ const client = {
27
+ ...(options.chain === false ? {} : { chain: { id: CHAIN_ID } }),
28
+ request,
29
+ } as unknown as ArkivClient
30
+ return { client, request }
31
+ }
32
+
33
+ /** The `eth_call` the client was asked to make. */
34
+ function ethCall(request: ReturnType<typeof vi.fn>) {
35
+ const call = request.mock.calls.find(([body]) => body.method === "eth_call")
36
+ return call?.[0].params[0] as { to: Hex; data: Hex }
37
+ }
38
+
39
+ describe("getEntityNonce", () => {
40
+ it("reads the owner's minting nonce off the registry", async () => {
41
+ const { client, request } = makeClient(7n)
42
+
43
+ expect(await getEntityNonce(client, OWNER)).toBe(7n)
44
+
45
+ // The selector is part of the node's ABI: `cast sig 'entityNonce(address)'` is 0x36917bfd.
46
+ const { to, data } = ethCall(request)
47
+ expect(to).toBe(ARKIV_ADDRESS)
48
+ expect(data).toBe(`0x36917bfd000000000000000000000000${OWNER.slice(2)}`)
49
+ })
50
+
51
+ it("reports a fresh account as zero rather than as an absence", async () => {
52
+ const { client } = makeClient(0n)
53
+ expect(await getEntityNonce(client, OWNER)).toBe(0n)
54
+ })
55
+ })
56
+
57
+ describe("predictEntityKeys", () => {
58
+ it("derives one pair per create, walking the nonce forward", async () => {
59
+ const { client } = makeClient(7n)
60
+
61
+ const pairs = await predictEntityKeys(client, { owner: OWNER, salts: [1n, 2n, 3n] })
62
+
63
+ expect(pairs).toEqual([
64
+ { salt: 1n, key: predictEntityKey({ owner: OWNER, nonce: 7n, salt: 1n, chainId: CHAIN_ID }) },
65
+ { salt: 2n, key: predictEntityKey({ owner: OWNER, nonce: 8n, salt: 2n, chainId: CHAIN_ID }) },
66
+ { salt: 3n, key: predictEntityKey({ owner: OWNER, nonce: 9n, salt: 3n, chainId: CHAIN_ID }) },
67
+ ])
68
+ // Distinct keys, which is the whole point of the nonce.
69
+ expect(new Set(pairs.map((pair) => pair.key)).size).toBe(3)
70
+ })
71
+
72
+ it("makes up the salts when only asked for a count, and pairs each with its key", async () => {
73
+ const { client } = makeClient(0n)
74
+
75
+ // A literal count is a tuple, so this destructures without a cast — that it compiles is the
76
+ // assertion.
77
+ const [first, second] = await predictEntityKeys(client, { owner: OWNER, count: 2 })
78
+
79
+ // Random salts, so the only thing worth asserting is that the keys follow from them.
80
+ expect(first.key).toBe(
81
+ predictEntityKey({ owner: OWNER, nonce: 0n, salt: first.salt, chainId: CHAIN_ID }),
82
+ )
83
+ expect(second.key).toBe(
84
+ predictEntityKey({ owner: OWNER, nonce: 1n, salt: second.salt, chainId: CHAIN_ID }),
85
+ )
86
+ expect(first.salt).not.toBe(second.salt)
87
+ })
88
+
89
+ it("keeps a runtime count a plain array rather than guessing its length", async () => {
90
+ const { client } = makeClient(0n)
91
+ const count: number = 3
92
+
93
+ const pairs = await predictEntityKeys(client, { owner: OWNER, count })
94
+
95
+ expect(pairs).toHaveLength(3)
96
+ expect(pairs.map((pair) => pair.key)).toEqual(
97
+ pairs.map((pair, index) =>
98
+ predictEntityKey({
99
+ owner: OWNER,
100
+ nonce: BigInt(index),
101
+ salt: pair.salt,
102
+ chainId: CHAIN_ID,
103
+ }),
104
+ ),
105
+ )
106
+ })
107
+
108
+ it("takes NO_SALT in a slot, and pairs the key with the zero it resolves to", async () => {
109
+ const { client } = makeClient(4n)
110
+ const [first, second] = await predictEntityKeys(client, {
111
+ owner: OWNER,
112
+ salts: [NO_SALT, 5n],
113
+ })
114
+ expect(first).toEqual({
115
+ salt: 0n,
116
+ key: predictEntityKey({ owner: OWNER, nonce: 4n, salt: 0n, chainId: CHAIN_ID }),
117
+ })
118
+ expect(second).toEqual({
119
+ salt: 5n,
120
+ key: predictEntityKey({ owner: OWNER, nonce: 5n, salt: 5n, chainId: CHAIN_ID }),
121
+ })
122
+ })
123
+
124
+ it("prefers the salts it was given over the count", async () => {
125
+ const { client } = makeClient(0n)
126
+ const pairs = await predictEntityKeys(client, { owner: OWNER, count: 5, salts: [9n] })
127
+ expect(pairs).toEqual([
128
+ { salt: 9n, key: predictEntityKey({ owner: OWNER, nonce: 0n, salt: 9n, chainId: CHAIN_ID }) },
129
+ ])
130
+ })
131
+
132
+ it("asks the chain for its id when the client was built without one", async () => {
133
+ const { client, request } = makeClient(0n, { chain: false })
134
+
135
+ const [only] = await predictEntityKeys(client, { owner: OWNER, salts: [1n] })
136
+
137
+ expect(request.mock.calls.some(([body]) => body.method === "eth_chainId")).toBe(true)
138
+ expect(only.key).toBe(
139
+ predictEntityKey({ owner: OWNER, nonce: 0n, salt: 1n, chainId: CHAIN_ID }),
140
+ )
141
+ })
142
+
143
+ it("refuses a request that does not say how many keys it wants", async () => {
144
+ const { client } = makeClient(0n)
145
+ await expect(predictEntityKeys(client, { owner: OWNER })).rejects.toThrow(/`count` or `salts`/)
146
+ await expect(predictEntityKeys(client, { owner: OWNER, salts: [] })).rejects.toThrow(/empty/)
147
+ await expect(predictEntityKeys(client, { owner: OWNER, count: 0 })).rejects.toThrow(
148
+ /positive whole number/,
149
+ )
150
+ await expect(predictEntityKeys(client, { owner: OWNER, count: 1.5 })).rejects.toThrow(
151
+ /positive whole number/,
152
+ )
153
+ })
154
+
155
+ it("rejects a salt too wide for the uint128 the engine mixes in", async () => {
156
+ const { client } = makeClient(0n)
157
+ await expect(predictEntityKeys(client, { owner: OWNER, salts: [2n ** 128n] })).rejects.toThrow()
158
+ })
159
+ })
@@ -0,0 +1,152 @@
1
+ import type { Address, Hex } from "viem"
2
+ import { getChainId } from "viem/actions"
3
+ import type { ArkivClient } from "../../clients/baseClient"
4
+ import {
5
+ NO_SALT,
6
+ predictEntityKey,
7
+ randomSalt,
8
+ resolveSalt,
9
+ type SaltInput,
10
+ } from "../../entity/key"
11
+ import { getEntityNonce } from "./getEntityNonce"
12
+
13
+ /**
14
+ * A key a create will be given, together with the salt that mints it.
15
+ *
16
+ * The two travel as a pair because neither is any use alone: a create defaults to a *random* salt,
17
+ * so the key only comes out as predicted if that same create carries this salt.
18
+ */
19
+ export type PredictedEntityKey = {
20
+ /** The key the create will be given. */
21
+ key: Hex
22
+ /**
23
+ * The salt the create must carry for its key to come out as {@link PredictedEntityKey.key}.
24
+ * Always a plain `uint128` — a requested {@link NO_SALT} comes back as the `0n` it resolves to,
25
+ * which a create accepts just the same.
26
+ */
27
+ salt: bigint
28
+ }
29
+
30
+ /** Parameters for `client.predictEntityKeys`. */
31
+ export type PredictEntityKeysParameters<
32
+ TCount extends number = number,
33
+ TSalts extends readonly SaltInput[] | undefined = readonly SaltInput[] | undefined,
34
+ > = {
35
+ /** The account that will sign the creates. Its nonce is what the keys are derived from. */
36
+ owner: Address
37
+ /**
38
+ * How many creates the batch will carry. Ignored when `salts` is given, which already says how
39
+ * many there are.
40
+ *
41
+ * A literal count comes back as a fixed-length tuple, so `count: 2` destructures into two named
42
+ * pairs without a cast.
43
+ */
44
+ count?: TCount | undefined
45
+ /**
46
+ * The salt each create will carry, in batch order. Defaults to `count` fresh random salts —
47
+ * either way each key comes back paired with the salt that mints it. Pass {@link NO_SALT} in a
48
+ * slot whose create opts out of salting.
49
+ */
50
+ salts?: TSalts
51
+ }
52
+
53
+ /**
54
+ * The keys a batch would mint, in batch order, each paired with its salt.
55
+ *
56
+ * The length follows the request whenever it is known statically: a literal `count`, or a `salts`
57
+ * tuple, gives a fixed-length tuple; a count computed at runtime gives a plain array.
58
+ *
59
+ * `TSalts` is wrapped in a tuple to stop the conditional distributing over it. Left naked, the
60
+ * `readonly SaltInput[] | undefined` default would take both branches and union a tuple with an
61
+ * array, which widens the length straight back to `number`.
62
+ */
63
+ export type PredictEntityKeysReturnType<
64
+ TCount extends number = number,
65
+ TSalts extends readonly SaltInput[] | undefined = readonly SaltInput[] | undefined,
66
+ > = [TSalts] extends [readonly SaltInput[]]
67
+ ? { -readonly [Index in keyof TSalts]: PredictedEntityKey }
68
+ : Repeat<PredictedEntityKey, TCount>
69
+
70
+ /**
71
+ * A tuple of `TLength` × `T`, or a plain array when the length is only known at runtime.
72
+ *
73
+ * Written tail-recursively so TypeScript can build the batch sizes anyone would actually send.
74
+ */
75
+ type Repeat<T, TLength extends number, TAcc extends T[] = []> = number extends TLength
76
+ ? T[]
77
+ : TAcc["length"] extends TLength
78
+ ? TAcc
79
+ : Repeat<T, TLength, [...TAcc, T]>
80
+
81
+ /**
82
+ * Works out the keys an account's next creates will be given, before sending them.
83
+ *
84
+ * A key is `keccak256(chainId ++ registry ++ owner ++ nonce ++ salt)`. Everything but the nonce is
85
+ * known to the caller, and the nonce is read here — so a batch can reference a key it is about to
86
+ * mint, storing it as a `key` attribute on a sibling entity created in the same transaction.
87
+ *
88
+ * Each key arrives paired with its salt, and the create must be given that salt: a create left to
89
+ * pick its own salt picks a random one, which mints a different key.
90
+ *
91
+ * The prediction is valid only while nothing else from this owner is in flight. Every create that
92
+ * lands first takes the nonce with it and moves these keys. For a key you can rely on
93
+ * unconditionally, read it back from the create instead — `createEntity` and `executeBatch`
94
+ * report the keys the engine actually minted.
95
+ *
96
+ * @param parameters - Owner, and either a count or the salts. {@link PredictEntityKeysParameters}
97
+ * @returns One `{ key, salt }` pair per create, in batch order. {@link PredictEntityKeysReturnType}
98
+ *
99
+ * @example A batch whose second entity points at its first.
100
+ * import { key } from "@arkiv-network/sdk/attr"
101
+ *
102
+ * const [parent, child] = await client.predictEntityKeys({ owner: account.address, count: 2 })
103
+ * await client.executeBatch({
104
+ * creates: [
105
+ * { payload, contentType, expires, salt: parent.salt },
106
+ * {
107
+ * payload,
108
+ * contentType,
109
+ * expires,
110
+ * salt: child.salt,
111
+ * attributes: { parent: key(parent.key) },
112
+ * },
113
+ * ],
114
+ * })
115
+ */
116
+ export async function predictEntityKeys<
117
+ const TCount extends number = number,
118
+ const TSalts extends readonly SaltInput[] | undefined = undefined,
119
+ >(
120
+ client: ArkivClient,
121
+ { owner, count, salts }: PredictEntityKeysParameters<TCount, TSalts>,
122
+ ): Promise<PredictEntityKeysReturnType<TCount, TSalts>> {
123
+ const resolvedSalts = resolveSalts(count, salts)
124
+
125
+ const chainId = client.chain?.id ?? (await getChainId(client))
126
+ const nonce = await getEntityNonce(client, owner)
127
+
128
+ return resolvedSalts.map((salt, index) => ({
129
+ key: predictEntityKey({ owner, nonce: nonce + BigInt(index), salt, chainId }),
130
+ salt,
131
+ })) as PredictEntityKeysReturnType<TCount, TSalts>
132
+ }
133
+
134
+ /** The salts to derive from: the ones given, or `count` fresh random ones. */
135
+ function resolveSalts(
136
+ count: number | undefined,
137
+ salts: readonly SaltInput[] | undefined,
138
+ ): bigint[] {
139
+ if (salts !== undefined) {
140
+ if (salts.length === 0) {
141
+ throw new Error("predictEntityKeys: `salts` is empty — there is nothing to predict")
142
+ }
143
+ return salts.map(resolveSalt)
144
+ }
145
+ if (count === undefined) {
146
+ throw new Error("predictEntityKeys: pass either `count` or `salts`")
147
+ }
148
+ if (!Number.isInteger(count) || count <= 0) {
149
+ throw new Error(`predictEntityKeys: \`count\` must be a positive whole number, got ${count}`)
150
+ }
151
+ return Array.from({ length: count }, () => randomSalt())
152
+ }
@@ -1,83 +1,51 @@
1
- import { numberToHex } from "viem"
2
1
  import type { ArkivClient } from "../../clients/baseClient"
2
+ import { runQuery } from "../../query/engine"
3
+ import type { Expression } from "../../query/expression"
4
+ import { type SelectArg, toRpcSelect } from "../../query/selection"
3
5
  import type { Entity } from "../../types/entity"
4
- import type { RpcQueryOptions } from "../../types/rpcSchema"
5
- import { entityFromRpcResult } from "../../utils/entities"
6
-
7
- export type QueryOptionsIncludeData = {
8
- attributes?: boolean
9
- payload?: boolean
10
- metadata?: boolean
11
- }
12
- /**
13
- * @deprecated Server-side ordering is not supported by the network. Sort the fetched
14
- * entities in JavaScript instead (e.g. `entities.sort(...)`). This type will be removed
15
- * in a future release.
16
- */
17
- export type QueryOptionsOrderBy = {
18
- name: string
19
- type: "string" | "numeric"
20
- desc: "asc" | "desc"
21
- }
22
6
 
23
7
  export type QueryOptions = {
24
- includeData?: QueryOptionsIncludeData
25
- atBlock?: bigint
8
+ /** What to return. Defaults to everything. */
9
+ select?: SelectArg | undefined
10
+ /** Block height to read at. Defaults to the head. */
11
+ atBlock?: bigint | undefined
12
+ /** Page size, up to the node maximum of 200. */
13
+ limit?: number | undefined
26
14
  /**
27
- * @deprecated Server-side ordering is not supported by the network, so this option has no
28
- * effect on the returned order. Sort the fetched entities in JavaScript instead
29
- * (e.g. `entities.sort(...)`). This option will be removed in a future release.
15
+ * Cursor from a previous page. Requires that page's `blockNumber` as {@link atBlock} alongside
16
+ * it — the cursor is bound to the block it was issued at, so persist the two together. A cursor
17
+ * without a block is rejected locally rather than left to fail whenever a block lands mid-walk.
30
18
  */
31
- orderBy?: QueryOptionsOrderBy[]
32
- resultsPerPage?: number | undefined
33
19
  cursor?: string | undefined
34
20
  }
35
21
 
36
22
  export type QueryReturnType = {
37
23
  entities: Entity[]
24
+ /** The cursor for the next page, or `undefined` when no pages remain. */
38
25
  cursor: string | undefined
39
- blockNumber: bigint | undefined
26
+ /** The block the page was read at. */
27
+ blockNumber: bigint
40
28
  }
41
29
 
42
- export async function query(client: ArkivClient, query: string, queryOptions?: QueryOptions) {
43
- const rpcQueryOptions: RpcQueryOptions = {
44
- includeData: {
45
- key: true,
46
- payload: queryOptions?.includeData?.payload ?? true,
47
- attributes: queryOptions?.includeData?.attributes ?? false,
48
- contentType: queryOptions?.includeData?.metadata ?? false,
49
- expiration: queryOptions?.includeData?.metadata ?? false,
50
- owner: queryOptions?.includeData?.metadata ?? false,
51
- creator: queryOptions?.includeData?.metadata ?? false,
52
- createdAtBlock: queryOptions?.includeData?.metadata ?? false,
53
- lastModifiedAtBlock: queryOptions?.includeData?.metadata ?? false,
54
- transactionIndexInBlock: queryOptions?.includeData?.metadata ?? false,
55
- operationIndexInTransaction: queryOptions?.includeData?.metadata ?? false,
56
- },
57
- ...(queryOptions?.atBlock !== undefined && { atBlock: numberToHex(queryOptions.atBlock) }),
58
- ...(queryOptions?.orderBy && {
59
- orderBy: queryOptions?.orderBy.map((order) => ({
60
- name: order.name,
61
- type: order.type,
62
- desc: order.desc === "desc",
63
- })),
64
- }),
65
- ...(queryOptions?.resultsPerPage !== undefined && {
66
- resultsPerPage: numberToHex(queryOptions.resultsPerPage),
67
- }),
68
- ...(queryOptions?.cursor !== undefined && { cursor: queryOptions?.cursor }),
69
- }
70
-
71
- const result = await client.request({
72
- method: "arkiv_query",
73
- params: [query, rpcQueryOptions],
30
+ /**
31
+ * Runs a single query and returns one page, with no builder in between.
32
+ *
33
+ * Takes either an {@link Expression} or a raw query string. The string form is an escape hatch for
34
+ * a query built elsewhere — it goes to the node exactly as written, with none of the name, type or
35
+ * operator checks the expression combinators apply.
36
+ *
37
+ * @throws {QueryError} If the node rejects the query.
38
+ */
39
+ export function query(
40
+ client: ArkivClient,
41
+ query: Expression | string,
42
+ options: QueryOptions = {},
43
+ ): Promise<QueryReturnType> {
44
+ return runQuery(client, {
45
+ query: query.toString(),
46
+ select: toRpcSelect(options.select),
47
+ limit: options.limit,
48
+ cursor: options.cursor,
49
+ atBlock: options.atBlock,
74
50
  })
75
-
76
- const entities = await Promise.all(result.data.map((entity) => entityFromRpcResult(entity)))
77
-
78
- return {
79
- entities,
80
- cursor: result.cursor,
81
- blockNumber: BigInt(result.blockNumber),
82
- }
83
51
  }