@arkiv-network/sdk 0.7.0-dev.1 → 0.8.0-advanced.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -16
- package/dist/actions/advanced/index.cjs +13 -0
- package/dist/actions/advanced/index.d.cts +8 -0
- package/dist/actions/advanced/index.d.ts +9 -0
- package/dist/actions/advanced/index.js +9 -0
- package/dist/advanced-8ZPTmmaN.js +598 -0
- package/dist/advanced-8ZPTmmaN.js.map +1 -0
- package/dist/advanced-fPImL6lW.cjs +656 -0
- package/dist/arkivTransactions-D74gERHG.d.cts +571 -0
- package/dist/arkivTransactions-D74gERHG.d.cts.map +1 -0
- package/dist/arkivTransactions-NtGD6uk5.d.ts +571 -0
- package/dist/arkivTransactions-NtGD6uk5.d.ts.map +1 -0
- package/dist/attr/index.cjs +35 -0
- package/dist/attr/index.d.cts +2 -0
- package/dist/attr/index.d.ts +3 -0
- package/dist/attr/index.js +4 -0
- package/dist/attributes-BhY5DwJp.js +809 -0
- package/dist/attributes-BhY5DwJp.js.map +1 -0
- package/dist/attributes-Df9IsCVs.cjs +1029 -0
- package/dist/chains/index.cjs +2 -3
- package/dist/chains/index.d.cts +22 -60
- package/dist/chains/index.d.cts.map +1 -1
- package/dist/chains/index.d.ts +22 -60
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js +2 -2
- package/dist/chains-BJrZs6sG.js +57 -0
- package/dist/chains-BJrZs6sG.js.map +1 -0
- package/dist/chains-BYRTTipa.cjs +73 -0
- package/dist/codec-Car3KkFF.d.ts +102 -0
- package/dist/codec-Car3KkFF.d.ts.map +1 -0
- package/dist/entity/index.cjs +20 -0
- package/dist/entity/index.d.cts +4 -0
- package/dist/entity/index.d.ts +5 -0
- package/dist/entity/index.js +7 -0
- package/dist/entity-B00Li04j.cjs +71 -0
- package/dist/entity-B7G8q_Eo.cjs +0 -0
- package/dist/entity-BYQR1uoe.js +67 -0
- package/dist/entity-BYQR1uoe.js.map +1 -0
- package/dist/entity-CD_qFF2M.js +0 -0
- package/dist/errors-BVbjKVMN.cjs +56 -0
- package/dist/errors-LTe7bOYS.js +33 -0
- package/dist/errors-LTe7bOYS.js.map +1 -0
- package/dist/expiry-BELY2Kpw.cjs +135 -0
- package/dist/expiry-BjpxZZQU.js +95 -0
- package/dist/expiry-BjpxZZQU.js.map +1 -0
- package/dist/expiry-DRV29F1r.d.cts +79 -0
- package/dist/expiry-DRV29F1r.d.cts.map +1 -0
- package/dist/expiry-nDbCgZX4.d.ts +79 -0
- package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
- package/dist/flags-5dTNRnow.d.ts +41 -0
- package/dist/flags-5dTNRnow.d.ts.map +1 -0
- package/dist/flags-CRjPpoh3.d.cts +41 -0
- package/dist/flags-CRjPpoh3.d.cts.map +1 -0
- package/dist/flags-DFBs2J94.js +33 -0
- package/dist/flags-DFBs2J94.js.map +1 -0
- package/dist/flags-kr276q6i.cjs +43 -0
- package/dist/index-B5j76711.d.ts +184 -0
- package/dist/index-B5j76711.d.ts.map +1 -0
- package/dist/index-BF7Mq9Is.d.cts +280 -0
- package/dist/index-BF7Mq9Is.d.cts.map +1 -0
- package/dist/index-CA4V7Pdh.d.cts +167 -0
- package/dist/index-CA4V7Pdh.d.cts.map +1 -0
- package/dist/index-CV5zCdpa.d.cts +417 -0
- package/dist/index-CV5zCdpa.d.cts.map +1 -0
- package/dist/index-DHDud2hC.d.ts +280 -0
- package/dist/index-DHDud2hC.d.ts.map +1 -0
- package/dist/index-DvYvrkdk.d.ts +320 -0
- package/dist/index-DvYvrkdk.d.ts.map +1 -0
- package/dist/index-EeUO0nnJ.d.cts +184 -0
- package/dist/index-EeUO0nnJ.d.cts.map +1 -0
- package/dist/index-fX_XyqLp.d.ts +167 -0
- package/dist/index-fX_XyqLp.d.ts.map +1 -0
- package/dist/index.cjs +411 -405
- package/dist/index.d.cts +360 -209
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +361 -209
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +342 -387
- package/dist/index.js.map +1 -1
- package/dist/key-D1Bh7MF0.js +70 -0
- package/dist/key-D1Bh7MF0.js.map +1 -0
- package/dist/key-WKhXnXuU.cjs +98 -0
- package/dist/logger-CVpvU8ik.js +153 -0
- package/dist/logger-CVpvU8ik.js.map +1 -0
- package/dist/logger-Cr15lPAQ.cjs +188 -0
- package/dist/payload-BNoud2KZ.d.cts +94 -0
- package/dist/payload-BNoud2KZ.d.cts.map +1 -0
- package/dist/payload-Bz6651ax.js +67 -0
- package/dist/payload-Bz6651ax.js.map +1 -0
- package/dist/payload-CcUj85ys.d.ts +94 -0
- package/dist/payload-CcUj85ys.d.ts.map +1 -0
- package/dist/payload-KC14SDlN.cjs +83 -0
- package/dist/query/index.cjs +26 -150
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +6 -3
- package/dist/query/index.js +8 -138
- package/dist/queryBuilder-DM6AWCBa.js +820 -0
- package/dist/queryBuilder-DM6AWCBa.js.map +1 -0
- package/dist/queryBuilder-DbUMEZln.cjs +944 -0
- package/dist/selection-C4UhsMOO.d.ts +434 -0
- package/dist/selection-C4UhsMOO.d.ts.map +1 -0
- package/dist/selection-DPg0uyJf.d.cts +433 -0
- package/dist/selection-DPg0uyJf.d.cts.map +1 -0
- package/dist/types/index.cjs +3 -3
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +8 -2
- package/dist/types/index.js +3 -3
- package/dist/types-Bu_ZwMbZ.js +0 -0
- package/dist/types-NRqymi6S.cjs +0 -0
- package/dist/utils/index.cjs +4 -17
- package/dist/utils/index.d.cts +3 -17
- package/dist/utils/index.d.ts +3 -17
- package/dist/utils/index.js +4 -18
- package/package.json +26 -5
- package/src/actions/advanced/advanced.test.ts +258 -0
- package/src/actions/advanced/buildMutation.ts +104 -0
- package/src/actions/advanced/getMutationResult.ts +112 -0
- package/src/actions/advanced/index.ts +21 -0
- package/src/actions/advanced/pingTransaction.ts +43 -0
- package/src/actions/advanced/sendMutation.ts +59 -0
- package/src/actions/public/getEntity.ts +27 -43
- package/src/actions/public/getEntityNonce.ts +30 -0
- package/src/actions/public/predictEntityKeys.test.ts +143 -0
- package/src/actions/public/predictEntityKeys.ts +138 -0
- package/src/actions/public/query.ts +35 -67
- package/src/actions/public/watchEntityEvents.test.ts +414 -0
- package/src/actions/public/watchEntityEvents.ts +225 -0
- package/src/actions/wallet/changeOwnership.ts +2 -2
- package/src/actions/wallet/createEntity.ts +99 -24
- package/src/actions/wallet/extendEntity.ts +37 -13
- package/src/actions/wallet/mutateEntities.test.ts +76 -0
- package/src/actions/wallet/mutateEntities.ts +16 -33
- package/src/actions/wallet/patchEntity.ts +90 -0
- package/src/attr/attributes.test.ts +120 -0
- package/src/attr/attributes.ts +190 -0
- package/src/attr/codec.test.ts +162 -0
- package/src/attr/codec.ts +220 -0
- package/src/attr/decimal.ts +86 -0
- package/src/attr/errors.ts +145 -0
- package/src/attr/index.ts +50 -0
- package/src/attr/names.ts +106 -0
- package/src/attr/types.ts +150 -0
- package/src/attr/values.test.ts +271 -0
- package/src/attr/values.ts +502 -0
- package/src/chains/cheesecake.ts +22 -0
- package/src/chains/index.ts +1 -2
- package/src/clients/createPublicClient.test.ts +3 -5
- package/src/clients/createPublicClient.ts +5 -4
- package/src/clients/createWalletClient.test.ts +2 -2
- package/src/clients/createWalletClient.ts +13 -7
- package/src/clients/decorators/arkivAdvanced.ts +144 -0
- package/src/clients/decorators/arkivPublic.ts +172 -139
- package/src/clients/decorators/arkivWallet.ts +101 -94
- package/src/entity/create.test.ts +184 -0
- package/src/entity/errors.ts +38 -0
- package/src/entity/events.ts +16 -0
- package/src/entity/expiry.test.ts +195 -0
- package/src/entity/expiry.ts +181 -0
- package/src/entity/flags.ts +65 -0
- package/src/entity/index.ts +15 -0
- package/src/entity/key.ts +68 -0
- package/src/entity/operations.ts +134 -0
- package/src/entity/params.ts +15 -0
- package/src/errors.ts +26 -49
- package/src/index.ts +19 -0
- package/src/query/engine.ts +68 -137
- package/src/query/errors.ts +151 -0
- package/src/query/expression.test.ts +286 -0
- package/src/query/expression.ts +493 -0
- package/src/query/index.ts +40 -4
- package/src/query/injection.test.ts +151 -0
- package/src/query/queryBuilder.test.ts +342 -637
- package/src/query/queryBuilder.ts +133 -359
- package/src/query/queryResult.ts +54 -51
- package/src/query/selection.test.ts +48 -98
- package/src/query/selection.ts +77 -65
- package/src/types/entity.test.ts +44 -155
- package/src/types/entity.ts +60 -52
- package/src/types/events.ts +59 -16
- package/src/types/index.ts +20 -17
- package/src/types/rpcSchema.ts +69 -37
- package/src/utils/arkivTransactions.test.ts +600 -155
- package/src/utils/arkivTransactions.ts +350 -219
- package/src/utils/chains.test.ts +21 -24
- package/src/utils/compression.ts +1 -1
- package/src/utils/entities.test.ts +62 -0
- package/src/utils/entities.ts +109 -37
- package/src/utils/expirationTime.ts +114 -10
- package/src/utils/logger.ts +2 -2
- package/src/utils/revert.test.ts +99 -0
- package/src/utils/revert.ts +145 -0
- package/dist/chains-BifaY04V.cjs +0 -108
- package/dist/chains-CrtkVbBh.js +0 -86
- package/dist/chains-CrtkVbBh.js.map +0 -1
- package/dist/consts-Bz0LZQZY.js +0 -7
- package/dist/consts-Bz0LZQZY.js.map +0 -1
- package/dist/consts-CxT1-RXU.cjs +0 -18
- package/dist/entity-BhSXaB23.cjs +0 -81
- package/dist/entity-CN41IOrq.d.cts +0 -401
- package/dist/entity-CN41IOrq.d.cts.map +0 -1
- package/dist/entity-DGfVyB-a.js +0 -71
- package/dist/entity-DGfVyB-a.js.map +0 -1
- package/dist/entity-NnEVlePN.d.ts +0 -401
- package/dist/entity-NnEVlePN.d.ts.map +0 -1
- package/dist/index-ClkpS6s_.d.ts +0 -481
- package/dist/index-ClkpS6s_.d.ts.map +0 -1
- package/dist/index-w4qop0Pe.d.cts +0 -481
- package/dist/index-w4qop0Pe.d.cts.map +0 -1
- package/dist/payload-BpNUPOnY.js +0 -13
- package/dist/payload-BpNUPOnY.js.map +0 -1
- package/dist/payload-C2KHOUoj.d.ts +0 -6
- package/dist/payload-C2KHOUoj.d.ts.map +0 -1
- package/dist/payload-CJN8fTvX.d.cts +0 -6
- package/dist/payload-CJN8fTvX.d.cts.map +0 -1
- package/dist/payload-D8YLKcJj.cjs +0 -23
- package/dist/query/index.js.map +0 -1
- package/dist/queryBuilder-BE_XhLLa.js +0 -735
- package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
- package/dist/queryBuilder-J37buGCi.cjs +0 -865
- package/dist/utils/index.d.cts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/src/actions/public/buildQuery.ts +0 -10
- package/src/actions/public/subscribeEntityEvents.ts +0 -93
- package/src/actions/wallet/updateEntity.ts +0 -59
- package/src/chains/braga.ts +0 -26
- package/src/chains/kaolin.ts +0 -29
- package/src/query/engine.test.ts +0 -571
- package/src/query/predicate.test.ts +0 -461
- package/src/query/predicate.ts +0 -144
- package/src/query/queryResult.test.ts +0 -270
- package/src/query/selectQueryBuilder.test.ts +0 -213
- package/src/types/attributes.ts +0 -28
- package/src/utils/validation.test.ts +0 -106
- package/src/utils/validation.ts +0 -80
- /package/dist/{types-Ci4BRClw.js → attr-DlmErkvR.cjs} +0 -0
- /package/dist/{types-a8gm_IaQ.cjs → attr-GcQKatub.js} +0 -0
package/src/utils/chains.test.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
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
|
|
8
|
-
const chain = chainFromName("
|
|
9
|
-
expect(chain).toBe(
|
|
10
|
-
expect(chain.id).toBe(
|
|
11
|
-
expect(chain.name).toBe("
|
|
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("
|
|
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("
|
|
38
|
-
expect(chainFromName("Kaolin")).toBe(kaolin)
|
|
29
|
+
expect(chainFromName("Cheesecake")).toBe(cheesecake)
|
|
39
30
|
expect(chainFromName("Localhost")).toBe(localhost)
|
|
40
|
-
expect(chainFromName("
|
|
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("
|
|
56
|
-
expect(() => chainFromName("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
})
|
package/src/utils/compression.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/utils/entities.ts
CHANGED
|
@@ -1,47 +1,119 @@
|
|
|
1
|
-
import { type Hex,
|
|
2
|
-
import type {
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
rpcEntity.key,
|
|
27
|
-
rpcEntity.
|
|
28
|
-
rpcEntity.
|
|
29
|
-
rpcEntity.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
rpcEntity.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
: undefined,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -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
|
+
})
|