@arkiv-network/sdk 0.7.0-dev.1 → 0.8.0-advanced.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 (237) hide show
  1. package/README.md +71 -16
  2. package/dist/actions/advanced/index.cjs +13 -0
  3. package/dist/actions/advanced/index.d.cts +8 -0
  4. package/dist/actions/advanced/index.d.ts +9 -0
  5. package/dist/actions/advanced/index.js +9 -0
  6. package/dist/advanced-8ZPTmmaN.js +598 -0
  7. package/dist/advanced-8ZPTmmaN.js.map +1 -0
  8. package/dist/advanced-fPImL6lW.cjs +656 -0
  9. package/dist/arkivTransactions-D74gERHG.d.cts +571 -0
  10. package/dist/arkivTransactions-D74gERHG.d.cts.map +1 -0
  11. package/dist/arkivTransactions-NtGD6uk5.d.ts +571 -0
  12. package/dist/arkivTransactions-NtGD6uk5.d.ts.map +1 -0
  13. package/dist/attr/index.cjs +35 -0
  14. package/dist/attr/index.d.cts +2 -0
  15. package/dist/attr/index.d.ts +3 -0
  16. package/dist/attr/index.js +4 -0
  17. package/dist/attributes-BhY5DwJp.js +809 -0
  18. package/dist/attributes-BhY5DwJp.js.map +1 -0
  19. package/dist/attributes-Df9IsCVs.cjs +1029 -0
  20. package/dist/chains/index.cjs +2 -3
  21. package/dist/chains/index.d.cts +22 -60
  22. package/dist/chains/index.d.cts.map +1 -1
  23. package/dist/chains/index.d.ts +22 -60
  24. package/dist/chains/index.d.ts.map +1 -1
  25. package/dist/chains/index.js +2 -2
  26. package/dist/chains-BJrZs6sG.js +57 -0
  27. package/dist/chains-BJrZs6sG.js.map +1 -0
  28. package/dist/chains-BYRTTipa.cjs +73 -0
  29. package/dist/codec-Car3KkFF.d.ts +102 -0
  30. package/dist/codec-Car3KkFF.d.ts.map +1 -0
  31. package/dist/entity/index.cjs +20 -0
  32. package/dist/entity/index.d.cts +4 -0
  33. package/dist/entity/index.d.ts +5 -0
  34. package/dist/entity/index.js +7 -0
  35. package/dist/entity-B00Li04j.cjs +71 -0
  36. package/dist/entity-B7G8q_Eo.cjs +0 -0
  37. package/dist/entity-BYQR1uoe.js +67 -0
  38. package/dist/entity-BYQR1uoe.js.map +1 -0
  39. package/dist/entity-CD_qFF2M.js +0 -0
  40. package/dist/errors-BVbjKVMN.cjs +56 -0
  41. package/dist/errors-LTe7bOYS.js +33 -0
  42. package/dist/errors-LTe7bOYS.js.map +1 -0
  43. package/dist/expiry-BELY2Kpw.cjs +135 -0
  44. package/dist/expiry-BjpxZZQU.js +95 -0
  45. package/dist/expiry-BjpxZZQU.js.map +1 -0
  46. package/dist/expiry-DRV29F1r.d.cts +79 -0
  47. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  48. package/dist/expiry-nDbCgZX4.d.ts +79 -0
  49. package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
  50. package/dist/flags-5dTNRnow.d.ts +41 -0
  51. package/dist/flags-5dTNRnow.d.ts.map +1 -0
  52. package/dist/flags-CRjPpoh3.d.cts +41 -0
  53. package/dist/flags-CRjPpoh3.d.cts.map +1 -0
  54. package/dist/flags-DFBs2J94.js +33 -0
  55. package/dist/flags-DFBs2J94.js.map +1 -0
  56. package/dist/flags-kr276q6i.cjs +43 -0
  57. package/dist/index-B5j76711.d.ts +184 -0
  58. package/dist/index-B5j76711.d.ts.map +1 -0
  59. package/dist/index-BF7Mq9Is.d.cts +280 -0
  60. package/dist/index-BF7Mq9Is.d.cts.map +1 -0
  61. package/dist/index-CA4V7Pdh.d.cts +167 -0
  62. package/dist/index-CA4V7Pdh.d.cts.map +1 -0
  63. package/dist/index-CV5zCdpa.d.cts +417 -0
  64. package/dist/index-CV5zCdpa.d.cts.map +1 -0
  65. package/dist/index-DHDud2hC.d.ts +280 -0
  66. package/dist/index-DHDud2hC.d.ts.map +1 -0
  67. package/dist/index-DvYvrkdk.d.ts +320 -0
  68. package/dist/index-DvYvrkdk.d.ts.map +1 -0
  69. package/dist/index-EeUO0nnJ.d.cts +184 -0
  70. package/dist/index-EeUO0nnJ.d.cts.map +1 -0
  71. package/dist/index-fX_XyqLp.d.ts +167 -0
  72. package/dist/index-fX_XyqLp.d.ts.map +1 -0
  73. package/dist/index.cjs +411 -405
  74. package/dist/index.d.cts +360 -209
  75. package/dist/index.d.cts.map +1 -1
  76. package/dist/index.d.ts +361 -209
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +342 -387
  79. package/dist/index.js.map +1 -1
  80. package/dist/key-D1Bh7MF0.js +70 -0
  81. package/dist/key-D1Bh7MF0.js.map +1 -0
  82. package/dist/key-WKhXnXuU.cjs +98 -0
  83. package/dist/logger-CVpvU8ik.js +153 -0
  84. package/dist/logger-CVpvU8ik.js.map +1 -0
  85. package/dist/logger-Cr15lPAQ.cjs +188 -0
  86. package/dist/payload-BNoud2KZ.d.cts +94 -0
  87. package/dist/payload-BNoud2KZ.d.cts.map +1 -0
  88. package/dist/payload-Bz6651ax.js +67 -0
  89. package/dist/payload-Bz6651ax.js.map +1 -0
  90. package/dist/payload-CcUj85ys.d.ts +94 -0
  91. package/dist/payload-CcUj85ys.d.ts.map +1 -0
  92. package/dist/payload-KC14SDlN.cjs +83 -0
  93. package/dist/query/index.cjs +26 -150
  94. package/dist/query/index.d.cts +5 -3
  95. package/dist/query/index.d.ts +6 -3
  96. package/dist/query/index.js +8 -138
  97. package/dist/queryBuilder-DM6AWCBa.js +820 -0
  98. package/dist/queryBuilder-DM6AWCBa.js.map +1 -0
  99. package/dist/queryBuilder-DbUMEZln.cjs +944 -0
  100. package/dist/selection-C4UhsMOO.d.ts +434 -0
  101. package/dist/selection-C4UhsMOO.d.ts.map +1 -0
  102. package/dist/selection-DPg0uyJf.d.cts +433 -0
  103. package/dist/selection-DPg0uyJf.d.cts.map +1 -0
  104. package/dist/types/index.cjs +3 -3
  105. package/dist/types/index.d.cts +7 -2
  106. package/dist/types/index.d.ts +8 -2
  107. package/dist/types/index.js +3 -3
  108. package/dist/types-Bu_ZwMbZ.js +0 -0
  109. package/dist/types-NRqymi6S.cjs +0 -0
  110. package/dist/utils/index.cjs +4 -17
  111. package/dist/utils/index.d.cts +3 -17
  112. package/dist/utils/index.d.ts +3 -17
  113. package/dist/utils/index.js +4 -18
  114. package/package.json +26 -5
  115. package/src/actions/advanced/advanced.test.ts +258 -0
  116. package/src/actions/advanced/buildMutation.ts +104 -0
  117. package/src/actions/advanced/getMutationResult.ts +112 -0
  118. package/src/actions/advanced/index.ts +21 -0
  119. package/src/actions/advanced/pingTransaction.ts +43 -0
  120. package/src/actions/advanced/sendMutation.ts +59 -0
  121. package/src/actions/public/getEntity.ts +27 -43
  122. package/src/actions/public/getEntityNonce.ts +30 -0
  123. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  124. package/src/actions/public/predictEntityKeys.ts +138 -0
  125. package/src/actions/public/query.ts +35 -67
  126. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  127. package/src/actions/public/watchEntityEvents.ts +225 -0
  128. package/src/actions/wallet/changeOwnership.ts +2 -2
  129. package/src/actions/wallet/createEntity.ts +99 -24
  130. package/src/actions/wallet/extendEntity.ts +37 -13
  131. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  132. package/src/actions/wallet/mutateEntities.ts +16 -33
  133. package/src/actions/wallet/patchEntity.ts +90 -0
  134. package/src/attr/attributes.test.ts +120 -0
  135. package/src/attr/attributes.ts +190 -0
  136. package/src/attr/codec.test.ts +162 -0
  137. package/src/attr/codec.ts +220 -0
  138. package/src/attr/decimal.ts +86 -0
  139. package/src/attr/errors.ts +145 -0
  140. package/src/attr/index.ts +50 -0
  141. package/src/attr/names.ts +106 -0
  142. package/src/attr/types.ts +150 -0
  143. package/src/attr/values.test.ts +271 -0
  144. package/src/attr/values.ts +502 -0
  145. package/src/chains/cheesecake.ts +22 -0
  146. package/src/chains/index.ts +1 -2
  147. package/src/clients/createPublicClient.test.ts +3 -5
  148. package/src/clients/createPublicClient.ts +5 -4
  149. package/src/clients/createWalletClient.test.ts +2 -2
  150. package/src/clients/createWalletClient.ts +13 -7
  151. package/src/clients/decorators/arkivAdvanced.ts +144 -0
  152. package/src/clients/decorators/arkivPublic.ts +172 -139
  153. package/src/clients/decorators/arkivWallet.ts +101 -94
  154. package/src/entity/create.test.ts +184 -0
  155. package/src/entity/errors.ts +38 -0
  156. package/src/entity/events.ts +16 -0
  157. package/src/entity/expiry.test.ts +195 -0
  158. package/src/entity/expiry.ts +181 -0
  159. package/src/entity/flags.ts +65 -0
  160. package/src/entity/index.ts +15 -0
  161. package/src/entity/key.ts +68 -0
  162. package/src/entity/operations.ts +134 -0
  163. package/src/entity/params.ts +15 -0
  164. package/src/errors.ts +26 -49
  165. package/src/index.ts +19 -0
  166. package/src/query/engine.ts +68 -137
  167. package/src/query/errors.ts +151 -0
  168. package/src/query/expression.test.ts +286 -0
  169. package/src/query/expression.ts +493 -0
  170. package/src/query/index.ts +40 -4
  171. package/src/query/injection.test.ts +151 -0
  172. package/src/query/queryBuilder.test.ts +342 -637
  173. package/src/query/queryBuilder.ts +133 -359
  174. package/src/query/queryResult.ts +54 -51
  175. package/src/query/selection.test.ts +48 -98
  176. package/src/query/selection.ts +77 -65
  177. package/src/types/entity.test.ts +44 -155
  178. package/src/types/entity.ts +60 -52
  179. package/src/types/events.ts +59 -16
  180. package/src/types/index.ts +20 -17
  181. package/src/types/rpcSchema.ts +69 -37
  182. package/src/utils/arkivTransactions.test.ts +600 -155
  183. package/src/utils/arkivTransactions.ts +350 -219
  184. package/src/utils/chains.test.ts +21 -24
  185. package/src/utils/compression.ts +1 -1
  186. package/src/utils/entities.test.ts +62 -0
  187. package/src/utils/entities.ts +109 -37
  188. package/src/utils/expirationTime.ts +114 -10
  189. package/src/utils/logger.ts +2 -2
  190. package/src/utils/revert.test.ts +99 -0
  191. package/src/utils/revert.ts +145 -0
  192. package/dist/chains-BifaY04V.cjs +0 -108
  193. package/dist/chains-CrtkVbBh.js +0 -86
  194. package/dist/chains-CrtkVbBh.js.map +0 -1
  195. package/dist/consts-Bz0LZQZY.js +0 -7
  196. package/dist/consts-Bz0LZQZY.js.map +0 -1
  197. package/dist/consts-CxT1-RXU.cjs +0 -18
  198. package/dist/entity-BhSXaB23.cjs +0 -81
  199. package/dist/entity-CN41IOrq.d.cts +0 -401
  200. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  201. package/dist/entity-DGfVyB-a.js +0 -71
  202. package/dist/entity-DGfVyB-a.js.map +0 -1
  203. package/dist/entity-NnEVlePN.d.ts +0 -401
  204. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  205. package/dist/index-ClkpS6s_.d.ts +0 -481
  206. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  207. package/dist/index-w4qop0Pe.d.cts +0 -481
  208. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  209. package/dist/payload-BpNUPOnY.js +0 -13
  210. package/dist/payload-BpNUPOnY.js.map +0 -1
  211. package/dist/payload-C2KHOUoj.d.ts +0 -6
  212. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  213. package/dist/payload-CJN8fTvX.d.cts +0 -6
  214. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  215. package/dist/payload-D8YLKcJj.cjs +0 -23
  216. package/dist/query/index.js.map +0 -1
  217. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  218. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  219. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  220. package/dist/utils/index.d.cts.map +0 -1
  221. package/dist/utils/index.d.ts.map +0 -1
  222. package/dist/utils/index.js.map +0 -1
  223. package/src/actions/public/buildQuery.ts +0 -10
  224. package/src/actions/public/subscribeEntityEvents.ts +0 -93
  225. package/src/actions/wallet/updateEntity.ts +0 -59
  226. package/src/chains/braga.ts +0 -26
  227. package/src/chains/kaolin.ts +0 -29
  228. package/src/query/engine.test.ts +0 -571
  229. package/src/query/predicate.test.ts +0 -461
  230. package/src/query/predicate.ts +0 -144
  231. package/src/query/queryResult.test.ts +0 -270
  232. package/src/query/selectQueryBuilder.test.ts +0 -213
  233. package/src/types/attributes.ts +0 -28
  234. package/src/utils/validation.test.ts +0 -106
  235. package/src/utils/validation.ts +0 -80
  236. /package/dist/{types-Ci4BRClw.js → attr-DlmErkvR.cjs} +0 -0
  237. /package/dist/{types-a8gm_IaQ.cjs → attr-GcQKatub.js} +0 -0
