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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +29 -16
  2. package/dist/attr/index.cjs +34 -0
  3. package/dist/attr/index.d.cts +2 -0
  4. package/dist/attr/index.d.ts +2 -0
  5. package/dist/attr/index.js +3 -0
  6. package/dist/attr-BDZ-APg5.js +809 -0
  7. package/dist/attr-BDZ-APg5.js.map +1 -0
  8. package/dist/attr-BvxHN99W.cjs +1029 -0
  9. package/dist/chains/index.cjs +2 -3
  10. package/dist/chains/index.d.cts +39 -77
  11. package/dist/chains/index.d.cts.map +1 -1
  12. package/dist/chains/index.d.ts +22 -60
  13. package/dist/chains/index.d.ts.map +1 -1
  14. package/dist/chains/index.js +2 -2
  15. package/dist/chains-BJrZs6sG.js +57 -0
  16. package/dist/chains-BJrZs6sG.js.map +1 -0
  17. package/dist/chains-BYRTTipa.cjs +73 -0
  18. package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
  19. package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
  20. package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
  21. package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
  22. package/dist/entity/index.cjs +19 -0
  23. package/dist/entity/index.d.cts +4 -0
  24. package/dist/entity/index.d.ts +4 -0
  25. package/dist/entity/index.js +6 -0
  26. package/dist/entity-B0t6sN7F.cjs +71 -0
  27. package/dist/entity-BIp_df9u.js +67 -0
  28. package/dist/entity-BIp_df9u.js.map +1 -0
  29. package/dist/entity-CFhbAv0R.js +70 -0
  30. package/dist/entity-CFhbAv0R.js.map +1 -0
  31. package/dist/entity-wfBnZd91.cjs +98 -0
  32. package/dist/errors-D-eJkGVc.cjs +56 -0
  33. package/dist/errors-DGB8AgT2.js +33 -0
  34. package/dist/errors-DGB8AgT2.js.map +1 -0
  35. package/dist/expiry-BUYIXTGb.cjs +135 -0
  36. package/dist/expiry-BxOlvVWC.d.ts +79 -0
  37. package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
  38. package/dist/expiry-DGRzrSan.js +95 -0
  39. package/dist/expiry-DGRzrSan.js.map +1 -0
  40. package/dist/expiry-DRV29F1r.d.cts +79 -0
  41. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  42. package/dist/flags-0Vl5rrHq.d.cts +41 -0
  43. package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
  44. package/dist/flags-Ct8k0gpB.cjs +43 -0
  45. package/dist/flags-ZtlvOIIk.js +33 -0
  46. package/dist/flags-ZtlvOIIk.js.map +1 -0
  47. package/dist/flags-wmcMoAnN.d.ts +41 -0
  48. package/dist/flags-wmcMoAnN.d.ts.map +1 -0
  49. package/dist/index-Cgw1u5aY.d.cts +278 -0
  50. package/dist/index-Cgw1u5aY.d.cts.map +1 -0
  51. package/dist/index-ClgIuxJ8.d.ts +159 -0
  52. package/dist/index-ClgIuxJ8.d.ts.map +1 -0
  53. package/dist/index-D0CmtiWT.d.cts +159 -0
  54. package/dist/index-D0CmtiWT.d.cts.map +1 -0
  55. package/dist/index-D8LZt-3J.d.cts +417 -0
  56. package/dist/index-D8LZt-3J.d.cts.map +1 -0
  57. package/dist/index-DVYFaXNV.d.ts +417 -0
  58. package/dist/index-DVYFaXNV.d.ts.map +1 -0
  59. package/dist/index-VCqgn29D.d.ts +278 -0
  60. package/dist/index-VCqgn29D.d.ts.map +1 -0
  61. package/dist/index.cjs +675 -357
  62. package/dist/index.d.cts +251 -203
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +251 -203
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -355
  67. package/dist/index.js.map +1 -1
  68. package/dist/payload-Bnw1hrvb.cjs +83 -0
  69. package/dist/payload-CIxSn_DW.js +67 -0
  70. package/dist/payload-CIxSn_DW.js.map +1 -0
  71. package/dist/payload-Dgx3PQeK.d.ts +94 -0
  72. package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
  73. package/dist/payload-P0nWyjLg.d.cts +94 -0
  74. package/dist/payload-P0nWyjLg.d.cts.map +1 -0
  75. package/dist/query/index.cjs +24 -149
  76. package/dist/query/index.d.cts +5 -3
  77. package/dist/query/index.d.ts +5 -3
  78. package/dist/query/index.js +6 -137
  79. package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
  80. package/dist/queryBuilder-CbLL4pG5.js +963 -0
  81. package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
  82. package/dist/selection-BvK6yX0g.d.cts +433 -0
  83. package/dist/selection-BvK6yX0g.d.cts.map +1 -0
  84. package/dist/selection-fpmI3ADQ.d.ts +433 -0
  85. package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
  86. package/dist/types/index.cjs +4 -1
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +4 -1
  90. package/dist/utils/index.cjs +4 -17
  91. package/dist/utils/index.d.cts +3 -17
  92. package/dist/utils/index.d.ts +3 -17
  93. package/dist/utils/index.js +4 -18
  94. package/package.json +26 -5
  95. package/src/actions/public/getEntity.ts +27 -43
  96. package/src/actions/public/getEntityNonce.ts +30 -0
  97. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  98. package/src/actions/public/predictEntityKeys.ts +138 -0
  99. package/src/actions/public/query.ts +33 -69
  100. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  101. package/src/actions/public/watchEntityEvents.ts +225 -0
  102. package/src/actions/wallet/changeOwnership.ts +8 -8
  103. package/src/actions/wallet/createEntity.ts +104 -26
  104. package/src/actions/wallet/deleteEntity.ts +2 -3
  105. package/src/actions/wallet/extendEntity.ts +39 -16
  106. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  107. package/src/actions/wallet/mutateEntities.ts +29 -74
  108. package/src/actions/wallet/patchEntity.ts +90 -0
  109. package/src/attr/attributes.test.ts +120 -0
  110. package/src/attr/attributes.ts +190 -0
  111. package/src/attr/codec.test.ts +162 -0
  112. package/src/attr/codec.ts +220 -0
  113. package/src/attr/decimal.ts +86 -0
  114. package/src/attr/errors.ts +145 -0
  115. package/src/attr/index.ts +50 -0
  116. package/src/attr/names.ts +106 -0
  117. package/src/attr/types.ts +150 -0
  118. package/src/attr/values.test.ts +271 -0
  119. package/src/attr/values.ts +502 -0
  120. package/src/chains/cheesecake.ts +22 -0
  121. package/src/chains/index.ts +1 -2
  122. package/src/clients/createPublicClient.test.ts +3 -5
  123. package/src/clients/createPublicClient.ts +5 -4
  124. package/src/clients/createWalletClient.test.ts +2 -2
  125. package/src/clients/createWalletClient.ts +13 -7
  126. package/src/clients/decorators/arkivPublic.ts +160 -139
  127. package/src/clients/decorators/arkivWallet.ts +91 -95
  128. package/src/consts.ts +2 -1
  129. package/src/entity/create.test.ts +184 -0
  130. package/src/entity/errors.ts +38 -0
  131. package/src/entity/events.ts +16 -0
  132. package/src/entity/expiry.test.ts +195 -0
  133. package/src/entity/expiry.ts +181 -0
  134. package/src/entity/flags.ts +65 -0
  135. package/src/entity/index.ts +14 -0
  136. package/src/entity/key.ts +68 -0
  137. package/src/entity/operations.ts +134 -0
  138. package/src/entity/params.ts +15 -0
  139. package/src/errors.ts +33 -28
  140. package/src/index.ts +6 -0
  141. package/src/query/engine.ts +59 -134
  142. package/src/query/errors.ts +151 -0
  143. package/src/query/expression.test.ts +286 -0
  144. package/src/query/expression.ts +493 -0
  145. package/src/query/index.ts +40 -4
  146. package/src/query/injection.test.ts +151 -0
  147. package/src/query/queryBuilder.test.ts +322 -637
  148. package/src/query/queryBuilder.ts +126 -359
  149. package/src/query/queryResult.ts +54 -51
  150. package/src/query/selection.test.ts +48 -98
  151. package/src/query/selection.ts +77 -65
  152. package/src/types/entity.test.ts +44 -155
  153. package/src/types/entity.ts +62 -44
  154. package/src/types/events.ts +64 -23
  155. package/src/types/index.ts +20 -16
  156. package/src/types/rpcSchema.ts +70 -33
  157. package/src/utils/arkivTransactions.test.ts +681 -38
  158. package/src/utils/arkivTransactions.ts +249 -104
  159. package/src/utils/chains.test.ts +21 -24
  160. package/src/utils/compression.ts +1 -1
  161. package/src/utils/entities.test.ts +62 -0
  162. package/src/utils/entities.ts +111 -29
  163. package/src/utils/expirationTime.ts +114 -10
  164. package/src/utils/logger.ts +2 -2
  165. package/src/utils/payload.ts +4 -2
  166. package/src/utils/revert.test.ts +99 -0
  167. package/src/utils/revert.ts +145 -0
  168. package/dist/chains-BifaY04V.cjs +0 -108
  169. package/dist/chains-CrtkVbBh.js +0 -86
  170. package/dist/chains-CrtkVbBh.js.map +0 -1
  171. package/dist/consts-AUoL-v42.cjs +0 -18
  172. package/dist/consts-BV4PR-de.js +0 -7
  173. package/dist/consts-BV4PR-de.js.map +0 -1
  174. package/dist/entity-B6g_EPm1.d.cts +0 -382
  175. package/dist/entity-B6g_EPm1.d.cts.map +0 -1
  176. package/dist/entity-qoLMAuuk.d.ts +0 -382
  177. package/dist/entity-qoLMAuuk.d.ts.map +0 -1
  178. package/dist/index-DdspxNAA.d.ts +0 -481
  179. package/dist/index-DdspxNAA.d.ts.map +0 -1
  180. package/dist/index-Ds9dwqb5.d.cts +0 -481
  181. package/dist/index-Ds9dwqb5.d.cts.map +0 -1
  182. package/dist/payload-B2kFJyah.d.ts +0 -6
  183. package/dist/payload-B2kFJyah.d.ts.map +0 -1
  184. package/dist/payload-CJN8fTvX.d.cts +0 -6
  185. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  186. package/dist/payload-Claf2jUO.js +0 -13
  187. package/dist/payload-Claf2jUO.js.map +0 -1
  188. package/dist/payload-DpA5PA-w.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-D2uwP9jI.js +0 -704
  191. package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
  192. package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
  193. package/dist/utils/index.d.cts.map +0 -1
  194. package/dist/utils/index.d.ts.map +0 -1
  195. package/dist/utils/index.js.map +0 -1
  196. package/src/actions/public/buildQuery.ts +0 -10
  197. package/src/actions/public/subscribeEntityEvents.ts +0 -119
  198. package/src/actions/wallet/updateEntity.ts +0 -60
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -526
  202. package/src/query/predicate.test.ts +0 -461
  203. package/src/query/predicate.ts +0 -144
  204. package/src/query/queryResult.test.ts +0 -270
  205. package/src/query/selectQueryBuilder.test.ts +0 -210
  206. package/src/types/attributes.ts +0 -16
  207. package/src/utils/validation.test.ts +0 -60
  208. package/src/utils/validation.ts +0 -32
  209. /package/dist/{types-H-TSmRpm.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-o3zWarRp.cjs → types-CKPddlfS.cjs} +0 -0
