@arkiv-network/sdk 0.7.0-dev.1 → 0.8.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +29 -16
  2. package/dist/attr/index.cjs +34 -0
  3. package/dist/attr/index.d.cts +2 -0
  4. package/dist/attr/index.d.ts +2 -0
  5. package/dist/attr/index.js +3 -0
  6. package/dist/attr-BDZ-APg5.js +809 -0
  7. package/dist/attr-BDZ-APg5.js.map +1 -0
  8. package/dist/attr-BvxHN99W.cjs +1029 -0
  9. package/dist/chains/index.cjs +2 -3
  10. package/dist/chains/index.d.cts +22 -60
  11. package/dist/chains/index.d.cts.map +1 -1
  12. package/dist/chains/index.d.ts +22 -60
  13. package/dist/chains/index.d.ts.map +1 -1
  14. package/dist/chains/index.js +2 -2
  15. package/dist/chains-BJrZs6sG.js +57 -0
  16. package/dist/chains-BJrZs6sG.js.map +1 -0
  17. package/dist/chains-BYRTTipa.cjs +73 -0
  18. package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
  19. package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
  20. package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
  21. package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
  22. package/dist/entity/index.cjs +19 -0
  23. package/dist/entity/index.d.cts +4 -0
  24. package/dist/entity/index.d.ts +4 -0
  25. package/dist/entity/index.js +6 -0
  26. package/dist/entity-B0t6sN7F.cjs +71 -0
  27. package/dist/entity-BIp_df9u.js +67 -0
  28. package/dist/entity-BIp_df9u.js.map +1 -0
  29. package/dist/entity-CFhbAv0R.js +70 -0
  30. package/dist/entity-CFhbAv0R.js.map +1 -0
  31. package/dist/entity-wfBnZd91.cjs +98 -0
  32. package/dist/errors-D-eJkGVc.cjs +56 -0
  33. package/dist/errors-DGB8AgT2.js +33 -0
  34. package/dist/errors-DGB8AgT2.js.map +1 -0
  35. package/dist/expiry-BUYIXTGb.cjs +135 -0
  36. package/dist/expiry-BxOlvVWC.d.ts +79 -0
  37. package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
  38. package/dist/expiry-DGRzrSan.js +95 -0
  39. package/dist/expiry-DGRzrSan.js.map +1 -0
  40. package/dist/expiry-DRV29F1r.d.cts +79 -0
  41. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  42. package/dist/flags-0Vl5rrHq.d.cts +41 -0
  43. package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
  44. package/dist/flags-Ct8k0gpB.cjs +43 -0
  45. package/dist/flags-ZtlvOIIk.js +33 -0
  46. package/dist/flags-ZtlvOIIk.js.map +1 -0
  47. package/dist/flags-wmcMoAnN.d.ts +41 -0
  48. package/dist/flags-wmcMoAnN.d.ts.map +1 -0
  49. package/dist/index-Cgw1u5aY.d.cts +278 -0
  50. package/dist/index-Cgw1u5aY.d.cts.map +1 -0
  51. package/dist/index-ClgIuxJ8.d.ts +159 -0
  52. package/dist/index-ClgIuxJ8.d.ts.map +1 -0
  53. package/dist/index-D0CmtiWT.d.cts +159 -0
  54. package/dist/index-D0CmtiWT.d.cts.map +1 -0
  55. package/dist/index-D8LZt-3J.d.cts +417 -0
  56. package/dist/index-D8LZt-3J.d.cts.map +1 -0
  57. package/dist/index-DVYFaXNV.d.ts +417 -0
  58. package/dist/index-DVYFaXNV.d.ts.map +1 -0
  59. package/dist/index-VCqgn29D.d.ts +278 -0
  60. package/dist/index-VCqgn29D.d.ts.map +1 -0
  61. package/dist/index.cjs +676 -375
  62. package/dist/index.d.cts +247 -208
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +247 -208
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -368
  67. package/dist/index.js.map +1 -1
  68. package/dist/payload-Bnw1hrvb.cjs +83 -0
  69. package/dist/payload-CIxSn_DW.js +67 -0
  70. package/dist/payload-CIxSn_DW.js.map +1 -0
  71. package/dist/payload-Dgx3PQeK.d.ts +94 -0
  72. package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
  73. package/dist/payload-P0nWyjLg.d.cts +94 -0
  74. package/dist/payload-P0nWyjLg.d.cts.map +1 -0
  75. package/dist/query/index.cjs +24 -150
  76. package/dist/query/index.d.cts +5 -3
  77. package/dist/query/index.d.ts +5 -3
  78. package/dist/query/index.js +6 -138
  79. package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
  80. package/dist/queryBuilder-CbLL4pG5.js +963 -0
  81. package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
  82. package/dist/selection-BvK6yX0g.d.cts +433 -0
  83. package/dist/selection-BvK6yX0g.d.cts.map +1 -0
  84. package/dist/selection-fpmI3ADQ.d.ts +433 -0
  85. package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
  86. package/dist/types/index.cjs +3 -3
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +3 -3
  90. package/dist/utils/index.cjs +4 -17
  91. package/dist/utils/index.d.cts +3 -17
  92. package/dist/utils/index.d.ts +3 -17
  93. package/dist/utils/index.js +4 -18
  94. package/package.json +26 -5
  95. package/src/actions/public/getEntity.ts +27 -43
  96. package/src/actions/public/getEntityNonce.ts +30 -0
  97. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  98. package/src/actions/public/predictEntityKeys.ts +138 -0
  99. package/src/actions/public/query.ts +33 -69
  100. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  101. package/src/actions/public/watchEntityEvents.ts +225 -0
  102. package/src/actions/wallet/changeOwnership.ts +2 -2
  103. package/src/actions/wallet/createEntity.ts +99 -24
  104. package/src/actions/wallet/extendEntity.ts +37 -13
  105. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  106. package/src/actions/wallet/mutateEntities.ts +19 -21
  107. package/src/actions/wallet/patchEntity.ts +90 -0
  108. package/src/attr/attributes.test.ts +120 -0
  109. package/src/attr/attributes.ts +190 -0
  110. package/src/attr/codec.test.ts +162 -0
  111. package/src/attr/codec.ts +220 -0
  112. package/src/attr/decimal.ts +86 -0
  113. package/src/attr/errors.ts +145 -0
  114. package/src/attr/index.ts +50 -0
  115. package/src/attr/names.ts +106 -0
  116. package/src/attr/types.ts +150 -0
  117. package/src/attr/values.test.ts +271 -0
  118. package/src/attr/values.ts +502 -0
  119. package/src/chains/cheesecake.ts +22 -0
  120. package/src/chains/index.ts +1 -2
  121. package/src/clients/createPublicClient.test.ts +3 -5
  122. package/src/clients/createPublicClient.ts +5 -4
  123. package/src/clients/createWalletClient.test.ts +2 -2
  124. package/src/clients/createWalletClient.ts +13 -7
  125. package/src/clients/decorators/arkivPublic.ts +160 -139
  126. package/src/clients/decorators/arkivWallet.ts +86 -94
  127. package/src/entity/create.test.ts +184 -0
  128. package/src/entity/errors.ts +38 -0
  129. package/src/entity/events.ts +16 -0
  130. package/src/entity/expiry.test.ts +195 -0
  131. package/src/entity/expiry.ts +181 -0
  132. package/src/entity/flags.ts +65 -0
  133. package/src/entity/index.ts +14 -0
  134. package/src/entity/key.ts +68 -0
  135. package/src/entity/operations.ts +134 -0
  136. package/src/entity/params.ts +15 -0
  137. package/src/errors.ts +26 -49
  138. package/src/index.ts +6 -0
  139. package/src/query/engine.ts +59 -139
  140. package/src/query/errors.ts +151 -0
  141. package/src/query/expression.test.ts +286 -0
  142. package/src/query/expression.ts +493 -0
  143. package/src/query/index.ts +40 -4
  144. package/src/query/injection.test.ts +151 -0
  145. package/src/query/queryBuilder.test.ts +322 -637
  146. package/src/query/queryBuilder.ts +126 -359
  147. package/src/query/queryResult.ts +54 -51
  148. package/src/query/selection.test.ts +48 -98
  149. package/src/query/selection.ts +77 -65
  150. package/src/types/entity.test.ts +44 -155
  151. package/src/types/entity.ts +60 -52
  152. package/src/types/events.ts +59 -16
  153. package/src/types/index.ts +20 -17
  154. package/src/types/rpcSchema.ts +69 -37
  155. package/src/utils/arkivTransactions.test.ts +600 -155
  156. package/src/utils/arkivTransactions.ts +181 -190
  157. package/src/utils/chains.test.ts +21 -24
  158. package/src/utils/compression.ts +1 -1
  159. package/src/utils/entities.test.ts +62 -0
  160. package/src/utils/entities.ts +109 -37
  161. package/src/utils/expirationTime.ts +114 -10
  162. package/src/utils/logger.ts +2 -2
  163. package/src/utils/revert.test.ts +99 -0
  164. package/src/utils/revert.ts +145 -0
  165. package/dist/chains-BifaY04V.cjs +0 -108
  166. package/dist/chains-CrtkVbBh.js +0 -86
  167. package/dist/chains-CrtkVbBh.js.map +0 -1
  168. package/dist/consts-Bz0LZQZY.js +0 -7
  169. package/dist/consts-Bz0LZQZY.js.map +0 -1
  170. package/dist/consts-CxT1-RXU.cjs +0 -18
  171. package/dist/entity-BhSXaB23.cjs +0 -81
  172. package/dist/entity-CN41IOrq.d.cts +0 -401
  173. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  174. package/dist/entity-DGfVyB-a.js +0 -71
  175. package/dist/entity-DGfVyB-a.js.map +0 -1
  176. package/dist/entity-NnEVlePN.d.ts +0 -401
  177. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  178. package/dist/index-ClkpS6s_.d.ts +0 -481
  179. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  180. package/dist/index-w4qop0Pe.d.cts +0 -481
  181. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  182. package/dist/payload-BpNUPOnY.js +0 -13
  183. package/dist/payload-BpNUPOnY.js.map +0 -1
  184. package/dist/payload-C2KHOUoj.d.ts +0 -6
  185. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  186. package/dist/payload-CJN8fTvX.d.cts +0 -6
  187. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  188. package/dist/payload-D8YLKcJj.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  191. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  192. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  193. package/dist/utils/index.d.cts.map +0 -1
  194. package/dist/utils/index.d.ts.map +0 -1
  195. package/dist/utils/index.js.map +0 -1
  196. package/src/actions/public/buildQuery.ts +0 -10
  197. package/src/actions/public/subscribeEntityEvents.ts +0 -93
  198. package/src/actions/wallet/updateEntity.ts +0 -59
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -571
  202. package/src/query/predicate.test.ts +0 -461
  203. package/src/query/predicate.ts +0 -144
  204. package/src/query/queryResult.test.ts +0 -270
  205. package/src/query/selectQueryBuilder.test.ts +0 -213
  206. package/src/types/attributes.ts +0 -28
  207. package/src/utils/validation.test.ts +0 -106
  208. package/src/utils/validation.ts +0 -80
  209. /package/dist/{types-Ci4BRClw.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-a8gm_IaQ.cjs → types-CKPddlfS.cjs} +0 -0