@@ -2,31 +2,41 @@ import {
2
2
  type Address,
3
3
  ContractFunctionExecutionError,
4
4
  ContractFunctionRevertedError,
5
- encodePacked,
5
+ decodeEventLog,
6
+ type Hash,
6
7
  type Hex,
7
- keccak256,
8
8
  parseAbi,
9
- stringToBytes,
10
9
  TransactionExecutionError,
11
10
  type TransactionReceipt,
12
- toBytes,
13
- toHex,
14
11
  } from "viem"
15
12
  import type { ChangeOwnershipParameters } from "../actions/wallet/changeOwnership"
16
13
  import type { CreateEntityParameters } from "../actions/wallet/createEntity"
17
14
  import type { DeleteEntityParameters } from "../actions/wallet/deleteEntity"
18
15
  import type { ExtendEntityParameters } from "../actions/wallet/extendEntity"
19
- import type { UpdateEntityParameters } from "../actions/wallet/updateEntity"
16
+ import type { PatchEntityParameters } from "../actions/wallet/patchEntity"
17
+ // Internal seam: the ABI encoders are not part of the package's public surface.
18
+ import { encodeAttributes, encodeMutations } from "../attr/attributes"
20
19
  import type { ArkivClient } from "../clients/baseClient"
21
20
  import type { WalletArkivClient } from "../clients/createWalletClient"