@@ -1,155 +1,300 @@
1
- import { type Hex, hexToString, TransactionExecutionError, toBytes, toHex, toRlp } from "viem"
1
+ import {
2
+ type Address,
3
+ ContractFunctionExecutionError,
4
+ ContractFunctionRevertedError,
5
+ decodeEventLog,
6
+ type Hex,
7
+ parseAbi,
8
+ TransactionExecutionError,
9
+ type TransactionReceipt,
10
+ } from "viem"
2
11
  import type { ChangeOwnershipParameters } from "../actions/wallet/changeOwnership"
3
12
  import type { CreateEntityParameters } from "../actions/wallet/createEntity"
4
13
  import type { DeleteEntityParameters } from "../actions/wallet/deleteEntity"
5
14
  import type { ExtendEntityParameters } from "../actions/wallet/extendEntity"
6
- 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"
7
18
  import type { ArkivClient } from "../clients/baseClient"
8
19
  import type { WalletArkivClient } from "../clients/createWalletClient"
9
- import { ARKIV_ADDRESS, BLOCK_TIME } from "../consts"
10
- import { EntityMutationError } 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"
11
35
  import type { TxParams } from "../types"
12
- import { compress } from "./compression"
36
+
13
37
  import { getLogger } from "./logger"
14
- import { validateAttribute, validateExpiresIn } from "./validation"
38
+ import { describeEntityRevert } from "./revert"
15
39
 
