@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
@@ -1,47 +1,119 @@
1
- import { type Hex, toBytes } from "viem"
2
- import type { Attribute } from "../types/attributes"
3
- import { Entity } from "../types/entity"
1
+ import { checksumAddress, type Hex, hexToBytes } from "viem"
2
+ import type { AttributeSchema, Attributes } from "../attr"
3
+ // Internal seam: the JSON-RPC decoder is not part of the package's public surface.
4
+ import { asTypeTag, decodeRpcValue } from "../attr/codec"
5
+ import {
6
+ decodeCreationFlags,
7
+ encodeCreationFlags,
8
+ type ResolvedCreationFlags,
9
+ } from "../entity/flags"
10
+ import { Entity, type EntityFields } from "../types/entity"
4
11
  import type { RpcEntity } from "../types/rpcSchema"
5
- import { RpcAttributeValueType } from "../types/rpcSchema"
6
12
  import { getLogger } from "./logger"
7
13
 
8
- function decodeAttributeValue(value: string, valueType: RpcAttributeValueType): Attribute["value"] {
9
- switch (valueType) {
10
- case RpcAttributeValueType.Uint:
11
- return Number(BigInt(value as Hex))
12
- case RpcAttributeValueType.EntityKey:
13
- return value as Hex
14
- case RpcAttributeValueType.String:
15
- default:
16
- return value
14
+ const logger = getLogger("utils:entities")
15
+
16
+ /**
17
+ * Turns the attribute array a query returns into a name-keyed map of typed values.
18
+ *
19
+ * Attribute names are unique per entity, so the map loses nothing and gives callers
20
+ * `entity.attributes.level.value` instead of a linear search.
21
+ */
22
+ function decodeAttributes(rpcAttributes: NonNullable<RpcEntity["attributes"]>): Attributes {
23
+ const attributes: Record<string, ReturnType<typeof decodeRpcValue>> = {}
24
+ for (const { name, type, value } of rpcAttributes) {
25
+ attributes[name] = decodeRpcValue(type, value)
17
26
  }
27
+ return attributes
18
28
  }
19
29
 
20
- const logger = getLogger("utils:entities")
30
+ /** The same, for the values-free `attributeSchema` projection. */
31
+ function decodeSchema(entries: NonNullable<RpcEntity["attributeSchema"]>): AttributeSchema {
32
+ const schema: Record<string, ReturnType<typeof asTypeTag>> = {}
33
+ for (const { name, type } of entries) {
34
+ schema[name] = asTypeTag(type)
35
+ }
36
+ return schema
37
+ }
21
38
 
22
- export async function entityFromRpcResult(rpcEntity: RpcEntity) {
39
+ /**
40
+ * Builds an {@link Entity} from a query result row.
41
+ *
42
+ * A field the query did not select is absent from the response and stays `undefined` here — the
43
+ * decoder never substitutes an empty value for one that was not asked for.
44
+ */
45
+ export function entityFromRpcResult(rpcEntity: RpcEntity): Entity {
23
46
  logger("entityFromRpcResult %o", rpcEntity)
24
47
 
25
- return new Entity(
26
- rpcEntity.key,
27
- rpcEntity.contentType,
28
- rpcEntity.owner,
29
- rpcEntity.creator,
30
- rpcEntity.expiresAt !== undefined ? BigInt(rpcEntity.expiresAt) : undefined,
31
- rpcEntity.createdAtBlock !== undefined ? BigInt(rpcEntity.createdAtBlock) : undefined,
32
- rpcEntity.lastModifiedAtBlock !== undefined ? BigInt(rpcEntity.lastModifiedAtBlock) : undefined,
33
- rpcEntity.transactionIndexInBlock !== undefined
34
- ? BigInt(rpcEntity.transactionIndexInBlock)
35
- : undefined,
36
- rpcEntity.operationIndexInTransaction !== undefined
37
- ? BigInt(rpcEntity.operationIndexInTransaction)
38
- : undefined,
39
- rpcEntity.value !== undefined ? toBytes(rpcEntity.value) : undefined,
40
- [
41
- ...(rpcEntity.attributes ?? []).map(({ key, value, valueType }) => ({
42
- key,
43
- value: decodeAttributeValue(value, valueType),
44
- })),
45
- ],
46
- )
48
+ const fields: EntityFields = {
49
+ key: rpcEntity.key,
50
+ owner: toAddress(rpcEntity.owner),
51
+ creator: toAddress(rpcEntity.creator),
52
+ createdAt: toBlock(rpcEntity.createdAt),
53
+ updatedAt: toBlock(rpcEntity.updatedAt),
54
+ expiresAt: toBlock(rpcEntity.expiresAt),
55
+ creationFlags: decodeFlags(rpcEntity.creationFlags),
56
+ contentType: rpcEntity.contentType,
57
+ payload: rpcEntity.payload !== undefined ? hexToBytes(rpcEntity.payload) : undefined,
58
+ attributeSchema:
59
+ rpcEntity.attributeSchema !== undefined ? decodeSchema(rpcEntity.attributeSchema) : undefined,
60
+ attributes:
61
+ rpcEntity.attributes !== undefined ? decodeAttributes(rpcEntity.attributes) : undefined,
62
+ }
63
+
64
+ return new Entity(fields)
65
+ }
66
+
67
+ function toBlock(value: string | undefined): bigint | undefined {
68
+ return value === undefined ? undefined : BigInt(value)
69
+ }
70
+
71
+ function toAddress(value: Hex | undefined): Hex | undefined {
72
+ if (value === undefined) return undefined
73
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
74
+ logger("unreadable address %o", value)
75
+ return value
76
+ }
77
+ return checksumAddress(value)
78
+ }
79
+
80
+ /**
81
+ * Resolves the `creationFlags` projection, preferring the byte.
82
+ */
83
+ function decodeFlags(
84
+ value: NonNullable<RpcEntity["creationFlags"]> | undefined,
85
+ ): ResolvedCreationFlags | undefined {
86
+ if (value === undefined) return undefined
87
+
88
+ const raw = toFlagsByte(typeof value === "number" ? value : value.raw)
89
+ if (raw !== undefined) return decodeCreationFlags(raw)
90
+
91
+ if (
92
+ typeof value === "object" &&
93
+ (value.readonly ?? value.permissionlessExtension) !== undefined
94
+ ) {
95
+ return decodeCreationFlags(
96
+ encodeCreationFlags({
97
+ readonly: value.readonly ?? false,
98
+ permissionlessExtension: value.permissionlessExtension ?? false,
99
+ }),
100
+ )
101
+ }
102
+
103
+ logger("unreadable creationFlags %o", value)
104
+ return undefined
105
+ }
106
+
107
+ /**
108
+ * A flags byte in whichever spelling arrived: a JSON number, or the `0x` QUANTITY that every other
109
+ * numeric field on {@link RpcEntity} already uses. `undefined` for anything that is not a byte.
110
+ */
111
+ function toFlagsByte(value: unknown): number | undefined {
112
+ const parsed =
113
+ typeof value === "number"
114
+ ? value
115
+ : typeof value === "string" && /^(0x[0-9a-fA-F]+|\d+)$/.test(value)
116
+ ? Number(value)
117
+ : Number.NaN
118
+ return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0xff ? parsed : undefined
47
119
  }
@@ -1,13 +1,117 @@
1
- import { BLOCK_TIME } from "../consts"
1
+ import { InvalidExpiryError } from "../entity/errors"
2
+ import { type Expiry, type Lifetime, makeExpiry, toBlocks } from "../entity/expiry"
3
+ import { MAX_EXPIRES_AT } from "../entity/params"
2
4
 
5
+ /**
6
+ * The optional floor a deadline can carry.
7
+ *
8
+ * `atLeast` takes a duration — one of the `from*` helpers — and never another deadline, so
9
+ * `atBlock(x, { atLeast: atBlock(y) })` does not compile. Two deadlines are not a thing the engine
10
+ * can resolve.
11
+ */
12
+ export type DeadlineOptions = {
13
+ /** Live at least this long from now, even if the deadline is sooner. */
14
+ atLeast?: Lifetime | undefined
15
+ }
16
+
17
+ function lifetimeOf(seconds: number): Lifetime {
18
+ return makeExpiry(toBlocks(seconds), 0n) as Lifetime
19
+ }
20
+
21
+ function deadlineOf(expiresAt: bigint | Date, options?: DeadlineOptions): Expiry {
22
+ return makeExpiry(options?.atLeast?.minLifetime ?? 0n, expiresAt)
23
+ }
24
+
25
+ /**
26
+ * Builds the `expires` of a create or an extension.
27
+ *
28
+ * Every helper returns the `{ minLifetime, expiresAt }` pair the ABI carries, so what you write is
29
+ * what goes on the wire. The `from*` helpers fill `minLifetime` and leave `expiresAt` at `0n`;
30
+ * {@link ExpirationTime.atBlock} and {@link ExpirationTime.atDate} do the reverse. The engine takes
31
+ * whichever comes later, which is why a deadline can also carry a floor.
32
+ *
33
+ * Values are plain data — no methods, no prototype — so they log, clone and compare like any other
34
+ * object.
35
+ *
36
+ * @example The 90% case: a duration, and nothing else to think about.
37
+ * await client.createEntity({ payload, contentType, expires: ExpirationTime.fromDays(30) })
38
+ *
39
+ * @example An exact block, or a wall-clock date.
40
+ * expires: ExpirationTime.atBlock(1_200_000n)
41
+ * expires: ExpirationTime.atDate(new Date("2027-01-01"))
42
+ *
43
+ * @example A deadline that still guarantees a minimum life.
44
+ * expires: ExpirationTime.atBlock(1_200_000n, { atLeast: ExpirationTime.fromDays(1) })
45
+ */
3
46
  export const ExpirationTime = {
4
- fromSeconds: (seconds: number) => seconds,
5
- fromMinutes: (minutes: number) => minutes * 60,
6
- fromHours: (hours: number) => hours * 60 * 60,
7
- fromDays: (days: number) => days * 24 * 60 * 60,
8
- fromWeeks: (weeks: number) => weeks * 7 * 24 * 60 * 60,
9
- fromMonths: (months: number) => months * 30 * 24 * 60 * 60,
10
- fromYears: (years: number) => years * 365 * 24 * 60 * 60,
11
- fromBlocks: (blocks: number) => blocks * BLOCK_TIME,
12
- fromDate: (date: Date) => Math.floor(date.getTime() / 1000) - Math.floor(Date.now() / 1000),
47
+ /**
48
+ * A raw number of seconds. Must be a whole number of blocks' worth — see the other helpers, which
49
+ * all produce one by construction.
50
+ *
51
+ * @throws {InvalidExpiryError} If the duration is not a positive multiple of the block time.
52
+ */
53
+ fromSeconds: (seconds: number): Lifetime => lifetimeOf(seconds),
54
+ fromMinutes: (minutes: number): Lifetime => lifetimeOf(minutes * 60),
55
+ fromHours: (hours: number): Lifetime => lifetimeOf(hours * 60 * 60),
56
+ fromDays: (days: number): Lifetime => lifetimeOf(days * 24 * 60 * 60),
57
+ fromWeeks: (weeks: number): Lifetime => lifetimeOf(weeks * 7 * 24 * 60 * 60),
58
+ fromMonths: (months: number): Lifetime => lifetimeOf(months * 30 * 24 * 60 * 60),
59
+ fromYears: (years: number): Lifetime => lifetimeOf(years * 365 * 24 * 60 * 60),
60
+
61
+ /**
62
+ * A number of blocks, for callers who think in the chain's own unit. Always exact — there is no
63
+ * conversion to be approximate about.
64
+ *
65
+ * @throws {InvalidExpiryError} If the count is not a positive whole number.
66
+ */
67
+ fromBlocks: (blocks: number): Lifetime => {
68
+ if (!Number.isInteger(blocks) || blocks <= 0) {
69
+ throw new InvalidExpiryError(
70
+ `a lifetime must be a positive whole number of blocks, got ${blocks}`,
71
+ )
72
+ }
73
+ return makeExpiry(BigInt(blocks), 0n) as Lifetime
74
+ },
75
+
76
+ /**
77
+ * An absolute block height.
78
+ *
79
+ * @param block - The block the entity expires at.
80
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
81
+ * @throws {InvalidExpiryError} If the height is negative.
82
+ */
83
+ atBlock: (block: bigint, options?: DeadlineOptions): Expiry => {
84
+ if (typeof block !== "bigint" || block < 0n) {
85
+ throw new InvalidExpiryError(`a deadline must be a non-negative block height, got ${block}`)
86
+ }
87
+ return deadlineOf(block, options)
88
+ },
89
+
90
+ /**
91
+ * A wall-clock instant, placed against the block the transaction is built on.
92
+ *
93
+ * A date cannot land on a block boundary in general, so it rounds **up** to the next block: the
94
+ * entity lives until at least the instant asked for. This is why a date is never rejected for
95
+ * "not being a whole number of blocks" the way a duration is — it becomes a block height, not a
96
+ * duration.
97
+ *
98
+ * @param date - When the entity should expire.
99
+ * @param options - Optionally, a floor: `{ atLeast: ExpirationTime.fromDays(1) }`.
100
+ * @throws {InvalidExpiryError} If the value is not a valid `Date`.
101
+ */
102
+ atDate: (date: Date, options?: DeadlineOptions): Expiry => {
103
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
104
+ throw new InvalidExpiryError(`a deadline must be a valid Date, got ${String(date)}`)
105
+ }
106
+ return deadlineOf(date, options)
107
+ },
108
+
109
+ /**
110
+ * An entity that should never expire.
111
+ *
112
+ * This is `atBlock(MAX_EXPIRES_AT)` — the largest expiry the `uint64` field holds — and the
113
+ * engine treats it as an ordinary deadline rather than a special case. There is no permanence
114
+ * flag; a block height no chain will reach is what permanence is.
115
+ */
116
+ permanent: (): Expiry => deadlineOf(MAX_EXPIRES_AT),
13
117
  }
@@ -144,8 +144,8 @@ function getColorForNamespace(namespace: string): { css: string; ansi: string }
144
144
 
145
145
  // Use absolute value and modulo to get index
146
146
  const colorIndex = Math.abs(hash) % COLOR_PALETTE.length
147
- const cssColor = COLOR_PALETTE[colorIndex]
148
- const ansiColor = ANSI_COLORS[colorIndex % ANSI_COLORS.length]
147
+ const cssColor = COLOR_PALETTE[colorIndex] ?? "inherit"
148
+ const ansiColor = ANSI_COLORS[colorIndex % ANSI_COLORS.length] ?? ""
149
149
 
150
150
  const color = { css: cssColor, ansi: ansiColor }
151
151
 
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { BaseError, ContractFunctionRevertedError, type Hex, toHex } from "viem"
3
+ import { describeEntityRevert } from "./revert"
4
+
5
+ /**
6
+ * Wraps a decoded revert the way viem delivers one: the `ContractFunctionRevertedError` is nested
7
+ * in a cause chain, which is why `describeEntityRevert` walks rather than type-checks the top.
8
+ */
9
+ function reverted(errorName: string, args: readonly unknown[]): BaseError {
10
+ const inner = new ContractFunctionRevertedError({
11
+ abi: [],
12
+ functionName: "execute",
13
+ })
14
+ // The constructor cannot build `data` without a real ABI decode, so it is set directly — the
15
+ // shape is what matters here, not how viem arrived at it.
16
+ Object.assign(inner, { data: { errorName, args } })
17
+ return new BaseError('The contract function "execute" reverted.', { cause: inner })
18
+ }
19
+
20
+ /** An Ident32 name cell: the name, left-aligned and null-padded to 32 bytes. */
21
+ const ident = (name: string): Hex => toHex(name, { size: 32 })
22
+
23
+ const KEY = `0x${"ab".repeat(32)}` as Hex
24
+
25
+ describe("describeEntityRevert", () => {
26
+ it("ignores anything that is not a decoded engine revert", () => {
27
+ expect(describeEntityRevert(new Error("boom"))).toBeUndefined()
28
+ expect(describeEntityRevert("boom")).toBeUndefined()
29
+ expect(describeEntityRevert(undefined)).toBeUndefined()
30
+ expect(describeEntityRevert(new BaseError("no cause"))).toBeUndefined()
31
+ })
32
+
33
+ describe("attribute names", () => {
34
+ it("names the offending character rather than printing the byte alone", () => {
35
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [5n, "0x21"]))
36
+ expect(message).toContain('"!"')
37
+ expect(message).toContain("0x21")
38
+ expect(message).toContain("byte 5")
39
+ })
40
+
41
+ it("quotes the name charset the SDK itself validates against", () => {
42
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [0n, "0x2f"]))
43
+ expect(message).toContain('"A"-"Z"')
44
+ expect(message).toContain('"a"-"z"')
45
+ expect(message).toContain("with a letter first")
46
+ })
47
+
48
+ it("renders an unprintable byte as a byte", () => {
49
+ const message = describeEntityRevert(reverted("Ident32InvalidByte", [1n, "0x07"]))
50
+ expect(message).toContain("byte 0x07")
51
+ })
52
+ })
53
+
54
+ describe("names carried in a bytes32 cell", () => {
55
+ it("reads the name back out of its null padding", () => {
56
+ expect(
57
+ describeEntityRevert(reverted("SystemAttributeNotWritable", [ident("$owner")])),
58
+ ).toContain('"$owner"')
59
+ expect(describeEntityRevert(reverted("TombstoneInCreate", [ident("gone")]))).toContain(
60
+ '"gone"',
61
+ )
62
+ expect(describeEntityRevert(reverted("InvalidValueType", [ident("level"), 99]))).toContain(
63
+ '"level"',
64
+ )
65
+ })
66
+ })
67
+
68
+ describe("entity state", () => {
69
+ it("abbreviates the key so it does not bury the sentence", () => {
70
+ const message = describeEntityRevert(reverted("EntityNotFound", [KEY]))
71
+ expect(message).toContain("…")
72
+ expect(message).not.toContain(KEY)
73
+ })
74
+
75
+ it("explains a readonly entity in terms of what is still allowed", () => {
76
+ const message = describeEntityRevert(reverted("ReadOnlyEntity", [KEY]))
77
+ expect(message).toContain("readonly")
78
+ expect(message).toContain("extended")
79
+ })
80
+
81
+ it("renders reserved creation flags as the bits they are", () => {
82
+ const message = describeEntityRevert(reverted("ReservedCreationFlags", [0b100]))
83
+ expect(message).toContain("0b00000100")
84
+ })
85
+ })
86
+
87
+ describe("expiry", () => {
88
+ it("says which way the extension went wrong", () => {
89
+ const message = describeEntityRevert(reverted("ExpiryNotExtended", [KEY, 100n, 500n]))
90
+ expect(message).toContain("already expires at block 500")
91
+ expect(message).toContain("shorten")
92
+ })
93
+ })
94
+
95
+ it("falls back to naming the error when it has no tailored explanation", () => {
96
+ const message = describeEntityRevert(reverted("SomeFutureError", []))
97
+ expect(message).toBe("the engine rejected the batch with SomeFutureError")
98
+ })
99
+ })
@@ -0,0 +1,145 @@
1
+ import { BaseError, ContractFunctionRevertedError, type Hex, hexToString } from "viem"
2
+
3
+ /**
4
+ * Turns a decoded engine revert into a sentence that says what to change.
5
+ *
6
+ * viem decodes the error and then renders it positionally — `Ident32InvalidByte(uint256 position,
7
+ * bytes1 value)` arrives as `(5, 0x41)`, which is the right data and no help at all. The engine's
8
+ * errors carry enough to name the actual problem, so this spends the args rather than printing
9
+ * them.
10
+ *
11
+ * @param error - Anything thrown by a write action.
12
+ * @returns The explanation, or `undefined` if this was not a decodable engine revert — in which
13
+ * case the caller should fall back to whatever the transport said.
14
+ */
15
+ export function describeEntityRevert(error: unknown): string | undefined {
16
+ if (!(error instanceof BaseError)) return undefined
17
+ const reverted = error.walk((e) => e instanceof ContractFunctionRevertedError)
18
+ if (!(reverted instanceof ContractFunctionRevertedError)) return undefined
19
+
20
+ const name = reverted.data?.errorName
21
+ if (name === undefined) return undefined
22
+ const args = (reverted.data?.args ?? []) as readonly unknown[]
23
+
24
+ return explain(name, args) ?? `the engine rejected the batch with ${name}`
25
+ }
26
+
27
+ /** The attribute-name charset, quoted the way an error message should read. */
28
+ const CHARSET = '"A"-"Z", "a"-"z", "0"-"9", ".", "-" and "_", with a letter first'
29
+
30
+ function explain(name: string, args: readonly unknown[]): string | undefined {
31
+ switch (name) {
32
+ // ── attribute names ──────────────────────────────────────────────────────
33
+ case "Ident32InvalidByte": {
34
+ const position = Number(args[0] ?? 0)
35
+ const char = printable(String(args[1] ?? "0x00"))
36
+ return (
37
+ `an attribute name holds ${char} at byte ${position}, which is outside the ` +
38
+ `name charset (${CHARSET})`
39
+ )
40
+ }
41
+ case "Ident32Empty":
42
+ return "an attribute name is empty"
43
+
44
+ // ── the attribute list ───────────────────────────────────────────────────
45
+ case "SystemAttributeNotWritable":
46
+ return (
47
+ `"${identName(args[0])}" is owned by the engine and cannot be written. Only $payload and ` +
48
+ "$contentType are yours to set"
49
+ )
50
+ case "TombstoneInCreate":
51
+ return (
52
+ `a create cannot unset "${identName(args[0])}" — nothing exists yet, so leave the name ` +
53
+ "out instead"
54
+ )
55
+ case "TombstoneValueNotEmpty":
56
+ return `the tombstone for "${identName(args[0])}" carries a value; unsetting takes no value`
57
+ case "InvalidValueType":
58
+ return `"${identName(args[0])}" names type id ${String(args[1])}, which is not a known type`
59
+ case "AttributesNotSorted":
60
+ return "attributes must be strictly ascending by name, which also makes each name unique"
61
+ case "TooManyAttributes":
62
+ return `${String(args[0])} attributes exceeds the limit of ${String(args[1])}`
63
+ case "EmptyMutations":
64
+ return `the patch of ${short(args[0])} has no mutations, so it would do nothing`
65
+
66
+ // ── entity state and authorisation ───────────────────────────────────────
67
+ case "EntityNotFound":
68
+ return `no entity ${short(args[0])} — it may have been deleted, or have expired`
69
+ case "EntityExpired":
70
+ return `entity ${short(args[0])} expired at block ${String(args[1])}`
71
+ case "NotOwner":
72
+ return `entity ${short(args[0])} is owned by ${String(args[2])}, not ${String(args[1])}`
73
+ case "ReadOnlyEntity":
74
+ return (
75
+ `entity ${short(args[0])} was created readonly, so its contents can never change. Its ` +
76
+ "expiry can still be extended, and it can be transferred or deleted"
77
+ )
78
+ case "ReservedCreationFlags":
79
+ return (
80
+ `creation flags 0b${Number(args[0] ?? 0)
81
+ .toString(2)
82
+ .padStart(8, "0")} set a reserved ` +
83
+ "bit; only readonly and permissionlessExtension are defined"
84
+ )
85
+
86
+ // ── expiry ───────────────────────────────────────────────────────────────
87
+ case "ExpiryDeadOnArrival":
88
+ return (
89
+ `the expiry resolves to block ${String(args[0])}, which is not after the current block ` +
90
+ `${String(args[1])}`
91
+ )
92
+ case "ExpiryNotExtended":
93
+ return (
94
+ `entity ${short(args[0])} already expires at block ${String(args[2])}, so extending it ` +
95
+ `to ${String(args[1])} would shorten its life`
96
+ )
97
+
98
+ // ── ownership ────────────────────────────────────────────────────────────
99
+ case "TransferToZeroAddress":
100
+ return `entity ${short(args[0])} cannot be transferred to the zero address`
101
+ case "TransferToSelf":
102
+ return `entity ${short(args[0])} is already owned by that account`
103
+
104
+ // ── batch framing ────────────────────────────────────────────────────────
105
+ case "EmptyBatch":
106
+ return "the batch has no operations"
107
+ case "InvalidOpType":
108
+ return `operation tag ${String(args[0])} is not one the engine knows`
109
+ case "NonCanonicalOperationData":
110
+ return (
111
+ `the payload for operation tag ${String(args[0])} is not its canonical encoding, so the ` +
112
+ "engine refused it rather than accept a second spelling of the same operation"
113
+ )
114
+
115
+ default:
116
+ return undefined
117
+ }
118
+ }
119
+
120
+ /** A `bytes32` Ident32 cell as the name it holds — left-aligned, null-padded. */
121
+ function identName(raw: unknown): string {
122
+ if (typeof raw !== "string") return String(raw)
123
+ try {
124
+ // Cut at the first null rather than trimming with a regex: the padding is whatever follows
125
+ // the name, so everything before it is the name and nothing after it is. Written as an escape
126
+ // because a literal NUL byte in source is invisible to every reader after you.
127
+ return hexToString(raw as Hex).split("\u0000")[0] ?? ""
128
+ } catch {
129
+ return raw
130
+ }
131
+ }
132
+
133
+ /** An entity key, abbreviated */
134
+ function short(raw: unknown): string {
135
+ const text = String(raw)
136
+ return text.length > 18 ? `${text.slice(0, 10)}…${text.slice(-6)}` : text
137
+ }
138
+
139
+ /** A `bytes1` as something readable: the character it is, plus the byte it was. */
140
+ function printable(byte: string): string {
141
+ const code = Number.parseInt(byte.replace(/^0x/, ""), 16)
142
+ if (!Number.isFinite(code)) return `byte ${byte}`
143
+ const char = String.fromCharCode(code)
144
+ return code >= 0x21 && code <= 0x7e ? `"${char}" (${byte})` : `byte ${byte}`
145
+ }
@@ -1,108 +0,0 @@
1
- //#region rolldown:runtime
2
- var __defProp = Object.defineProperty;
3
- var __export = (all) => {
4
- let target = {};
5
- for (var name in all) __defProp(target, name, {
6
- get: all[name],
7
- enumerable: true
8
- });
9
- return target;
10
- };
11
-
12
- //#endregion
13
- let viem = require("viem");
14
-
15
- //#region src/chains/braga.ts
16
- const braga = (0, viem.defineChain)({
17
- id: 60138453102,
18
- name: "Braga",
19
- network: "braga",
20
- nativeCurrency: {
21
- name: "Golem",
22
- symbol: "GLM",
23
- decimals: 18
24
- },
25
- rpcUrls: { default: {
26
- http: ["https://braga.hoodi.arkiv.network/rpc"],
27
- webSocket: ["wss://braga.hoodi.arkiv.network/rpc/ws"]
28
- } },
29
- blockExplorers: { default: {
30
- name: "Braga Arkiv Explorer",
31
- url: "https://explorer.braga.hoodi.arkiv.network",
32
- apiUrl: "https://explorer.braga.hoodi.arkiv.network/api"
33
- } },
34
- testnet: true
35
- });
36
-
37
- //#endregion
38
- //#region src/chains/kaolin.ts
39
- /**
40
- * @deprecated The kaolin network is getting removed on may 15th 2026. Migration guide: https://docs.arkiv.network/networks/migrate-from-kaolin/
41
- */
42
- const kaolin = (0, viem.defineChain)({
43
- id: 60138453025,
44
- name: "Kaolin",
45
- network: "kaolin",
46
- nativeCurrency: {
47
- name: "Ethereum",
48
- symbol: "ETH",
49
- decimals: 18
50
- },
51
- rpcUrls: { default: {
52
- http: ["https://kaolin.hoodi.arkiv.network/rpc"],
53
- webSocket: ["wss://kaolin.hoodi.arkiv.network/rpc/ws"]
54
- } },
55
- blockExplorers: { default: {
56
- name: "Kaolin Arkiv Explorer",
57
- url: "https://explorer.kaolin.hoodi.arkiv.network",
58
- apiUrl: "https://explorer.kaolin.hoodi.arkiv.network/api"
59
- } },
60
- testnet: true
61
- });
62
-
63
- //#endregion
64
- //#region src/chains/localhost.ts
65
- const localhost = (0, viem.defineChain)({
66
- id: 1337,
67
- name: "Localhost",
68
- nativeCurrency: {
69
- decimals: 18,
70
- name: "Ether",
71
- symbol: "ETH"
72
- },
73
- rpcUrls: { default: { http: ["http://127.0.0.1:8545"] } }
74
- });
75
-
76
- //#endregion
77
- //#region src/chains/index.ts
78
- var chains_exports = /* @__PURE__ */ __export({
79
- braga: () => braga,
80
- kaolin: () => kaolin,
81
- localhost: () => localhost
82
- });
83
-
84
- //#endregion
85
- Object.defineProperty(exports, 'braga', {
86
- enumerable: true,
87
- get: function () {
88
- return braga;
89
- }
90
- });
91
- Object.defineProperty(exports, 'chains_exports', {
92
- enumerable: true,
93
- get: function () {
94
- return chains_exports;
95
- }
96
- });
97
- Object.defineProperty(exports, 'kaolin', {
98
- enumerable: true,
99
- get: function () {
100
- return kaolin;
101
- }
102
- });
103
- Object.defineProperty(exports, 'localhost', {
104
- enumerable: true,
105
- get: function () {
106
- return localhost;
107
- }
108
- });