@arkiv-network/sdk 0.8.1-dev.1 → 0.9.0-next.2

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 (184) hide show
  1. package/README.md +180 -61
  2. package/dist/chains/index.d.ts +17 -17
  3. package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
  4. package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
  5. package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
  6. package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
  7. package/dist/entity/index.cjs +4 -10
  8. package/dist/entity/index.d.cts +3 -3
  9. package/dist/entity/index.d.ts +3 -3
  10. package/dist/entity/index.js +3 -4
  11. package/dist/entity-BM1unrQf.cjs +41 -0
  12. package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
  13. package/dist/entity-CFAI1KyG.js +36 -0
  14. package/dist/entity-CFAI1KyG.js.map +1 -0
  15. package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
  16. package/dist/entity-CGqx_AU0.js.map +1 -0
  17. package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
  18. package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
  19. package/dist/expirationTime-CkIGNAg8.cjs +140 -0
  20. package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
  21. package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
  22. package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
  23. package/dist/expirationTime-UKPuA-LM.js.map +1 -0
  24. package/dist/expiry-B48I-yTA.d.cts +40 -0
  25. package/dist/expiry-B48I-yTA.d.cts.map +1 -0
  26. package/dist/expiry-S2PUL8Hg.d.ts +40 -0
  27. package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
  28. package/dist/index-C-mMF5WZ.d.cts +735 -0
  29. package/dist/index-C-mMF5WZ.d.cts.map +1 -0
  30. package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
  31. package/dist/index-C4zZqJ9S.d.ts.map +1 -0
  32. package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
  33. package/dist/index-CcVWYB6v.d.cts.map +1 -0
  34. package/dist/index-DCx2XC6S.d.ts +489 -0
  35. package/dist/index-DCx2XC6S.d.ts.map +1 -0
  36. package/dist/index-DLb7BfIw.d.ts +735 -0
  37. package/dist/index-DLb7BfIw.d.ts.map +1 -0
  38. package/dist/index-DzriOUm8.d.cts +489 -0
  39. package/dist/index-DzriOUm8.d.cts.map +1 -0
  40. package/dist/index.cjs +162 -146
  41. package/dist/index.d.cts +180 -133
  42. package/dist/index.d.cts.map +1 -1
  43. package/dist/index.d.ts +180 -133
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +93 -83
  46. package/dist/index.js.map +1 -1
  47. package/dist/params-BuIrJA9a.cjs +45 -0
  48. package/dist/params-DDJOnsNi.js +22 -0
  49. package/dist/params-DDJOnsNi.js.map +1 -0
  50. package/dist/query/index.cjs +9 -4
  51. package/dist/query/index.d.cts +6 -6
  52. package/dist/query/index.d.ts +6 -6
  53. package/dist/query/index.js +4 -5
  54. package/dist/query-D0KtuFh2.cjs +1625 -0
  55. package/dist/query-DpRDozmj.js +1417 -0
  56. package/dist/query-DpRDozmj.js.map +1 -0
  57. package/dist/schema/index.cjs +46 -0
  58. package/dist/schema/index.d.cts +2 -0
  59. package/dist/schema/index.d.ts +2 -0
  60. package/dist/schema/index.js +3 -0
  61. package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
  62. package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
  63. package/dist/schema-DY4sqehl.js.map +1 -0
  64. package/dist/types/index.cjs +1 -1
  65. package/dist/types/index.d.cts +5 -5
  66. package/dist/types/index.d.ts +5 -5
  67. package/dist/types/index.js +1 -1
  68. package/dist/utils/index.cjs +3 -5
  69. package/dist/utils/index.d.cts +3 -3
  70. package/dist/utils/index.d.ts +3 -3
  71. package/dist/utils/index.js +3 -3
  72. package/package.json +9 -12
  73. package/src/actions/public/getEntity.test.ts +69 -0
  74. package/src/actions/public/getEntity.ts +34 -20
  75. package/src/actions/public/predictEntityKeys.ts +8 -7
  76. package/src/actions/public/query.test.ts +58 -0
  77. package/src/actions/public/query.ts +41 -13
  78. package/src/actions/wallet/createEntity.ts +34 -63
  79. package/src/actions/wallet/executeBatch.test.ts +38 -0
  80. package/src/actions/wallet/executeBatch.ts +26 -9
  81. package/src/actions/wallet/extendEntity.ts +9 -8
  82. package/src/actions/wallet/patchEntity.ts +43 -52
  83. package/src/clients/createPublicClient.test.ts +10 -2
  84. package/src/clients/createWalletClient.ts +4 -1
  85. package/src/clients/decorators/arkivPublic.ts +123 -80
  86. package/src/clients/decorators/arkivWallet.ts +113 -65
  87. package/src/entity/create.test.ts +25 -21
  88. package/src/entity/expiry.ts +8 -6
  89. package/src/entity/flags.ts +1 -1
  90. package/src/entity/index.ts +2 -4
  91. package/src/entity/key.ts +3 -3
  92. package/src/entity/operations.ts +1 -1
  93. package/src/errors.ts +2 -11
  94. package/src/index.ts +18 -6
  95. package/src/query/builderBase.ts +166 -0
  96. package/src/query/countBuilder.test.ts +91 -0
  97. package/src/query/countBuilder.ts +67 -0
  98. package/src/query/engine.ts +9 -3
  99. package/src/query/errors.ts +24 -14
  100. package/src/query/expression.test.ts +129 -21
  101. package/src/query/expression.ts +323 -94
  102. package/src/query/index.ts +26 -4
  103. package/src/query/injection.test.ts +5 -11
  104. package/src/query/queryBuilder.test.ts +467 -58
  105. package/src/query/queryBuilder.ts +191 -98
  106. package/src/query/queryResult.ts +35 -9
  107. package/src/query/selection.test.ts +278 -24
  108. package/src/query/selection.ts +427 -86
  109. package/src/schema/attributes.test.ts +266 -0
  110. package/src/schema/attributes.ts +212 -0
  111. package/src/{attr → schema}/codec.test.ts +37 -5
  112. package/src/{attr → schema}/codec.ts +40 -11
  113. package/src/schema/column.ts +193 -0
  114. package/src/{attr → schema}/errors.ts +67 -10
  115. package/src/schema/index.ts +76 -0
  116. package/src/{attr → schema}/names.ts +4 -2
  117. package/src/schema/schema.test.ts +171 -0
  118. package/src/schema/schema.ts +250 -0
  119. package/src/{attr → schema}/types.ts +36 -30
  120. package/src/{attr → schema}/values.test.ts +62 -3
  121. package/src/{attr → schema}/values.ts +246 -65
  122. package/src/types/entity.test.ts +1 -44
  123. package/src/types/entity.ts +9 -55
  124. package/src/types/events.ts +1 -1
  125. package/src/types/index.ts +11 -4
  126. package/src/types/rpcSchema.ts +22 -16
  127. package/src/utils/arkivTransactions.test.ts +356 -116
  128. package/src/utils/arkivTransactions.ts +30 -38
  129. package/src/utils/entities.ts +10 -9
  130. package/src/utils/expirationTime.ts +5 -5
  131. package/src/utils/index.ts +0 -1
  132. package/src/utils/logger.test.ts +45 -0
  133. package/src/utils/logger.ts +5 -39
  134. package/src/utils/revert.test.ts +25 -0
  135. package/src/utils/revert.ts +28 -3
  136. package/dist/attr/index.cjs +0 -34
  137. package/dist/attr/index.d.cts +0 -2
  138. package/dist/attr/index.d.ts +0 -2
  139. package/dist/attr/index.js +0 -3
  140. package/dist/attr-tmyREHtw.js.map +0 -1
  141. package/dist/entity-B0t6sN7F.cjs +0 -71
  142. package/dist/entity-BIp_df9u.js +0 -67
  143. package/dist/entity-BIp_df9u.js.map +0 -1
  144. package/dist/entity-CniBgBSd.js.map +0 -1
  145. package/dist/expiry-BxOlvVWC.d.ts +0 -79
  146. package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
  147. package/dist/expiry-CG9GhVXv.cjs +0 -135
  148. package/dist/expiry-Cgdb_dbq.js.map +0 -1
  149. package/dist/expiry-DRV29F1r.d.cts +0 -79
  150. package/dist/expiry-DRV29F1r.d.cts.map +0 -1
  151. package/dist/flags-DCD26uhe.cjs +0 -43
  152. package/dist/flags-DUT2gHEU.js +0 -33
  153. package/dist/flags-DUT2gHEU.js.map +0 -1
  154. package/dist/index-BkqSnGJH.d.ts.map +0 -1
  155. package/dist/index-C7fbakoI.d.cts +0 -278
  156. package/dist/index-C7fbakoI.d.cts.map +0 -1
  157. package/dist/index-CIcHPx2j.d.cts +0 -424
  158. package/dist/index-CIcHPx2j.d.cts.map +0 -1
  159. package/dist/index-CLhsFyVH.d.cts.map +0 -1
  160. package/dist/index-Cd2aaWme.d.ts +0 -424
  161. package/dist/index-Cd2aaWme.d.ts.map +0 -1
  162. package/dist/index-Cq6fa8AI.d.ts +0 -278
  163. package/dist/index-Cq6fa8AI.d.ts.map +0 -1
  164. package/dist/index-DaNeJUw8.d.ts +0 -955
  165. package/dist/index-DaNeJUw8.d.ts.map +0 -1
  166. package/dist/index-Dv16LO2s.d.cts +0 -955
  167. package/dist/index-Dv16LO2s.d.cts.map +0 -1
  168. package/dist/payload-AfUfSctb.d.ts.map +0 -1
  169. package/dist/payload-D167BlJR.d.cts.map +0 -1
  170. package/dist/payload-DE7wMztL.js +0 -92
  171. package/dist/payload-DE7wMztL.js.map +0 -1
  172. package/dist/payload-DqFweqiO.cjs +0 -108
  173. package/dist/query-9oojUdM7.cjs +0 -1128
  174. package/dist/query-Ber-lOJQ.js +0 -968
  175. package/dist/query-Ber-lOJQ.js.map +0 -1
  176. package/src/actions/public/getEntityCount.ts +0 -10
  177. package/src/attr/attributes.test.ts +0 -120
  178. package/src/attr/attributes.ts +0 -196
  179. package/src/attr/index.ts +0 -50
  180. package/src/types/mimeTypes.ts +0 -27
  181. package/src/utils/compression.test.ts +0 -32
  182. package/src/utils/compression.ts +0 -102
  183. package/src/utils/payload.ts +0 -36
  184. /package/src/{attr → schema}/decimal.ts +0 -0