16
40
  const logger = getLogger("utils:arkiv-transactions")
17
41
 
18
- export function opsToTxData({
19
- creates,
20
- updates,
21
- deletes,
22
- extensions,
23
- ownershipChanges,
24
- }: {
25
- creates?: CreateEntityParameters[]
26
- updates?: UpdateEntityParameters[]
27
- deletes?: DeleteEntityParameters[]
28
- extensions?: ExtendEntityParameters[]
29
- ownershipChanges?: ChangeOwnershipParameters[]
30
- }) {
31
- function formatAttributes<T extends string | number | bigint | boolean>(attribute: {
32
- key: string
33
- value: T
34
- }): [Hex, Hex] {
35
- return [
36
- toHex(attribute.key),
37
- toHex(typeof attribute.value === "number" && attribute.value === 0 ? "" : attribute.value),
38
- ]
42
+ // RFC 2045 token chars, lowercase only. Rejects uppercase to prevent text/plain vs Text/Plain ambiguity.
43
+ const MIME_REGEX = /^[a-z][a-z0-9!#$&\-^_]*\/[a-z0-9][a-z0-9!#$&\-^_.+]*$/
44
+
45
+ function validateContentType(contentType: string): void {
46
+ if (!MIME_REGEX.test(contentType)) {
47
+ throw new InvalidContentTypeError(contentType)
39
48
  }
49
+ }
40
50
 
41
- const payload = [
42
- //creates
43
- (creates ?? []).map((item) => {
44
- validateExpiresIn(item.expiresIn)
45
- item.attributes.forEach(validateAttribute)
46
- return [
47
- toHex(item.expiresIn / BLOCK_TIME),
48
- toHex(item.contentType),
49
- toHex(item.payload),
50
- item.attributes
51
- .filter((attribute) => typeof attribute.value === "string")
52
- .map(formatAttributes),
53
- item.attributes
54
- .filter((attribute) => typeof attribute.value === "number")
55
- .map(formatAttributes),
56
- ]
57
- }),
58
- //updates
59
- (updates ?? []).map((item) => {
60
- validateExpiresIn(item.expiresIn)
61
- item.attributes.forEach(validateAttribute)
62
- return [
63
- item.entityKey,
64
- toHex(item.contentType),
65
- toHex(item.expiresIn / BLOCK_TIME),
66
- toHex(item.payload),
67
- item.attributes
68
- .filter((attribute) => typeof attribute.value === "string")
69
- .map(formatAttributes),
70
- item.attributes
71
- .filter((attribute) => typeof attribute.value === "number")
72
- .map(formatAttributes),
73
- ]
74
- }),
75
- //deletes
76
- (deletes ?? []).map((item) => item.entityKey),
77
- //extends
78
- (extensions ?? []).map((item) => {
79
- validateExpiresIn(item.expiresIn)
80
- return [item.entityKey, toHex(item.expiresIn / BLOCK_TIME)]
81
- }),
82
- //ownershipChanges TODO
83
- (ownershipChanges ?? []).map((item) => [item.entityKey, item.newOwner]),
84
- ]
51
+ export const ENTITY_ERRORS_ABI = parseAbi([
52
+ // Batch and operation framing.
53
+ "error EmptyBatch()",
54
+ "error InvalidOpType(uint8 operation)",
55
+ "error NonCanonicalOperationData(uint8 operation)",
56
+ // The attribute triple list, structural.
57
+ "error AttributesNotSorted()",
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.
68
+ "error EntityNotFound(bytes32 entityKey)",
69
+ "error EntityExpired(bytes32 entityKey, uint64 expiresAt)",
70
+ "error NotOwner(bytes32 entityKey, address caller, address owner)",
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.
78
+ "error TransferToZeroAddress(bytes32 entityKey)",
79
+ "error TransferToSelf(bytes32 entityKey)",
80
+ ])
85
81
 
86
- logger("txData to send as RLP %o length %d", payload, payload.length)
82
+ const EXECUTE_ABI = [...EXECUTE_FUNCTION_ABI, ...ENTITY_ERRORS_ABI]
87
83
 
88
- return toRlp(payload)
84
+ export type SendArkivTransactionResult = {
85
+ receipt: TransactionReceipt
86
+ /** The keys the engine minted for the entities this transaction created, in batch order. */
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[]
89
92
  }
90
93
 
91
- export async function sendArkivTransaction(client: ArkivClient, data: Hex, txParams?: TxParams) {
94
+ export async function sendArkivTransaction(
95
+ client: ArkivClient,
96
+ ops: {
97
+ creates?: CreateEntityParameters[]
98
+ patches?: PatchEntityParameters[]
99
+ deletes?: DeleteEntityParameters[]
100
+ extensions?: ExtendEntityParameters[]
101
+ ownershipChanges?: ChangeOwnershipParameters[]
102
+ },
103
+ txParams?: TxParams,
104
+ ): Promise<SendArkivTransactionResult> {
92
105
  if (!client.account) throw new Error("Account required")
106
+ if (!client.chain) throw new Error("Chain required")
93
107
  const walletClient = client as WalletArkivClient
94
108
 
95
- logger("Sending transaction %o", {
96
- account: client.account,
97
- chain: client.chain,
98
- to: ARKIV_ADDRESS,
99
- value: 0n,
100
- data,
101
- ...txParams,
109
+ const { creates, patches, deletes, extensions, ownershipChanges } = ops
110
+
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
115
+
116
+ const expiryContext = { currentBlock }
117
+
118
+ const createOps = (creates ?? []).map((item) => {
119
+ validateContentType(item.contentType)
120
+
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
+ })
102
133
  })
103
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
+ ),
163
+ ]
164
+
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)
170
+
104
171
  try {
105
- const compressed = await compress(toBytes(data))
106
- const txHash = await walletClient.sendTransaction({
172
+ const txHash = await walletClient.writeContract({
173
+ address: ARKIV_ADDRESS,
174
+ abi: EXECUTE_ABI,
175
+ functionName: "execute",
176
+ args: [operations],
107
177
  account: client.account,
108
178
  chain: client.chain,
109
- to: ARKIV_ADDRESS,
110
- value: 0n,
111
- data: toHex(compressed),
112
179
  ...txParams,
113
180
  })
114
181
 
115
182
  const receipt = await walletClient.waitForTransactionReceipt({ hash: txHash })
116
183
  logger("Tx receipt %o", receipt)
184
+
117
185
  if (receipt.status === "reverted") {
118
186
  try {
119
- const callResult = await walletClient.call({
187
+ await walletClient.simulateContract({
188
+ address: ARKIV_ADDRESS,
189
+ abi: EXECUTE_ABI,
190
+ functionName: "execute",
191
+ args: [operations],
120
192
  account: client.account,
121
- to: ARKIV_ADDRESS,
122
- value: 0n,
123
- data: toHex(compressed),
124
- ...txParams,
193
+ chain: client.chain,
125
194
  })
126
- throw new EntityMutationError(
127
- `Transaction ${receipt.transactionHash} reverted. Please make sure the data for entities doesn't contain invalid characters or invalid data types.
128
- Reason: ${callResult.data ? hexToString(callResult.data) : "No reason provided by backend."}`,
129
- )
130
195
  } catch (err) {
131
- const error = err as { cause?: { details?: string } }
132
- const reason = error.cause?.details ?? "No reason provided by backend."
133
- logger(
134
- "Error while calling to get more details about reverted transaction. Reason: %s",
135
- reason,
136
- )
196
+ const error = err as { shortMessage?: string; cause?: { details?: string } }
197
+ const reason =
198
+ describeEntityRevert(err) ??
199
+ error.shortMessage ??
200
+ error.cause?.details ??
201
+ "No reason provided by backend."
137
202
  throw new EntityMutationError(
138
203
  `Transaction ${receipt.transactionHash} reverted. Reason: ${reason}`,
139
204
  )
140
205
  }
206
+ throw new EntityMutationError(
207
+ `Transaction ${receipt.transactionHash} reverted. No reason provided by backend.`,
208
+ )
141
209
  }
142
210
 
143
- return receipt
211
+ return {
212
+ receipt,
213
+ ...readAppliedOperations(receipt, {
214
+ creates: createOps.length,
215
+ extensions: extendOps.length,
216
+ }),
217
+ }
144
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
+
145
224
  let message = "Transaction failed"
146
225
  if (error instanceof TransactionExecutionError) {
147
226
  message += `: ${error.details}`
227
+ } else if (error instanceof ContractFunctionExecutionError) {
228
+ logger("Contract function execution error data:", error.shortMessage)
229
+ if (error.cause instanceof ContractFunctionRevertedError) {
230
+ message += `: ${error.cause.message}`
231
+ } else {
232
+ message += ": Execution error without revert data"
233
+ }
148
234
  } else if (error instanceof EntityMutationError) {
149
235
  throw error
236
+ } else if (error instanceof Error) {
237
+ message += `: ${error.message}`
150
238
  }
151
239
 
152
- logger("%s Detailed error stack: %o", message, error)
153
- throw new EntityMutationError(message)
240
+ throw new EntityMutationError(message, { cause: error })
154
241
  }
155
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)
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
+ }
280
+
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
288
+ }
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
+ })