22
- import { ARKIV_ADDRESS, BLOCK_TIME } from "../consts"
23
- import { DuplicateAttributeError, EntityMutationError, InvalidContentTypeError } from "../errors"
21
+ import { ARKIV_ADDRESS } from "../consts"
22
+ import { ENTITY_EVENTS_ABI } from "../entity/events"
23
+ import { type Expiry, type ExpiryContext, resolveExpiry } from "../entity/expiry"
24
+ import { encodeCreationFlags } from "../entity/flags"
25
+ import { randomSalt, validateSalt } from "../entity/key"
26
+ import {
27
+ createOperation,
28
+ deleteOperation,
29
+ EXECUTE_ABI as EXECUTE_FUNCTION_ABI,
30
+ extendExpiryOperation,
31
+ type Operation,
32
+ patchOperation,
33
+ transferOwnershipOperation,
34
+ } from "../entity/operations"
35
+ import { EmptyPatchError, EntityMutationError, InvalidContentTypeError } from "../errors"
24
36
  import type { TxParams } from "../types"
25
- import { EntityOperationType } from "../types/entity"
26
- import { RpcAttributeValueType as AttributeValueType } from "../types/rpcSchema"
27
37
 
28
38
  import { getLogger } from "./logger"
29
- import { isEntityKey, validateAttribute, validateExpiresIn } from "./validation"
39
+ import { describeEntityRevert } from "./revert"
30
40
 