@@ -1,47 +1,75 @@
1
1
  import type { ArkivClient } from "../../clients/baseClient"
2
2
  import { runQuery } from "../../query/engine"
3
3
  import type { Expression } from "../../query/expression"
4
- import { type SelectArg, toRpcSelect } from "../../query/selection"
5
- import type { Entity } from "../../types/entity"
4
+ import {
5
+ type FullEntity,
6
+ projectSelection,
7
+ type ResultOf,
8
+ type SelectArg,
9
+ type SelectionOf,
10
+ toRow,
11
+ } from "../../query/selection"
6
12
 
7
- export type QueryOptions = {
8
- /** What to return. Defaults to everything. */
9
- select?: SelectArg | undefined
13
+ export type QueryOptions<TSelection extends SelectArg = SelectArg> = {
14
+ /** What to return — the same selection `select()` takes. Defaults to everything. */
15
+ select?: TSelection | undefined
10
16
  /** Block height to read at. Defaults to the head. */
11
17
  atBlock?: bigint | undefined
12
- /** Page size, up to the node maximum of 200. */
18
+ /** Page size, up to the node maximum of 100. */
13
19
  limit?: number | undefined
14
20
  /** Cursor from a previous page. */
15
21
  cursor?: string | undefined
22
+ /** Whether to also count every entity the query matches, returned as `count`. */
23
+ count?: boolean | undefined
16
24
  }
17
25
 
18
- export type QueryReturnType = {
19
- entities: Entity[]
26
+ /**
27
+ * What one result of a `query()` reads as for selection `S`: the same row `select(S)` returns
28
+ * without a schema — every field when everything is selected, and every field optional when the
29
+ * selection is only known at runtime.
30
+ */
31
+ export type QueryRow<S> = ResultOf<SelectionOf<S>, undefined>
32
+
33
+ export type QueryReturnType<TResult = FullEntity> = {
34
+ entities: TResult[]
20
35
  /** The cursor for the next page, or `undefined` when no pages remain. */
21
36
  cursor: string | undefined
22
37
  /** The block the page was read at. */
23
38
  blockNumber: bigint
39
+ /** How many entities the query matches, if `count` was set. */
40
+ count: bigint | undefined
24
41
  }
25
42
 
26
43
  /**
27
44
  * Runs a single query and returns one page, with no builder in between.
28
45
  *
46
+ * The selection means what it does in `select()`, and each result is the same flat row a query
47
+ * without a schema returns — typed from the selection, with every attribute an `ArkivValue` or
48
+ * `undefined`.
49
+ *
29
50
  * Takes either an {@link Expression} or a raw query string. The string form is an escape hatch for
30
51
  * a query built elsewhere — it goes to the node exactly as written, with none of the name, type or
31
52
  * operator checks the expression combinators apply.
32
53
  *
54
+ * @throws If the selection asks for nothing, or names an attribute outside the name grammar.
33
55
  * @throws {QueryError} If the node rejects the query.
34
56
  */
35
- export function query(
57
+ export async function query<const S extends SelectArg = "*">(
36
58
  client: ArkivClient,
37
59
  query: Expression | string,
38
- options: QueryOptions = {},
39
- ): Promise<QueryReturnType> {
40
- return runQuery(client, {
60
+ options: QueryOptions<S> = {},
61
+ ): Promise<QueryReturnType<QueryRow<S>>> {
62
+ const projection = projectSelection(options.select)
63
+ const page = await runQuery(client, {
41
64
  query: query.toString(),
42
- select: toRpcSelect(options.select),
65
+ select: projection.select,
43
66
  limit: options.limit,
44
67
  cursor: options.cursor,
45
68
  atBlock: options.atBlock,
69
+ count: options.count,
46
70
  })
71
+ return {
72
+ ...page,
73
+ entities: page.entities.map((entity) => toRow(entity, projection) as QueryRow<S>),
74
+ }
47
75
  }
@@ -1,9 +1,9 @@
1
1
  import type { Hash, Hex } from "viem"
2
- import type { AttributeInputs } from "../../attr"
3
2
  import type { ArkivClient } from "../../clients/baseClient"
4
3
  import type { CreationFlags, Expiry, NO_SALT, SaltInput } from "../../entity"
5
4
  import { EntityMutationError } from "../../errors"
6
- import type { MimeType, TxParams } from "../../types"
5
+ import type { CreateEntityValues, Schema } from "../../schema"
6
+ import type { TxParams } from "../../types"
7
7
  import { sendArkivTransaction } from "../../utils/arkivTransactions"
8
8
  import type { ExpirationTime } from "../../utils/expirationTime"
9
9
  import { getLogger } from "../../utils/logger"
@@ -11,69 +11,27 @@ import { getLogger } from "../../utils/logger"
11
11
  const logger = getLogger("actions:wallet:create-entity")
12
12
 
13
13
  /**
14
- * Parameters for creating an entity.
15
- *
16
- * An entity always carries contents and a type for them, plus a lifetime. Attributes, flags and a
17
- * salt are optional on top of that.
14
+ * How an created entity lives: its lifetime, plus the flags and salt it is created with.
18
15
  *
19
16
  * @example
20
- * const { entityKey } = await client.createEntity({
21
- * attributes: {
22
- * level: i32(10),
23
- * balance: u256(1_000_000n),
24
- * score: dec("3.5"),
25
- * name: "Bob", // bare string -> str
26
- * flagged: true, // bare boolean -> bool
27
- * },
28
- * payload: jsonToPayload({ hello: "world" }),
29
- * contentType: "application/json",
30
- * expires: ExpirationTime.fromDays(30),
31
- * flags: { readonly: true },
32
- * })
17
+ * { expiration: ExpirationTime.fromDays(30) }
33
18
  *
34
- * @example Pin an absolute deadline, but refuse to create something nearly dead:
35
- * await client.createEntity({
36
- * payload, contentType: "application/json",
37
- * expires: ExpirationTime.atDate(someDeadline, {
19
+ * @example Pin an absolute deadline, but never let it leave the entity with less than a day:
20
+ * {
21
+ * expiration: ExpirationTime.atDate(someDeadline, {
38
22
  * atLeast: ExpirationTime.fromDays(1), // ...but live at least a day regardless
39
23
  * }),
40
- * })
41
- *
42
- * @example An entity that is nothing but queryable attributes still declares its (empty) contents:
43
- * await client.createEntity({
44
- * attributes: { parent: key(parentKey), rank: i32(3) },
45
- * payload: new Uint8Array(),
46
- * contentType: "application/octet-stream",
47
- * expires: ExpirationTime.fromDays(30),
48
- * })
24
+ * flags: { readonly: true },
25
+ * }
49
26
  */
50
- export type CreateEntityParameters = {
27
+ export type CreateEntityOptions = {
51
28
  /**
52
29
  * How long the entity should live, built with {@link ExpirationTime}.
53
30
  *
54
31
  * `ExpirationTime.fromDays(30)` is the everyday form. `atBlock` / `atDate` pin an absolute
55
32
  * deadline instead, and either can carry `{ atLeast }` to guarantee a minimum life on top of it.
56
33
  */
57
- expires: Expiry
58
- /**
59
- * The entity's opaque payload. Travels to the engine as the `$payload` system attribute; on this
60
- * surface it is just the entity's contents.
61
- *
62
- * Pass an empty `Uint8Array` for an entity that is nothing but queryable attributes — it is
63
- * written as an empty payload rather than left unset, so what you pass is what is stored.
64
- */
65
- payload: Uint8Array
66
- /** The payload's MIME type, e.g. `"application/json"`. Sent as the `$contentType` cell. */
67
- contentType: MimeType | (string & {})
68
- /**
69
- * The entity's queryable attributes, keyed by name. Values are the tagged constructors from
70
- * `@arkiv-network/sdk/attr` — `i32`, `u64`, `u256`, `dec`, `str`, `addr`, `key`, `bytes32`,
71
- * `bool` — or a bare `boolean`, `number`, `bigint` or `string` where the type is unambiguous.
72
- *
73
- * @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
74
- * @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
75
- */
76
- attributes?: AttributeInputs | undefined
34
+ expiration: Expiry
77
35
  /**
78
36
  * Properties fixed at creation: `readonly` and `permissionlessExtension`. No operation changes
79
37
  * them afterwards, so an entity is whatever it was created as for life. Both default to `false`.
@@ -89,6 +47,21 @@ export type CreateEntityParameters = {
89
47
  salt?: SaltInput | undefined
90
48
  }
91
49
 
50
+ /**
51
+ * One entity to create, as a batch carries it: the schema, the values, and the {@link CreateEntityOptions}.
52
+ *
53
+ * @typeParam S - The schema the values are typed by.
54
+ */
55
+ export type CreateEntityParameters<S extends Schema = Schema> = CreateEntityOptions & {
56
+ /** The schema the entity is written through. */
57
+ schema: S
58
+ /**
59
+ * The entity's values, keyed by column. Every required column must be given; an optional one may
60
+ * be left out, and a defaulted one left out is written with its default.
61
+ */
62
+ values: CreateEntityValues<S>
63
+ }
64
+
92
65
  /**
93
66
  * The result of creating an entity.
94
67
  */
@@ -97,25 +70,23 @@ export type CreateEntityReturnType = {
97
70
  entityKey: Hex
98
71
  txHash: Hash
99
72
  /**
100
- * The block the entity is expected to expire at, resolved against the block the transaction was
101
- * built on.
102
- *
103
- * The engine resolves a duration against the block the transaction actually lands in, so for
104
- * `ExpirationTime.fromDays(30)` and friends this is a lower bound: the real expiry is later by
105
- * however many blocks passed between building and inclusion. An `atBlock` deadline is exact.
73
+ * The block the entity expires at, as the engine recorded it — read from the `EntityCreated`
74
+ * event, so a duration is already resolved against the block the transaction landed in.
106
75
  */
107
76
  expiresAt: bigint
108
77
  }
109
78
 
110
- export async function createEntity(
79
+ export async function createEntity<S extends Schema>(
111
80
  client: ArkivClient,
112
- data: CreateEntityParameters,
81
+ schema: S,
82
+ values: CreateEntityValues<S>,
83
+ options: CreateEntityOptions,
113
84
  txParams?: TxParams,
114
85
  ): Promise<CreateEntityReturnType> {
115
- logger("createEntity %o", data)
86
+ logger("createEntity %o", values)
116
87
  const { receipt, createdEntityKeys, createdExpiries } = await sendArkivTransaction(
117
88
  client,
118
- { creates: [data] },
89
+ { creates: [{ ...options, schema, values }] },
119
90
  txParams,
120
91
  )
121
92
 
@@ -1,10 +1,18 @@
1
1
  import { describe, expect, it, vi } from "bun:test"
2
+ import { decodeAbiParameters, parseAbiParameters, toHex } from "viem"
2
3
  import type { ArkivClient } from "../../clients/baseClient"
4
+ import { OperationType } from "../../entity/params"
5
+ import { bytes, defineSchema, i32, str } from "../../schema"
3
6
  import { executeBatch } from "./executeBatch"
4
7
 
5
8
  const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
9
+ const OTHER_KEY = `0x${"cd".repeat(32)}` as const
6
10
  const NEW_OWNER = "0x2222222222222222222222222222222222222222"
7
11
 
12
+ const PATCH_PARAMS = parseAbiParameters(
13
+ "(bytes32 entityKey, (bytes32 name, uint8 typeId, bytes value)[] mutations)",
14
+ )
15
+
8
16
  function makeClient() {
9
17
  const writeContract = vi.fn().mockResolvedValue("0xdeadbeef")
10
18
  return {
@@ -74,3 +82,33 @@ describe("the empty-batch guard", () => {
74
82
  expect(writeContract.mock.calls[0][0].args[0]).toHaveLength(1)
75
83
  })
76
84
  })
85
+
86
+ describe("schemas in a batch", () => {
87
+ it("types each patch by its own schema", async () => {
88
+ const Posts = defineSchema({ title: str(), score: i32().indexed() })
89
+ const Files = defineSchema({ path: str().indexed(), body: bytes().optional() })
90
+ const { client, writeContract } = makeClient()
91
+
92
+ const result = await executeBatch(client, {
93
+ patches: [
94
+ { schema: Posts, entityKey: ENTITY_KEY, set: { score: 5 } },
95
+ { schema: Files, entityKey: OTHER_KEY, set: { path: "/a" }, unset: ["body"] },
96
+ ],
97
+ })
98
+
99
+ expect(result.patchedEntities).toEqual([ENTITY_KEY, OTHER_KEY])
100
+ const ops = writeContract.mock.calls[0][0].args[0] as {
101
+ operation: number
102
+ operationData: `0x${string}`
103
+ }[]
104
+ expect(ops.map((op) => op.operation)).toEqual([OperationType.Patch, OperationType.Patch])
105
+ const [posts, files] = ops.map((op) => decodeAbiParameters(PATCH_PARAMS, op.operationData)[0])
106
+ expect(posts.mutations.map((m) => [m.name, m.typeId])).toEqual([
107
+ [toHex("score", { size: 32 }), 0x80 | 2],
108
+ ])
109
+ expect(files.mutations.map((m) => [m.name, m.typeId])).toEqual([
110
+ [toHex("body", { size: 32 }), 0],
111
+ [toHex("path", { size: 32 }), 0x80 | 8],
112
+ ])
113
+ })
114
+ })
@@ -1,5 +1,6 @@
1
1
  import type { Hash, Hex } from "viem"
2
2
  import type { ArkivClient } from "../../clients/baseClient"
3
+ import type { Schema } from "../../schema"
3
4
  import type { TxParams } from "../../types"
4
5
  import { sendArkivTransaction } from "../../utils/arkivTransactions"
5
6
  import { getLogger } from "../../utils/logger"
@@ -16,18 +17,27 @@ const logger = getLogger("actions:wallet:execute-batch")
16
17
  *
17
18
  * At least one operation is required; a batch with nothing in it throws rather than spending gas on
18
19
  * an empty transaction.
20
+ *
21
+ * Each create and patch names its own schema, and is typed by it — a batch can write entities of
22
+ * several schemas at once.
23
+ *
24
+ * @typeParam TCreates - The schema of each create, in order. Inferred.
25
+ * @typeParam TPatches - The schema of each patch, in order. Inferred.
19
26
  */
20
- export type ExecuteBatchParameters = {
27
+ export type ExecuteBatchParameters<
28
+ TCreates extends readonly Schema[] = readonly Schema[],
29
+ TPatches extends readonly Schema[] = readonly Schema[],
30
+ > = {
21
31
  /** The entities to create. */
22
- creates?: CreateEntityParameters[]
32
+ creates?: { readonly [I in keyof TCreates]: CreateEntityParameters<TCreates[I]> } | undefined
23
33
  /** The patches to apply. */
24
- patches?: PatchEntityParameters[]
34
+ patches?: { readonly [I in keyof TPatches]: PatchEntityParameters<TPatches[I]> } | undefined
25
35
  /** The entities to delete. */
26
- deletes?: DeleteEntityParameters[]
36
+ deletes?: readonly DeleteEntityParameters[] | undefined
27
37
  /** The expiries to set. */
28
- extensions?: ExtendEntityParameters[]
38
+ extensions?: readonly ExtendEntityParameters[] | undefined
29
39
  /** The ownership transfers to perform. */
30
- ownershipChanges?: ChangeOwnershipParameters[]
40
+ ownershipChanges?: readonly ChangeOwnershipParameters[] | undefined
31
41
  }
32
42
 
33
43
  /** Return type for the executeBatch function. */
@@ -46,12 +56,19 @@ export type ExecuteBatchReturnType = {
46
56
  ownershipChanges: Hex[]
47
57
  }
48
58
 
49
- export async function executeBatch(
59
+ export async function executeBatch<
60
+ const TCreates extends readonly Schema[] = [],
61
+ const TPatches extends readonly Schema[] = [],
62
+ >(
50
63
  client: ArkivClient,
51
- data: ExecuteBatchParameters,
64
+ data: ExecuteBatchParameters<TCreates, TPatches>,
52
65
  txParams?: TxParams,
53
66
  ): Promise<ExecuteBatchReturnType> {
54
- const { receipt, createdEntityKeys } = await sendArkivTransaction(client, data, txParams)
67
+ const { receipt, createdEntityKeys } = await sendArkivTransaction(
68
+ client,
69
+ data as ExecuteBatchParameters,
70
+ txParams,
71
+ )
55
72
 
56
73
  logger("Receipt from executeBatch %o", receipt)
57
74
 
@@ -17,17 +17,18 @@ const logger = getLogger("actions:wallet:extend-entity")
17
17
  * `ExpirationTime.fromDays(30)` means "alive for thirty more days from now" whatever the entity's
18
18
  * remaining life happened to be.
19
19
  *
20
- * The engine rejects an extension that would not move the expiry later, so extending an entity by
21
- * less than it already has left is an error rather than a silent shortening.
20
+ * The engine rejects an extension that would move the expiry earlier, so extending an entity by
21
+ * less than it already has left is an error rather than a silent shortening. One that resolves to
22
+ * the current expiry changes nothing.
22
23
  *
23
24
  * @example
24
- * await client.extendEntity({ entityKey, expires: ExpirationTime.fromDays(30) })
25
- * await client.extendEntity({ entityKey, expires: ExpirationTime.atBlock(1_200_000n) })
25
+ * await client.extendEntity({ entityKey, expiration: ExpirationTime.fromDays(30) })
26
+ * await client.extendEntity({ entityKey, expiration: ExpirationTime.atBlock(1_200_000n) })
26
27
  */
27
28
  export type ExtendEntityParameters = {
28
29
  entityKey: Hex
29
30
  /** The entity's new lifetime, built with {@link ExpirationTime}. */
30
- expires: Expiry
31
+ expiration: Expiry
31
32
  }
32
33
 
33
34
  /**
@@ -37,9 +38,9 @@ export type ExtendEntityReturnType = {
37
38
  entityKey: Hex
38
39
  txHash: Hash
39
40
  /**
40
- * The block the entity is now expected to expire at, resolved against the block the transaction
41
- * was built on. Durations are approximate, so treat this as the estimate the operation was built
42
- * from rather than the block the engine settled on.
41
+ * The block the entity now expires at, as the engine recorded it — read from the
42
+ * `ExpiryExtended` event, so a duration is already resolved against the block the transaction
43
+ * landed in.
43
44
  */
44
45
  expiresAt: bigint
45
46
  }
@@ -1,90 +1,81 @@
1
1
  import type { Hash, Hex } from "viem"
2
- import type { AttributeInputs } from "../../attr"
3
2
  import type { ArkivClient } from "../../clients/baseClient"
4
- import type { MimeType, TxParams } from "../../types"
3
+ import type { OptionalColumnName, PatchEntityValues, Schema } from "../../schema"
4
+ import type { TxParams } from "../../types"
5
5
  import { sendArkivTransaction } from "../../utils/arkivTransactions"
6
6
  import { getLogger } from "../../utils/logger"
7
7
 
8
8
  const logger = getLogger("actions:wallet:patch-entity")
9
9
 
10
10
  /**
11
- * Parameters for patching an entity.
11
+ * What a patch changes.
12
12
  *
13
- * A patch names only what changes. Attributes it does not mention keep whatever value they have, and
14
- * the payload and content type stay as they are unless given — so a caller can change one field
15
- * without first reading the entity to learn what the others are, and without racing anyone else who
16
- * is changing a different field.
13
+ * A patch names only what changes. Columns it does not mention keep whatever value they have — so
14
+ * a caller can change one column without first reading the entity to learn what the others are,
15
+ * and without racing anyone else who is changing a different one.
17
16
  *
18
- * The entity's identity is fixed: a patch never moves the key, the owner, the creation flags or the
19
- * expiry. Use `extendEntity` for the expiry and `changeOwnership` for the owner.
20
- *
21
- * @example Change one attribute and remove another, leaving the payload alone:
22
- * await client.patchEntity({
23
- * entityKey,
24
- * set: { status: "published", level: i32(11) },
25
- * unset: ["draft"],
26
- * })
17
+ * @typeParam S - The schema the changes are typed by.
27
18
  *
28
- * @example Replace the contents without touching a single attribute:
29
- * await client.patchEntity({
30
- * entityKey,
31
- * payload: jsonToPayload({ hello: "again" }),
32
- * })
19
+ * @example
20
+ * { set: { title: "Cells, not entities", score: dec("4.2") }, unset: ["thumbnail"] }
33
21
  */
34
- export type PatchEntityParameters = {
35
- /** The entity to patch. */
36
- entityKey: Hex
22
+ export type PatchEntityChanges<S extends Schema = Schema> = {
37
23
  /**
38
- * Attributes to write, keyed by name. An existing attribute is overwritten — including with a
39
- * different type — and a name the entity does not carry yet is added.
24
+ * Columns to write, keyed by name. Each is written with the indexed flag its column declares,
25
+ * so writing a column whose indexing the schema has since changed changes it on the entity too.
26
+ * Defaults do not apply: a patch writes only what it names.
40
27
  *
41
- * Values are the tagged constructors from `@arkiv-network/sdk/attr` — `i32`, `u64`, `u256`,
42
- * `dec`, `str`, `addr`, `key`, `bytes32`, `bool` — or a bare `boolean`, `number`, `bigint` or
43
- * `string` where the type is unambiguous.
44
- *
45
- * @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
46
- * @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
28
+ * @throws {SchemaMismatchError} If a name is not a column, or a value is not of its column's type.
47
29
  */
48
- set?: AttributeInputs | undefined
30
+ set?: PatchEntityValues<S> | undefined
49
31
  /**
50
- * Attribute names to remove. Naming one the entity does not carry is not an error — the outcome
51
- * is the same either way, and a caller clearing a field should not have to know whether it was
32
+ * Optional columns to remove. Naming one the entity does not carry is not an error — the outcome
33
+ * is the same either way, and a caller clearing a column should not have to know whether it was
52
34
  * ever set.
53
35
  *
54
36
  * @throws {ConflictingMutationError} If a name also appears in `set`.
55
37
  */
56
- unset?: readonly string[] | undefined
57
- /**
58
- * The entity's new payload. Omit it to leave the current contents untouched; pass an empty
59
- * `Uint8Array` to replace them with nothing.
60
- */
61
- payload?: Uint8Array | undefined
62
- /**
63
- * The payload's new MIME type. Omit it to keep the current one — which is what you want when the
64
- * new payload is the same kind of thing as the old.
65
- */
66
- contentType?: MimeType | (string & {}) | undefined
38
+ unset?: readonly OptionalColumnName<S>[] | undefined
39
+ }
40
+
41
+ /**
42
+ * One patch, as a batch carries it: the schema, the entity, and the {@link PatchEntityChanges}.
43
+ *
44
+ * The entity's identity is fixed: a patch never moves the key, the owner, the creation flags or the
45
+ * expiry. Use `extendEntity` for the expiry and `changeOwnership` for the owner.
46
+ */
47
+ export type PatchEntityParameters<S extends Schema = Schema> = PatchEntityChanges<S> & {
48
+ /** The schema the changes are typed by. */
49
+ schema: S
50
+ /** The entity to patch. */
51
+ entityKey: Hex
67
52
  }
68
53
 
69
- /** The result of patching an entity. */
54
+ /** The result of a patch. */
70
55
  export type PatchEntityReturnType = {
71
56
  /** The patched entity's key — unchanged, since a patch never moves it. */
72
57
  entityKey: Hex
73
58
  txHash: Hash
74
59
  }
75
60
 
76
- export async function patchEntity(
61
+ export async function patchEntity<S extends Schema>(
77
62
  client: ArkivClient,
78
- data: PatchEntityParameters,
63
+ schema: S,
64
+ entityKey: Hex,
65
+ changes: PatchEntityChanges<S>,
79
66
  txParams?: TxParams,
80
67
  ): Promise<PatchEntityReturnType> {
81
- logger("patchEntity %o", data)
82
- const { receipt } = await sendArkivTransaction(client, { patches: [data] }, txParams)
68
+ logger("patchEntity %s %o", entityKey, changes)
69
+ const { receipt } = await sendArkivTransaction(
70
+ client,
71
+ { patches: [{ ...changes, schema, entityKey }] },
72
+ txParams,
73
+ )
83
74
 
84
75
  logger("Receipt from patchEntity %o", receipt)
85
76
 
86
77
  return {
87
78
  txHash: receipt.transactionHash as Hash,
88
- entityKey: data.entityKey,
79
+ entityKey,
89
80
  }
90
81
  }
@@ -22,8 +22,6 @@ test("creates", () => {
22
22
  expect(typeof client.select).toBe("function")
23
23
  expect(client.query).toBeDefined()
24
24
  expect(typeof client.query).toBe("function")
25
- expect(client.getEntityCount).toBeDefined()
26
- expect(typeof client.getEntityCount).toBe("function")
27
25
  expect(client.getBlockTiming).toBeDefined()
28
26
  expect(typeof client.getBlockTiming).toBe("function")
29
27
  expect(client.watchEntityEvents).toBeDefined()
@@ -37,3 +35,13 @@ test("creates", () => {
37
35
  expect(client.changeOwnership).not.toBeDefined()
38
36
  expect(client.executeBatch).not.toBeDefined()
39
37
  })
38
+
39
+ test("select() rejects a $ name that is not an entity field, at compile time and at runtime", () => {
40
+ const client = createPublicClient({ transport: mockTransport })
41
+ // @ts-expect-error - $title is not an entity field; an attribute name cannot start with $
42
+ expect(() => client.select({ $title: true })).toThrow(/not an entity field/)
43
+ // @ts-expect-error - $attributes reads every attribute; select the ones you want by name
44
+ expect(() => client.select({ $attributes: { title: true } })).toThrow(/select each by name/)
45
+ // Without the $, `owner` is an attribute like any other, read alongside the entity field.
46
+ expect(() => client.select({ $owner: true, owner: true })).not.toThrow()
47
+ })
@@ -28,7 +28,10 @@ export type WalletArkivClient<
28
28
  *
29
29
  * A Wallet Client signs and sends transactions, so it carries an `account` and exposes everything
30
30
  * that mutates state — `createEntity`, `patchEntity`, `deleteEntity`, `extendEntity`,
31
- * `changeOwnership` and `executeBatch` — on top of the read actions a Public Client has.
31
+ * `changeOwnership` and `executeBatch` — alongside the viem actions those need, such as
32
+ * `getBlockNumber`, `readContract` and `waitForTransactionReceipt`. It does not have the Arkiv
33
+ * read actions (`getEntity`, `query`, `select`, `watchEntityEvents`, `predictEntityKeys`); read
34
+ * through a Public Client from `createPublicClient` for those.
32
35
  *
33
36
  * @param parameters - Configuration object for the wallet client (chain, transport, account, etc.)
34
37
  * @returns An Arkiv Wallet Client. {@link WalletArkivClient}