@@ -2,31 +2,40 @@ import {
2
2
  type Address,
3
3
  ContractFunctionExecutionError,
4
4
  ContractFunctionRevertedError,
5
- encodePacked,
5
+ decodeEventLog,
6
6
  type Hex,
7
- keccak256,
8
7
  parseAbi,
9
- stringToBytes,
10
8
  TransactionExecutionError,
11
9
  type TransactionReceipt,
12
- toBytes,
13
- toHex,
14
10
  } from "viem"
15
11
  import type { ChangeOwnershipParameters } from "../actions/wallet/changeOwnership"
16
12
  import type { CreateEntityParameters } from "../actions/wallet/createEntity"
17
13
  import type { DeleteEntityParameters } from "../actions/wallet/deleteEntity"
18
14
  import type { ExtendEntityParameters } from "../actions/wallet/extendEntity"
19
- import type { UpdateEntityParameters } from "../actions/wallet/updateEntity"
15
+ import type { PatchEntityParameters } from "../actions/wallet/patchEntity"
16
+ // Internal seam: the ABI encoders are not part of the package's public surface.
17
+ import { encodeAttributes, encodeMutations } from "../attr/attributes"
20
18
  import type { ArkivClient } from "../clients/baseClient"
21
19
  import type { WalletArkivClient } from "../clients/createWalletClient"
