@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
@@ -11,22 +11,29 @@ import {
11
11
  parseAbiParameters,
12
12
  toHex,
13
13
  } from "viem"
14
+ import type { ArkivClient } from "../clients/baseClient"
15
+ import { ARKIV_ADDRESS } from "../consts"
16
+ import { InvalidExpiryError, NO_SALT } from "../entity"
17
+ import { ENTITY_EVENTS_ABI, type EntityEventName } from "../entity/events"
18
+ import { MAX_EXPIRES_AT, OperationType } from "../entity/params"
19
+ import { EmptyPatchError, EntityMutationError } from "../errors"
14
20
  import {
15
- type AttributeInputs,
21
+ addr,
22
+ bytes,
16
23
  ConflictingMutationError,
17
24
  dec,
25
+ defineSchema,
26
+ EntityTooLargeError,
18
27
  InvalidAttributeNameError,
19
28
  InvalidValueError,
20
29
  i32,
30
+ MAX_BYTES_LENGTH,
31
+ MAX_ENTITY_BYTES,
21
32
  MissingValueError,
33
+ SchemaMismatchError,
34
+ str,
22
35
  u256,
23
- } from "../attr"
24
- import type { ArkivClient } from "../clients/baseClient"
25
- import { ARKIV_ADDRESS } from "../consts"
26
- import { InvalidExpiryError, NO_SALT } from "../entity"
27
- import { ENTITY_EVENTS_ABI, type EntityEventName } from "../entity/events"
28
- import { MAX_EXPIRES_AT, OperationType } from "../entity/params"
29
- import { EmptyPatchError, EntityMutationError } from "../errors"
36
+ } from "../schema"
30
37
  import { sendArkivTransaction } from "./arkivTransactions"
31
38
  import { ExpirationTime } from "./expirationTime"
32
39
 
@@ -151,13 +158,22 @@ function sentExtension(writeContract: ReturnType<typeof vi.fn>) {
151
158
  return decodeAbiParameters(EXTEND_PARAMS, op.operationData)[0]
152
159
  }
153
160
 
154
- const CONTENTS = {
155
- payload: new Uint8Array([1, 2, 3]),
156
- contentType: "application/octet-stream",
157
- }
161
+ /** The typeId bit that asks the node to index an attribute. */
162
+ const INDEXED = 0x80
163
+
164
+ const Docs = defineSchema({
165
+ category: str().indexed(),
166
+ level: i32().indexed().optional(),
167
+ title: str().optional(),
168
+ })
169
+
170
+ /** A minimal valid create's schema and values, without its expiry. */
171
+ const CONTENTS = { schema: Docs, values: { category: "docs" } }
158
172
 
159
173
  const DAY = ExpirationTime.fromDays(1) // 86400s -> 43200 blocks at 2s
160
- const validCreate = { ...CONTENTS, expires: DAY }
174
+ const validCreate = { ...CONTENTS, expiration: DAY }
175
+
176
+ const name = (n: string) => toHex(n, { size: 32 })
161
177
 
