@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
@@ -1,50 +1,34 @@
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"
5
8
 
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
- ],
9
+ /**
10
+ * Reads one entity by key, with every field selected.
11
+ *
12
+ * Returns a {@link FullEntity} rather than an {@link Entity}: the selection is fixed at `"*"`, so
13
+ * every field is populated and none of them needs a `?` at the call site. The bare `Entity` type
14
+ * has every field optional because a `select()` result carries only what it asked for.
15
+ *
16
+ * @throws {InvalidValueError} If the key is not 32 bytes.
17
+ * @throws {NoEntityFoundError} If no live entity has that key — it never existed, or it was
18
+ * deleted or has expired.
19
+ */
20
+ export async function getEntity(client: ArkivClient, entityKey: Hex): Promise<FullEntity> {
21
+ const { entities } = await runQuery(client, {
22
+ query: eq("$key", key(entityKey)).toString(),
23
+ select: toRpcSelect("*"),
24
+ limit: 1,
43
25
  })
44
26
 
45
- if (!result.data.length) {
46
- throw new NoEntityFoundError()
27
+ const entity = entities[0]
28
+ if (entity === undefined) {
29
+ throw new NoEntityFoundError(entityKey)
47
30
  }
48
-
49
- return await entityFromRpcResult(result.data[0])
31
+ // Every field was selected, so the runtime Entity carries them all; the cast is the same
32
+ // narrowing `client.select("*")` gets from its own inference.
33
+ return entity as unknown as FullEntity
50
34
  }
@@ -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,143 @@
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 { 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("prefers the salts it was given over the count", async () => {
109
+ const { client } = makeClient(0n)
110
+ const pairs = await predictEntityKeys(client, { owner: OWNER, count: 5, salts: [9n] })
111
+ expect(pairs).toEqual([
112
+ { salt: 9n, key: predictEntityKey({ owner: OWNER, nonce: 0n, salt: 9n, chainId: CHAIN_ID }) },
113
+ ])
114
+ })
115
+
116
+ it("asks the chain for its id when the client was built without one", async () => {
117
+ const { client, request } = makeClient(0n, { chain: false })
118
+
119
+ const [only] = await predictEntityKeys(client, { owner: OWNER, salts: [1n] })
120
+
121
+ expect(request.mock.calls.some(([body]) => body.method === "eth_chainId")).toBe(true)
122
+ expect(only.key).toBe(
123
+ predictEntityKey({ owner: OWNER, nonce: 0n, salt: 1n, chainId: CHAIN_ID }),
124
+ )
125
+ })
126
+
127
+ it("refuses a request that does not say how many keys it wants", async () => {
128
+ const { client } = makeClient(0n)
129
+ await expect(predictEntityKeys(client, { owner: OWNER })).rejects.toThrow(/`count` or `salts`/)
130
+ await expect(predictEntityKeys(client, { owner: OWNER, salts: [] })).rejects.toThrow(/empty/)
131
+ await expect(predictEntityKeys(client, { owner: OWNER, count: 0 })).rejects.toThrow(
132
+ /positive whole number/,
133
+ )
134
+ await expect(predictEntityKeys(client, { owner: OWNER, count: 1.5 })).rejects.toThrow(
135
+ /positive whole number/,
136
+ )
137
+ })
138
+
139
+ it("rejects a salt too wide for the uint128 the engine mixes in", async () => {
140
+ const { client } = makeClient(0n)
141
+ await expect(predictEntityKeys(client, { owner: OWNER, salts: [2n ** 128n] })).rejects.toThrow()
142
+ })
143
+ })
@@ -0,0 +1,138 @@
1
+ import type { Address, Hex } from "viem"
2
+ import { getChainId } from "viem/actions"
3
+ import type { ArkivClient } from "../../clients/baseClient"
4
+ import { predictEntityKey, randomSalt, validateSalt } from "../../entity/key"
5
+ import { getEntityNonce } from "./getEntityNonce"
6
+
7
+ /**
8
+ * A key a create will be given, together with the salt that mints it.
9
+ *
10
+ * The two travel as a pair because neither is any use alone: a create defaults to a *random* salt,
11
+ * so the key only comes out as predicted if that same create carries this salt.
12
+ */
13
+ export type PredictedEntityKey = {
14
+ /** The key the create will be given. */
15
+ key: Hex
16
+ /** The salt the create must carry for its key to come out as {@link PredictedEntityKey.key}. */
17
+ salt: bigint
18
+ }
19
+
20
+ /** Parameters for {@link predictEntityKeys}. */
21
+ export type PredictEntityKeysParameters<
22
+ TCount extends number = number,
23
+ TSalts extends readonly bigint[] | undefined = readonly bigint[] | undefined,
24
+ > = {
25
+ /** The account that will sign the creates. Its nonce is what the keys are derived from. */
26
+ owner: Address
27
+ /**
28
+ * How many creates the batch will carry. Ignored when `salts` is given, which already says how
29
+ * many there are.
30
+ *
31
+ * A literal count comes back as a fixed-length tuple, so `count: 2` destructures into two named
32
+ * pairs without a cast.
33
+ */
34
+ count?: TCount | undefined
35
+ /**
36
+ * The salt each create will carry, in batch order. Defaults to `count` fresh random salts —
37
+ * either way each key comes back paired with the salt that mints it.
38
+ */
39
+ salts?: TSalts
40
+ }
41
+
42
+ /**
43
+ * The keys a batch would mint, in batch order, each paired with its salt.
44
+ *
45
+ * The length follows the request whenever it is known statically: a literal `count`, or a `salts`
46
+ * tuple, gives a fixed-length tuple; a count computed at runtime gives a plain array.
47
+ *
48
+ * `TSalts` is wrapped in a tuple to stop the conditional distributing over it. Left naked, the
49
+ * `readonly bigint[] | undefined` default would take both branches and union a tuple with an array,
50
+ * which widens the length straight back to `number`.
51
+ */
52
+ export type PredictEntityKeysReturnType<
53
+ TCount extends number = number,
54
+ TSalts extends readonly bigint[] | undefined = readonly bigint[] | undefined,
55
+ > = [TSalts] extends [readonly bigint[]]
56
+ ? { -readonly [Index in keyof TSalts]: PredictedEntityKey }
57
+ : Repeat<PredictedEntityKey, TCount>
58
+
59
+ /**
60
+ * A tuple of `TLength` × `T`, or a plain array when the length is only known at runtime.
61
+ *
62
+ * Written tail-recursively so TypeScript can build the batch sizes anyone would actually send.
63
+ */
64
+ type Repeat<T, TLength extends number, TAcc extends T[] = []> = number extends TLength
65
+ ? T[]
66
+ : TAcc["length"] extends TLength
67
+ ? TAcc
68
+ : Repeat<T, TLength, [...TAcc, T]>
69
+
70
+ /**
71
+ * Works out the keys an account's next creates will be given, before sending them.
72
+ *
73
+ * A key is `keccak256(chainId ++ registry ++ owner ++ nonce ++ salt)`. Everything but the nonce is
74
+ * known to the caller, and the nonce is read here — so a batch can reference a key it is about to
75
+ * mint, storing it as a `key` attribute on a sibling entity created in the same transaction.
76
+ *
77
+ * Each key arrives paired with its salt, and the create must be given that salt: a create left to
78
+ * pick its own salt picks a random one, which mints a different key.
79
+ *
80
+ * The prediction is valid only while nothing else from this owner is in flight. Every create that
81
+ * lands first takes the nonce with it and moves these keys. For a key you can rely on
82
+ * unconditionally, read it back from the create instead — `createEntity` and `mutateEntities`
83
+ * report the keys the engine actually minted.
84
+ *
85
+ * @param parameters - Owner, and either a count or the salts. {@link PredictEntityKeysParameters}
86
+ * @returns One `{ key, salt }` pair per create, in batch order. {@link PredictEntityKeysReturnType}
87
+ *
88
+ * @example A batch whose second entity points at its first.
89
+ * import { key } from "@arkiv-network/sdk/attr"
90
+ *
91
+ * const [parent, child] = await client.predictEntityKeys({ owner: account.address, count: 2 })
92
+ * await wallet.mutateEntities({
93
+ * creates: [
94
+ * { payload, contentType, expires, salt: parent.salt },
95
+ * {
96
+ * payload,
97
+ * contentType,
98
+ * expires,
99
+ * salt: child.salt,
100
+ * attributes: { parent: key(parent.key) },
101
+ * },
102
+ * ],
103
+ * })
104
+ */
105
+ export async function predictEntityKeys<
106
+ const TCount extends number = number,
107
+ const TSalts extends readonly bigint[] | undefined = undefined,
108
+ >(
109
+ client: ArkivClient,
110
+ { owner, count, salts }: PredictEntityKeysParameters<TCount, TSalts>,
111
+ ): Promise<PredictEntityKeysReturnType<TCount, TSalts>> {
112
+ const resolvedSalts = resolveSalts(count, salts)
113
+
114
+ const chainId = client.chain?.id ?? (await getChainId(client))
115
+ const nonce = await getEntityNonce(client, owner)
116
+
117
+ return resolvedSalts.map((salt, index) => ({
118
+ key: predictEntityKey({ owner, nonce: nonce + BigInt(index), salt, chainId }),
119
+ salt,
120
+ })) as PredictEntityKeysReturnType<TCount, TSalts>
121
+ }
122
+
123
+ /** The salts to derive from: the ones given, or `count` fresh random ones. */
124
+ function resolveSalts(count: number | undefined, salts: readonly bigint[] | undefined): bigint[] {
125
+ if (salts !== undefined) {
126
+ if (salts.length === 0) {
127
+ throw new Error("predictEntityKeys: `salts` is empty — there is nothing to predict")
128
+ }
129
+ return salts.map(validateSalt)
130
+ }
131
+ if (count === undefined) {
132
+ throw new Error("predictEntityKeys: pass either `count` or `salts`")
133
+ }
134
+ if (!Number.isInteger(count) || count <= 0) {
135
+ throw new Error(`predictEntityKeys: \`count\` must be a positive whole number, got ${count}`)
136
+ }
137
+ return Array.from({ length: count }, () => randomSalt())
138
+ }
@@ -1,83 +1,47 @@
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
26
- /**
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.
30
- */
31
- orderBy?: QueryOptionsOrderBy[]
32
- resultsPerPage?: number | undefined
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
14
+ /** Cursor from a previous page. */
33
15
  cursor?: string | undefined
34
16
  }
35
17
 
36
18
  export type QueryReturnType = {
37
19
  entities: Entity[]
20
+ /** The cursor for the next page, or `undefined` when no pages remain. */
38
21
  cursor: string | undefined
39
- blockNumber: bigint | undefined
22
+ /** The block the page was read at. */
23
+ blockNumber: bigint
40
24
  }
41
25
 
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],
26
+ /**
27
+ * Runs a single query and returns one page, with no builder in between.
28
+ *
29
+ * Takes either an {@link Expression} or a raw query string. The string form is an escape hatch for
30
+ * a query built elsewhere — it goes to the node exactly as written, with none of the name, type or
31
+ * operator checks the expression combinators apply.
32
+ *
33
+ * @throws {QueryError} If the node rejects the query.
34
+ */
35
+ export function query(
36
+ client: ArkivClient,
37
+ query: Expression | string,
38
+ options: QueryOptions = {},
39
+ ): Promise<QueryReturnType> {
40
+ return runQuery(client, {
41
+ query: query.toString(),
42
+ select: toRpcSelect(options.select),
43
+ limit: options.limit,
44
+ cursor: options.cursor,
45
+ atBlock: options.atBlock,
74
46
  })
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
47
  }