31
41
  const logger = getLogger("utils:arkiv-transactions")
32
42
 
@@ -39,229 +49,75 @@ function validateContentType(contentType: string): void {
39
49
  }
40
50
  }
41
51
 
42
- // Mime128 = struct { bytes32[4] data } (128-byte fixed MIME type container)
43
- // Attribute = struct { bytes32 name, uint8 valueType, bytes32[4] value }
44
- // BlockNumber = type BlockNumber is uint32
45
- export const ENTITY_EXECUTE_ABI = parseAbi([
46
- "function execute((uint8 operationType, bytes32 entityKey, bytes payload, (bytes32[4] data) contentType, (bytes32 name, uint8 valueType, bytes32[4] value)[] attributes, uint32 expiresIn, address newOwner)[] ops) external",
47
- ])
48
-
49
- export const ENTITY_OPERATION_EVENT_ABI = parseAbi([
50
- "event EntityOperation(bytes32 indexed entityKey, uint8 indexed operationType, address indexed owner, uint32 expiresAt, bytes32 entityHash)",
51
- ])
52
-
53
52
  export const ENTITY_ERRORS_ABI = parseAbi([
53
+ // Batch and operation framing.
54
54
  "error EmptyBatch()",
55
+ "error InvalidOpType(uint8 operation)",
56
+ "error NonCanonicalOperationData(uint8 operation)",
57
+ // The attribute triple list, structural.
55
58
  "error AttributesNotSorted()",
56
- "error InvalidValueType(bytes32 name, uint8 valueType)",
57
- "error InvalidOpType(uint8 operationType)",
58
- "error ExpiryInPast(uint32 expiresAt, uint32 currentBlock)",
59
59
  "error TooManyAttributes(uint256 count, uint256 maxCount)",
60
+ "error EmptyMutations(bytes32 entityKey)",
61
+ "error TombstoneInCreate(bytes32 name)",
62
+ "error TombstoneValueNotEmpty(bytes32 name)",
63
+ "error SystemAttributeNotWritable(bytes32 name)",
64
+ "error InvalidValueType(bytes32 name, uint8 typeId)",
65
+ // Attribute-name validation. `Ident32` is a bytes32 on the wire.
66
+ "error Ident32Empty()",
67
+ "error Ident32InvalidByte(uint256 position, bytes1 value)",
68
+ // Entity state and authorisation.
60
69
  "error EntityNotFound(bytes32 entityKey)",
70
+ "error EntityExpired(bytes32 entityKey, uint64 expiresAt)",
61
71
  "error NotOwner(bytes32 entityKey, address caller, address owner)",
62
- "error EntityExpired(bytes32 entityKey, uint32 expiresAt)",
63
- "error ExpiryNotExtended(bytes32 entityKey, uint32 newExpiresAt, uint32 currentExpiresAt)",
72
+ "error ReadOnlyEntity(bytes32 entityKey)",
73
+ "error ReservedCreationFlags(uint8 creationFlags)",
74
+ // Expiry. The engine bounds an expiry only by the width of the field and by liveness — there is
75
+ // no MAX_EXPIRES_AT or MAX_LIFETIME, so there is no error for exceeding one.
76
+ "error ExpiryDeadOnArrival(uint64 target, uint64 currentBlock)",
77
+ "error ExpiryNotExtended(bytes32 entityKey, uint64 newExpiresAt, uint64 currentExpiresAt)",
78
+ // Ownership transfer.
64
79
  "error TransferToZeroAddress(bytes32 entityKey)",
65
80
  "error TransferToSelf(bytes32 entityKey)",
66
- "error EntityNotExpired(bytes32 entityKey, uint32 expiresAt)",
67
- "error Ident32Empty()",
68
- "error Ident32TooLong(uint256 length)",
69
- "error Ident32InvalidByte(uint256 position, bytes1 value)",
70
- "error MimeEmpty()",
71
- "error MimeTooLong(uint256 length, uint256 maxLength)",
72
- "error MimeInvalidByte(uint256 position, bytes1 value)",
73
- "error MimeIncomplete()",
74
81
  ])
75
82
 