22
- import { ARKIV_ADDRESS, BLOCK_TIME } from "../consts"
23
- import { DuplicateAttributeError, EntityMutationError, InvalidContentTypeError } from "../errors"
20
+ import { ARKIV_ADDRESS } from "../consts"
21
+ import { ENTITY_EVENTS_ABI } from "../entity/events"
22
+ import { resolveExpiry } from "../entity/expiry"
23
+ import { encodeCreationFlags } from "../entity/flags"
24
+ import { randomSalt, validateSalt } from "../entity/key"
25
+ import {
26
+ createOperation,
27
+ deleteOperation,
28
+ EXECUTE_ABI as EXECUTE_FUNCTION_ABI,
29
+ extendExpiryOperation,
30
+ type Operation,
31
+ patchOperation,
32
+ transferOwnershipOperation,
33
+ } from "../entity/operations"
34
+ import { EmptyPatchError, EntityMutationError, InvalidContentTypeError } from "../errors"
24
35
  import type { TxParams } from "../types"
25
- import { EntityOperationType } from "../types/entity"
26
- import { RpcAttributeValueType as AttributeValueType } from "../types/rpcSchema"
27
36
 
28
37
  import { getLogger } from "./logger"
29
- import { isEntityKey, validateAttribute, validateExpiresIn } from "./validation"
38
+ import { describeEntityRevert } from "./revert"
30
39
 
