@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.
- package/README.md +29 -16
- package/dist/attr/index.cjs +34 -0
- package/dist/attr/index.d.cts +2 -0
- package/dist/attr/index.d.ts +2 -0
- package/dist/attr/index.js +3 -0
- package/dist/attr-BDZ-APg5.js +809 -0
- package/dist/attr-BDZ-APg5.js.map +1 -0
- package/dist/attr-BvxHN99W.cjs +1029 -0
- package/dist/chains/index.cjs +2 -3
- package/dist/chains/index.d.cts +39 -77
- 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/changeOwnership-B7bEgla4.d.cts +509 -0
- package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
- package/dist/entity/index.cjs +19 -0
- package/dist/entity/index.d.cts +4 -0
- package/dist/entity/index.d.ts +4 -0
- package/dist/entity/index.js +6 -0
- package/dist/entity-B0t6sN7F.cjs +71 -0
- package/dist/entity-BIp_df9u.js +67 -0
- package/dist/entity-BIp_df9u.js.map +1 -0
- package/dist/entity-CFhbAv0R.js +70 -0
- package/dist/entity-CFhbAv0R.js.map +1 -0
- package/dist/entity-wfBnZd91.cjs +98 -0
- package/dist/errors-D-eJkGVc.cjs +56 -0
- package/dist/errors-DGB8AgT2.js +33 -0
- package/dist/errors-DGB8AgT2.js.map +1 -0
- package/dist/expiry-BUYIXTGb.cjs +135 -0
- package/dist/expiry-BxOlvVWC.d.ts +79 -0
- package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
- package/dist/expiry-DGRzrSan.js +95 -0
- package/dist/expiry-DGRzrSan.js.map +1 -0
- package/dist/expiry-DRV29F1r.d.cts +79 -0
- package/dist/expiry-DRV29F1r.d.cts.map +1 -0
- package/dist/flags-0Vl5rrHq.d.cts +41 -0
- package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
- package/dist/flags-Ct8k0gpB.cjs +43 -0
- package/dist/flags-ZtlvOIIk.js +33 -0
- package/dist/flags-ZtlvOIIk.js.map +1 -0
- package/dist/flags-wmcMoAnN.d.ts +41 -0
- package/dist/flags-wmcMoAnN.d.ts.map +1 -0
- package/dist/index-Cgw1u5aY.d.cts +278 -0
- package/dist/index-Cgw1u5aY.d.cts.map +1 -0
- package/dist/index-ClgIuxJ8.d.ts +159 -0
- package/dist/index-ClgIuxJ8.d.ts.map +1 -0
- package/dist/index-D0CmtiWT.d.cts +159 -0
- package/dist/index-D0CmtiWT.d.cts.map +1 -0
- package/dist/index-D8LZt-3J.d.cts +417 -0
- package/dist/index-D8LZt-3J.d.cts.map +1 -0
- package/dist/index-DVYFaXNV.d.ts +417 -0
- package/dist/index-DVYFaXNV.d.ts.map +1 -0
- package/dist/index-VCqgn29D.d.ts +278 -0
- package/dist/index-VCqgn29D.d.ts.map +1 -0
- package/dist/index.cjs +675 -357
- package/dist/index.d.cts +251 -203
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +251 -203
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +626 -355
- package/dist/index.js.map +1 -1
- package/dist/payload-Bnw1hrvb.cjs +83 -0
- package/dist/payload-CIxSn_DW.js +67 -0
- package/dist/payload-CIxSn_DW.js.map +1 -0
- package/dist/payload-Dgx3PQeK.d.ts +94 -0
- package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
- package/dist/payload-P0nWyjLg.d.cts +94 -0
- package/dist/payload-P0nWyjLg.d.cts.map +1 -0
- package/dist/query/index.cjs +24 -149
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +5 -3
- package/dist/query/index.js +6 -137
- package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
- package/dist/queryBuilder-CbLL4pG5.js +963 -0
- package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
- package/dist/selection-BvK6yX0g.d.cts +433 -0
- package/dist/selection-BvK6yX0g.d.cts.map +1 -0
- package/dist/selection-fpmI3ADQ.d.ts +433 -0
- package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -1
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.js +4 -1
- 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/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 +33 -69
- package/src/actions/public/watchEntityEvents.test.ts +414 -0
- package/src/actions/public/watchEntityEvents.ts +225 -0
- package/src/actions/wallet/changeOwnership.ts +8 -8
- package/src/actions/wallet/createEntity.ts +104 -26
- package/src/actions/wallet/deleteEntity.ts +2 -3
- package/src/actions/wallet/extendEntity.ts +39 -16
- package/src/actions/wallet/mutateEntities.test.ts +76 -0
- package/src/actions/wallet/mutateEntities.ts +29 -74
- 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/arkivPublic.ts +160 -139
- package/src/clients/decorators/arkivWallet.ts +91 -95
- package/src/consts.ts +2 -1
- 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 +14 -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 +33 -28
- package/src/index.ts +6 -0
- package/src/query/engine.ts +59 -134
- 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 +322 -637
- package/src/query/queryBuilder.ts +126 -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 +62 -44
- package/src/types/events.ts +64 -23
- package/src/types/index.ts +20 -16
- package/src/types/rpcSchema.ts +70 -33
- package/src/utils/arkivTransactions.test.ts +681 -38
- package/src/utils/arkivTransactions.ts +249 -104
- 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 +111 -29
- package/src/utils/expirationTime.ts +114 -10
- package/src/utils/logger.ts +2 -2
- package/src/utils/payload.ts +4 -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-AUoL-v42.cjs +0 -18
- package/dist/consts-BV4PR-de.js +0 -7
- package/dist/consts-BV4PR-de.js.map +0 -1
- package/dist/entity-B6g_EPm1.d.cts +0 -382
- package/dist/entity-B6g_EPm1.d.cts.map +0 -1
- package/dist/entity-qoLMAuuk.d.ts +0 -382
- package/dist/entity-qoLMAuuk.d.ts.map +0 -1
- package/dist/index-DdspxNAA.d.ts +0 -481
- package/dist/index-DdspxNAA.d.ts.map +0 -1
- package/dist/index-Ds9dwqb5.d.cts +0 -481
- package/dist/index-Ds9dwqb5.d.cts.map +0 -1
- package/dist/payload-B2kFJyah.d.ts +0 -6
- package/dist/payload-B2kFJyah.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-Claf2jUO.js +0 -13
- package/dist/payload-Claf2jUO.js.map +0 -1
- package/dist/payload-DpA5PA-w.cjs +0 -23
- package/dist/query/index.js.map +0 -1
- package/dist/queryBuilder-D2uwP9jI.js +0 -704
- package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
- package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
- 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 -119
- package/src/actions/wallet/updateEntity.ts +0 -60
- package/src/chains/braga.ts +0 -26
- package/src/chains/kaolin.ts +0 -29
- package/src/query/engine.test.ts +0 -526
- 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 -210
- package/src/types/attributes.ts +0 -16
- package/src/utils/validation.test.ts +0 -60
- package/src/utils/validation.ts +0 -32
- /package/dist/{types-H-TSmRpm.js → types-Bx1EKe0v.js} +0 -0
- /package/dist/{types-o3zWarRp.cjs → types-CKPddlfS.cjs} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { encodeAttributes, encodeMutations, MAX_ATTRIBUTES, resolveAttributes } from "./attributes"
|
|
3
|
+
import { InvalidAttributeNameError, TooManyAttributesError } from "./errors"
|
|
4
|
+
import { isValidAttributeName, validateAttributeName } from "./names"
|
|
5
|
+
import { dec, i32, str, u256 } from "./values"
|
|
6
|
+
|
|
7
|
+
describe("resolveAttributes", () => {
|
|
8
|
+
it("types bare values and leaves tagged ones alone", () => {
|
|
9
|
+
const resolved = resolveAttributes({
|
|
10
|
+
level: 10,
|
|
11
|
+
balance: 1_000_000n,
|
|
12
|
+
name: "Bob",
|
|
13
|
+
flagged: true,
|
|
14
|
+
score: dec("3.5"),
|
|
15
|
+
})
|
|
16
|
+
expect(resolved.level).toEqual(i32(10))
|
|
17
|
+
expect(resolved.balance).toEqual(u256(1_000_000n))
|
|
18
|
+
expect(resolved.name).toEqual(str("Bob"))
|
|
19
|
+
expect(resolved.flagged.type).toBe("bool")
|
|
20
|
+
expect(resolved.score).toEqual(dec("3.5"))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("reports the attribute count when there are too many", () => {
|
|
24
|
+
const tooMany = Object.fromEntries(
|
|
25
|
+
Array.from({ length: MAX_ATTRIBUTES + 1 }, (_, i) => [`a${i}`, i]),
|
|
26
|
+
)
|
|
27
|
+
expect(() => resolveAttributes(tooMany)).toThrow(TooManyAttributesError)
|
|
28
|
+
expect(() => resolveAttributes(tooMany)).toThrow(/at most 32 attributes, got 33/)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("the attribute cap", () => {
|
|
33
|
+
const attributes = (count: number) =>
|
|
34
|
+
Object.fromEntries(Array.from({ length: count }, (_, i) => [`a${i}`, i]))
|
|
35
|
+
|
|
36
|
+
it("counts the payload and content-type cells, which the engine also counts", () => {
|
|
37
|
+
// Under the cap on its own, over it once the two system cells are appended. Letting this
|
|
38
|
+
// through would spend gas on a transaction the engine answers with TooManyAttributes(34, 32).
|
|
39
|
+
expect(() =>
|
|
40
|
+
encodeAttributes(attributes(MAX_ATTRIBUTES), {
|
|
41
|
+
payload: new Uint8Array([1]),
|
|
42
|
+
contentType: "application/json",
|
|
43
|
+
}),
|
|
44
|
+
).toThrow(/at most 32 attributes, got 34/)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("leaves room for them rather than rejecting a full-but-legal operation", () => {
|
|
48
|
+
expect(
|
|
49
|
+
encodeAttributes(attributes(MAX_ATTRIBUTES - 2), {
|
|
50
|
+
payload: new Uint8Array([1]),
|
|
51
|
+
contentType: "application/json",
|
|
52
|
+
}),
|
|
53
|
+
).toHaveLength(MAX_ATTRIBUTES)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("counts tombstones in a patch — a removal occupies a cell like any other", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
encodeMutations({
|
|
59
|
+
set: attributes(MAX_ATTRIBUTES - 1),
|
|
60
|
+
unset: ["gone_a", "gone_b"],
|
|
61
|
+
}),
|
|
62
|
+
).toThrow(/at most 32 attributes, got 33/)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("encodeAttributes", () => {
|
|
67
|
+
it("sorts strictly ascending by the encoded name, whatever order they were written in", () => {
|
|
68
|
+
const encoded = encodeAttributes({ zebra: 1, apple: 2, mango: 3 })
|
|
69
|
+
const names = encoded.map((attribute) => attribute.name)
|
|
70
|
+
expect(names).toEqual([...names].sort())
|
|
71
|
+
expect(encoded).toHaveLength(3)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("carries each value's typeId", () => {
|
|
75
|
+
const ids = encodeAttributes({ level: i32(1), score: dec("1"), tag: "x" }).map((a) => a.typeId)
|
|
76
|
+
expect([...ids].sort()).toEqual([2, 5, 8]) // i32, dec, str
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("accepts an empty attribute set", () => {
|
|
80
|
+
expect(encodeAttributes({})).toEqual([])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe("validateAttributeName", () => {
|
|
85
|
+
it("accepts the Ident32 grammar, in either case", () => {
|
|
86
|
+
for (const name of ["a", "tag", "Tag", "projectId", "num_attr", "a.b-c_D9", "a".repeat(32)]) {
|
|
87
|
+
expect(isValidAttributeName(name)).toBe(true)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("treats names as case-sensitive, so two casings are two attributes", () => {
|
|
92
|
+
const resolved = resolveAttributes({ level: 1, Level: 2 })
|
|
93
|
+
expect(resolved.level).toEqual(i32(1))
|
|
94
|
+
expect(resolved.Level).toEqual(i32(2))
|
|
95
|
+
expect(encodeAttributes({ level: 1, Level: 2 })).toHaveLength(2)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("rejects names outside the grammar, pointing at the offending character", () => {
|
|
99
|
+
expect(() => validateAttributeName("")).toThrow(InvalidAttributeNameError)
|
|
100
|
+
expect(() => validateAttributeName("1tag")).toThrow(/starts with "1"/)
|
|
101
|
+
expect(() => validateAttributeName("_tag")).toThrow(/starts with "_"/)
|
|
102
|
+
expect(() => validateAttributeName("has space")).toThrow(/" " at position 3/)
|
|
103
|
+
expect(() => validateAttributeName("héllo")).toThrow(/"é" at position 1/)
|
|
104
|
+
expect(() => validateAttributeName("a:b")).toThrow(/":" at position 1/)
|
|
105
|
+
expect(() => validateAttributeName("a".repeat(33))).toThrow(/33 bytes long/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("reserves $ for the system attributes the engine sets", () => {
|
|
109
|
+
expect(() => validateAttributeName("$owner")).toThrow(/reserved for system attributes/)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("rejects names the query language could never parse back, whatever their casing", () => {
|
|
113
|
+
for (const reserved of ["and", "OR", "Not", "key", "str", "u256", "EXISTS", "Bytes32"]) {
|
|
114
|
+
expect(() => validateAttributeName(reserved)).toThrow(/reserved by the query language/)
|
|
115
|
+
}
|
|
116
|
+
// Only the exact word is reserved — a name containing it is fine.
|
|
117
|
+
expect(isValidAttributeName("android")).toBe(true)
|
|
118
|
+
expect(isValidAttributeName("sortKey")).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { type AbiAttribute, encodeAbiAttribute, encodeTombstone } from "./codec"
|
|
2
|
+
import { ConflictingMutationError, InvalidValueError, TooManyAttributesError } from "./errors"
|
|
3
|
+
import { validateAttributeName } from "./names"
|
|
4
|
+
import type { AnyArkivValue, ArkivValue, TypeTag } from "./types"
|
|
5
|
+
import { makeValue } from "./types"
|
|
6
|
+
import { str, toValue, type ValueInput } from "./values"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The most attribute cells one operation may carry.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_ATTRIBUTES = 32
|
|
12
|
+
|
|
13
|
+
export const MAX_PAYLOAD_BYTES = 128 * 1024
|
|
14
|
+
|
|
15
|
+
/** The system attribute holding an entity's opaque payload. */
|
|
16
|
+
const PAYLOAD_CELL = "$payload"
|
|
17
|
+
/** The system attribute holding an entity's content type. */
|
|
18
|
+
const CONTENT_TYPE_CELL = "$contentType"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The attributes written on an entity, as a plain object keyed by attribute name.
|
|
22
|
+
*
|
|
23
|
+
* Values may be tagged ({@link ValueInput}) or bare where the type is unambiguous — see
|
|
24
|
+
* {@link toValue} for the bare-form defaults.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const attributes = {
|
|
28
|
+
* level: i32(10),
|
|
29
|
+
* balance: u256(1_000_000n),
|
|
30
|
+
* score: dec("3.5"),
|
|
31
|
+
* parent: key(parentKey),
|
|
32
|
+
* name: "Bob", // bare string -> str
|
|
33
|
+
* flagged: true, // bare boolean -> bool
|
|
34
|
+
* } satisfies AttributeInputs
|
|
35
|
+
*/
|
|
36
|
+
export type AttributeInputs = Readonly<Record<string, ValueInput>>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The attributes read back from an entity: the same shape, with every value typed.
|
|
40
|
+
*
|
|
41
|
+
* A value read from here drops straight back into an {@link AttributeInputs} — the vocabulary is the
|
|
42
|
+
* same in both directions.
|
|
43
|
+
*/
|
|
44
|
+
export type Attributes = Readonly<Record<string, AnyArkivValue>>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The names and types of an entity's attributes, with no values — what the `attributeSchema`
|
|
48
|
+
* projection returns.
|
|
49
|
+
*
|
|
50
|
+
* Useful for asking what shape the data has without paying to transfer it, and for telling apart
|
|
51
|
+
* two entities that use the same name with different types.
|
|
52
|
+
*/
|
|
53
|
+
export type AttributeSchema = Readonly<Record<string, TypeTag>>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The typed form of an attribute map: names validated, bare values resolved to their default type.
|
|
57
|
+
*
|
|
58
|
+
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
59
|
+
* @throws {MissingValueError} If a value is `undefined` or `null` — omit the name instead.
|
|
60
|
+
* @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
|
|
61
|
+
* @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* resolveAttributes({ level: 10, name: "Bob" })
|
|
65
|
+
* // { level: { type: "i32", value: 10 }, name: { type: "str", value: "Bob" } }
|
|
66
|
+
*/
|
|
67
|
+
export function resolveAttributes(attributes: AttributeInputs): Record<string, ArkivValue> {
|
|
68
|
+
const entries = Object.entries(attributes)
|
|
69
|
+
if (entries.length > MAX_ATTRIBUTES) {
|
|
70
|
+
throw new TooManyAttributesError(entries.length, MAX_ATTRIBUTES)
|
|
71
|
+
}
|
|
72
|
+
const resolved: Record<string, ArkivValue> = {}
|
|
73
|
+
for (const [name, input] of entries) {
|
|
74
|
+
validateAttributeName(name)
|
|
75
|
+
resolved[name] = toValue(input, name)
|
|
76
|
+
}
|
|
77
|
+
return resolved
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The payload and content type, which travel to the engine as system attributes.
|
|
82
|
+
*
|
|
83
|
+
* They are separate fields on the SDK's surface because that is what they are to an application —
|
|
84
|
+
* an entity's contents and its type — but on the wire an operation carries no dedicated field for
|
|
85
|
+
* either: `$payload` is the one `bytes`-typed cell, and `$contentType` is a `str`.
|
|
86
|
+
*/
|
|
87
|
+
export type SystemCells = {
|
|
88
|
+
payload?: Uint8Array | undefined
|
|
89
|
+
contentType?: string | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Encodes an attribute map, plus the payload and content-type system cells, into the ABI attribute
|
|
94
|
+
* array an operation carries.
|
|
95
|
+
*
|
|
96
|
+
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
97
|
+
* @throws {MissingValueError} If a value is `undefined` or `null`.
|
|
98
|
+
* @throws {InvalidValueError} If a value does not fit its type.
|
|
99
|
+
* @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
|
|
100
|
+
*/
|
|
101
|
+
export function encodeAttributes(
|
|
102
|
+
attributes: AttributeInputs = {},
|
|
103
|
+
system: SystemCells = {},
|
|
104
|
+
): AbiAttribute[] {
|
|
105
|
+
const cells: [string, AnyArkivValue][] = Object.entries(resolveAttributes(attributes))
|
|
106
|
+
|
|
107
|
+
// Written whenever the field is given, empty or not: an empty payload is a payload, and dropping
|
|
108
|
+
// it would mean the entity does not hold what the caller passed. Absence is for the operations
|
|
109
|
+
// that genuinely leave a cell alone, not for a value that happens to be empty.
|
|
110
|
+
if (system.payload !== undefined) {
|
|
111
|
+
if (system.payload.length > MAX_PAYLOAD_BYTES) {
|
|
112
|
+
throw new InvalidValueError(
|
|
113
|
+
"bytes",
|
|
114
|
+
`<${system.payload.length} bytes>`,
|
|
115
|
+
`${system.payload.length} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte payload limit`,
|
|
116
|
+
"Store the bulk elsewhere and keep a reference to it on the entity.",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
cells.push([PAYLOAD_CELL, makeValue("bytes", toHexBytes(system.payload))])
|
|
120
|
+
}
|
|
121
|
+
if (system.contentType !== undefined) {
|
|
122
|
+
cells.push([CONTENT_TYPE_CELL, str(system.contentType)])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (cells.length > MAX_ATTRIBUTES) {
|
|
126
|
+
throw new TooManyAttributesError(cells.length, MAX_ATTRIBUTES)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return cells.map(([name, value]) => encodeAbiAttribute(name, value)).sort(byName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type MutationInputs = SystemCells & {
|
|
133
|
+
/** Attributes to write. Existing ones are overwritten; new ones are added. */
|
|
134
|
+
set?: AttributeInputs | undefined
|
|
135
|
+
/** Attribute names to remove. Each becomes a tombstone on the wire. */
|
|
136
|
+
unset?: readonly string[] | undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Encodes a patch's mutations into the ABI attribute array the operation carries.
|
|
141
|
+
*
|
|
142
|
+
* @throws {ConflictingMutationError} If a name appears in both `set` and `unset`.
|
|
143
|
+
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
144
|
+
* @throws {InvalidValueError} If a value does not fit its type.
|
|
145
|
+
* @throws {TooManyAttributesError} If `set` names more than {@link MAX_ATTRIBUTES} attributes.
|
|
146
|
+
*/
|
|
147
|
+
export function encodeMutations({
|
|
148
|
+
set,
|
|
149
|
+
unset,
|
|
150
|
+
payload,
|
|
151
|
+
contentType,
|
|
152
|
+
}: MutationInputs): AbiAttribute[] {
|
|
153
|
+
const written = encodeAttributes(set, { payload, contentType })
|
|
154
|
+
const tombstones = resolveUnset(unset, set).map(encodeTombstone)
|
|
155
|
+
const mutations = [...written, ...tombstones]
|
|
156
|
+
if (mutations.length > MAX_ATTRIBUTES) {
|
|
157
|
+
throw new TooManyAttributesError(mutations.length, MAX_ATTRIBUTES)
|
|
158
|
+
}
|
|
159
|
+
return mutations.sort(byName)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveUnset(unset: readonly string[] = [], set: AttributeInputs = {}): string[] {
|
|
163
|
+
if (!Array.isArray(unset)) {
|
|
164
|
+
throw new TypeError(`"unset" must be an array of attribute names`)
|
|
165
|
+
}
|
|
166
|
+
const names = new Set(unset)
|
|
167
|
+
for (const name of names) {
|
|
168
|
+
validateAttributeName(name)
|
|
169
|
+
if (Object.hasOwn(set, name)) {
|
|
170
|
+
throw new ConflictingMutationError(name)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...names]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The engine requires attributes **strictly ascending by their `bytes32` name**: that ordering is
|
|
178
|
+
* what the canonical serialization hashes into the state root, and it doubles as the uniqueness
|
|
179
|
+
* check.
|
|
180
|
+
*/
|
|
181
|
+
function byName(a: AbiAttribute, b: AbiAttribute): number {
|
|
182
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Raw bytes as lowercase `0x` hex, without viem's hex-string ambiguity. */
|
|
186
|
+
function toHexBytes(bytes: Uint8Array): `0x${string}` {
|
|
187
|
+
let out = "0x"
|
|
188
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0")
|
|
189
|
+
return out as `0x${string}`
|
|
190
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import type { Hex } from "viem"
|
|
3
|
+
import {
|
|
4
|
+
decodeRpcValue,
|
|
5
|
+
decodeValueBytes,
|
|
6
|
+
encodeAbiAttribute,
|
|
7
|
+
encodeTombstone,
|
|
8
|
+
encodeValueBytes,
|
|
9
|
+
} from "./codec"
|
|
10
|
+
import { InvalidValueError, UnknownAttributeTypeError } from "./errors"
|
|
11
|
+
import type { ArkivValue } from "./types"
|
|
12
|
+
import { addr, bool, bytes32, dec, i32, key, str, u64, u256 } from "./values"
|
|
13
|
+
|
|
14
|
+
const ZERO = `0x${"00".repeat(32)}` as Hex
|
|
15
|
+
const KEY = `0x${"ab".repeat(32)}` as Hex
|
|
16
|
+
const VITALIK = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
|
17
|
+
|
|
18
|
+
/** One value of every user-settable type, including the awkward edges of each. */
|
|
19
|
+
const SAMPLES: ArkivValue[] = [
|
|
20
|
+
bool(true),
|
|
21
|
+
bool(false),
|
|
22
|
+
i32(0),
|
|
23
|
+
i32(-42),
|
|
24
|
+
i32(2147483647),
|
|
25
|
+
i32(-2147483648),
|
|
26
|
+
u64(0n),
|
|
27
|
+
u64(2n ** 64n - 1n),
|
|
28
|
+
u256(0n),
|
|
29
|
+
u256(2n ** 256n - 1n),
|
|
30
|
+
dec("0"),
|
|
31
|
+
dec("3.5"),
|
|
32
|
+
dec("-3.5"),
|
|
33
|
+
dec("-0.000000000000000001"),
|
|
34
|
+
bytes32(KEY),
|
|
35
|
+
key(KEY),
|
|
36
|
+
addr(VITALIK),
|
|
37
|
+
str(""),
|
|
38
|
+
str("hello"),
|
|
39
|
+
str("é".repeat(64)),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
describe("ABI value encoding", () => {
|
|
43
|
+
it("round-trips every type", () => {
|
|
44
|
+
for (const value of SAMPLES) {
|
|
45
|
+
const back = decodeValueBytes(value.type, encodeValueBytes(value))
|
|
46
|
+
expect(back.type).toBe(value.type)
|
|
47
|
+
expect(back.value).toEqual(value.value)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("gives every word type exactly 32 bytes", () => {
|
|
52
|
+
for (const value of SAMPLES) {
|
|
53
|
+
if (value.type === "str") continue
|
|
54
|
+
expect(encodeValueBytes(value)).toHaveLength(2 + 64)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("sign-extends a negative i32 across the whole word, as Solidity does", () => {
|
|
59
|
+
expect(encodeValueBytes(i32(-1))).toBe(`0x${"ff".repeat(32)}`)
|
|
60
|
+
expect(encodeValueBytes(i32(-42))).toBe(`0x${"ff".repeat(31)}d6`)
|
|
61
|
+
expect(encodeValueBytes(i32(42))).toBe(`0x${"00".repeat(31)}2a`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("stores a dec as its two's-complement scaled int256", () => {
|
|
65
|
+
const scaled = (value: string) => BigInt.asIntN(256, BigInt(encodeValueBytes(dec(value))))
|
|
66
|
+
expect(scaled("1.5")).toBe(1_500_000_000_000_000_000n)
|
|
67
|
+
expect(scaled("-1.5")).toBe(-1_500_000_000_000_000_000n)
|
|
68
|
+
expect(scaled("0")).toBe(0n)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("right-aligns an address and a bool", () => {
|
|
72
|
+
expect(encodeValueBytes(addr(VITALIK))).toBe(
|
|
73
|
+
`0x${"00".repeat(12)}${VITALIK.slice(2).toLowerCase()}`,
|
|
74
|
+
)
|
|
75
|
+
expect(encodeValueBytes(bool(true))).toBe(`0x${"00".repeat(31)}01`)
|
|
76
|
+
expect(encodeValueBytes(bool(false))).toBe(ZERO)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("gives a str its raw UTF-8 bytes, with no padding", () => {
|
|
80
|
+
expect(encodeValueBytes(str("hi"))).toBe("0x6869")
|
|
81
|
+
expect(encodeValueBytes(str(""))).toBe("0x")
|
|
82
|
+
// Two bytes per "é", so no relationship to a 32-byte word.
|
|
83
|
+
expect(encodeValueBytes(str("é"))).toBe("0xc3a9")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("rejects a word a well-formed encoder would not have produced", () => {
|
|
87
|
+
expect(() => decodeValueBytes("bool", `0x${"00".repeat(31)}02`)).toThrow(/exactly 0 or 1/)
|
|
88
|
+
expect(() => decodeValueBytes("u256", "0x2a")).toThrow(/must be 32 bytes, got 1/)
|
|
89
|
+
// An i32 whose upper bytes contradict its sign bit.
|
|
90
|
+
expect(() => decodeValueBytes("i32", `0xff${"00".repeat(31)}`)).toThrow(/sign-extended/)
|
|
91
|
+
// An address with junk above its 20 bytes.
|
|
92
|
+
expect(() => decodeValueBytes("addr", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/)
|
|
93
|
+
// A u64 with junk above its 8 bytes — truncating it would invent a plausible block height.
|
|
94
|
+
expect(() => decodeValueBytes("u64", `0x${"ab".repeat(32)}`)).toThrow(/zero-padded/)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("encodeAbiAttribute", () => {
|
|
99
|
+
it("packs the name left-aligned into its bytes32 cell", () => {
|
|
100
|
+
const attribute = encodeAbiAttribute("level", i32(10))
|
|
101
|
+
// "level" is 6c6576656c, null-padded to 32 bytes.
|
|
102
|
+
expect(attribute.name).toBe(`0x6c6576656c${"00".repeat(27)}`)
|
|
103
|
+
expect(attribute.typeId).toBe(2)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("encodes a tombstone as a zero-length value under typeId 0", () => {
|
|
107
|
+
expect(encodeTombstone("flagged")).toEqual({
|
|
108
|
+
name: `0x666c6167676564${"00".repeat(25)}`,
|
|
109
|
+
typeId: 0,
|
|
110
|
+
value: "0x",
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe("decodeRpcValue", () => {
|
|
116
|
+
it("reads the JSON encoding each type declares", () => {
|
|
117
|
+
expect(decodeRpcValue("bool", true).value).toBe(true)
|
|
118
|
+
expect(decodeRpcValue("i32", -42).value).toBe(-42)
|
|
119
|
+
expect(decodeRpcValue("u256", "0xf4240").value).toBe(1_000_000n)
|
|
120
|
+
expect(decodeRpcValue("dec", "3.5").value).toBe("3.5")
|
|
121
|
+
expect(decodeRpcValue("dec", "-0.25").value).toBe("-0.25")
|
|
122
|
+
expect(decodeRpcValue("bytes32", KEY).value).toBe(KEY)
|
|
123
|
+
expect(decodeRpcValue("str", "hello").value).toBe("hello")
|
|
124
|
+
expect(decodeRpcValue("addr", VITALIK.toLowerCase()).value).toBe(VITALIK)
|
|
125
|
+
expect(decodeRpcValue("key", KEY).value).toBe(KEY)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("takes an integer however it is rendered", () => {
|
|
129
|
+
// QUANTITY is the spec's encoding; decimal strings and numbers are tolerated.
|
|
130
|
+
expect(decodeRpcValue("u256", "0x2a").value).toBe(42n)
|
|
131
|
+
expect(decodeRpcValue("u256", "42").value).toBe(42n)
|
|
132
|
+
expect(decodeRpcValue("u256", 42).value).toBe(42n)
|
|
133
|
+
expect(decodeRpcValue("i32", "-42").value).toBe(-42)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("decodes the system-only bytes type, which only ever arrives as a payload", () => {
|
|
137
|
+
expect(decodeRpcValue("bytes", "0xDEADBEEF").value).toBe("0xdeadbeef")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("names the type it does not recognise, and says to upgrade", () => {
|
|
141
|
+
expect(() => decodeRpcValue("int", "x")).toThrow(UnknownAttributeTypeError)
|
|
142
|
+
expect(() => decodeRpcValue("int", "x")).toThrow(/upgrade @arkiv-network\/sdk/)
|
|
143
|
+
// The pre-spec wire form: a typeId where the JSON-RPC surface names a tag.
|
|
144
|
+
expect(() => decodeRpcValue(3 as unknown as string, "0x2a")).toThrow(UnknownAttributeTypeError)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("does not mistake an inherited Object key for a type tag", () => {
|
|
148
|
+
// `type in TYPE_IDS` walks the prototype chain, so these passed the check and then fell off
|
|
149
|
+
// the end of the exhaustive switch, returning `undefined` — the caller got a TypeError from
|
|
150
|
+
// `entity.attributes.foo.value` instead of "upgrade the SDK".
|
|
151
|
+
for (const inherited of ["constructor", "toString", "valueOf", "hasOwnProperty"]) {
|
|
152
|
+
expect(() => decodeRpcValue(inherited, 1)).toThrow(UnknownAttributeTypeError)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("rejects a value whose JSON shape contradicts its declared type", () => {
|
|
157
|
+
expect(() => decodeRpcValue("u256", "not a number")).toThrow(InvalidValueError)
|
|
158
|
+
expect(() => decodeRpcValue("str", 42)).toThrow(/expected a string/)
|
|
159
|
+
expect(() => decodeRpcValue("bool", "yes")).toThrow(/not a boolean/)
|
|
160
|
+
expect(() => decodeRpcValue("i32", 1.5)).toThrow(/not an integer/)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { bytesToString, type Hex, hexToBytes, pad, size, stringToBytes, toHex } from "viem"
|
|
2
|
+
import { unitsToDecimal } from "./decimal"
|
|
3
|
+
import { InvalidValueError, UnknownAttributeTypeError } from "./errors"
|
|
4
|
+
import { type AnyArkivValue, makeValue, type StrValue, TYPE_IDS, type TypeTag } from "./types"
|
|
5
|
+
import { addr, bool, bytes32, dec, decUnits, i32, key, u64, u256 } from "./values"
|
|
6
|
+
|
|
7
|
+
function strFromChain(value: string): StrValue {
|
|
8
|
+
return makeValue("str", value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `typeId` marking a tombstone — an attribute being unset. It carries a zero-length value and
|
|
13
|
+
* is legal only in a patch's mutation list, never in a create.
|
|
14
|
+
*/
|
|
15
|
+
export const TOMBSTONE_TYPE_ID = 0
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An attribute in its ABI form: `(Ident32 name, uint8 typeId, bytes value)`.
|
|
19
|
+
*
|
|
20
|
+
* The `value` encoding is selected by `typeId`: an exact 32-byte word for the word types, the raw
|
|
21
|
+
* bytes for `str` and `bytes`, and zero-length for a tombstone.
|
|
22
|
+
*/
|
|
23
|
+
export type AbiAttribute = {
|
|
24
|
+
name: Hex
|
|
25
|
+
typeId: number
|
|
26
|
+
value: Hex
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Encodes a typed value into the `bytes` the ABI carries for it.
|
|
31
|
+
*
|
|
32
|
+
* This and {@link decodeValueBytes} are the only place the value/wire mapping is written down.
|
|
33
|
+
*
|
|
34
|
+
* - Word types (`bool`, `i32`, `u64`, `u256`, `dec`, `bytes32`, `addr`, `key`) become exactly 32
|
|
35
|
+
* bytes, in the encoding Solidity would use — right-aligned, and sign-extended for `i32`.
|
|
36
|
+
* - `str` becomes its raw UTF-8 bytes, and `bytes` its raw bytes: variable length, no padding.
|
|
37
|
+
*/
|
|
38
|
+
export function encodeValueBytes(value: AnyArkivValue): Hex {
|
|
39
|
+
switch (value.type) {
|
|
40
|
+
case "bool":
|
|
41
|
+
return pad(value.value ? "0x01" : "0x00", { size: 32 })
|
|
42
|
+
case "i32":
|
|
43
|
+
// int32 is sign-extended across the whole word, as Solidity encodes it.
|
|
44
|
+
return toHex(BigInt.asUintN(256, BigInt(value.value)), { size: 32 })
|
|
45
|
+
case "u64":
|
|
46
|
+
case "u256":
|
|
47
|
+
return toHex(value.value, { size: 32 })
|
|
48
|
+
case "dec":
|
|
49
|
+
return toHex(BigInt.asUintN(256, decUnits(value)), { size: 32 })
|
|
50
|
+
case "bytes32":
|
|
51
|
+
case "key":
|
|
52
|
+
return value.value
|
|
53
|
+
case "addr":
|
|
54
|
+
return pad(value.value.toLowerCase() as Hex, { size: 32 })
|
|
55
|
+
case "str":
|
|
56
|
+
return toHex(stringToBytes(value.value))
|
|
57
|
+
case "bytes":
|
|
58
|
+
return value.value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decodes the ABI `bytes` of an attribute of type `tag`.
|
|
64
|
+
*
|
|
65
|
+
* Rejects anything a well-formed encoder would not have produced — a word type whose value is not
|
|
66
|
+
* 32 bytes, an `i32` that is not sign-extended, a `bool` that is neither 0 nor 1 — rather than
|
|
67
|
+
* silently truncating it.
|
|
68
|
+
*/
|
|
69
|
+
export function decodeValueBytes(tag: TypeTag, value: Hex): AnyArkivValue {
|
|
70
|
+
if (tag === "str") return strFromChain(bytesToString(hexToBytes(value)))
|
|
71
|
+
if (tag === "bytes") return makeValue("bytes", value.toLowerCase() as Hex)
|
|
72
|
+
|
|
73
|
+
const width = size(value)
|
|
74
|
+
if (width !== 32) {
|
|
75
|
+
throw new InvalidValueError(tag, value, `a ${tag} value must be 32 bytes, got ${width}`)
|
|
76
|
+
}
|
|
77
|
+
const raw = BigInt(value)
|
|
78
|
+
|
|
79
|
+
switch (tag) {
|
|
80
|
+
case "bool":
|
|
81
|
+
if (raw !== 0n && raw !== 1n) {
|
|
82
|
+
throw new InvalidValueError("bool", value, "a bool word must hold exactly 0 or 1")
|
|
83
|
+
}
|
|
84
|
+
return bool(raw === 1n)
|
|
85
|
+
case "i32": {
|
|
86
|
+
const narrowed = BigInt.asIntN(32, raw)
|
|
87
|
+
if (BigInt.asUintN(256, narrowed) !== raw) {
|
|
88
|
+
throw new InvalidValueError("i32", value, "an i32 word must be sign-extended")
|
|
89
|
+
}
|
|
90
|
+
return i32(narrowed)
|
|
91
|
+
}
|
|
92
|
+
case "u64":
|
|
93
|
+
if (raw >> 64n !== 0n) {
|
|
94
|
+
throw new InvalidValueError("u64", value, "a u64 word must be zero-padded to 8 bytes")
|
|
95
|
+
}
|
|
96
|
+
return u64(raw)
|
|
97
|
+
case "u256":
|
|
98
|
+
return u256(raw)
|
|
99
|
+
case "dec":
|
|
100
|
+
return dec(unitsToDecimal(BigInt.asIntN(256, raw)))
|
|
101
|
+
case "bytes32":
|
|
102
|
+
return bytes32(value)
|
|
103
|
+
case "key":
|
|
104
|
+
return key(value)
|
|
105
|
+
case "addr": {
|
|
106
|
+
if (raw >> 160n !== 0n) {
|
|
107
|
+
throw new InvalidValueError(
|
|
108
|
+
"addr",
|
|
109
|
+
value,
|
|
110
|
+
"an address word must be zero-padded to 20 bytes",
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
return addr(`0x${value.slice(-40)}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Encodes a named, typed attribute into its ABI form.
|
|
120
|
+
*
|
|
121
|
+
* @param name - The attribute name. Assumed already validated, or a system (`$`) name the SDK sets
|
|
122
|
+
* itself.
|
|
123
|
+
*/
|
|
124
|
+
export function encodeAbiAttribute(name: string, value: AnyArkivValue): AbiAttribute {
|
|
125
|
+
return {
|
|
126
|
+
// Ident32 is left-aligned and null-padded, which is what `toHex` does for a string.
|
|
127
|
+
name: toHex(name, { size: 32 }),
|
|
128
|
+
typeId: TYPE_IDS[value.type],
|
|
129
|
+
value: encodeValueBytes(value),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Encodes a tombstone — the mutation that unsets `name`. Legal only in a patch. */
|
|
134
|
+
export function encodeTombstone(name: string): AbiAttribute {
|
|
135
|
+
return { name: toHex(name, { size: 32 }), typeId: TOMBSTONE_TYPE_ID, value: "0x" }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Decodes an attribute value as it comes back over JSON-RPC.
|
|
140
|
+
*
|
|
141
|
+
* The JSON encoding follows the value's **declared type**, never its magnitude: `bool` is a JSON
|
|
142
|
+
* boolean, `i32` a JSON number, `u64`/`u256` a `0x` QUANTITY, `dec` a decimal string, `str` a
|
|
143
|
+
* string, and
|
|
144
|
+
* the byte-shaped types `0x` DATA. Decimal strings are also accepted for the integer types, so this
|
|
145
|
+
* keeps working against a node that renders them that way.
|
|
146
|
+
*
|
|
147
|
+
* @param type - The type tag the response declared, e.g. `"u256"`.
|
|
148
|
+
* @param value - The JSON value.
|
|
149
|
+
* @throws {UnknownAttributeTypeError} If the tag names no known type.
|
|
150
|
+
* @throws {InvalidValueError} If the value does not parse as that type.
|
|
151
|
+
*/
|
|
152
|
+
export function decodeRpcValue(type: string, value: unknown): AnyArkivValue {
|
|
153
|
+
const tag = asTypeTag(type)
|
|
154
|
+
switch (tag) {
|
|
155
|
+
case "bool":
|
|
156
|
+
return bool(asBoolean(value))
|
|
157
|
+
case "i32":
|
|
158
|
+
return i32(Number(asIntegerish("i32", value)))
|
|
159
|
+
case "u64":
|
|
160
|
+
return u64(asIntegerish("u64", value))
|
|
161
|
+
case "u256":
|
|
162
|
+
return u256(asIntegerish("u256", value))
|
|
163
|
+
case "dec":
|
|
164
|
+
return dec(asString("dec", value))
|
|
165
|
+
case "bytes32":
|
|
166
|
+
return bytes32(asString("bytes32", value) as Hex)
|
|
167
|
+
case "key":
|
|
168
|
+
return key(asString("key", value) as Hex)
|
|
169
|
+
case "addr":
|
|
170
|
+
return addr(asString("addr", value))
|
|
171
|
+
case "str":
|
|
172
|
+
return strFromChain(asString("str", value))
|
|
173
|
+
case "bytes":
|
|
174
|
+
return makeValue("bytes", asString("bytes", value).toLowerCase() as Hex)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Narrows a type tag as the node spelled it.
|
|
180
|
+
*
|
|
181
|
+
* @throws {UnknownAttributeTypeError} If it names no type this SDK knows.
|
|
182
|
+
*/
|
|
183
|
+
export function asTypeTag(type: string): TypeTag {
|
|
184
|
+
// `Object.hasOwn`, not `in`: `in` walks the prototype chain, so "constructor" and "toString"
|
|
185
|
+
// would pass as type tags and then fall off the end of the exhaustive switch below as `undefined`.
|
|
186
|
+
if (typeof type !== "string" || !Object.hasOwn(TYPE_IDS, type)) {
|
|
187
|
+
throw new UnknownAttributeTypeError(type)
|
|
188
|
+
}
|
|
189
|
+
return type as TypeTag
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function asBoolean(value: unknown): boolean {
|
|
193
|
+
if (typeof value === "boolean") return value
|
|
194
|
+
// Tolerated because a node may render it as text; anything else is a protocol error.
|
|
195
|
+
if (value === "true" || value === "false") return value === "true"
|
|
196
|
+
throw new InvalidValueError("bool", value, "not a boolean")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function asString(tag: TypeTag, value: unknown): string {
|
|
200
|
+
if (typeof value !== "string") {
|
|
201
|
+
throw new InvalidValueError(tag, value, `expected a string, got ${typeof value}`)
|
|
202
|
+
}
|
|
203
|
+
return value
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A `0x` QUANTITY, a JSON number, or a decimal string — all of which name one integer. */
|
|
207
|
+
function asIntegerish(tag: TypeTag, value: unknown): bigint {
|
|
208
|
+
if (typeof value === "number") {
|
|
209
|
+
if (!Number.isInteger(value)) {
|
|
210
|
+
throw new InvalidValueError(tag, value, "not an integer")
|
|
211
|
+
}
|
|
212
|
+
return BigInt(value)
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "bigint") return value
|
|
215
|
+
if (typeof value === "string") {
|
|
216
|
+
if (/^0x[0-9a-fA-F]+$/.test(value)) return BigInt(value)
|
|
217
|
+
if (/^-?\d+$/.test(value)) return BigInt(value)
|
|
218
|
+
}
|
|
219
|
+
throw new InvalidValueError(tag, value, "not an integer")
|
|
220
|
+
}
|