76
- const EXECUTE_ABI = [...ENTITY_EXECUTE_ABI, ...ENTITY_ERRORS_ABI]
77
-
78
- const ENTITY_NONCE_ABI = parseAbi(["function nonces(address owner) view returns (uint32)"])
79
-
80
- const ZERO_ADDRESS: Address = "0x0000000000000000000000000000000000000000"
81
- const ZERO_32 = `0x${"00".repeat(32)}` as Hex
82
- const EMPTY_BYTES128 = [ZERO_32, ZERO_32, ZERO_32, ZERO_32] as const
83
-
84
- // Encode a string or bytes into a bytes32[4] (128-byte) container, left-aligned.
85
- function encodeBytes128(data: Uint8Array): readonly [Hex, Hex, Hex, Hex] {
86
- const padded = new Uint8Array(128)
87
- padded.set(data.slice(0, 128))
88
- return [
89
- toHex(padded.slice(0, 32)),
90
- toHex(padded.slice(32, 64)),
91
- toHex(padded.slice(64, 96)),
92
- toHex(padded.slice(96, 128)),
93
- ]
94
- }
95
-
96
- // Mime128 struct: bytes32[4] data, string packed left-aligned into 128 bytes.
97
- function encodeMime128(contentType: string): { data: readonly [Hex, Hex, Hex, Hex] } {
98
- return { data: contentType ? encodeBytes128(stringToBytes(contentType)) : EMPTY_BYTES128 }
99
- }
100
-
101
- function encodeAttribute(attr: { key: string; value: Hex | string | number }) {
102
- validateAttribute(attr)
103
- const name = toHex(attr.key, { size: 32 })
104
-
105
- if (typeof attr.value === "string") {
106
- // Only exact 32-byte hex (an entity key) gets the EntityKey type; shorter or
107
- // longer hex-looking strings are stored as plain strings so they round-trip
108
- // unchanged and stay matchable by quoted string queries.
109
- if (isEntityKey(attr.value)) {
110
- return {
111
- name,
112
- valueType: AttributeValueType.EntityKey,
113
- value: [toHex(toBytes(attr.value), { size: 32 }), ZERO_32, ZERO_32, ZERO_32] as const,
114
- }
115
- }
116
- return {
117
- name,
118
- valueType: AttributeValueType.String,
119
- // stringToBytes, not toBytes: toBytes hex-decodes hex-looking strings, which
120
- // would corrupt string values like "0xab" instead of UTF-8 encoding them
121
- value: encodeBytes128(stringToBytes(attr.value)),
122
- }
123
- }
124
-
125
- return {
126
- name,
127
- valueType: AttributeValueType.Uint,
128
- value: [toHex(BigInt(attr.value), { size: 32 }), ZERO_32, ZERO_32, ZERO_32] as const,
129
- }
130
- }
131
-
132
- // The registry requires attributes strictly ascending by their bytes32 name — the
133
- // canonical serialization hashes into the state root, and strict ordering doubles as
134
- // the uniqueness check (AttributesNotSorted). Sort here so callers can pass any order;
135
- // duplicates would revert on-chain, so reject them client-side with a clear error.
136
- function encodeAttributes(attrs: { key: string; value: Hex | string | number }[]) {
137
- const encoded = attrs
138
- .map((attr) => ({ key: attr.key, ...encodeAttribute(attr) }))
139
- .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
140
- for (let i = 1; i < encoded.length; i++) {
141
- if (encoded[i].name === encoded[i - 1].name) {
142
- throw new DuplicateAttributeError(encoded[i].key)
143
- }
144
- }
145
- return encoded.map(({ key: _, ...attr }) => attr)
146
- }
147
-
148
- function toBTL(expiresIn: number): number {
149
- validateExpiresIn(expiresIn)
150
- return Math.ceil(expiresIn / BLOCK_TIME)
151
- }
152
-
153
- // entityKey = keccak256(chainId || registryAddress || ownerAddress || nonce)
154
- function deriveEntityKey(chainId: number, owner: Address, nonce: bigint): Hex {
155
- return keccak256(
156
- encodePacked(
157
- ["uint256", "address", "address", "uint32"],
158
- [BigInt(chainId), ARKIV_ADDRESS, owner, Number(nonce)],
159
- ),
160
- )
161
- }
83
+ const EXECUTE_ABI = [...EXECUTE_FUNCTION_ABI, ...ENTITY_ERRORS_ABI]
162
84
 
163
85
  export type SendArkivTransactionResult = {
164
86
  receipt: TransactionReceipt
87
+ /** The keys the engine minted for the entities this transaction created, in batch order. */
165
88
  createdEntityKeys: Hex[]
89
+ /** The block each created entity expires at, as the engine recorded it, in the same order. */
90
+ createdExpiries: bigint[]
91
+ /** The block each extended entity now expires at, as the engine recorded it, in batch order. */
92
+ extendedExpiries: bigint[]
166
93
  }
167
94
 