31
40
  const logger = getLogger("utils:arkiv-transactions")
32
41
 
@@ -39,137 +48,54 @@ function validateContentType(contentType: string): void {
39
48
  }
40
49
  }
41
50
 
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
51
  export const ENTITY_ERRORS_ABI = parseAbi([
52
+ // Batch and operation framing.
54
53
  "error EmptyBatch()",
54
+ "error InvalidOpType(uint8 operation)",
55
+ "error NonCanonicalOperationData(uint8 operation)",
56
+ // The attribute triple list, structural.
55
57
  "error AttributesNotSorted()",
56
- "error InvalidValueType(bytes32 name, uint8 valueType)",
57
- "error InvalidOpType(uint8 operationType)",
58
- "error ExpiryInPast(uint32 expiresAt, uint32 currentBlock)",
59
58
  "error TooManyAttributes(uint256 count, uint256 maxCount)",
59
+ "error EmptyMutations(bytes32 entityKey)",
60
+ "error TombstoneInCreate(bytes32 name)",
61
+ "error TombstoneValueNotEmpty(bytes32 name)",
62
+ "error SystemAttributeNotWritable(bytes32 name)",
63
+ "error InvalidValueType(bytes32 name, uint8 typeId)",
64
+ // Attribute-name validation. `Ident32` is a bytes32 on the wire.
65
+ "error Ident32Empty()",
66
+ "error Ident32InvalidByte(uint256 position, bytes1 value)",
67
+ // Entity state and authorisation.
60
68
  "error EntityNotFound(bytes32 entityKey)",
69
+ "error EntityExpired(bytes32 entityKey, uint64 expiresAt)",
61
70
  "error NotOwner(bytes32 entityKey, address caller, address owner)",
62
- "error EntityExpired(bytes32 entityKey, uint32 expiresAt)",
63
- "error ExpiryNotExtended(bytes32 entityKey, uint32 newExpiresAt, uint32 currentExpiresAt)",
71
+ "error ReadOnlyEntity(bytes32 entityKey)",
72
+ "error ReservedCreationFlags(uint8 creationFlags)",
73
+ // Expiry. The engine bounds an expiry only by the width of the field and by liveness — there is
74
+ // no MAX_EXPIRES_AT or MAX_LIFETIME, so there is no error for exceeding one.
75
+ "error ExpiryDeadOnArrival(uint64 target, uint64 currentBlock)",
76
+ "error ExpiryNotExtended(bytes32 entityKey, uint64 newExpiresAt, uint64 currentExpiresAt)",
77
+ // Ownership transfer.
64
78
  "error TransferToZeroAddress(bytes32 entityKey)",
65
79
  "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
80
  ])
75
81
 
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
- }
82
+ const EXECUTE_ABI = [...EXECUTE_FUNCTION_ABI, ...ENTITY_ERRORS_ABI]
162
83
 
163
84
  export type SendArkivTransactionResult = {
164
85
  receipt: TransactionReceipt
86
+ /** The keys the engine minted for the entities this transaction created, in batch order. */
165
87
  createdEntityKeys: Hex[]
88
+ /** The block each created entity expires at, as the engine recorded it, in the same order. */
89
+ createdExpiries: bigint[]
90
+ /** The block each extended entity now expires at, as the engine recorded it, in batch order. */
91
+ extendedExpiries: bigint[]
166
92
  }