162
178
  describe("create expiry", () => {
163
179
  it("sends a create for a valid expiry", async () => {
@@ -173,7 +189,7 @@ describe("create expiry", () => {
173
189
  creates: [
174
190
  {
175
191
  ...CONTENTS,
176
- expires: ExpirationTime.atBlock(5_000n, { atLeast: ExpirationTime.fromSeconds(200) }),
192
+ expiration: ExpirationTime.atBlock(5_000n, { atLeast: ExpirationTime.fromSeconds(200) }),
177
193
  },
178
194
  ],
179
195
  })
@@ -185,19 +201,21 @@ describe("create expiry", () => {
185
201
  it("reports the block the entity will actually expire at", async () => {
186
202
  const { client } = makeClient()
187
203
  const relative = await sendArkivTransaction(client, {
188
- creates: [{ ...CONTENTS, expires: DAY }],
204
+ creates: [{ ...CONTENTS, expiration: DAY }],
189
205
  })
190
206
  expect(relative.createdExpiries[0]).toBe(CURRENT_BLOCK + 43_200n)
191
207
 
192
208
  const { client: client2 } = makeClient()
193
209
  const absolute = await sendArkivTransaction(client2, {
194
- creates: [{ ...CONTENTS, expires: ExpirationTime.atBlock(5_000n) }],
210
+ creates: [{ ...CONTENTS, expiration: ExpirationTime.atBlock(5_000n) }],
195
211
  })
196
212
  expect(absolute.createdExpiries[0]).toBe(5_000n)
197
213
 
198
214
  const { client: client3 } = makeClient()
199
215
  const dated = await sendArkivTransaction(client3, {
200
- creates: [{ ...CONTENTS, expires: ExpirationTime.atDate(new Date(Date.now() + 3600_000)) }],
216
+ creates: [
217
+ { ...CONTENTS, expiration: ExpirationTime.atDate(new Date(Date.now() + 3600_000)) },
218
+ ],
201
219
  })
202
220
  // An hour at 2s/block, counted from the block the transaction was built on.
203
221
  expect(dated.createdExpiries[0]).toBe(CURRENT_BLOCK + 1800n)
@@ -214,7 +232,7 @@ describe("create expiry", () => {
214
232
  const { client, writeContract } = makeClient()
215
233
  await expect(
216
234
  sendArkivTransaction(client, {
217
- creates: [{ ...CONTENTS, expires: ExpirationTime.atBlock(CURRENT_BLOCK - 1n) }],
235
+ creates: [{ ...CONTENTS, expiration: ExpirationTime.atBlock(CURRENT_BLOCK - 1n) }],
218
236
  }),
219
237
  ).rejects.toThrow(/dead on arrival/)
220
238
  expect(writeContract).not.toHaveBeenCalled()
@@ -233,7 +251,7 @@ describe("extend expiry", () => {
233
251
  it("resolves an extension exactly like a create, both forms", async () => {
234
252
  const { client, writeContract } = makeClient()
235
253
  const relative = await sendArkivTransaction(client, {
236
- extensions: [{ entityKey: ENTITY_KEY, expires: DAY }],
254
+ extensions: [{ entityKey: ENTITY_KEY, expiration: DAY }],
237
255
  })
238
256
  const extension = sentExtension(writeContract)
239
257
  expect(extension.entityKey).toBe(ENTITY_KEY)
@@ -244,7 +262,7 @@ describe("extend expiry", () => {
244
262
 
245
263
  const { client: client2, writeContract: writeContract2 } = makeClient()
246
264
  const absolute = await sendArkivTransaction(client2, {
247
- extensions: [{ entityKey: ENTITY_KEY, expires: ExpirationTime.atBlock(5_000n) }],
265
+ extensions: [{ entityKey: ENTITY_KEY, expiration: ExpirationTime.atBlock(5_000n) }],
248
266
  })
249
267
  expect(sentExtension(writeContract2).expiresAt).toBe(5_000n)
250
268
  expect(absolute.extendedExpiries[0]).toBe(5_000n)
@@ -254,7 +272,7 @@ describe("extend expiry", () => {
254
272
  const { client, writeContract } = makeClient()
255
273
  const tooFar = ExpirationTime.atBlock(MAX_EXPIRES_AT + 1n)
256
274
  await expect(
257
- sendArkivTransaction(client, { extensions: [{ entityKey: ENTITY_KEY, expires: tooFar }] }),
275
+ sendArkivTransaction(client, { extensions: [{ entityKey: ENTITY_KEY, expiration: tooFar }] }),
258
276
  ).rejects.toThrow(/does not fit the uint64/)
259
277
  expect(writeContract).not.toHaveBeenCalled()
260
278
  })
@@ -270,7 +288,10 @@ describe("extend expiry", () => {
270
288
  const { client, writeContract } = makeClient()
271
289
  await sendArkivTransaction(client, {
272
290
  extensions: [
273
- { entityKey: ENTITY_KEY, expires: ExpirationTime.atDate(new Date(Date.now() + 3600_000)) },
291
+ {
292
+ entityKey: ENTITY_KEY,
293
+ expiration: ExpirationTime.atDate(new Date(Date.now() + 3600_000)),
294
+ },
274
295
  ],
275
296
  })
276
297
  // A Date can only be placed once the current block is known, so it must have been fetched even
@@ -401,107 +422,248 @@ describe("entity keys", () => {
401
422
  })
402
423
  })
403
424
 
404
- describe("attribute typing through the write path", () => {
425
+ describe("create through a schema", () => {
426
+ /** The create's attributes, keyed by their bytes32 name. */
427
+ function sentCells(writeContract: ReturnType<typeof vi.fn>) {
428
+ return Object.fromEntries(sentCreate(writeContract).attributes.map((a) => [a.name, a]))
429
+ }
430
+
405
431
  it("rejects a value that does not fit the type it defaults to", async () => {
406
432
  const { client } = makeClient()
433
+ const schema = defineSchema({ n: i32() })
407
434
  await expect(
408
- sendArkivTransaction(client, { creates: [{ ...validCreate, attributes: { n: 1.5 } }] }),
435
+ sendArkivTransaction(client, { creates: [{ schema, values: { n: 1.5 }, expiration: DAY }] }),
409
436
  ).rejects.toThrow(InvalidValueError)
410
437
  })
411
438
 
412
- it("rejects an attribute name outside the grammar", async () => {
439
+ it("rejects a column name outside the grammar before anything is written", () => {
440
+ expect(() => defineSchema({ "1bad": i32() })).toThrow(InvalidAttributeNameError)
441
+ })
442
+
443
+ it("rejects a null value, rather than writing a typeless one", async () => {
444
+ const { client, writeContract } = makeClient()
445
+ await expect(
446
+ sendArkivTransaction(client, {
447
+ creates: [{ ...validCreate, values: { category: "docs", title: null as never } }],
448
+ }),
449
+ ).rejects.toThrow(MissingValueError)
450
+ expect(writeContract).not.toHaveBeenCalled()
451
+ })
452
+
453
+ it("rejects a create that leaves out a required column", async () => {
454
+ const { client, writeContract } = makeClient()
455
+ await expect(
456
+ sendArkivTransaction(client, { creates: [{ ...validCreate, values: {} }] }),
457
+ ).rejects.toThrow(SchemaMismatchError)
458
+ // Explicitly undefined is the same as left out.
459
+ await expect(
460
+ sendArkivTransaction(client, {
461
+ creates: [{ ...validCreate, values: { category: undefined } as never }],
462
+ }),
463
+ ).rejects.toThrow(/required str column/)
464
+ expect(writeContract).not.toHaveBeenCalled()
465
+ })
466
+
467
+ it("checks the schema before any network call, so a transport failure cannot mask it", async () => {
413
468
  const { client } = makeClient()
469
+ const getBlockNumber = (client as unknown as { getBlockNumber: ReturnType<typeof vi.fn> })
470
+ .getBlockNumber
471
+ getBlockNumber.mockRejectedValue(new Error("fetch failed"))
414
472
  await expect(
415
- sendArkivTransaction(client, { creates: [{ ...validCreate, attributes: { "1bad": 1 } }] }),
416
- ).rejects.toThrow(InvalidAttributeNameError)
473
+ sendArkivTransaction(client, { creates: [{ ...validCreate, values: {} }] }),
474
+ ).rejects.toThrow(SchemaMismatchError)
475
+ expect(getBlockNumber).not.toHaveBeenCalled()
417
476
  })
418
477
 
419
- it("rejects an attribute whose value is absent, rather than writing a typeless one", async () => {
478
+ it("rejects a column the schema does not have", async () => {
420
479
  const { client } = makeClient()
480
+ const failure = await sendArkivTransaction(client, {
481
+ creates: [{ ...validCreate, values: { category: "docs", nope: 1 } }],
482
+ }).catch((error: Error) => error)
483
+ expect(failure).toBeInstanceOf(SchemaMismatchError)
484
+ expect((failure as SchemaMismatchError).columnName).toBe("nope")
485
+ })
486
+
487
+ it("rejects a value whose type is not its column's", async () => {
488
+ const { client } = makeClient()
489
+ await expect(
490
+ sendArkivTransaction(client, { creates: [{ ...validCreate, values: { category: i32(1) } }] }),
491
+ ).rejects.toThrow(SchemaMismatchError)
492
+ // A bare string is a str, never coerced into the column's type.
493
+ const schema = defineSchema({ score: dec() })
421
494
  await expect(
422
495
  sendArkivTransaction(client, {
423
- creates: [
424
- {
425
- ...validCreate,
426
- // What a spread of an optional-shaped object produces at runtime.
427
- attributes: { category: "docs", score: undefined } as unknown as AttributeInputs,
428
- },
429
- ],
496
+ creates: [{ schema, values: { score: "4.1" }, expiration: DAY }],
430
497
  }),
431
- ).rejects.toThrow(MissingValueError)
498
+ ).rejects.toThrow(/a dec column, but the value is a str/)
432
499
  })
433
500
 
434
- it("carries only the system cells when there are no user attributes", async () => {
501
+ it("lets an optional column be left out entirely", async () => {
435
502
  const { client, writeContract } = makeClient()
436
503
  await sendArkivTransaction(client, { creates: [validCreate] })
437
- expect(sentCreate(writeContract).attributes.map((a) => a.name)).toEqual([
438
- toHex("$contentType", { size: 32 }),
439
- toHex("$payload", { size: 32 }),
440
- ])
504
+ expect(sentCreate(writeContract).attributes.map((a) => a.name)).toEqual([name("category")])
441
505
  })
442
506
 
443
- it("writes an empty payload rather than dropping it", async () => {
507
+ it("writes a defaulted column's default when the create leaves it out", async () => {
508
+ const schema = defineSchema({ category: str(), mimeType: str().default("text/markdown") })
444
509
  const { client, writeContract } = makeClient()
445
510
  await sendArkivTransaction(client, {
446
- creates: [{ ...validCreate, payload: new Uint8Array() }],
511
+ creates: [{ schema, values: { category: "docs" }, expiration: DAY }],
512
+ })
513
+ expect(sentCells(writeContract)[name("mimeType")]).toMatchObject({
514
+ typeId: 8,
515
+ value: toHex("text/markdown"),
516
+ })
517
+
518
+ // ...and the value given, when it is given.
519
+ const { client: c2, writeContract: w2 } = makeClient()
520
+ await sendArkivTransaction(c2, {
521
+ creates: [{ schema, values: { category: "docs", mimeType: "text/plain" }, expiration: DAY }],
447
522
  })
448
- const byName = Object.fromEntries(sentCreate(writeContract).attributes.map((a) => [a.name, a]))
449
- expect(byName[toHex("$payload", { size: 32 })]).toMatchObject({ typeId: 7, value: "0x" })
523
+ expect(sentCells(w2)[name("mimeType")].value).toBe(toHex("text/plain"))
450
524
  })
451
525
 
452
- it("carries the protocol typeId of whichever type the value names", async () => {
526
+ it("flags indexed columns in bit 7 of the typeId, and leaves the rest unindexed", async () => {
527
+ const schema = defineSchema({
528
+ count: i32().indexed(),
529
+ balance: u256().indexed(),
530
+ score: dec().indexed(),
531
+ author: addr(),
532
+ title: str(),
533
+ })
453
534
  const { client, writeContract } = makeClient()
454
535
  await sendArkivTransaction(client, {
455
536
  creates: [
456
- { ...validCreate, attributes: { count: 42, balance: u256(1n), score: dec("3.5") } },
537
+ {
538
+ schema,
539
+ values: {
540
+ count: 42,
541
+ balance: u256(1n),
542
+ score: dec("3.5"),
543
+ author: addr("0x1111111111111111111111111111111111111111"),
544
+ title: "Cells",
545
+ },
546
+ expiration: DAY,
547
+ },
457
548
  ],
458
549
  })
459
- const byName = Object.fromEntries(
550
+ const typeIds = Object.fromEntries(
460
551
  sentCreate(writeContract).attributes.map((a) => [a.name, a.typeId]),
461
552
  )
462
- expect(byName[toHex("count", { size: 32 })]).toBe(2)
463
- expect(byName[toHex("balance", { size: 32 })]).toBe(4)
464
- expect(byName[toHex("score", { size: 32 })]).toBe(5)
553
+ expect(typeIds[name("count")]).toBe(INDEXED | 2)
554
+ expect(typeIds[name("balance")]).toBe(INDEXED | 4)
555
+ expect(typeIds[name("score")]).toBe(INDEXED | 5)
556
+ expect(typeIds[name("author")]).toBe(9)
557
+ expect(typeIds[name("title")]).toBe(8)
465
558
  })
466
559
 
467
- it("sends the payload and content type as system cells", async () => {
560
+ it("writes any number of bytes columns, unindexed, from bare Uint8Arrays or bytes()", async () => {
561
+ const schema = defineSchema({ body: bytes(), thumbnail: bytes(), empty: bytes() })
468
562
  const { client, writeContract } = makeClient()
469
563
  await sendArkivTransaction(client, {
470
564
  creates: [
471
565
  {
472
- ...validCreate,
473
- payload: new Uint8Array([1, 2, 3]),
474
- contentType: "application/json",
475
- attributes: { level: i32(1) },
566
+ schema,
567
+ values: {
568
+ body: new Uint8Array([1, 2, 3]),
569
+ thumbnail: bytes("0xDEAD"),
570
+ empty: new Uint8Array(),
571
+ },
572
+ expiration: DAY,
476
573
  },
477
574
  ],
478
575
  })
479
- const byName = Object.fromEntries(sentCreate(writeContract).attributes.map((a) => [a.name, a]))
480
- expect(byName[toHex("$payload", { size: 32 })]).toMatchObject({ typeId: 7, value: "0x010203" })
481
- expect(byName[toHex("$contentType", { size: 32 })].typeId).toBe(8)
576
+ const cells = sentCells(writeContract)
577
+ expect(cells[name("body")]).toMatchObject({ typeId: 7, value: "0x010203" })
578
+ expect(cells[name("thumbnail")]).toMatchObject({ typeId: 7, value: "0xdead" })
579
+ // Empty is a value, not an absence: it is written rather than dropped.
580
+ expect(cells[name("empty")]).toMatchObject({ typeId: 7, value: "0x" })
482
581
  })
483
582
 
484
- it("rejects a content type that is not a MIME token", async () => {
485
- const { client } = makeClient()
583
+ it("caps each bytes value at 128 KiB", async () => {
584
+ const schema = defineSchema({ body: bytes(), thumbnail: bytes().optional() })
585
+ const { client, writeContract } = makeClient()
586
+ await sendArkivTransaction(client, {
587
+ creates: [{ schema, values: { body: new Uint8Array(MAX_BYTES_LENGTH) }, expiration: DAY }],
588
+ })
589
+ expect(writeContract).toHaveBeenCalledTimes(1)
590
+
591
+ const { client: c2 } = makeClient()
486
592
  await expect(
487
- sendArkivTransaction(client, { creates: [{ ...validCreate, contentType: "Not A Mime" }] }),
488
- ).rejects.toThrow(/Invalid content type/)
593
+ sendArkivTransaction(c2, {
594
+ creates: [
595
+ { schema, values: { body: new Uint8Array(MAX_BYTES_LENGTH + 1) }, expiration: DAY },
596
+ ],
597
+ }),
598
+ ).rejects.toThrow(InvalidValueError)
599
+ })
600
+
601
+ it("caps the bytes of one create at 128 KiB in total", async () => {
602
+ const schema = defineSchema({ body: bytes(), thumbnail: bytes() })
603
+ const half = new Uint8Array(MAX_ENTITY_BYTES / 2)
604
+ const { client, writeContract } = makeClient()
605
+ await sendArkivTransaction(client, {
606
+ creates: [{ schema, values: { body: half, thumbnail: half }, expiration: DAY }],
607
+ })
608
+ expect(writeContract).toHaveBeenCalledTimes(1)
609
+
610
+ const { client: c2, writeContract: w2 } = makeClient()
611
+ await expect(
612
+ sendArkivTransaction(c2, {
613
+ creates: [
614
+ {
615
+ schema,
616
+ values: { body: half, thumbnail: new Uint8Array(MAX_ENTITY_BYTES / 2 + 1) },
617
+ expiration: DAY,
618
+ },
619
+ ],
620
+ }),
621
+ ).rejects.toThrow(EntityTooLargeError)
622
+ expect(w2).not.toHaveBeenCalled()
623
+ })
624
+
625
+ it("carries no cells for a schema with no columns", async () => {
626
+ const { client, writeContract } = makeClient()
627
+ await sendArkivTransaction(client, {
628
+ creates: [{ schema: defineSchema({}), values: {}, expiration: DAY }],
629
+ })
630
+ expect(sentCreate(writeContract).attributes).toEqual([])
489
631
  })
490
632
 
491
633
  it("sorts attributes ascending by their bytes32 name", async () => {
634
+ const schema = defineSchema({ tag: str(), status: str().indexed(), score: i32() })
492
635
  const { client, writeContract } = makeClient()
493
636
  await sendArkivTransaction(client, {
494
- creates: [{ ...validCreate, attributes: { tag: "zz", status: "active", score: 30 } }],
495
- })
496
- const names = sentCreate(writeContract).attributes.map((a) => a.name)
497
- expect(names).toEqual([...names].sort())
498
- // The "$" system cells lead; the user attributes follow in name order.
499
- expect(names.slice(2)).toEqual([
500
- toHex("score", { size: 32 }),
501
- toHex("status", { size: 32 }),
502
- toHex("tag", { size: 32 }),
637
+ creates: [{ schema, values: { tag: "zz", status: "active", score: 30 }, expiration: DAY }],
638
+ })
639
+ expect(sentCreate(writeContract).attributes.map((a) => a.name)).toEqual([
640
+ name("score"),
641
+ name("status"),
642
+ name("tag"),
503
643
  ])
504
644
  })
645
+
646
+ it("writes each create of a batch through its own schema", async () => {
647
+ const Files = defineSchema({ path: str().indexed(), body: bytes() })
648
+ const { client, writeContract } = makeClient()
649
+ const result = await sendArkivTransaction(client, {
650
+ creates: [
651
+ validCreate,
652
+ { schema: Files, values: { path: "/a", body: new Uint8Array([7]) }, expiration: DAY },
653
+ ],
654
+ })
655
+ const [docs, files] = sentOperations(writeContract).map(
656
+ (op) => decodeAbiParameters(CREATE_PARAMS, op.operationData)[0],
657
+ )
658
+ expect(docs.attributes.map((a) => [a.name, a.typeId])).toEqual([
659
+ [name("category"), INDEXED | 8],
660
+ ])
661
+ expect(files.attributes.map((a) => [a.name, a.typeId])).toEqual([
662
+ [name("body"), 7],
663
+ [name("path"), INDEXED | 8],
664
+ ])
665
+ expect(result.createdEntityKeys).toEqual([mintedKey(0), mintedKey(1)])
666
+ })
505
667
  })
506
668
 
507
669
  describe("patch", () => {
@@ -512,64 +674,145 @@ describe("patch", () => {
512
674
  return decodeAbiParameters(PATCH_PARAMS, op.operationData)[0]
513
675
  }
514
676
 
515
- it("carries only what the patch named", async () => {
677
+ it("carries only what the patch named, flagged as its column declares", async () => {
516
678
  const { client, writeContract } = makeClient()
517
679
  await sendArkivTransaction(client, {
518
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(11) } }],
680
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(11) } }],
519
681
  })
520
682
  const patch = sentPatch(writeContract)
521
683
  expect(patch.entityKey).toBe(ENTITY_KEY)
522
- // No $payload, no $contentType, no tombstones: everything unnamed is left alone.
684
+ // No tombstones, nothing unnamed: everything else is left alone.
523
685
  expect(patch.mutations).toEqual([
524
- { name: toHex("level", { size: 32 }), typeId: 2, value: expect.any(String) },
686
+ { name: name("level"), typeId: INDEXED | 2, value: expect.any(String) },
687
+ ])
688
+ })
689
+
690
+ it("flips a column's indexing when the schema writing it says so", async () => {
691
+ const Indexed = defineSchema({ title: str().indexed() })
692
+ const { client, writeContract } = makeClient()
693
+ await sendArkivTransaction(client, {
694
+ patches: [{ schema: Indexed, entityKey: ENTITY_KEY, set: { title: "x" } }],
695
+ })
696
+ expect(sentPatch(writeContract).mutations[0].typeId).toBe(INDEXED | 8)
697
+
698
+ const { client: c2, writeContract: w2 } = makeClient()
699
+ await sendArkivTransaction(c2, {
700
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { title: "x" } }],
701
+ })
702
+ expect(sentPatch(w2).mutations[0].typeId).toBe(8)
703
+ })
704
+
705
+ it("does not apply defaults — a patch writes only what it names", async () => {
706
+ const schema = defineSchema({ category: str(), mimeType: str().default("text/markdown") })
707
+ const { client, writeContract } = makeClient()
708
+ await sendArkivTransaction(client, {
709
+ patches: [{ schema, entityKey: ENTITY_KEY, set: { category: "x" } }],
710
+ })
711
+ expect(sentPatch(writeContract).mutations.map((m) => m.name)).toEqual([name("category")])
712
+ })
713
+
714
+ it("tombstones an unset optional column with typeId 0 and an empty value", async () => {
715
+ const { client, writeContract } = makeClient()
716
+ await sendArkivTransaction(client, {
717
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, unset: ["title"] }],
718
+ })
719
+ expect(sentPatch(writeContract).mutations).toEqual([
720
+ { name: name("title"), typeId: 0, value: "0x" },
525
721
  ])
526
722
  })
527
723
 
528
- it("writes the system cells only when the patch gives them", async () => {
724
+ it("refuses to unset a required column", async () => {
725
+ const { client, writeContract } = makeClient()
726
+ await expect(
727
+ sendArkivTransaction(client, {
728
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, unset: ["category" as never] }],
729
+ }),
730
+ ).rejects.toThrow(SchemaMismatchError)
731
+ expect(writeContract).not.toHaveBeenCalled()
732
+ })
733
+
734
+ it("rejects a set of a column the schema does not have, or of the wrong type", async () => {
735
+ const { client } = makeClient()
736
+ await expect(
737
+ sendArkivTransaction(client, {
738
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { nope: 1 } as never }],
739
+ }),
740
+ ).rejects.toThrow(SchemaMismatchError)
741
+ await expect(
742
+ sendArkivTransaction(client, {
743
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: "high" } as never }],
744
+ }),
745
+ ).rejects.toThrow(/i32 column, but the value is a str/)
746
+ })
747
+
748
+ it("leaves out a column set to undefined, as a create does", async () => {
529
749
  const { client, writeContract } = makeClient()
530
750
  await sendArkivTransaction(client, {
531
- patches: [{ entityKey: ENTITY_KEY, payload: new Uint8Array([9]) }],
751
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: undefined, title: "x" } }],
532
752
  })
533
- const names = sentPatch(writeContract).mutations.map((m) => m.name)
534
- expect(names).toEqual([toHex("$payload", { size: 32 })])
753
+ expect(sentPatch(writeContract).mutations.map((m) => m.name)).toEqual([name("title")])
754
+ })
755
+
756
+ it("treats a patch of nothing but undefined values as empty", async () => {
757
+ const { client } = makeClient()
758
+ await expect(
759
+ sendArkivTransaction(client, {
760
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: undefined } }],
761
+ }),
762
+ ).rejects.toThrow(EmptyPatchError)
535
763
  })
536
764
 
537
- it("tombstones an unset attribute with typeId 0 and an empty value", async () => {
765
+ it("does not count an undefined set as conflicting with an unset", async () => {
538
766
  const { client, writeContract } = makeClient()
539
767
  await sendArkivTransaction(client, {
540
- patches: [{ entityKey: ENTITY_KEY, unset: ["draft"] }],
768
+ patches: [
769
+ { schema: Docs, entityKey: ENTITY_KEY, set: { title: undefined }, unset: ["title"] },
770
+ ],
541
771
  })
542
772
  expect(sentPatch(writeContract).mutations).toEqual([
543
- { name: toHex("draft", { size: 32 }), typeId: 0, value: "0x" },
773
+ { name: name("title"), typeId: 0, value: "0x" },
544
774
  ])
545
775
  })
546
776
 
777
+ it("still rejects null, which is not 'left out'", async () => {
778
+ const { client } = makeClient()
779
+ await expect(
780
+ sendArkivTransaction(client, {
781
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: null } as never }],
782
+ }),
783
+ ).rejects.toThrow(MissingValueError)
784
+ })
785
+
547
786
  it("sorts writes and tombstones together, not each half on its own", async () => {
787
+ const schema = defineSchema({
788
+ zeta: str(),
789
+ beta: str(),
790
+ alpha: str().optional(),
791
+ gamma: str().optional(),
792
+ })
548
793
  const { client, writeContract } = makeClient()
549
794
  await sendArkivTransaction(client, {
550
795
  patches: [
551
796
  {
797
+ schema,
552
798
  entityKey: ENTITY_KEY,
553
799
  set: { zeta: "z", beta: "b" },
554
800
  unset: ["alpha", "gamma"],
555
- payload: new Uint8Array([1]),
556
801
  },
557
802
  ],
558
803
  })
559
- const names = sentPatch(writeContract).mutations.map((m) => m.name)
560
- expect(names).toEqual([
561
- toHex("$payload", { size: 32 }),
562
- toHex("alpha", { size: 32 }),
563
- toHex("beta", { size: 32 }),
564
- toHex("gamma", { size: 32 }),
565
- toHex("zeta", { size: 32 }),
804
+ expect(sentPatch(writeContract).mutations.map((m) => m.name)).toEqual([
805
+ name("alpha"),
806
+ name("beta"),
807
+ name("gamma"),
808
+ name("zeta"),
566
809
  ])
567
810
  })
568
811
 
569
812
  it("collapses a name repeated in unset, since it is the same intent twice", async () => {
570
813
  const { client, writeContract } = makeClient()
571
814
  await sendArkivTransaction(client, {
572
- patches: [{ entityKey: ENTITY_KEY, unset: ["draft", "draft"] }],
815
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, unset: ["title", "title"] }],
573
816
  })
574
817
  // Two identical tombstones would break the engine's strict-ascending uniqueness rule.
575
818
  expect(sentPatch(writeContract).mutations).toHaveLength(1)
@@ -579,7 +822,9 @@ describe("patch", () => {
579
822
  const { client, writeContract } = makeClient()
580
823
  await expect(
581
824
  sendArkivTransaction(client, {
582
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) }, unset: ["level"] }],
825
+ patches: [
826
+ { schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) }, unset: ["level"] },
827
+ ],
583
828
  }),
584
829
  ).rejects.toThrow(ConflictingMutationError)
585
830
  expect(writeContract).not.toHaveBeenCalled()
@@ -588,47 +833,42 @@ describe("patch", () => {
588
833
  it("rejects a patch with nothing to apply", async () => {
589
834
  const { client, writeContract } = makeClient()
590
835
  await expect(
591
- sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY }] }),
836
+ sendArkivTransaction(client, { patches: [{ schema: Docs, entityKey: ENTITY_KEY }] }),
592
837
  ).rejects.toThrow(EmptyPatchError)
593
838
  // Present but empty is just as empty.
594
- await expect(
595
- sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY, set: {}, unset: [] }] }),
596
- ).rejects.toThrow(EmptyPatchError)
597
- expect(writeContract).not.toHaveBeenCalled()
598
- })
599
-
600
- it("still validates a content type when one is given", async () => {
601
- const { client } = makeClient()
602
839
  await expect(
603
840
  sendArkivTransaction(client, {
604
- patches: [{ entityKey: ENTITY_KEY, payload: new Uint8Array(), contentType: "Not A Mime" }],
841
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: {}, unset: [] }],
605
842
  }),
606
- ).rejects.toThrow(/Invalid content type/)
843
+ ).rejects.toThrow(EmptyPatchError)
844
+ expect(writeContract).not.toHaveBeenCalled()
607
845
  })
608
846
 
609
847
  it("rejects a bare string as unset, rather than tombstoning its letters", async () => {
610
848
  const { client, writeContract } = makeClient()
611
- // A Set is built from any iterable, so "draft" would become tombstones for d, r, a, f and t —
612
- // five valid attribute names, none of them the one meant, and the real "draft" left untouched.
849
+ // A Set is built from any iterable, so "title" would become tombstones for t, i, t, l and e —
850
+ // and the real "title" left untouched.
613
851
  await expect(
614
852
  sendArkivTransaction(client, {
615
- patches: [{ entityKey: ENTITY_KEY, unset: "draft" as unknown as string[] }],
853
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, unset: "title" as never }],
616
854
  }),
617
- ).rejects.toThrow(/must be an array of attribute names/)
855
+ ).rejects.toThrow(/must be an array of column names/)
618
856
  expect(writeContract).not.toHaveBeenCalled()
619
857
  })