168
95
  export async function sendArkivTransaction(
169
96
  client: ArkivClient,
170
- ops: {
171
- creates?: CreateEntityParameters[]
172
- updates?: UpdateEntityParameters[]
173
- deletes?: DeleteEntityParameters[]
174
- extensions?: ExtendEntityParameters[]
175
- ownershipChanges?: ChangeOwnershipParameters[]
176
- },
97
+ ops: EntityMutationOps,
177
98
  txParams?: TxParams,
178
99
  ): Promise<SendArkivTransactionResult> {
179
100
  if (!client.account) throw new Error("Account required")
180
101
  if (!client.chain) throw new Error("Chain required")
181
102
  const walletClient = client as WalletArkivClient
182
103
 
183
- const { creates, updates, deletes, extensions, ownershipChanges } = ops
184
- const owner = client.account.address as Address
104
+ const { creates, extensions } = ops
185
105
 
186
- const ownerNonce: bigint = creates?.length
187
- ? BigInt(
188
- await walletClient.readContract({
189
- address: ARKIV_ADDRESS,
190
- abi: ENTITY_NONCE_ABI,
191
- functionName: "nonces",
192
- args: [owner],
193
- }),
194
- )
195
- : 0n
106
+ // The block height, to resolve any relative expiry — which both creates and extensions carry.
107
+ // Skipped entirely for a batch with neither. Deliberately fetched even when every expiry is
108
+ // absolute (where `mutationNeedsBlockNumber` would say no): the head is what makes the local
109
+ // dead-on-arrival check in `resolveExpiry` work, and the everyday path buys that check — the
110
+ // advanced path is where both are traded away for the saved call.
111
+ const currentBlock =
112
+ creates?.length || extensions?.length ? await walletClient.getBlockNumber() : 0n
196
113
 
197
- const createdEntityKeys: Hex[] = (creates ?? []).map((_, i) =>
198
- deriveEntityKey(client.chain!.id, owner, ownerNonce + BigInt(i)),
199
- )
114
+ const operations = buildEntityOperations(ops, { currentBlock })
200
115
 
201
- for (const item of creates ?? []) validateContentType(item.contentType)
202
- for (const item of updates ?? []) validateContentType(item.contentType)
203
-
204
- const operations = [
205
- ...(creates ?? []).map((item, i) => ({
206
- operationType: EntityOperationType.Create,
207
- entityKey: createdEntityKeys[i],
208
- payload: toHex(item.payload),
209
- contentType: encodeMime128(item.contentType),
210
- attributes: encodeAttributes(item.attributes),
211
- expiresIn: toBTL(item.expiresIn),
212
- newOwner: ZERO_ADDRESS,
213
- })),
214
- ...(updates ?? []).map((item) => ({
215
- operationType: EntityOperationType.Update,
216
- entityKey: item.entityKey,
217
- payload: toHex(item.payload),
218
- contentType: encodeMime128(item.contentType),
219
- attributes: encodeAttributes(item.attributes),
220
- expiresIn: 0, // contract ignores expiresAt on UPDATE
221
- newOwner: ZERO_ADDRESS,
222
- })),
223
- ...(deletes ?? []).map((item) => ({
224
- operationType: EntityOperationType.Delete,
225
- entityKey: item.entityKey,
226
- payload: "0x" as Hex,
227
- contentType: encodeMime128(""),
228
- attributes: [] as never[],
229
- expiresIn: 0,
230
- newOwner: ZERO_ADDRESS,
231
- })),
232
- ...(extensions ?? []).map((item) => ({
233
- operationType: EntityOperationType.Extend,
234
- entityKey: item.entityKey,
235
- payload: "0x" as Hex,
236
- contentType: encodeMime128(""),
237
- attributes: [] as never[],
238
- expiresIn: toBTL(item.expiresIn),
239
- newOwner: ZERO_ADDRESS,
240
- })),
241
- ...(ownershipChanges ?? []).map((item) => ({
242
- operationType: EntityOperationType.Transfer,
243
- entityKey: item.entityKey,
244
- payload: "0x" as Hex,
245
- contentType: encodeMime128(""),
246
- attributes: [] as never[],
247
- expiresIn: 0,
248
- newOwner: item.newOwner as Address,
249
- })),
250
- ]
116
+ logger("Sending execute with %d operations %o", operations.length, operations)
251
117
 
252
- logger("Sending execute with %d operations %s", operations.length, JSON.stringify(operations))
118
+ const txHash = await submitMutation(client, operations, txParams)
253
119
 
254
120
  try {
255
- const txHash = await walletClient.writeContract({
256
- address: ARKIV_ADDRESS,
257
- abi: EXECUTE_ABI,
258
- functionName: "execute",
259
- args: [operations],
260
- account: client.account,
261
- chain: client.chain,
262
- ...txParams,
263
- })
264
-
265
121
  const receipt = await walletClient.waitForTransactionReceipt({ hash: txHash })
266
122
  logger("Tx receipt %o", receipt)
267
123
 
@@ -278,7 +134,10 @@ export async function sendArkivTransaction(
278
134
  } catch (err) {
279
135
  const error = err as { shortMessage?: string; cause?: { details?: string } }
280
136
  const reason =
281
- error.shortMessage ?? error.cause?.details ?? "No reason provided by backend."
137
+ describeEntityRevert(err) ??
138
+ error.shortMessage ??
139
+ error.cause?.details ??
140
+ "No reason provided by backend."
282
141
  throw new EntityMutationError(
283
142
  `Transaction ${receipt.transactionHash} reverted. Reason: ${reason}`,
284
143
  )
@@ -288,22 +147,294 @@ export async function sendArkivTransaction(
288
147
  )
289
148
  }
290
149
 
291
- return { receipt, createdEntityKeys }
150
+ return {
151
+ receipt,
152
+ ...readAppliedOperations(receipt, {
153
+ creates: creates?.length ?? 0,
154
+ extensions: extensions?.length ?? 0,
155
+ }),
156
+ }
292
157
  } catch (error) {
293
- let message = "Transaction failed"
294
- if (error instanceof TransactionExecutionError) {
295
- message += `: ${error.details}`
296
- } else if (error instanceof ContractFunctionExecutionError) {
297
- logger("Contract function execution error data:", error.shortMessage)
298
- if (error.cause instanceof ContractFunctionRevertedError) {
299
- message += `: ${error.cause.message}`
300
- } else {
301
- message += ": Execution error without revert data"
302
- }
303
- } else if (error instanceof EntityMutationError) {
304
- throw error
158
+ throw asEntityMutationError(error)
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Wraps whatever a write step threw into an {@link EntityMutationError} — engine terms when the
164
+ * revert data decodes, the node's own message otherwise. An {@link EntityMutationError} passes
165
+ * through unchanged, so wrapped steps compose without double-wrapping.
166
+ */
167
+ function asEntityMutationError(error: unknown): EntityMutationError {
168
+ if (error instanceof EntityMutationError) return error
169
+
170
+ const described = describeEntityRevert(error)
171
+ if (described !== undefined) {
172
+ return new EntityMutationError(`Transaction failed: ${described}`, { cause: error })
173
+ }
174
+
175
+ let message = "Transaction failed"
176
+ if (error instanceof TransactionExecutionError) {
177
+ message += `: ${error.details}`
178
+ } else if (error instanceof ContractFunctionExecutionError) {
179
+ logger("Contract function execution error data:", error.shortMessage)
180
+ if (error.cause instanceof ContractFunctionRevertedError) {
181
+ message += `: ${error.cause.message}`
182
+ } else {
183
+ message += ": Execution error without revert data"
305
184
  }
185
+ } else if (error instanceof Error) {
186
+ message += `: ${error.message}`
187
+ }
188
+
189
+ return new EntityMutationError(message, { cause: error })
190
+ }
191
+
192
+ /**
193
+ * The keys and expiries the engine recorded, read back from the events the batch emitted.
194
+ */
195
+ function readAppliedOperations(
196
+ receipt: TransactionReceipt,
197
+ expected: { creates: number; extensions: number },
198
+ ): Omit<SendArkivTransactionResult, "receipt"> {
199
+ const events = collectMutationEvents(receipt.logs)
306
200
 
307
- throw new EntityMutationError(message)
201
+ const createdEntityKeys = events.created.map((e) => e.entityKey)
202
+ const createdExpiries = events.created.map((e) => e.expiresAt)
203
+ const extendedExpiries = events.extended.map((e) => e.expiresAt)
204
+
205
+ // Operations apply in batch order and emit one event each, and creates lead the batch — so these
206
+ // arrive already aligned with the parameters they came from. A count that disagrees means that
207
+ // assumption broke, and a misaligned array would hand back a key belonging to another entity.
208
+ assertCount(receipt, "create", createdEntityKeys.length, expected.creates)
209
+ assertCount(receipt, "extension", extendedExpiries.length, expected.extensions)
210
+
211
+ return { createdEntityKeys, createdExpiries, extendedExpiries }
212
+ }
213
+
214
+ /** One log as an entity event, or `undefined` for anything this SDK has no name for. */
215
+ function decodeEntityLog(log: TransactionReceipt["logs"][number]) {
216
+ try {
217
+ return decodeEventLog({ abi: ENTITY_EVENTS_ABI, topics: log.topics, data: log.data })
218
+ } catch {
219
+ // An event a newer engine emits and this ABI does not describe. Not this function's business.
220
+ return undefined
221
+ }
222
+ }
223
+
224
+ function assertCount(receipt: TransactionReceipt, what: string, got: number, want: number): void {
225
+ if (got === want) return
226
+ // Deliberately explicit that the write succeeded: this throws *after* a successful transaction,
227
+ // and a caller who reads it as a failure and retries would create the entities a second time.
228
+ throw new EntityMutationError(
229
+ `Transaction ${receipt.transactionHash} succeeded — do not retry it — but emitted ${got} ` +
230
+ `${what} event(s) for ${want} ${what} operation(s). The batch was applied; the SDK cannot ` +
231
+ `say which entity each operation produced, so read them back to find them.`,
232
+ )
233
+ }
234
+
235
+ // ─────────────────────────────────────────────────────────────────────────────────────────────────
236
+ // The seams the advanced path is built on.
237
+ //
238
+ // `sendArkivTransaction` above is the everyday path: encode, send, wait, decode, diagnose. These
239
+ // are the same steps as separate pieces, so `actions/advanced` can spend exactly the RPC calls a
240
+ // caller asks for and no more — without a second copy of the encoding or the event decoding.
241
+ // ─────────────────────────────────────────────────────────────────────────────────────────────────
242
+
243
+ /** The five kinds of entity operation one Arkiv transaction can batch. */
244
+ export type EntityMutationOps = {
245
+ /** The entities to create. */
246
+ creates?: CreateEntityParameters[]
247
+ /** The patches to apply. */
248
+ patches?: PatchEntityParameters[]
249
+ /** The entities to delete. */
250
+ deletes?: DeleteEntityParameters[]
251
+ /** The expiries to set. */
252
+ extensions?: ExtendEntityParameters[]
253
+ /** The ownership transfers to perform. */
254
+ ownershipChanges?: ChangeOwnershipParameters[]
255
+ }
256
+
257
+ /**
258
+ * Whether encoding the batch actually requires the chain head.
259
+ *
260
+ * Only a relative lifetime (`minLifetime > 0`) or a wall-clock deadline (a `Date`) is measured
261
+ * from the current block. A purely absolute `atBlock(n)` expiry encodes the same bytes whatever
262
+ * the head is, so a batch of those — or one with no expiries at all — needs no `eth_blockNumber`.
263
+ */
264
+ export function mutationNeedsBlockNumber(ops: EntityMutationOps): boolean {
265
+ return [...(ops.creates ?? []), ...(ops.extensions ?? [])].some((item) => {
266
+ const expires = item.expires as Expiry | undefined
267
+ // Malformed expiries are rejected by buildEntityOperations without needing the head.
268
+ if (expires === undefined || typeof expires.minLifetime !== "bigint") return false
269
+ return expires.minLifetime !== 0n || expires.expiresAt instanceof Date
270
+ })
271
+ }
272
+
273
+ /**
274
+ * Encodes a batch into the `execute` operations, in the order the engine applies them: creates,
275
+ * patches, deletes, extensions, ownership transfers.
276
+ *
277
+ * Pure — every check here is local (content types, salts, expiry bounds, non-empty patches and a
278
+ * non-empty batch), so the only chain input is `context.currentBlock`. Callers that carry no
279
+ * relative expiry (see {@link mutationNeedsBlockNumber}) can pass `0n`, which disables the
280
+ * dead-on-arrival pre-check and leaves that to the engine.
281
+ */
282
+ export function buildEntityOperations(ops: EntityMutationOps, context: ExpiryContext): Operation[] {
283
+ const { creates, patches, deletes, extensions, ownershipChanges } = ops
284
+
285
+ const createOps = (creates ?? []).map((item) => {
286
+ validateContentType(item.contentType)
287
+
288
+ const salt = item.salt === undefined ? randomSalt() : validateSalt(item.salt)
289
+ const expiry = resolveExpiry(item.expires, context)
290
+
291
+ return createOperation({
292
+ salt,
293
+ expiry,
294
+ creationFlags: encodeCreationFlags(item.flags),
295
+ attributes: encodeAttributes(item.attributes, {
296
+ payload: item.payload,
297
+ contentType: item.contentType,
298
+ }),
299
+ })
300
+ })
301
+
302
+ const patchOps = (patches ?? []).map((item) => {
303
+ if (item.contentType !== undefined) validateContentType(item.contentType)
304
+ const mutations = encodeMutations(item)
305
+ if (mutations.length === 0) {
306
+ throw new EmptyPatchError(item.entityKey)
307
+ }
308
+ return patchOperation({ entityKey: item.entityKey, mutations })
309
+ })
310
+
311
+ // An extension resolves its expiry exactly as a create does — the engine applies the same
312
+ // `max(expiresAt, currentBlock + minLifetime)` to both — so the same bounds are checked here
313
+ // rather than left to a revert.
314
+ const extendOps = (extensions ?? []).map((item) => {
315
+ const expiry = resolveExpiry(item.expires, context)
316
+ return extendExpiryOperation({ entityKey: item.entityKey, expiry })
317
+ })
318
+
319
+ const operations: Operation[] = [
320
+ ...createOps,
321
+ ...patchOps,
322
+ ...(deletes ?? []).map((item) => deleteOperation({ entityKey: item.entityKey })),
323
+ ...extendOps,
324
+ ...(ownershipChanges ?? []).map((item) =>
325
+ transferOwnershipOperation({
326
+ entityKey: item.entityKey,
327
+ newOwner: item.newOwner as Address,
328
+ }),
329
+ ),
330
+ ]
331
+
332
+ if (operations.length === 0) {
333
+ throw new Error("No operations to perform")
334
+ }
335
+
336
+ return operations
337
+ }
338
+
339
+ /**
340
+ * Signs and sends an encoded batch, wrapping any node rejection in {@link EntityMutationError}.
341
+ *
342
+ * The submission half both write paths share: one `writeContract` — an `eth_sendRawTransaction`,
343
+ * plus only the lookups viem needs for whatever `txParams` leaves out — with no waiting and no
344
+ * polling. A rejection is reported in engine terms when the node returned decodable revert data,
345
+ * and with the node's own message otherwise; either way the caller sees an
346
+ * {@link EntityMutationError}.
347
+ *
348
+ * Internal seam shared with `actions/advanced/sendMutation`; not part of the package's public
349
+ * surface.
350
+ */
351
+ export async function submitMutation(
352
+ client: ArkivClient,
353
+ operations: Operation[],
354
+ txParams?: TxParams,
355
+ ): Promise<Hash> {
356
+ if (!client.account) throw new Error("Account required")
357
+ if (!client.chain) throw new Error("Chain required")
358
+ const walletClient = client as WalletArkivClient
359
+
360
+ try {
361
+ return await walletClient.writeContract({
362
+ address: ARKIV_ADDRESS,
363
+ abi: EXECUTE_ABI,
364
+ functionName: "execute",
365
+ args: [operations],
366
+ account: client.account,
367
+ chain: client.chain,
368
+ ...txParams,
369
+ })
370
+ } catch (error) {
371
+ throw asEntityMutationError(error)
372
+ }
373
+ }
374
+
375
+ /** Everything the batch's events say happened, one entry per applied operation, per kind. */
376
+ export type MutationEvents = {
377
+ created: { entityKey: Hex; owner: Address; expiresAt: bigint }[]
378
+ patched: { entityKey: Hex; owner: Address }[]
379
+ deleted: { entityKey: Hex; owner: Address }[]
380
+ extended: { entityKey: Hex; owner: Address; expiresAt: bigint }[]
381
+ ownershipTransferred: { entityKey: Hex; previousOwner: Address; newOwner: Address }[]
382
+ }
383
+
384
+ /**
385
+ * Reads the entity events out of a receipt's logs, exactly as they were emitted.
386
+ *
387
+ * Lenient by design: it reports what the events say and nothing else — no counting against what
388
+ * was asked for (the caller may not have the request at hand) and no error for a receipt with no
389
+ * entity events at all. Logs from other contracts and events this SDK has no name for are skipped.
390
+ */
391
+ export function collectMutationEvents(logs: TransactionReceipt["logs"]): MutationEvents {
392
+ const events: MutationEvents = {
393
+ created: [],
394
+ patched: [],
395
+ deleted: [],
396
+ extended: [],
397
+ ownershipTransferred: [],
308
398
  }
399
+
400
+ // `?? []` so a receipt without logs decodes to "nothing happened" rather than a TypeError.
401
+ for (const log of logs ?? []) {
402
+ // Only the operation address emits these, and a batch's transaction can carry logs from
403
+ // anything else it touched.
404
+ if (log.address.toLowerCase() !== ARKIV_ADDRESS.toLowerCase()) continue
405
+ const decoded = decodeEntityLog(log)
406
+ if (decoded === undefined) continue
407
+
408
+ switch (decoded.eventName) {
409
+ case "EntityCreated":
410
+ events.created.push({
411
+ entityKey: decoded.args.entityKey,
412
+ owner: decoded.args.owner,
413
+ expiresAt: decoded.args.expiresAt,
414
+ })
415
+ break
416
+ case "EntityPatched":
417
+ events.patched.push({ entityKey: decoded.args.entityKey, owner: decoded.args.owner })
418
+ break
419
+ case "EntityDeleted":
420
+ events.deleted.push({ entityKey: decoded.args.entityKey, owner: decoded.args.owner })
421
+ break
422
+ case "ExpiryExtended":
423
+ events.extended.push({
424
+ entityKey: decoded.args.entityKey,
425
+ owner: decoded.args.owner,
426
+ expiresAt: decoded.args.expiresAt,
427
+ })
428
+ break
429
+ case "OwnershipTransferred":
430
+ events.ownershipTransferred.push({
431
+ entityKey: decoded.args.entityKey,
432
+ previousOwner: decoded.args.previousOwner,
433
+ newOwner: decoded.args.newOwner,
434
+ })
435
+ break
436
+ }
437
+ }
438
+
439
+ return events
309
440
  }