167
93
 
168
94
  export async function sendArkivTransaction(
169
95
  client: ArkivClient,
170
96
  ops: {
171
97
  creates?: CreateEntityParameters[]
172
- updates?: UpdateEntityParameters[]
98
+ patches?: PatchEntityParameters[]
173
99
  deletes?: DeleteEntityParameters[]
174
100
  extensions?: ExtendEntityParameters[]
175
101
  ownershipChanges?: ChangeOwnershipParameters[]
@@ -180,76 +106,67 @@ export async function sendArkivTransaction(
180
106
  if (!client.chain) throw new Error("Chain required")
181
107
  const walletClient = client as WalletArkivClient
182
108
 
183
- const { creates, updates, deletes, extensions, ownershipChanges } = ops
184
- const owner = client.account.address as Address
109
+ const { creates, patches, deletes, extensions, ownershipChanges } = ops
185
110
 
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
111
+ // The block height, to resolve any relative expiry — which both creates and extensions carry.
112
+ // Skipped entirely for a batch with neither.
113
+ const currentBlock =
114
+ creates?.length || extensions?.length ? await walletClient.getBlockNumber() : 0n
196
115
 
197
- const createdEntityKeys: Hex[] = (creates ?? []).map((_, i) =>
198
- deriveEntityKey(client.chain!.id, owner, ownerNonce + BigInt(i)),
199
- )
116
+ const expiryContext = { currentBlock }
117
+
118
+ const createOps = (creates ?? []).map((item) => {
119
+ validateContentType(item.contentType)
200
120
 
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
- })),
121
+ const salt = item.salt === undefined ? randomSalt() : validateSalt(item.salt)
122
+ const expiry = resolveExpiry(item.expires, expiryContext)
123
+
124
+ return createOperation({
125
+ salt,
126
+ expiry,
127
+ creationFlags: encodeCreationFlags(item.flags),
128
+ attributes: encodeAttributes(item.attributes, {
129
+ payload: item.payload,
130
+ contentType: item.contentType,
131
+ }),
132
+ })
133
+ })
134
+
135
+ const patchOps = (patches ?? []).map((item) => {
136
+ if (item.contentType !== undefined) validateContentType(item.contentType)
137
+ const mutations = encodeMutations(item)
138
+ if (mutations.length === 0) {
139
+ throw new EmptyPatchError(item.entityKey)
140
+ }
141
+ return patchOperation({ entityKey: item.entityKey, mutations })
142
+ })
143
+
144
+ // An extension resolves its expiry exactly as a create does — the engine applies the same
145
+ // `max(expiresAt, currentBlock + minLifetime)` to both — so the same bounds are checked here
146
+ // rather than left to a revert.
147
+ const extendOps = (extensions ?? []).map((item) => {
148
+ const expiry = resolveExpiry(item.expires, expiryContext)
149
+ return extendExpiryOperation({ entityKey: item.entityKey, expiry })
150
+ })
151
+
152
+ const operations: Operation[] = [
153
+ ...createOps,
154
+ ...patchOps,
155
+ ...(deletes ?? []).map((item) => deleteOperation({ entityKey: item.entityKey })),
156
+ ...extendOps,
157
+ ...(ownershipChanges ?? []).map((item) =>
158
+ transferOwnershipOperation({
159
+ entityKey: item.entityKey,
160
+ newOwner: item.newOwner as Address,
161
+ }),
162
+ ),
250
163
  ]
251
164
 
252
- logger("Sending execute with %d operations %s", operations.length, JSON.stringify(operations))
165
+ if (operations.length === 0) {
166
+ throw new Error("No operations to perform")
167
+ }
168
+
169
+ logger("Sending execute with %d operations %o", operations.length, operations)
253
170
 