620
858
 
621
- it("rejects unsetting a system cell, which belongs to the engine", async () => {
859
+ it("rejects unsetting a system attribute, which is no column of any schema", async () => {
622
860
  const { client } = makeClient()
623
861
  await expect(
624
- sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY, unset: ["$payload"] }] }),
625
- ).rejects.toThrow(InvalidAttributeNameError)
862
+ sendArkivTransaction(client, {
863
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, unset: ["$owner" as never] }],
864
+ }),
865
+ ).rejects.toThrow(SchemaMismatchError)
626
866
  })
627
867
 
628
868
  it("does not fetch a block height for a patch-only batch", async () => {
629
869
  const { client } = makeClient()
630
870
  await sendArkivTransaction(client, {
631
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
871
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) } }],
632
872
  })
633
873
  // A patch carries no expiry, so there is nothing to resolve against the chain.
634
874
  expect(client.getBlockNumber).not.toHaveBeenCalled()
@@ -645,7 +885,7 @@ describe("failures that are not reverts", () => {
645
885
  ;(client.waitForTransactionReceipt as ReturnType<typeof vi.fn>).mockRejectedValue(timeout)
646
886
 
647
887
  const failure = await sendArkivTransaction(client, {
648
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
888
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) } }],
649
889
  }).catch((error: Error) => error)
