@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.
- 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 +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/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 +676 -375
- package/dist/index.d.cts +247 -208
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +247 -208
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +626 -368
- 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 -150
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +5 -3
- package/dist/query/index.js +6 -138
- 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 +3 -3
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.js +3 -3
- 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 +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 +19 -21
- 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 +86 -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 +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 +26 -49
- package/src/index.ts +6 -0
- package/src/query/engine.ts +59 -139
- 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 +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 +181 -190
- 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 → types-Bx1EKe0v.js} +0 -0
- /package/dist/{types-a8gm_IaQ.cjs → types-CKPddlfS.cjs} +0 -0
|
@@ -1,43 +1,109 @@
|
|
|
1
1
|
import type { Hash, Hex } from "viem"
|
|
2
|
+
import type { AttributeInputs } from "../../attr"
|
|
2
3
|
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
-
import type {
|
|
4
|
+
import type { CreationFlags, Expiry } from "../../entity"
|
|
5
|
+
import { EntityMutationError } from "../../errors"
|
|
6
|
+
import type { MimeType, TxParams } from "../../types"
|
|
4
7
|
import { sendArkivTransaction } from "../../utils/arkivTransactions"
|
|
5
8
|
import { getLogger } from "../../utils/logger"
|
|
6
9
|
|
|
7
10
|
const logger = getLogger("actions:wallet:create-entity")
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* Parameters for
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* Parameters for creating an entity.
|
|
14
|
+
*
|
|
15
|
+
* An entity always carries contents and a type for them, plus a lifetime. Attributes, flags and a
|
|
16
|
+
* salt are optional on top of that.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const { entityKey } = await client.createEntity({
|
|
20
|
+
* attributes: {
|
|
21
|
+
* level: i32(10),
|
|
22
|
+
* balance: u256(1_000_000n),
|
|
23
|
+
* score: dec("3.5"),
|
|
24
|
+
* name: "Bob", // bare string -> str
|
|
25
|
+
* flagged: true, // bare boolean -> bool
|
|
26
|
+
* },
|
|
27
|
+
* payload: jsonToPayload({ hello: "world" }),
|
|
28
|
+
* contentType: "application/json",
|
|
29
|
+
* expires: ExpirationTime.fromDays(30),
|
|
30
|
+
* flags: { readonly: true },
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* @example Pin an absolute deadline, but refuse to create something nearly dead:
|
|
34
|
+
* await client.createEntity({
|
|
35
|
+
* payload, contentType: "application/json",
|
|
36
|
+
* expires: ExpirationTime.atDate(someDeadline, {
|
|
37
|
+
* atLeast: ExpirationTime.fromDays(1), // ...but live at least a day regardless
|
|
38
|
+
* }),
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* @example An entity that is nothing but queryable attributes still declares its (empty) contents:
|
|
42
|
+
* await client.createEntity({
|
|
43
|
+
* attributes: { parent: key(parentKey), rank: i32(3) },
|
|
44
|
+
* payload: new Uint8Array(),
|
|
45
|
+
* contentType: "application/octet-stream",
|
|
46
|
+
* expires: ExpirationTime.fromDays(30),
|
|
47
|
+
* })
|
|
22
48
|
*/
|
|
23
49
|
export type CreateEntityParameters = {
|
|
50
|
+
/**
|
|
51
|
+
* How long the entity should live, built with {@link ExpirationTime}.
|
|
52
|
+
*
|
|
53
|
+
* `ExpirationTime.fromDays(30)` is the everyday form. `atBlock` / `atDate` pin an absolute
|
|
54
|
+
* deadline instead, and either can carry `{ atLeast }` to guarantee a minimum life on top of it.
|
|
55
|
+
*/
|
|
56
|
+
expires: Expiry
|
|
57
|
+
/**
|
|
58
|
+
* The entity's opaque payload. Travels to the engine as the `$payload` system attribute; on this
|
|
59
|
+
* surface it is just the entity's contents.
|
|
60
|
+
*
|
|
61
|
+
* Pass an empty `Uint8Array` for an entity that is nothing but queryable attributes — it is
|
|
62
|
+
* written as an empty payload rather than left unset, so what you pass is what is stored.
|
|
63
|
+
*/
|
|
24
64
|
payload: Uint8Array
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
|
|
65
|
+
/** The payload's MIME type, e.g. `"application/json"`. Sent as the `$contentType` cell. */
|
|
66
|
+
contentType: MimeType | (string & {})
|
|
67
|
+
/**
|
|
68
|
+
* The entity's queryable attributes, keyed by name. Values are the tagged constructors from
|
|
69
|
+
* `@arkiv-network/sdk/attr` — `i32`, `u256`, `dec`, `str`, `addr`, `key`, `bytes32`, `bool` — or
|
|
70
|
+
* a bare `boolean`, `number`, `bigint` or `string` where the type is unambiguous.
|
|
71
|
+
*
|
|
72
|
+
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
73
|
+
* @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
|
|
74
|
+
*/
|
|
75
|
+
attributes?: AttributeInputs | undefined
|
|
76
|
+
/**
|
|
77
|
+
* Properties fixed at creation: `readonly` and `permissionlessExtension`. No operation changes
|
|
78
|
+
* them afterwards, so an entity is whatever it was created as for life. Both default to `false`.
|
|
79
|
+
*/
|
|
80
|
+
flags?: CreationFlags | undefined
|
|
81
|
+
/**
|
|
82
|
+
* The salt mixed into the entity key. Defaults to 128 random bits, which makes the key
|
|
83
|
+
* unpredictable to everyone but the creator.
|
|
84
|
+
*
|
|
85
|
+
* Pass `0n` for a key derived from the owner and nonce alone — predictable by anyone, which is
|
|
86
|
+
* what you want only when a third party must be able to compute the key in advance.
|
|
87
|
+
*/
|
|
88
|
+
salt?: bigint | undefined
|
|
31
89
|
}
|
|
32
90
|
|
|
33
91
|
/**
|
|
34
|
-
*
|
|
35
|
-
* - entityKey: The key of the entity.
|
|
36
|
-
* - txHash: The transaction hash.
|
|
92
|
+
* The result of creating an entity.
|
|
37
93
|
*/
|
|
38
94
|
export type CreateEntityReturnType = {
|
|
95
|
+
/** The new entity's key. */
|
|
39
96
|
entityKey: Hex
|
|
40
97
|
txHash: Hash
|
|
98
|
+
/**
|
|
99
|
+
* The block the entity is expected to expire at, resolved against the block the transaction was
|
|
100
|
+
* built on.
|
|
101
|
+
*
|
|
102
|
+
* The engine resolves a duration against the block the transaction actually lands in, so for
|
|
103
|
+
* `ExpirationTime.fromDays(30)` and friends this is a lower bound: the real expiry is later by
|
|
104
|
+
* however many blocks passed between building and inclusion. An `atBlock` deadline is exact.
|
|
105
|
+
*/
|
|
106
|
+
expiresAt: bigint
|
|
41
107
|
}
|
|
42
108
|
|
|
43
109
|
export async function createEntity(
|
|
@@ -46,7 +112,7 @@ export async function createEntity(
|
|
|
46
112
|
txParams?: TxParams,
|
|
47
113
|
): Promise<CreateEntityReturnType> {
|
|
48
114
|
logger("createEntity %o", data)
|
|
49
|
-
const { receipt, createdEntityKeys } = await sendArkivTransaction(
|
|
115
|
+
const { receipt, createdEntityKeys, createdExpiries } = await sendArkivTransaction(
|
|
50
116
|
client,
|
|
51
117
|
{ creates: [data] },
|
|
52
118
|
txParams,
|
|
@@ -54,8 +120,17 @@ export async function createEntity(
|
|
|
54
120
|
|
|
55
121
|
logger("Receipt from createEntity %o", receipt)
|
|
56
122
|
|
|
123
|
+
const [entityKey] = createdEntityKeys
|
|
124
|
+
const [expiresAt] = createdExpiries
|
|
125
|
+
if (entityKey === undefined || expiresAt === undefined) {
|
|
126
|
+
throw new EntityMutationError(
|
|
127
|
+
`Transaction ${receipt.transactionHash} succeeded, but the receipt did not carry the expected entity key or expiry.`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
57
131
|
return {
|
|
58
132
|
txHash: receipt.transactionHash as Hash,
|
|
59
|
-
entityKey
|
|
133
|
+
entityKey,
|
|
134
|
+
expiresAt,
|
|
60
135
|
}
|
|
61
136
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Hash, Hex } from "viem"
|
|
2
2
|
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import type { Expiry } from "../../entity"
|
|
4
|
+
import { EntityMutationError } from "../../errors"
|
|
3
5
|
import type { TxParams } from "../../types"
|
|
4
6
|
import { sendArkivTransaction } from "../../utils/arkivTransactions"
|
|
5
7
|
import { getLogger } from "../../utils/logger"
|
|
@@ -7,28 +9,38 @@ import { getLogger } from "../../utils/logger"
|
|
|
7
9
|
const logger = getLogger("actions:wallet:extend-entity")
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* Parameters for
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Parameters for extending an entity's life.
|
|
13
|
+
*
|
|
14
|
+
* An extension **sets a new expiry**; it does not add to the existing one. It is resolved exactly
|
|
15
|
+
* as a create's is — a duration counts from now, not from the entity's current expiry — so
|
|
16
|
+
* `ExpirationTime.fromDays(30)` means "alive for thirty more days from now" whatever the entity's
|
|
17
|
+
* remaining life happened to be.
|
|
18
|
+
*
|
|
19
|
+
* The engine rejects an extension that would not move the expiry later, so extending an entity by
|
|
20
|
+
* less than it already has left is an error rather than a silent shortening.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* await client.extendEntity({ entityKey, expires: ExpirationTime.fromDays(30) })
|
|
24
|
+
* await client.extendEntity({ entityKey, expires: ExpirationTime.atBlock(1_200_000n) })
|
|
16
25
|
*/
|
|
17
26
|
export type ExtendEntityParameters = {
|
|
18
27
|
entityKey: Hex
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
expiresIn: number
|
|
28
|
+
/** The entity's new lifetime, built with {@link ExpirationTime}. */
|
|
29
|
+
expires: Expiry
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
26
|
-
* - entityKey: The key of the entity.
|
|
27
|
-
* - txHash: The transaction hash.
|
|
33
|
+
* The result of extending an entity.
|
|
28
34
|
*/
|
|
29
35
|
export type ExtendEntityReturnType = {
|
|
30
36
|
entityKey: Hex
|
|
31
37
|
txHash: Hash
|
|
38
|
+
/**
|
|
39
|
+
* The block the entity is now expected to expire at, resolved against the block the transaction
|
|
40
|
+
* was built on. Durations are approximate, so treat this as the estimate the operation was built
|
|
41
|
+
* from rather than the block the engine settled on.
|
|
42
|
+
*/
|
|
43
|
+
expiresAt: bigint
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export async function extendEntity(
|
|
@@ -37,12 +49,24 @@ export async function extendEntity(
|
|
|
37
49
|
txParams?: TxParams,
|
|
38
50
|
): Promise<ExtendEntityReturnType> {
|
|
39
51
|
logger("extendEntity %o", data)
|
|
40
|
-
const { receipt } = await sendArkivTransaction(
|
|
52
|
+
const { receipt, extendedExpiries } = await sendArkivTransaction(
|
|
53
|
+
client,
|
|
54
|
+
{ extensions: [data] },
|
|
55
|
+
txParams,
|
|
56
|
+
)
|
|
41
57
|
|
|
42
58
|
logger("Receipt from extendEntity %o", receipt)
|
|
43
59
|
|
|
60
|
+
const [expiresAt] = extendedExpiries
|
|
61
|
+
if (expiresAt === undefined) {
|
|
62
|
+
throw new EntityMutationError(
|
|
63
|
+
`Transaction ${receipt.transactionHash} succeeded, but the receipt did not carry the expected entity expiry.`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
return {
|
|
45
68
|
txHash: receipt.transactionHash as Hash,
|
|
46
69
|
entityKey: data.entityKey,
|
|
70
|
+
expiresAt,
|
|
47
71
|
}
|
|
48
72
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test"
|
|
2
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import { mutateEntities } from "./mutateEntities"
|
|
4
|
+
|
|
5
|
+
const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
|
|
6
|
+
const NEW_OWNER = "0x2222222222222222222222222222222222222222"
|
|
7
|
+
|
|
8
|
+
function makeClient() {
|
|
9
|
+
const writeContract = vi.fn().mockResolvedValue("0xdeadbeef")
|
|
10
|
+
return {
|
|
11
|
+
client: {
|
|
12
|
+
account: { address: "0x1111111111111111111111111111111111111111" },
|
|
13
|
+
chain: { id: 1 },
|
|
14
|
+
getBlockNumber: vi.fn().mockResolvedValue(100n),
|
|
15
|
+
readContract: vi.fn().mockResolvedValue(7n),
|
|
16
|
+
writeContract,
|
|
17
|
+
waitForTransactionReceipt: vi.fn().mockResolvedValue({
|
|
18
|
+
status: "success",
|
|
19
|
+
transactionHash: "0xdeadbeef",
|
|
20
|
+
logs: [],
|
|
21
|
+
}),
|
|
22
|
+
} as unknown as ArkivClient,
|
|
23
|
+
writeContract,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("the empty-batch guard", () => {
|
|
28
|
+
it("counts every kind of operation, not just the first four", async () => {
|
|
29
|
+
// An ownership-only batch is a batch: it used to be rejected because the guard listed
|
|
30
|
+
// creates/updates/deletes/extensions and forgot ownershipChanges.
|
|
31
|
+
const { client, writeContract } = makeClient()
|
|
32
|
+
const result = await mutateEntities(client, {
|
|
33
|
+
ownershipChanges: [{ entityKey: ENTITY_KEY, newOwner: NEW_OWNER }],
|
|
34
|
+
})
|
|
35
|
+
expect(writeContract).toHaveBeenCalledTimes(1)
|
|
36
|
+
expect(result.ownershipChanges).toEqual([ENTITY_KEY])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("rejects a batch with nothing in it", async () => {
|
|
40
|
+
const { client, writeContract } = makeClient()
|
|
41
|
+
await expect(mutateEntities(client, {})).rejects.toThrow(/No operations/)
|
|
42
|
+
// Present but empty is just as empty — the engine would revert with EmptyBatch().
|
|
43
|
+
await expect(mutateEntities(client, { deletes: [], ownershipChanges: [] })).rejects.toThrow(
|
|
44
|
+
/No operations/,
|
|
45
|
+
)
|
|
46
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("counts only real operation kinds, not whatever keys the caller passed", async () => {
|
|
50
|
+
const { client, writeContract } = makeClient()
|
|
51
|
+
// `updates` was this API's name for patches until they were renamed. Counting the caller's own
|
|
52
|
+
// keys would score this batch as non-empty and then drop it on the way to the wire, sending
|
|
53
|
+
// execute([]) — so the guard counts the five kinds that actually reach the transaction.
|
|
54
|
+
const stale = { updates: [{ entityKey: ENTITY_KEY, set: { level: 1 } }] } as never
|
|
55
|
+
await expect(mutateEntities(client, stale)).rejects.toThrow(/No operations/)
|
|
56
|
+
// A typo is the same mistake with a different name.
|
|
57
|
+
await expect(
|
|
58
|
+
mutateEntities(client, { delete: [{ entityKey: ENTITY_KEY }] } as never),
|
|
59
|
+
).rejects.toThrow(/No operations/)
|
|
60
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("does not let a stale key smuggle itself into a batch that is otherwise valid", async () => {
|
|
64
|
+
const { client, writeContract } = makeClient()
|
|
65
|
+
// The dangerous shape: the creates apply and the transaction succeeds, so nothing surfaces the
|
|
66
|
+
// fact that the `updates` never happened. The guard cannot catch this one — only the caller's
|
|
67
|
+
// types can — but the result must at least not claim they did.
|
|
68
|
+
const result = await mutateEntities(client, {
|
|
69
|
+
deletes: [{ entityKey: ENTITY_KEY }],
|
|
70
|
+
updates: [{ entityKey: ENTITY_KEY, set: { level: 1 } }],
|
|
71
|
+
} as never)
|
|
72
|
+
expect(result.patchedEntities).toEqual([])
|
|
73
|
+
// Only the delete reached the wire, and the report says so.
|
|
74
|
+
expect(writeContract.mock.calls[0][0].args[0]).toHaveLength(1)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -7,40 +7,42 @@ import type { ChangeOwnershipParameters } from "./changeOwnership"
|
|
|
7
7
|
import type { CreateEntityParameters } from "./createEntity"
|
|
8
8
|
import type { DeleteEntityParameters } from "./deleteEntity"
|
|
9
9
|
import type { ExtendEntityParameters } from "./extendEntity"
|
|
10
|
-
import type {
|
|
10
|
+
import type { PatchEntityParameters } from "./patchEntity"
|
|
11
11
|
|
|
12
12
|
const logger = getLogger("actions:wallet:mutate-entities")
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Parameters for the mutateEntities function.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - extensions: The extensions to perform.
|
|
16
|
+
*
|
|
17
|
+
* At least one operation is required; a batch with nothing in it throws rather than spending gas on
|
|
18
|
+
* an empty transaction.
|
|
20
19
|
*/
|
|
21
20
|
export type MutateEntitiesParameters = {
|
|
21
|
+
/** The entities to create. */
|
|
22
22
|
creates?: CreateEntityParameters[]
|
|
23
|
-
|
|
23
|
+
/** The patches to apply. */
|
|
24
|
+
patches?: PatchEntityParameters[]
|
|
25
|
+
/** The entities to delete. */
|
|
24
26
|
deletes?: DeleteEntityParameters[]
|
|
27
|
+
/** The expiries to set. */
|
|
25
28
|
extensions?: ExtendEntityParameters[]
|
|
29
|
+
/** The ownership transfers to perform. */
|
|
26
30
|
ownershipChanges?: ChangeOwnershipParameters[]
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* Return type for the mutateEntities function.
|
|
31
|
-
* - txHash: The transaction hash.
|
|
32
|
-
* - createdEntities: The keys of the created entities.
|
|
33
|
-
* - updatedEntities: The keys of the updated entities.
|
|
34
|
-
* - deletedEntities: The keys of the deleted entities.
|
|
35
|
-
* - extendedEntities: The keys of the extended entities.
|
|
36
|
-
* - ownershipChanges: The keys of the ownership changes.
|
|
37
|
-
*/
|
|
33
|
+
/** Return type for the mutateEntities function. */
|
|
38
34
|
export type MutateEntitiesReturnType = {
|
|
35
|
+
/** The transaction hash. */
|
|
39
36
|
txHash: Hash
|
|
37
|
+
/** The keys of the created entities, in batch order. */
|
|
40
38
|
createdEntities: Hex[]
|
|
41
|
-
|
|
39
|
+
/** The keys of the patched entities. */
|
|
40
|
+
patchedEntities: Hex[]
|
|
41
|
+
/** The keys of the deleted entities. */
|
|
42
42
|
deletedEntities: Hex[]
|
|
43
|
+
/** The keys of the extended entities. */
|
|
43
44
|
extendedEntities: Hex[]
|
|
45
|
+
/** The keys of the entities handed to a new owner. */
|
|
44
46
|
ownershipChanges: Hex[]
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -49,10 +51,6 @@ export async function mutateEntities(
|
|
|
49
51
|
data: MutateEntitiesParameters,
|
|
50
52
|
txParams?: TxParams,
|
|
51
53
|
): Promise<MutateEntitiesReturnType> {
|
|
52
|
-
if (!data.creates && !data.updates && !data.deletes && !data.extensions) {
|
|
53
|
-
throw new Error("No operations to perform")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
54
|
const { receipt, createdEntityKeys } = await sendArkivTransaction(client, data, txParams)
|
|
57
55
|
|
|
58
56
|
logger("Receipt from mutateEntities %o", receipt)
|
|
@@ -60,7 +58,7 @@ export async function mutateEntities(
|
|
|
60
58
|
return {
|
|
61
59
|
txHash: receipt.transactionHash as Hash,
|
|
62
60
|
createdEntities: createdEntityKeys,
|
|
63
|
-
|
|
61
|
+
patchedEntities: (data.patches ?? []).map((p) => p.entityKey),
|
|
64
62
|
deletedEntities: (data.deletes ?? []).map((d) => d.entityKey),
|
|
65
63
|
extendedEntities: (data.extensions ?? []).map((e) => e.entityKey),
|
|
66
64
|
ownershipChanges: (data.ownershipChanges ?? []).map((o) => o.entityKey),
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Hash, Hex } from "viem"
|
|
2
|
+
import type { AttributeInputs } from "../../attr"
|
|
3
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
4
|
+
import type { MimeType, TxParams } from "../../types"
|
|
5
|
+
import { sendArkivTransaction } from "../../utils/arkivTransactions"
|
|
6
|
+
import { getLogger } from "../../utils/logger"
|
|
7
|
+
|
|
8
|
+
const logger = getLogger("actions:wallet:patch-entity")
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parameters for patching an entity.
|
|
12
|
+
*
|
|
13
|
+
* A patch names only what changes. Attributes it does not mention keep whatever value they have, and
|
|
14
|
+
* the payload and content type stay as they are unless given — so a caller can change one field
|
|
15
|
+
* without first reading the entity to learn what the others are, and without racing anyone else who
|
|
16
|
+
* is changing a different field.
|
|
17
|
+
*
|
|
18
|
+
* The entity's identity is fixed: a patch never moves the key, the owner, the creation flags or the
|
|
19
|
+
* expiry. Use `extendEntity` for the expiry and `changeOwnership` for the owner.
|
|
20
|
+
*
|
|
21
|
+
* @example Change one attribute and remove another, leaving the payload alone:
|
|
22
|
+
* await client.patchEntity({
|
|
23
|
+
* entityKey,
|
|
24
|
+
* set: { status: "published", level: i32(11) },
|
|
25
|
+
* unset: ["draft"],
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* @example Replace the contents without touching a single attribute:
|
|
29
|
+
* await client.patchEntity({
|
|
30
|
+
* entityKey,
|
|
31
|
+
* payload: jsonToPayload({ hello: "again" }),
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
export type PatchEntityParameters = {
|
|
35
|
+
/** The entity to patch. */
|
|
36
|
+
entityKey: Hex
|
|
37
|
+
/**
|
|
38
|
+
* Attributes to write, keyed by name. An existing attribute is overwritten — including with a
|
|
39
|
+
* different type — and a name the entity does not carry yet is added.
|
|
40
|
+
*
|
|
41
|
+
* Values are the tagged constructors from `@arkiv-network/sdk/attr` — `i32`, `u256`, `dec`, `str`,
|
|
42
|
+
* `addr`, `key`, `bytes32`, `bool` — or a bare `boolean`, `number`, `bigint` or `string` where the
|
|
43
|
+
* type is unambiguous.
|
|
44
|
+
*
|
|
45
|
+
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
46
|
+
* @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
|
|
47
|
+
*/
|
|
48
|
+
set?: AttributeInputs | undefined
|
|
49
|
+
/**
|
|
50
|
+
* Attribute names to remove. Naming one the entity does not carry is not an error — the outcome
|
|
51
|
+
* is the same either way, and a caller clearing a field should not have to know whether it was
|
|
52
|
+
* ever set.
|
|
53
|
+
*
|
|
54
|
+
* @throws {ConflictingMutationError} If a name also appears in `set`.
|
|
55
|
+
*/
|
|
56
|
+
unset?: readonly string[] | undefined
|
|
57
|
+
/**
|
|
58
|
+
* The entity's new payload. Omit it to leave the current contents untouched; pass an empty
|
|
59
|
+
* `Uint8Array` to replace them with nothing.
|
|
60
|
+
*/
|
|
61
|
+
payload?: Uint8Array | undefined
|
|
62
|
+
/**
|
|
63
|
+
* The payload's new MIME type. Omit it to keep the current one — which is what you want when the
|
|
64
|
+
* new payload is the same kind of thing as the old.
|
|
65
|
+
*/
|
|
66
|
+
contentType?: MimeType | (string & {}) | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The result of patching an entity. */
|
|
70
|
+
export type PatchEntityReturnType = {
|
|
71
|
+
/** The patched entity's key — unchanged, since a patch never moves it. */
|
|
72
|
+
entityKey: Hex
|
|
73
|
+
txHash: Hash
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function patchEntity(
|
|
77
|
+
client: ArkivClient,
|
|
78
|
+
data: PatchEntityParameters,
|
|
79
|
+
txParams?: TxParams,
|
|
80
|
+
): Promise<PatchEntityReturnType> {
|
|
81
|
+
logger("patchEntity %o", data)
|
|
82
|
+
const { receipt } = await sendArkivTransaction(client, { patches: [data] }, txParams)
|
|
83
|
+
|
|
84
|
+
logger("Receipt from patchEntity %o", receipt)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
txHash: receipt.transactionHash as Hash,
|
|
88
|
+
entityKey: data.entityKey,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -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
|
+
})
|