254
171
  try {
255
172
  const txHash = await walletClient.writeContract({
@@ -278,7 +195,10 @@ export async function sendArkivTransaction(
278
195
  } catch (err) {
279
196
  const error = err as { shortMessage?: string; cause?: { details?: string } }
280
197
  const reason =
281
- error.shortMessage ?? error.cause?.details ?? "No reason provided by backend."
198
+ describeEntityRevert(err) ??
199
+ error.shortMessage ??
200
+ error.cause?.details ??
201
+ "No reason provided by backend."
282
202
  throw new EntityMutationError(
283
203
  `Transaction ${receipt.transactionHash} reverted. Reason: ${reason}`,
284
204
  )
@@ -288,8 +208,19 @@ export async function sendArkivTransaction(
288
208
  )
289
209
  }
290
210
 
291
- return { receipt, createdEntityKeys }
211
+ return {
212
+ receipt,
213
+ ...readAppliedOperations(receipt, {
214
+ creates: createOps.length,
215
+ extensions: extendOps.length,
216
+ }),
217
+ }
292
218
  } catch (error) {
219
+ const described = error instanceof EntityMutationError ? undefined : describeEntityRevert(error)
220
+ if (described !== undefined) {
221
+ throw new EntityMutationError(`Transaction failed: ${described}`, { cause: error })
222
+ }
223
+
293
224
  let message = "Transaction failed"
294
225
  if (error instanceof TransactionExecutionError) {
295
226
  message += `: ${error.details}`
@@ -302,8 +233,68 @@ export async function sendArkivTransaction(
302
233
  }
303
234
  } else if (error instanceof EntityMutationError) {
304
235
  throw error
236
+ } else if (error instanceof Error) {
237
+ message += `: ${error.message}`
238
+ }
239
+
240
+ throw new EntityMutationError(message, { cause: error })
241
+ }
242
+ }
243
+
244
+ /**
245
+ * The keys and expiries the engine recorded, read back from the events the batch emitted.
246
+ */
247
+ function readAppliedOperations(
248
+ receipt: TransactionReceipt,
249
+ expected: { creates: number; extensions: number },
250
+ ): Omit<SendArkivTransactionResult, "receipt"> {
251
+ const createdEntityKeys: Hex[] = []
252
+ const createdExpiries: bigint[] = []
253
+ const extendedExpiries: bigint[] = []
254
+
255
+ // `?? []` so a receipt without logs is reported by the count check below, which explains what is
256
+ // missing and that the write succeeded, rather than as a TypeError from this loop.
257
+ for (const log of receipt.logs ?? []) {
258
+ // Only the operation address emits these, and a batch's transaction can carry logs from
259
+ // anything else it touched.
260
+ if (log.address.toLowerCase() !== ARKIV_ADDRESS.toLowerCase()) continue
261
+ const decoded = decodeEntityLog(log)
262
+ if (decoded === undefined) continue
263
+
264
+ if (decoded.eventName === "EntityCreated") {
265
+ createdEntityKeys.push(decoded.args.entityKey)
266
+ createdExpiries.push(decoded.args.expiresAt)
267
+ } else if (decoded.eventName === "ExpiryExtended") {
268
+ extendedExpiries.push(decoded.args.expiresAt)
305
269
  }
270
+ }
271
+
272
+ // Operations apply in batch order and emit one event each, and creates lead the batch — so these
273
+ // arrive already aligned with the parameters they came from. A count that disagrees means that
274
+ // assumption broke, and a misaligned array would hand back a key belonging to another entity.
275
+ assertCount(receipt, "create", createdEntityKeys.length, expected.creates)
276
+ assertCount(receipt, "extension", extendedExpiries.length, expected.extensions)
277
+
278
+ return { createdEntityKeys, createdExpiries, extendedExpiries }
279
+ }
306
280
 
307
- throw new EntityMutationError(message)
281
+ /** One log as an entity event, or `undefined` for anything this SDK has no name for. */
282
+ function decodeEntityLog(log: TransactionReceipt["logs"][number]) {
283
+ try {
284
+ return decodeEventLog({ abi: ENTITY_EVENTS_ABI, topics: log.topics, data: log.data })
285
+ } catch {
286
+ // An event a newer engine emits and this ABI does not describe. Not this function's business.
287
+ return undefined
308
288
  }
309
289
  }
290
+
291
+ function assertCount(receipt: TransactionReceipt, what: string, got: number, want: number): void {
292
+ if (got === want) return
293
+ // Deliberately explicit that the write succeeded: this throws *after* a successful transaction,
294
+ // and a caller who reads it as a failure and retries would create the entities a second time.
295
+ throw new EntityMutationError(
296
+ `Transaction ${receipt.transactionHash} succeeded — do not retry it — but emitted ${got} ` +
297
+ `${what} event(s) for ${want} ${what} operation(s). The batch was applied; the SDK cannot ` +
298
+ `say which entity each operation produced, so read them back to find them.`,
299
+ )
300
+ }
@@ -1,21 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test"
2
- import { braga, kaolin, localhost } from "../chains"
2
+ import { cheesecake, localhost } from "../chains"
3
3
  import { chainFromName } from "./chains"
4
4
 
5
5
  describe("chainFromName", () => {
6
6
  describe("valid chain names", () => {
7
- test("returns braga chain for 'braga'", () => {
8
- const chain = chainFromName("braga")
9
- expect(chain).toBe(braga)
10
- expect(chain.id).toBe(60138453102)
11
- expect(chain.name).toBe("Braga")
12
- })
13
-
14
- test("returns kaolin chain for 'kaolin'", () => {
15
- const chain = chainFromName("kaolin")
16
- expect(chain).toBe(kaolin)
17
- expect(chain.id).toBe(60138453025)
18
- expect(chain.name).toBe("Kaolin")
7
+ test("returns cheesecake chain for 'cheesecake'", () => {
8
+ const chain = chainFromName("cheesecake")
9
+ expect(chain).toBe(cheesecake)
10
+ expect(chain.id).toBe(7733102)
11
+ expect(chain.name).toBe("Cheesecake")
19
12
  })
20
13
 
21
14
  test("returns localhost chain for 'localhost'", () => {
@@ -28,17 +21,14 @@ describe("chainFromName", () => {
28
21
 
29
22
  describe("case insensitivity", () => {
30
23
  test("handles uppercase chain names", () => {
31
- expect(chainFromName("BRAGA")).toBe(braga)
32
- expect(chainFromName("KAOLIN")).toBe(kaolin)
24
+ expect(chainFromName("CHEESECAKE")).toBe(cheesecake)
33
25
  expect(chainFromName("LOCALHOST")).toBe(localhost)
34
26
  })
35
27
 
36
28
  test("handles mixed case chain names", () => {
37
- expect(chainFromName("Braga")).toBe(braga)
38
- expect(chainFromName("Kaolin")).toBe(kaolin)
29
+ expect(chainFromName("Cheesecake")).toBe(cheesecake)
39
30
  expect(chainFromName("Localhost")).toBe(localhost)
40
- expect(chainFromName("bRaGa")).toBe(braga)
41
- expect(chainFromName("kAoLiN")).toBe(kaolin)
31
+ expect(chainFromName("cHeEsEcAkE")).toBe(cheesecake)
42
32
  })
43
33
  })
44
34
 
@@ -52,22 +42,28 @@ describe("chainFromName", () => {
52
42
  })
53
43
 
54
44
  test("throws error for chain name with whitespace", () => {
55
- expect(() => chainFromName("kaolin ")).toThrow("Unknown chain: kaolin ")
56
- expect(() => chainFromName(" kaolin")).toThrow("Unknown chain: kaolin")
45
+ expect(() => chainFromName("cheesecake ")).toThrow("Unknown chain: cheesecake ")
46
+ expect(() => chainFromName(" cheesecake")).toThrow("Unknown chain: cheesecake")
57
47
  })
58
48
 
59
49
  test("throws error for similar but incorrect chain names", () => {
60
- expect(() => chainFromName("kaolinn")).toThrow("Unknown chain: kaolinn")
50
+ expect(() => chainFromName("cheesecakee")).toThrow("Unknown chain: cheesecakee")
51
+ expect(() => chainFromName("cheesecak")).toThrow("Unknown chain: cheesecak")
61
52
  expect(() => chainFromName("mendoza")).toThrow("Unknown chain: mendoza")
62
53
  expect(() => chainFromName("marketplace")).toThrow("Unknown chain: marketplace")
63
54
  expect(() => chainFromName("rosario")).toThrow("Unknown chain: rosario")
64
55
  expect(() => chainFromName("local")).toThrow("Unknown chain: local")
65
56
  })
57
+
58
+ test("throws error for the retired chains", () => {
59
+ expect(() => chainFromName("braga")).toThrow("Unknown chain: braga")
60
+ expect(() => chainFromName("kaolin")).toThrow("Unknown chain: kaolin")
61
+ })
66
62
  })
67
63
 
68
64
  describe("chain properties", () => {
69
65
  test("returned chain has required viem Chain properties", () => {
70
- const chain = chainFromName("kaolin")
66
+ const chain = chainFromName("cheesecake")
71
67
 
72
68
  expect(chain).toHaveProperty("id")
73
69
  expect(chain).toHaveProperty("name")
@@ -79,12 +75,13 @@ describe("chainFromName", () => {
79
75
  })
80
76
 
81
77
  test("returned chain has rpcUrls configured", () => {
82
- const chain = chainFromName("braga")
78
+ const chain = chainFromName("cheesecake")
83
79
 
84
80
  expect(chain.rpcUrls).toBeDefined()
85
81
  expect(chain.rpcUrls.default).toBeDefined()
86
82
  expect(chain.rpcUrls.default.http).toBeInstanceOf(Array)
87
83
  expect(chain.rpcUrls.default.http.length).toBeGreaterThan(0)
84
+ expect(chain.rpcUrls.default.webSocket?.length).toBeGreaterThan(0)
88
85
  })
89
86
  })
90
87
  })
@@ -17,7 +17,7 @@ async function getZlib(): Promise<typeof import("zlib") | null> {
17
17
 
18
18
  if (!zlib) {
19
19
  try {
20
- zlib = await import("zlib")
20
+ zlib = await import("node:zlib")
21
21
  } catch (error) {
22
22
  console.error("Error importing zlib", error)
23
23
  return null
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { RpcEntity } from "../types/rpcSchema"
3
+ import { entityFromRpcResult } from "./entities"
4
+
5
+ const KEY = `0x${"aa".repeat(32)}` as const
6
+
7
+ /** A minimal row, with only the field under test varying. */
8
+ function rowWith(creationFlags: RpcEntity["creationFlags"]): RpcEntity {
9
+ return { key: KEY, creationFlags }
10
+ }
11
+
12
+ describe("the creationFlags projection", () => {
13
+ it("decodes the bare byte a pre-spec node sends", () => {
14
+ expect(entityFromRpcResult(rowWith(0b11)).creationFlags).toEqual({
15
+ readonly: true,
16
+ permissionlessExtension: true,
17
+ raw: 0b11,
18
+ })
19
+ })
20
+
21
+ it("prefers raw over the booleans sent alongside it", () => {
22
+ // `raw` is the byte the engine stored, so it is the one reading that cannot disagree with what
23
+ // is on chain — and it keeps bits this SDK has no name for visible.
24
+ const flags = entityFromRpcResult(
25
+ rowWith({ readonly: false, permissionlessExtension: false, raw: 0b101 }),
26
+ ).creationFlags
27
+ expect(flags).toEqual({ readonly: true, permissionlessExtension: false, raw: 0b101 })
28
+ })
29
+
30
+ it("reads raw as the 0x quantity every other numeric field already uses", () => {
31
+ // createdAt, updatedAt and expiresAt are all hex on this surface, so a node rendering the flags
32
+ // byte the same way is the expected spelling, not a malformed response.
33
+ expect(
34
+ entityFromRpcResult(rowWith({ raw: "0x03" } as unknown as RpcEntity["creationFlags"]))
35
+ .creationFlags,
36
+ ).toEqual({ readonly: true, permissionlessExtension: true, raw: 3 })
37
+ })
38
+
39
+ it("falls back to the booleans when no byte arrives", () => {
40
+ expect(
41
+ entityFromRpcResult(
42
+ rowWith({ readonly: true, permissionlessExtension: false } as RpcEntity["creationFlags"]),
43
+ ).creationFlags,
44
+ ).toEqual({ readonly: true, permissionlessExtension: false, raw: 0b1 })
45
+ })
46
+
47
+ it("drops an unreadable encoding instead of failing the whole read", () => {
48
+ // Nothing on the decode path catches, so a throw here would not fail this field — it would
49
+ // fail the entire getEntity/fetch and take every other entity on the page with it.
50
+ for (const unreadable of [{}, { raw: "nonsense" }, { raw: 999 }, "flags"]) {
51
+ const entity = entityFromRpcResult(
52
+ rowWith(unreadable as unknown as RpcEntity["creationFlags"]),
53
+ )
54
+ expect(entity.creationFlags).toBeUndefined()
55
+ expect(entity.key).toBe(KEY)
56
+ }
57
+ })
58
+
59
+ it("stays undefined when the projection did not ask for it", () => {
60
+ expect(entityFromRpcResult({ key: KEY }).creationFlags).toBeUndefined()
61
+ })
62
+ })