@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
@@ -0,0 +1,414 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { type Abi, encodeAbiParameters, encodeEventTopics, getAbiItem, type Hex } from "viem"
3
+ import type { ArkivClient } from "../../clients/baseClient"
4
+ import { ARKIV_ADDRESS } from "../../consts"
5
+ import { ENTITY_EVENTS_ABI, type EntityEventName } from "../../entity/events"
6
+ import type { EntityEvent } from "../../types/events"
7
+ import { watchEntityEvents } from "./watchEntityEvents"
8
+
9
+ const KEY_A = `0x${"aa".repeat(32)}` as Hex
10
+ const KEY_B = `0x${"bb".repeat(32)}` as Hex
11
+ const ALICE = "0x1111111111111111111111111111111111111111" as const
12
+ const BOB = "0x2222222222222222222222222222222222222222" as const
13
+
14
+ type Log = {
15
+ topics: Hex[]
16
+ data: Hex
17
+ blockNumber: bigint | null
18
+ transactionHash: Hex | null
19
+ logIndex: number | null
20
+ }
21
+
22
+ /**
23
+ * Builds the log the engine would emit for an event, from the same ABI the watcher decodes with.
24
+ *
25
+ * Encoding rather than hand-writing topics is what keeps the test honest about the indexed/
26
+ * non-indexed split: change the ABI and the log changes with it.
27
+ */
28
+ function logFor(
29
+ eventName: EntityEventName,
30
+ args: Record<string, unknown>,
31
+ position: { blockNumber?: bigint; transactionHash?: Hex; logIndex?: number } = {},
32
+ ): Log {
33
+ const item = getAbiItem({ abi: ENTITY_EVENTS_ABI as Abi, name: eventName })
34
+ if (item === undefined || item.type !== "event") throw new Error(`no event ${eventName}`)
35
+
36
+ const unindexed = item.inputs.filter((input) => !("indexed" in input && input.indexed))
37
+ return {
38
+ topics: encodeEventTopics({
39
+ abi: ENTITY_EVENTS_ABI,
40
+ eventName,
41
+ // biome-ignore lint/suspicious/noExplicitAny: the arg shape varies per event by design.
42
+ args: args as any,
43
+ }) as Hex[],
44
+ data:
45
+ unindexed.length === 0
46
+ ? "0x"
47
+ : encodeAbiParameters(
48
+ unindexed,
49
+ unindexed.map((input) => args[input.name ?? ""]),
50
+ ),
51
+ blockNumber: position.blockNumber ?? 100n,
52
+ transactionHash: position.transactionHash ?? (`0x${"cc".repeat(32)}` as Hex),
53
+ logIndex: position.logIndex ?? 0,
54
+ }
55
+ }
56
+
57
+ type Harness = {
58
+ client: ArkivClient
59
+ /** Feeds logs to the watcher, as the poller would. */
60
+ emitLogs: (logs: Log[]) => void
61
+ /** Raises a transport error on the log watcher. */
62
+ failLogs: (error: Error) => void
63
+ watchEventCalls: Record<string, unknown>[]
64
+ stops: string[]
65
+ }
66
+
67
+ /** A client that hands back its watcher callbacks so a test can drive them directly. */
68
+ function harness(): Harness {
69
+ const state: Partial<{
70
+ onLogs: (logs: Log[]) => void
71
+ onLogsError: (error: Error) => void
72
+ }> = {}
73
+ const watchEventCalls: Record<string, unknown>[] = []
74
+ const stops: string[] = []
75
+
76
+ const client = {
77
+ // biome-ignore lint/suspicious/noExplicitAny: a stand-in for viem's generic watcher.
78
+ watchEvent(parameters: any) {
79
+ watchEventCalls.push(parameters)
80
+ state.onLogs = parameters.onLogs
81
+ state.onLogsError = parameters.onError
82
+ return () => stops.push("logs")
83
+ },
84
+ } as unknown as ArkivClient
85
+
86
+ return {
87
+ client,
88
+ emitLogs: (logs) => state.onLogs?.(logs),
89
+ failLogs: (error) => state.onLogsError?.(error),
90
+ watchEventCalls,
91
+ stops,
92
+ }
93
+ }
94
+
95
+ /** Runs `body` with `console.error` captured, so a test can assert on the default error path. */
96
+ function captureConsoleError(body: () => void): unknown[][] {
97
+ const calls: unknown[][] = []
98
+ const original = console.error
99
+ console.error = (...args: unknown[]) => {
100
+ calls.push(args)
101
+ }
102
+ try {
103
+ body()
104
+ } finally {
105
+ console.error = original
106
+ }
107
+ return calls
108
+ }
109
+
110
+ describe("decoding the five on-chain events", () => {
111
+ it("decodes a create, with the position it was applied at", () => {
112
+ const h = harness()
113
+ const seen: EntityEvent[] = []
114
+ watchEntityEvents(h.client, { onEntityCreated: (event) => seen.push(event) })
115
+
116
+ h.emitLogs([
117
+ logFor(
118
+ "EntityCreated",
119
+ { entityKey: KEY_A, owner: ALICE, expiresAt: 1_200_000n, creationFlags: 0b11 },
120
+ { blockNumber: 42n, logIndex: 3 },
121
+ ),
122
+ ])
123
+
124
+ expect(seen).toEqual([
125
+ {
126
+ type: "EntityCreated",
127
+ entityKey: KEY_A,
128
+ owner: ALICE,
129
+ expiresAt: 1_200_000n,
130
+ // Decoded, with the byte kept alongside so a flag this SDK has no name for stays visible.
131
+ creationFlags: { readonly: true, permissionlessExtension: true, raw: 0b11 },
132
+ blockNumber: 42n,
133
+ transactionHash: `0x${"cc".repeat(32)}`,
134
+ logIndex: 3,
135
+ },
136
+ ])
137
+ })
138
+
139
+ it("decodes a patch", () => {
140
+ const h = harness()
141
+ const seen: EntityEvent[] = []
142
+ watchEntityEvents(h.client, { onEntityPatched: (event) => seen.push(event) })
143
+
144
+ h.emitLogs([logFor("EntityPatched", { entityKey: KEY_A, owner: ALICE })])
145
+
146
+ expect(seen).toMatchObject([{ type: "EntityPatched", entityKey: KEY_A, owner: ALICE }])
147
+ })
148
+
149
+ it("decodes an extension", () => {
150
+ const h = harness()
151
+ const seen: EntityEvent[] = []
152
+ watchEntityEvents(h.client, { onExpiryExtended: (event) => seen.push(event) })
153
+
154
+ h.emitLogs([logFor("ExpiryExtended", { entityKey: KEY_A, owner: ALICE, expiresAt: 999n })])
155
+
156
+ // `owner` is the entity's owner, not the caller — a permissionless extension is by a non-owner.
157
+ expect(seen).toMatchObject([
158
+ { type: "ExpiryExtended", entityKey: KEY_A, owner: ALICE, expiresAt: 999n },
159
+ ])
160
+ })
161
+
162
+ it("decodes a transfer, naming both sides", () => {
163
+ const h = harness()
164
+ const seen: EntityEvent[] = []
165
+ watchEntityEvents(h.client, { onOwnershipTransferred: (event) => seen.push(event) })
166
+
167
+ h.emitLogs([
168
+ logFor("OwnershipTransferred", { entityKey: KEY_A, previousOwner: ALICE, newOwner: BOB }),
169
+ ])
170
+
171
+ expect(seen).toMatchObject([
172
+ { type: "OwnershipTransferred", entityKey: KEY_A, previousOwner: ALICE, newOwner: BOB },
173
+ ])
174
+ })
175
+
176
+ it("decodes a delete", () => {
177
+ const h = harness()
178
+ const seen: EntityEvent[] = []
179
+ watchEntityEvents(h.client, { onEntityDeleted: (event) => seen.push(event) })
180
+
181
+ h.emitLogs([logFor("EntityDeleted", { entityKey: KEY_A, owner: ALICE })])
182
+
183
+ expect(seen).toMatchObject([{ type: "EntityDeleted", entityKey: KEY_A, owner: ALICE }])
184
+ })
185
+ })
186
+
187
+ describe("onEvent", () => {
188
+ it("receives every event in the order it was applied", () => {
189
+ const h = harness()
190
+ const seen: string[] = []
191
+ watchEntityEvents(h.client, { onEvent: (event) => seen.push(event.type) })
192
+
193
+ h.emitLogs([
194
+ logFor(
195
+ "EntityCreated",
196
+ { entityKey: KEY_A, owner: ALICE, expiresAt: 500n, creationFlags: 0 },
197
+ { logIndex: 0 },
198
+ ),
199
+ logFor("EntityPatched", { entityKey: KEY_A, owner: ALICE }, { logIndex: 1 }),
200
+ logFor(
201
+ "OwnershipTransferred",
202
+ { entityKey: KEY_A, previousOwner: ALICE, newOwner: BOB },
203
+ { logIndex: 2 },
204
+ ),
205
+ logFor("EntityDeleted", { entityKey: KEY_A, owner: BOB }, { logIndex: 3 }),
206
+ ])
207
+
208
+ expect(seen).toEqual([
209
+ "EntityCreated",
210
+ "EntityPatched",
211
+ "OwnershipTransferred",
212
+ "EntityDeleted",
213
+ ])
214
+ })
215
+
216
+ it("runs before the per-event handler for the same event", () => {
217
+ const h = harness()
218
+ const order: string[] = []
219
+ watchEntityEvents(h.client, {
220
+ onEvent: () => order.push("onEvent"),
221
+ onEntityCreated: () => order.push("onEntityCreated"),
222
+ })
223
+
224
+ h.emitLogs([
225
+ logFor("EntityCreated", { entityKey: KEY_A, owner: ALICE, expiresAt: 1n, creationFlags: 0 }),
226
+ ])
227
+
228
+ expect(order).toEqual(["onEvent", "onEntityCreated"])
229
+ })
230
+ })
231
+
232
+ describe("the watcher itself", () => {
233
+ it("only listens to the Arkiv operation address", () => {
234
+ const h = harness()
235
+ watchEntityEvents(h.client, { onEntityCreated: () => {} })
236
+
237
+ expect(h.watchEventCalls[0]).toMatchObject({ address: ARKIV_ADDRESS })
238
+ })
239
+
240
+ it("polls twice a block by default", () => {
241
+ const h = harness()
242
+ watchEntityEvents(h.client, { onEntityCreated: () => {} })
243
+
244
+ // Blocks are 2s, so a default that leaves an event a whole block stale is too slow.
245
+ expect(h.watchEventCalls[0]).toMatchObject({ pollingInterval: 1000 })
246
+ })
247
+
248
+ it("forwards fromBlock and pollingInterval, and omits fromBlock when unset", () => {
249
+ const withOptions = harness()
250
+ watchEntityEvents(withOptions.client, {
251
+ onEntityCreated: () => {},
252
+ fromBlock: 90n,
253
+ pollingInterval: 250,
254
+ })
255
+ expect(withOptions.watchEventCalls[0]).toMatchObject({ fromBlock: 90n, pollingInterval: 250 })
256
+
257
+ const bare = harness()
258
+ watchEntityEvents(bare.client, { onEntityCreated: () => {} })
259
+ expect(bare.watchEventCalls[0]).not.toHaveProperty("fromBlock")
260
+ })
261
+
262
+ it("stops the watcher when unwatched", () => {
263
+ const h = harness()
264
+ const unwatch = watchEntityEvents(h.client, { onEntityCreated: () => {} })
265
+
266
+ expect(h.stops).toEqual([])
267
+ unwatch()
268
+ expect(h.stops).toEqual(["logs"])
269
+ })
270
+
271
+ it("skips a log with no position rather than inventing one", () => {
272
+ const h = harness()
273
+ const seen: EntityEvent[] = []
274
+ watchEntityEvents(h.client, { onEvent: (event) => seen.push(event) })
275
+
276
+ const log = logFor("EntityCreated", {
277
+ entityKey: KEY_A,
278
+ owner: ALICE,
279
+ expiresAt: 1n,
280
+ creationFlags: 0,
281
+ })
282
+ h.emitLogs([{ ...log, blockNumber: null }])
283
+ h.emitLogs([{ ...log, transactionHash: null }])
284
+ h.emitLogs([{ ...log, logIndex: null }])
285
+
286
+ expect(seen).toEqual([])
287
+ })
288
+
289
+ it("reports a log it cannot decode instead of throwing out of the poller", () => {
290
+ const h = harness()
291
+ const errors: Error[] = []
292
+ const seen: EntityEvent[] = []
293
+ watchEntityEvents(h.client, {
294
+ onEvent: (event) => seen.push(event),
295
+ onError: (error) => errors.push(error),
296
+ })
297
+
298
+ const good = logFor("EntityDeleted", { entityKey: KEY_A, owner: ALICE })
299
+ // A create's topic with no data — the expiresAt word is missing.
300
+ const broken = {
301
+ ...logFor("EntityCreated", {
302
+ entityKey: KEY_A,
303
+ owner: ALICE,
304
+ expiresAt: 1n,
305
+ creationFlags: 0,
306
+ }),
307
+ data: "0x" as Hex,
308
+ }
309
+
310
+ expect(() => h.emitLogs([broken, good])).not.toThrow()
311
+ expect(errors).toHaveLength(1)
312
+ // The rest of the batch still gets through.
313
+ expect(seen).toMatchObject([{ type: "EntityDeleted" }])
314
+ })
315
+
316
+ it("forwards transport errors", () => {
317
+ const h = harness()
318
+ const errors: Error[] = []
319
+ watchEntityEvents(h.client, {
320
+ onEntityCreated: () => {},
321
+ onError: (error) => errors.push(error),
322
+ })
323
+
324
+ const boom = new Error("transport died")
325
+ h.failLogs(boom)
326
+
327
+ expect(errors).toEqual([boom])
328
+ })
329
+
330
+ it("logs to the console when no onError was given, so a dead watcher is not silent", () => {
331
+ const h = harness()
332
+ watchEntityEvents(h.client, { onEntityCreated: () => {} })
333
+
334
+ const boom = new Error("filter not found")
335
+ const logged = captureConsoleError(() => h.failLogs(boom))
336
+
337
+ expect(logged).toEqual([["watchEntityEvents error", boom]])
338
+ })
339
+ })
340
+
341
+ describe("a handler that throws", () => {
342
+ it("is reported, not allowed to escape into the poller", () => {
343
+ const h = harness()
344
+ const errors: Error[] = []
345
+ watchEntityEvents(h.client, {
346
+ onEntityCreated: () => {
347
+ throw new Error("consumer blew up")
348
+ },
349
+ onError: (error) => errors.push(error),
350
+ })
351
+
352
+ expect(() =>
353
+ h.emitLogs([
354
+ logFor("EntityCreated", {
355
+ entityKey: KEY_A,
356
+ owner: ALICE,
357
+ expiresAt: 200n,
358
+ creationFlags: 0,
359
+ }),
360
+ ]),
361
+ ).not.toThrow()
362
+ expect(errors).toMatchObject([{ message: "consumer blew up" }])
363
+ })
364
+
365
+ it("does not stop the other handlers for the same event", () => {
366
+ const h = harness()
367
+ const seen: string[] = []
368
+ watchEntityEvents(h.client, {
369
+ onEvent: () => {
370
+ throw new Error("onEvent blew up")
371
+ },
372
+ onEntityCreated: () => seen.push("onEntityCreated"),
373
+ onError: () => {},
374
+ })
375
+
376
+ h.emitLogs([
377
+ logFor("EntityCreated", {
378
+ entityKey: KEY_A,
379
+ owner: ALICE,
380
+ expiresAt: 200n,
381
+ creationFlags: 0,
382
+ }),
383
+ ])
384
+
385
+ expect(seen).toEqual(["onEntityCreated"])
386
+ })
387
+
388
+ it("does not stop the rest of the batch", () => {
389
+ const h = harness()
390
+ const seen: Hex[] = []
391
+ watchEntityEvents(h.client, {
392
+ onEntityCreated: ({ entityKey }) => {
393
+ seen.push(entityKey)
394
+ if (entityKey === KEY_A) throw new Error("first one blew up")
395
+ },
396
+ onError: () => {},
397
+ })
398
+
399
+ h.emitLogs([
400
+ logFor(
401
+ "EntityCreated",
402
+ { entityKey: KEY_A, owner: ALICE, expiresAt: 200n, creationFlags: 0 },
403
+ { logIndex: 0 },
404
+ ),
405
+ logFor(
406
+ "EntityCreated",
407
+ { entityKey: KEY_B, owner: BOB, expiresAt: 200n, creationFlags: 0 },
408
+ { logIndex: 1 },
409
+ ),
410
+ ])
411
+
412
+ expect(seen).toEqual([KEY_A, KEY_B])
413
+ })
414
+ })
@@ -0,0 +1,225 @@
1
+ import { decodeEventLog, type Hex } from "viem"
2
+ import type { ArkivClient } from "../../clients/baseClient"
3
+ import type { PublicArkivClient } from "../../clients/createPublicClient"
4
+ import { ARKIV_ADDRESS, BLOCK_TIME } from "../../consts"
5
+ import { ENTITY_EVENTS_ABI } from "../../entity/events"
6
+ import { decodeCreationFlags } from "../../entity/flags"
7
+ import type {
8
+ EntityCreatedEvent,
9
+ EntityDeletedEvent,
10
+ EntityEvent,
11
+ EntityEventContext,
12
+ EntityPatchedEvent,
13
+ ExpiryExtendedEvent,
14
+ OwnershipTransferredEvent,
15
+ } from "../../types/events"
16
+ import { getLogger } from "../../utils/logger"
17
+
18
+ const logger = getLogger("actions:public:watch-entity-events")
19
+
20
+ /**
21
+ * How often to poll, when the caller does not say.
22
+ *
23
+ * Half a block: fast enough that an event is never a whole block stale, slow enough not to hammer
24
+ * the node. viem's own default is derived from a 12-second Ethereum block and is far too slow here.
25
+ */
26
+ const DEFAULT_POLLING_INTERVAL = (BLOCK_TIME * 1000) / 2
27
+
28
+ /**
29
+ * Handlers and options for `client.watchEntityEvents`. Every handler is optional; the watcher does
30
+ * the work for the ones you pass.
31
+ */
32
+ export type WatchEntityEventsParameters = {
33
+ onEntityCreated?: ((event: EntityCreatedEvent) => void) | undefined
34
+ onEntityPatched?: ((event: EntityPatchedEvent) => void) | undefined
35
+ onExpiryExtended?: ((event: ExpiryExtendedEvent) => void) | undefined
36
+ onOwnershipTransferred?: ((event: OwnershipTransferredEvent) => void) | undefined
37
+ onEntityDeleted?: ((event: EntityDeletedEvent) => void) | undefined
38
+ /**
39
+ * Every event, in application order, whatever its type. Runs *before* the per-event handler for
40
+ * the same event.
41
+ *
42
+ * This is the replay seam: operations apply in batch order and emit one event each, so an
43
+ * off-chain replica that consumes this in order sees exactly what the engine did.
44
+ */
45
+ onEvent?: ((event: EntityEvent) => void) | undefined
46
+ /**
47
+ * Transport failures, logs that will not decode, and anything one of your own handlers throws.
48
+ *
49
+ * Defaults to `console.error`. A watcher whose filter the node dropped otherwise just goes quiet,
50
+ * and a quiet watcher is indistinguishable from a quiet chain.
51
+ */
52
+ onError?: ((error: Error) => void) | undefined
53
+ /** Replay from this block before following the head. Defaults to the head. */
54
+ fromBlock?: bigint | undefined
55
+ /** How often to poll, in milliseconds. Defaults to half a block. */
56
+ pollingInterval?: number | undefined
57
+ }
58
+
59
+ /**
60
+ * Watches entity events, calling the handlers you pass as they arrive.
61
+ *
62
+ * Every event here is decoded from a log the Arkiv operation address emitted, and carries the
63
+ * block, transaction and log index it came from — so consuming them in order replays exactly what
64
+ * the engine did.
65
+ *
66
+ * @param parameters - Handlers and options. {@link WatchEntityEventsParameters}
67
+ * @returns A function that stops the watcher.
68
+ *
69
+ *
70
+ * @example
71
+ * import { createPublicClient } from "@arkiv-network/sdk"
72
+ * import { cheesecake } from "@arkiv-network/sdk/chains"
73
+ * import { http } from "viem"
74
+ *
75
+ * const client = createPublicClient({ chain: cheesecake, transport: http() })
76
+ *
77
+ * const unwatch = client.watchEntityEvents({
78
+ * onEntityCreated: ({ entityKey, owner, expiresAt }) =>
79
+ * console.log("created", entityKey, "by", owner, "until block", expiresAt),
80
+ * onEntityDeleted: ({ entityKey }) => console.log("deleted", entityKey),
81
+ * onError: (error) => console.error(error),
82
+ * })
83
+ *
84
+ * unwatch()
85
+ */
86
+ export function watchEntityEvents(
87
+ client: ArkivClient,
88
+ parameters: WatchEntityEventsParameters,
89
+ ): () => void {
90
+ const {
91
+ onEntityCreated,
92
+ onEntityPatched,
93
+ onExpiryExtended,
94
+ onOwnershipTransferred,
95
+ onEntityDeleted,
96
+ onEvent,
97
+ onError,
98
+ fromBlock,
99
+ pollingInterval = DEFAULT_POLLING_INTERVAL,
100
+ } = parameters
101
+
102
+ const publicClient = client as PublicArkivClient
103
+ const reportError = onError ?? logToConsole
104
+
105
+ function emit(event: EntityEvent): void {
106
+ deliver(onEvent, event)
107
+ switch (event.type) {
108
+ case "EntityCreated":
109
+ deliver(onEntityCreated, event)
110
+ break
111
+ case "EntityPatched":
112
+ deliver(onEntityPatched, event)
113
+ break
114
+ case "ExpiryExtended":
115
+ deliver(onExpiryExtended, event)
116
+ break
117
+ case "OwnershipTransferred":
118
+ deliver(onOwnershipTransferred, event)
119
+ break
120
+ case "EntityDeleted":
121
+ deliver(onEntityDeleted, event)
122
+ break
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Runs one handler, keeping its failure to itself.
128
+ *
129
+ * A handler that throws is the consumer's problem, not the watcher's: letting it escape would
130
+ * reject the poll, drop the rest of the batch, and take down every other handler with it.
131
+ */
132
+ function deliver<TEvent>(handler: ((event: TEvent) => void) | undefined, event: TEvent): void {
133
+ if (handler === undefined) return
134
+ try {
135
+ handler(event)
136
+ } catch (error) {
137
+ reportError(asError(error))
138
+ }
139
+ }
140
+
141
+ const unwatchLogs = publicClient.watchEvent({
142
+ // Only the Arkiv operation address emits these. Without the filter any contract whose event
143
+ // happens to hash to the same topic lands in `onLogs`, and viem's observer key — which covers
144
+ // the address but not `events` — would make this subscription collide with any other
145
+ // unaddressed `watchEvent` on the same client.
146
+ address: ARKIV_ADDRESS,
147
+ events: ENTITY_EVENTS_ABI,
148
+ pollingInterval,
149
+ ...(fromBlock !== undefined && { fromBlock }),
150
+ onLogs: (logs) => {
151
+ logger("logs %o", logs)
152
+ for (const log of logs) {
153
+ // A log with no position cannot be placed in application order, which is the one thing an
154
+ // event consumer needs from it. Dropping it beats inventing a position it never had.
155
+ if (log.blockNumber === null || log.transactionHash === null || log.logIndex === null) {
156
+ continue
157
+ }
158
+ const context: EntityEventContext = {
159
+ blockNumber: log.blockNumber,
160
+ transactionHash: log.transactionHash,
161
+ logIndex: log.logIndex,
162
+ }
163
+
164
+ // Only the decode is guarded here. Dispatch guards each handler separately, so a consumer's
165
+ // exception is never reported as if the log were the thing that was malformed.
166
+ let event: EntityEvent
167
+ try {
168
+ event = toEntityEvent(log.topics, log.data, context)
169
+ } catch (error) {
170
+ reportError(asError(error))
171
+ continue
172
+ }
173
+ emit(event)
174
+ }
175
+ },
176
+ onError: (error) => reportError(error),
177
+ })
178
+
179
+ return unwatchLogs
180
+ }
181
+
182
+ /** The fallback when no `onError` was given — see {@link WatchEntityEventsParameters.onError}. */
183
+ function logToConsole(error: Error): void {
184
+ console.error("watchEntityEvents error", error)
185
+ }
186
+
187
+ /** Handlers take an `Error`, and a `throw` can be anything. */
188
+ function asError(error: unknown): Error {
189
+ return error instanceof Error ? error : new Error(String(error))
190
+ }
191
+
192
+ /**
193
+ * Decodes one log into the {@link EntityEvent} it carries.
194
+ *
195
+ * @throws If the log does not decode against {@link ENTITY_EVENTS_ABI}.
196
+ */
197
+ function toEntityEvent(
198
+ topics: readonly Hex[],
199
+ data: Hex,
200
+ context: EntityEventContext,
201
+ ): EntityEvent {
202
+ const decoded = decodeEventLog({
203
+ abi: ENTITY_EVENTS_ABI,
204
+ topics: topics as [Hex, ...Hex[]] | [],
205
+ data,
206
+ })
207
+
208
+ switch (decoded.eventName) {
209
+ case "EntityCreated":
210
+ return {
211
+ type: "EntityCreated",
212
+ ...context,
213
+ ...decoded.args,
214
+ creationFlags: decodeCreationFlags(decoded.args.creationFlags),
215
+ }
216
+ case "EntityPatched":
217
+ return { type: "EntityPatched", ...context, ...decoded.args }
218
+ case "ExpiryExtended":
219
+ return { type: "ExpiryExtended", ...context, ...decoded.args }
220
+ case "OwnershipTransferred":
221
+ return { type: "OwnershipTransferred", ...context, ...decoded.args }
222
+ case "EntityDeleted":
223
+ return { type: "EntityDeleted", ...context, ...decoded.args }
224
+ }
225
+ }
@@ -1,14 +1,15 @@
1
- import type { Hex } from "viem"
1
+ import type { Hash, Hex } from "viem"
2
2
  import type { ArkivClient } from "../../clients/baseClient"
3
3
  import type { TxParams } from "../../types"
4
- import { opsToTxData, sendArkivTransaction } from "../../utils/arkivTransactions"
4
+ import { sendArkivTransaction } from "../../utils/arkivTransactions"
5
5
  import { getLogger } from "../../utils/logger"
6
6
 
7
7
  const logger = getLogger("actions:wallet:change-ownership")
8
8
 
9
9
  /**
10
- * Parameters for the deleteEntity function.
11
- * - entityKey: The key of the entity to delete.
10
+ * Parameters for the changeOwnership function.
11
+ * - entityKey: The key of the entity.
12
+ * - newOwner: The address of the new owner.
12
13
  */
13
14
  export type ChangeOwnershipParameters = {
14
15
  entityKey: Hex
@@ -16,13 +17,13 @@ export type ChangeOwnershipParameters = {
16
17
  }
17
18
 
18
19
  /**
19
- * Return type for the deleteEntity function.
20
+ * Return type for the changeOwnership function.
20
21
  * - entityKey: The key of the entity.
21
22
  * - txHash: The transaction hash.
22
23
  */
23
24
  export type ChangeOwnershipReturnType = {
24
25
  entityKey: Hex
25
- txHash: string
26
+ txHash: Hash
26
27
  }
27
28
 
28
29
  export async function changeOwnership(
@@ -31,8 +32,7 @@ export async function changeOwnership(
31
32
  txParams?: TxParams,
32
33
  ): Promise<ChangeOwnershipReturnType> {
33
34
  logger("changeOwnership %o", data)
34
- const txData = opsToTxData({ ownershipChanges: [data] })
35
- const receipt = await sendArkivTransaction(client, txData, txParams)
35
+ const { receipt } = await sendArkivTransaction(client, { ownershipChanges: [data] }, txParams)
36
36
 
37
37
  logger("Receipt from changeOwnership %o", receipt)
38
38