650
890
 
651
891
  expect(failure).toBeInstanceOf(EntityMutationError)
@@ -672,7 +912,7 @@ describe("a revert the node catches before there is a receipt", () => {
672
912
  )
673
913
 
674
914
  const failure = await sendArkivTransaction(client, {
675
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
915
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) } }],
676
916
  }).catch((error: Error) => error)
677
917
 
678
918
  expect(failure).toBeInstanceOf(EntityMutationError)
@@ -689,7 +929,7 @@ describe("a revert the node catches before there is a receipt", () => {
689
929
  )
690
930
 
691
931
  const failure = await sendArkivTransaction(client, {
692
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
932
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) } }],
693
933
  }).catch((error: Error) => error)
694
934
 
695
935
  expect(failure.message).toContain('"/"')
@@ -702,7 +942,7 @@ describe("a revert the node catches before there is a receipt", () => {
702
942
  ;(client.writeContract as ReturnType<typeof vi.fn>).mockRejectedValue(opaque)
703
943
 
704
944
  const failure = await sendArkivTransaction(client, {
705
- patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
945
+ patches: [{ schema: Docs, entityKey: ENTITY_KEY, set: { level: i32(1) } }],
706
946
  }).catch((error: Error) => error)
707
947
 
708
948
  expect(failure.message).toContain("nonce too low")