@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,260 +1,705 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "bun:test"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Abi,
|
|
4
|
+
BaseError,
|
|
5
|
+
ContractFunctionRevertedError,
|
|
6
|
+
decodeAbiParameters,
|
|
7
|
+
encodeAbiParameters,
|
|
8
|
+
encodeEventTopics,
|
|
9
|
+
getAbiItem,
|
|
10
|
+
type Hex,
|
|
11
|
+
parseAbiParameters,
|
|
12
|
+
toHex,
|
|
13
|
+
} from "viem"
|
|
14
|
+
import {
|
|
15
|
+
type AttributeInputs,
|
|
16
|
+
ConflictingMutationError,
|
|
17
|
+
dec,
|
|
18
|
+
InvalidAttributeNameError,
|
|
19
|
+
InvalidValueError,
|
|
20
|
+
i32,
|
|
21
|
+
MissingValueError,
|
|
22
|
+
u256,
|
|
23
|
+
} from "../attr"
|
|
3
24
|
import type { ArkivClient } from "../clients/baseClient"
|
|
4
|
-
import {
|
|
25
|
+
import { ARKIV_ADDRESS } from "../consts"
|
|
26
|
+
import { InvalidExpiryError } from "../entity"
|
|
27
|
+
import { ENTITY_EVENTS_ABI, type EntityEventName } from "../entity/events"
|
|
28
|
+
import { MAX_EXPIRES_AT, OperationType } from "../entity/params"
|
|
29
|
+
import { EmptyPatchError, EntityMutationError } from "../errors"
|
|
5
30
|
import { sendArkivTransaction } from "./arkivTransactions"
|
|
31
|
+
import { ExpirationTime } from "./expirationTime"
|
|
6
32
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
|
|
34
|
+
const OWNER = "0x1111111111111111111111111111111111111111" as const
|
|
35
|
+
const CURRENT_BLOCK = 100n
|
|
36
|
+
const OWNER_NONCE = 7n
|
|
37
|
+
|
|
38
|
+
const CREATE_PARAMS = parseAbiParameters(
|
|
39
|
+
"(uint128 salt, uint64 expiresAt, uint64 minLifetime, uint8 creationFlags, (bytes32 name, uint8 typeId, bytes value)[] attributes)",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const EXTEND_PARAMS = parseAbiParameters(
|
|
43
|
+
"(bytes32 entityKey, uint64 expiresAt, uint64 minLifetime)",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const PATCH_PARAMS = parseAbiParameters(
|
|
47
|
+
"(bytes32 entityKey, (bytes32 name, uint8 typeId, bytes value)[] mutations)",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const mintedKey = (index: number) =>
|
|
51
|
+
`0x${(index + 1).toString(16).padStart(2, "0").repeat(32)}` as Hex
|
|
52
|
+
|
|
53
|
+
/** What the engine stores: `max(expiresAt, currentBlock + minLifetime)`, resolved at execution. */
|
|
54
|
+
const engineExpiry = (expiresAt: bigint, minLifetime: bigint) =>
|
|
55
|
+
expiresAt > CURRENT_BLOCK + minLifetime ? expiresAt : CURRENT_BLOCK + minLifetime
|
|
56
|
+
|
|
57
|
+
/** One receipt log, built from the same ABI the SDK reads it back with. */
|
|
58
|
+
function entityLog(eventName: EntityEventName, args: Record<string, unknown>) {
|
|
59
|
+
const item = getAbiItem({ abi: ENTITY_EVENTS_ABI as Abi, name: eventName })
|
|
60
|
+
if (item === undefined || item.type !== "event") throw new Error(`no event ${eventName}`)
|
|
61
|
+
const unindexed = item.inputs.filter((input) => !("indexed" in input && input.indexed))
|
|
62
|
+
return {
|
|
63
|
+
address: ARKIV_ADDRESS,
|
|
64
|
+
// biome-ignore lint/suspicious/noExplicitAny: the arg shape varies per event by design.
|
|
65
|
+
topics: encodeEventTopics({ abi: ENTITY_EVENTS_ABI, eventName, args: args as any }),
|
|
66
|
+
data:
|
|
67
|
+
unindexed.length === 0
|
|
68
|
+
? "0x"
|
|
69
|
+
: encodeAbiParameters(
|
|
70
|
+
unindexed,
|
|
71
|
+
unindexed.map((input) => args[input.name ?? ""]),
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The events the engine would emit for what was actually put on the wire: one `EntityCreated` per
|
|
78
|
+
* create and one `ExpiryExtended` per extension, each carrying the expiry the engine resolved
|
|
79
|
+
* rather than the one the SDK asked for.
|
|
80
|
+
*/
|
|
81
|
+
function emittedLogs(writeContract: ReturnType<typeof vi.fn>) {
|
|
82
|
+
const logs: ReturnType<typeof entityLog>[] = []
|
|
83
|
+
let createIndex = 0
|
|
84
|
+
for (const op of sentOperations(writeContract)) {
|
|
85
|
+
if (op.operation === OperationType.Create) {
|
|
86
|
+
const create = decodeAbiParameters(CREATE_PARAMS, op.operationData)[0]
|
|
87
|
+
logs.push(
|
|
88
|
+
entityLog("EntityCreated", {
|
|
89
|
+
entityKey: mintedKey(createIndex++),
|
|
90
|
+
owner: OWNER,
|
|
91
|
+
expiresAt: engineExpiry(create.expiresAt, create.minLifetime),
|
|
92
|
+
creationFlags: create.creationFlags,
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
} else if (op.operation === OperationType.ExtendExpiry) {
|
|
96
|
+
const extend = decodeAbiParameters(EXTEND_PARAMS, op.operationData)[0]
|
|
97
|
+
logs.push(
|
|
98
|
+
entityLog("ExpiryExtended", {
|
|
99
|
+
entityKey: extend.entityKey,
|
|
100
|
+
owner: OWNER,
|
|
101
|
+
expiresAt: engineExpiry(extend.expiresAt, extend.minLifetime),
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return logs
|
|
20
107
|
}
|
|
21
108
|
|
|
22
109
|
function makeClient() {
|
|
23
110
|
const writeContract = vi.fn().mockResolvedValue("0xdeadbeef")
|
|
24
|
-
const waitForTransactionReceipt = vi.fn()
|
|
111
|
+
const waitForTransactionReceipt = vi.fn(async () => ({
|
|
25
112
|
status: "success",
|
|
26
113
|
transactionHash: "0xdeadbeef",
|
|
27
|
-
|
|
114
|
+
logs: emittedLogs(writeContract),
|
|
115
|
+
}))
|
|
116
|
+
const readContract = vi.fn().mockResolvedValue(OWNER_NONCE)
|
|
28
117
|
return {
|
|
29
118
|
client: {
|
|
30
|
-
account: { address:
|
|
119
|
+
account: { address: OWNER },
|
|
31
120
|
chain: { id: 1 },
|
|
32
|
-
getBlockNumber: vi.fn().mockResolvedValue(
|
|
33
|
-
readContract
|
|
121
|
+
getBlockNumber: vi.fn().mockResolvedValue(CURRENT_BLOCK),
|
|
122
|
+
readContract,
|
|
34
123
|
writeContract,
|
|
35
124
|
waitForTransactionReceipt,
|
|
36
125
|
} as unknown as ArkivClient,
|
|
37
126
|
writeContract,
|
|
127
|
+
waitForTransactionReceipt,
|
|
128
|
+
readContract,
|
|
38
129
|
}
|
|
39
130
|
}
|
|
40
131
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
132
|
+
/** The operations a call put on the wire. */
|
|
133
|
+
function sentOperations(writeContract: ReturnType<typeof vi.fn>) {
|
|
134
|
+
return writeContract.mock.calls[0][0].args[0] as {
|
|
135
|
+
operation: number
|
|
136
|
+
operationData: `0x${string}`
|
|
137
|
+
}[]
|
|
45
138
|
}
|
|
46
139
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
140
|
+
/** The decoded Create struct of the first operation. */
|
|
141
|
+
function sentCreate(writeContract: ReturnType<typeof vi.fn>) {
|
|
142
|
+
const [op] = sentOperations(writeContract)
|
|
143
|
+
expect(op.operation).toBe(OperationType.Create)
|
|
144
|
+
return decodeAbiParameters(CREATE_PARAMS, op.operationData)[0]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The decoded ExtendExpiry struct of the batch's only extension. */
|
|
148
|
+
function sentExtension(writeContract: ReturnType<typeof vi.fn>) {
|
|
149
|
+
const op = sentOperations(writeContract).find((o) => o.operation === OperationType.ExtendExpiry)
|
|
150
|
+
if (!op) throw new Error("no ExtendExpiry operation was sent")
|
|
151
|
+
return decodeAbiParameters(EXTEND_PARAMS, op.operationData)[0]
|
|
54
152
|
}
|
|
55
153
|
|
|
56
|
-
const
|
|
154
|
+
const CONTENTS = {
|
|
57
155
|
payload: new Uint8Array([1, 2, 3]),
|
|
58
|
-
contentType: "
|
|
59
|
-
attributes: [{ key: "k", value: "v" as string | number }],
|
|
60
|
-
expiresIn: 1000,
|
|
156
|
+
contentType: "application/octet-stream",
|
|
61
157
|
}
|
|
62
158
|
|
|
63
|
-
|
|
64
|
-
|
|
159
|
+
const DAY = ExpirationTime.fromDays(1) // 86400s -> 43200 blocks at 2s
|
|
160
|
+
const validCreate = { ...CONTENTS, expires: DAY }
|
|
161
|
+
|
|
162
|
+
describe("create expiry", () => {
|
|
163
|
+
it("sends a create for a valid expiry", async () => {
|
|
65
164
|
const { client, writeContract } = makeClient()
|
|
66
165
|
const result = await sendArkivTransaction(client, { creates: [validCreate] })
|
|
67
166
|
expect(writeContract).toHaveBeenCalledTimes(1)
|
|
68
167
|
expect(result.receipt.status).toBe("success")
|
|
69
168
|
})
|
|
70
169
|
|
|
71
|
-
it("
|
|
72
|
-
const { client } = makeClient()
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
170
|
+
it("puts the expiry pair on the wire untouched, for the engine to resolve", async () => {
|
|
171
|
+
const { client, writeContract } = makeClient()
|
|
172
|
+
await sendArkivTransaction(client, {
|
|
173
|
+
creates: [
|
|
174
|
+
{
|
|
175
|
+
...CONTENTS,
|
|
176
|
+
expires: ExpirationTime.atBlock(5_000n, { atLeast: ExpirationTime.fromSeconds(200) }),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
})
|
|
180
|
+
const create = sentCreate(writeContract)
|
|
181
|
+
expect(create.expiresAt).toBe(5_000n)
|
|
182
|
+
expect(create.minLifetime).toBe(100n) // 200s at 2s/block
|
|
76
183
|
})
|
|
77
184
|
|
|
78
|
-
it("
|
|
185
|
+
it("reports the block the entity will actually expire at", async () => {
|
|
79
186
|
const { client } = makeClient()
|
|
80
|
-
await
|
|
81
|
-
|
|
82
|
-
)
|
|
187
|
+
const relative = await sendArkivTransaction(client, {
|
|
188
|
+
creates: [{ ...CONTENTS, expires: DAY }],
|
|
189
|
+
})
|
|
190
|
+
expect(relative.createdExpiries[0]).toBe(CURRENT_BLOCK + 43_200n)
|
|
191
|
+
|
|
192
|
+
const { client: client2 } = makeClient()
|
|
193
|
+
const absolute = await sendArkivTransaction(client2, {
|
|
194
|
+
creates: [{ ...CONTENTS, expires: ExpirationTime.atBlock(5_000n) }],
|
|
195
|
+
})
|
|
196
|
+
expect(absolute.createdExpiries[0]).toBe(5_000n)
|
|
197
|
+
|
|
198
|
+
const { client: client3 } = makeClient()
|
|
199
|
+
const dated = await sendArkivTransaction(client3, {
|
|
200
|
+
creates: [{ ...CONTENTS, expires: ExpirationTime.atDate(new Date(Date.now() + 3600_000)) }],
|
|
201
|
+
})
|
|
202
|
+
// An hour at 2s/block, counted from the block the transaction was built on.
|
|
203
|
+
expect(dated.createdExpiries[0]).toBe(CURRENT_BLOCK + 1800n)
|
|
83
204
|
})
|
|
84
205
|
|
|
85
|
-
it("
|
|
206
|
+
it("refuses a create with no expiry at all", async () => {
|
|
86
207
|
const { client } = makeClient()
|
|
87
208
|
await expect(
|
|
88
|
-
sendArkivTransaction(client, {
|
|
89
|
-
).rejects.toThrow(
|
|
209
|
+
sendArkivTransaction(client, { creates: [{ ...CONTENTS } as never] }),
|
|
210
|
+
).rejects.toThrow(InvalidExpiryError)
|
|
90
211
|
})
|
|
91
|
-
})
|
|
92
212
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const { client } = makeClient()
|
|
213
|
+
it("catches a dead-on-arrival expiry before it costs gas", async () => {
|
|
214
|
+
const { client, writeContract } = makeClient()
|
|
96
215
|
await expect(
|
|
97
216
|
sendArkivTransaction(client, {
|
|
98
|
-
creates: [{ ...
|
|
217
|
+
creates: [{ ...CONTENTS, expires: ExpirationTime.atBlock(CURRENT_BLOCK - 1n) }],
|
|
99
218
|
}),
|
|
100
|
-
).rejects.toThrow(
|
|
219
|
+
).rejects.toThrow(/dead on arrival/)
|
|
220
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
101
221
|
})
|
|
102
222
|
|
|
103
|
-
it("
|
|
223
|
+
it("rejects a malformed duration at the call site, long before the transaction", () => {
|
|
224
|
+
// 999 seconds is 499.5 blocks, which is not a lifetime the chain can store. Building the value
|
|
225
|
+
// throws, so nothing malformed ever reaches sendArkivTransaction.
|
|
226
|
+
expect(() => ExpirationTime.fromSeconds(999)).toThrow(/multiple of the 2s block time/)
|
|
227
|
+
expect(() => ExpirationTime.fromSeconds(0)).toThrow(InvalidExpiryError)
|
|
228
|
+
expect(() => ExpirationTime.fromSeconds(1.5)).toThrow(/positive whole number of seconds/)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("extend expiry", () => {
|
|
233
|
+
it("resolves an extension exactly like a create, both forms", async () => {
|
|
104
234
|
const { client, writeContract } = makeClient()
|
|
105
|
-
await sendArkivTransaction(client, {
|
|
106
|
-
|
|
235
|
+
const relative = await sendArkivTransaction(client, {
|
|
236
|
+
extensions: [{ entityKey: ENTITY_KEY, expires: DAY }],
|
|
107
237
|
})
|
|
108
|
-
|
|
238
|
+
const extension = sentExtension(writeContract)
|
|
239
|
+
expect(extension.entityKey).toBe(ENTITY_KEY)
|
|
240
|
+
// A duration is a floor measured from now, not an increment on the entity's current expiry.
|
|
241
|
+
expect(extension.minLifetime).toBe(43_200n)
|
|
242
|
+
expect(extension.expiresAt).toBe(0n)
|
|
243
|
+
expect(relative.extendedExpiries[0]).toBe(CURRENT_BLOCK + 43_200n)
|
|
244
|
+
|
|
245
|
+
const { client: client2, writeContract: writeContract2 } = makeClient()
|
|
246
|
+
const absolute = await sendArkivTransaction(client2, {
|
|
247
|
+
extensions: [{ entityKey: ENTITY_KEY, expires: ExpirationTime.atBlock(5_000n) }],
|
|
248
|
+
})
|
|
249
|
+
expect(sentExtension(writeContract2).expiresAt).toBe(5_000n)
|
|
250
|
+
expect(absolute.extendedExpiries[0]).toBe(5_000n)
|
|
109
251
|
})
|
|
110
252
|
|
|
111
|
-
it("
|
|
253
|
+
it("checks the wire bounds before spending gas, as a create does", async () => {
|
|
254
|
+
const { client, writeContract } = makeClient()
|
|
255
|
+
const tooFar = ExpirationTime.atBlock(MAX_EXPIRES_AT + 1n)
|
|
256
|
+
await expect(
|
|
257
|
+
sendArkivTransaction(client, { extensions: [{ entityKey: ENTITY_KEY, expires: tooFar }] }),
|
|
258
|
+
).rejects.toThrow(/does not fit the uint64/)
|
|
259
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("refuses an extension that says nothing", async () => {
|
|
263
|
+
const { client } = makeClient()
|
|
264
|
+
await expect(
|
|
265
|
+
sendArkivTransaction(client, { extensions: [{ entityKey: ENTITY_KEY } as never] }),
|
|
266
|
+
).rejects.toThrow(/must be built with ExpirationTime/)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("reads the block height for an extension-only batch", async () => {
|
|
112
270
|
const { client, writeContract } = makeClient()
|
|
113
271
|
await sendArkivTransaction(client, {
|
|
114
|
-
|
|
272
|
+
extensions: [
|
|
273
|
+
{ entityKey: ENTITY_KEY, expires: ExpirationTime.atDate(new Date(Date.now() + 3600_000)) },
|
|
274
|
+
],
|
|
115
275
|
})
|
|
116
|
-
|
|
276
|
+
// A Date can only be placed once the current block is known, so it must have been fetched even
|
|
277
|
+
// though the batch creates nothing.
|
|
278
|
+
expect(sentExtension(writeContract).expiresAt).toBe(CURRENT_BLOCK + 1800n)
|
|
117
279
|
})
|
|
118
280
|
})
|
|
119
281
|
|
|
120
|
-
describe("
|
|
121
|
-
it("
|
|
122
|
-
const
|
|
282
|
+
describe("creation flags and salt", () => {
|
|
283
|
+
it("packs flags into the creationFlags byte", async () => {
|
|
284
|
+
const { client, writeContract } = makeClient()
|
|
285
|
+
await sendArkivTransaction(client, {
|
|
286
|
+
creates: [{ ...validCreate, flags: { readonly: true, permissionlessExtension: true } }],
|
|
287
|
+
})
|
|
288
|
+
expect(sentCreate(writeContract).creationFlags).toBe(0b11)
|
|
289
|
+
})
|
|
123
290
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
291
|
+
it("defaults flags to none", async () => {
|
|
292
|
+
const { client, writeContract } = makeClient()
|
|
293
|
+
await sendArkivTransaction(client, { creates: [validCreate] })
|
|
294
|
+
expect(sentCreate(writeContract).creationFlags).toBe(0)
|
|
127
295
|
})
|
|
128
296
|
|
|
129
|
-
it("
|
|
130
|
-
const
|
|
297
|
+
it("defaults the salt to random bits, and honours an explicit one", async () => {
|
|
298
|
+
const { client, writeContract } = makeClient()
|
|
299
|
+
await sendArkivTransaction(client, { creates: [validCreate] })
|
|
300
|
+
expect(sentCreate(writeContract).salt).toBeGreaterThan(0n)
|
|
131
301
|
|
|
132
|
-
|
|
133
|
-
|
|
302
|
+
const { client: c2, writeContract: w2 } = makeClient()
|
|
303
|
+
await sendArkivTransaction(c2, { creates: [{ ...validCreate, salt: 0n }] })
|
|
304
|
+
expect(sentCreate(w2).salt).toBe(0n)
|
|
134
305
|
})
|
|
135
306
|
|
|
136
|
-
it("
|
|
137
|
-
const
|
|
138
|
-
|
|
307
|
+
it("rejects a salt wider than the uint128 field", async () => {
|
|
308
|
+
const { client } = makeClient()
|
|
309
|
+
await expect(
|
|
310
|
+
sendArkivTransaction(client, { creates: [{ ...validCreate, salt: 2n ** 128n }] }),
|
|
311
|
+
).rejects.toThrow(/uint128/)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
139
314
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
315
|
+
describe("entity keys", () => {
|
|
316
|
+
it("reports the key the engine minted, in batch order", async () => {
|
|
317
|
+
const { client } = makeClient()
|
|
318
|
+
const result = await sendArkivTransaction(client, {
|
|
319
|
+
creates: [
|
|
320
|
+
{ ...validCreate, salt: 1n },
|
|
321
|
+
{ ...validCreate, salt: 1n },
|
|
322
|
+
{ ...validCreate, salt: 1n },
|
|
323
|
+
],
|
|
324
|
+
})
|
|
325
|
+
// These come from the EntityCreated logs, so they are what the engine recorded rather than
|
|
326
|
+
// what the SDK guessed — and the same salt three times cannot make them collide.
|
|
327
|
+
expect(result.createdEntityKeys).toEqual([mintedKey(0), mintedKey(1), mintedKey(2)])
|
|
143
328
|
})
|
|
144
329
|
|
|
145
|
-
it("
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
330
|
+
it("never reads the entity nonce, so two creates in flight cannot collide", async () => {
|
|
331
|
+
// The nonce read was the race: two creates issued before either was mined both read the same
|
|
332
|
+
// value, and one of them then returned a key the engine never minted. Reading the key back
|
|
333
|
+
// from the receipt removes the window rather than narrowing it — and saves a round trip.
|
|
334
|
+
const { client, readContract } = makeClient()
|
|
335
|
+
await sendArkivTransaction(client, { creates: [validCreate] })
|
|
336
|
+
expect(readContract).not.toHaveBeenCalled()
|
|
337
|
+
})
|
|
153
338
|
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
339
|
+
it("reports the expiry the engine resolved, not the one the SDK asked for", async () => {
|
|
340
|
+
// The engine applies max(expiresAt, currentBlock + minLifetime) against the block the batch
|
|
341
|
+
// actually lands on. Computing it here is a guess about which block that is.
|
|
342
|
+
const { client, waitForTransactionReceipt } = makeClient()
|
|
343
|
+
waitForTransactionReceipt.mockResolvedValue({
|
|
344
|
+
status: "success",
|
|
345
|
+
transactionHash: "0xdeadbeef",
|
|
346
|
+
logs: [
|
|
347
|
+
entityLog("EntityCreated", {
|
|
348
|
+
entityKey: mintedKey(0),
|
|
349
|
+
owner: OWNER,
|
|
350
|
+
expiresAt: 999_999n,
|
|
351
|
+
creationFlags: 0,
|
|
352
|
+
}),
|
|
353
|
+
],
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const result = await sendArkivTransaction(client, { creates: [validCreate] })
|
|
357
|
+
expect(result.createdExpiries[0]).toBe(999_999n)
|
|
158
358
|
})
|
|
159
359
|
|
|
160
|
-
it("
|
|
161
|
-
const
|
|
360
|
+
it("ignores logs from another contract in the same transaction", async () => {
|
|
361
|
+
const { client, waitForTransactionReceipt } = makeClient()
|
|
362
|
+
const created = entityLog("EntityCreated", {
|
|
363
|
+
entityKey: mintedKey(0),
|
|
364
|
+
owner: OWNER,
|
|
365
|
+
expiresAt: 500n,
|
|
366
|
+
creationFlags: 0,
|
|
367
|
+
})
|
|
368
|
+
waitForTransactionReceipt.mockResolvedValue({
|
|
369
|
+
status: "success",
|
|
370
|
+
transactionHash: "0xdeadbeef",
|
|
371
|
+
logs: [{ ...created, address: "0x9999999999999999999999999999999999999999" }, created],
|
|
372
|
+
})
|
|
162
373
|
|
|
163
|
-
|
|
164
|
-
expect(
|
|
165
|
-
expect(attr.value).toEqual([toHex(42n, { size: 32 }), ZERO_32, ZERO_32, ZERO_32])
|
|
374
|
+
const result = await sendArkivTransaction(client, { creates: [validCreate] })
|
|
375
|
+
expect(result.createdEntityKeys).toEqual([mintedKey(0)])
|
|
166
376
|
})
|
|
167
377
|
|
|
168
|
-
it("
|
|
169
|
-
const
|
|
378
|
+
it("refuses to guess when the events do not account for every create", async () => {
|
|
379
|
+
const { client, waitForTransactionReceipt } = makeClient()
|
|
380
|
+
waitForTransactionReceipt.mockResolvedValue({
|
|
381
|
+
status: "success",
|
|
382
|
+
transactionHash: "0xdeadbeef",
|
|
383
|
+
logs: [],
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const failure = await sendArkivTransaction(client, {
|
|
387
|
+
creates: [validCreate, validCreate],
|
|
388
|
+
}).catch((error: Error) => error)
|
|
170
389
|
|
|
171
|
-
expect(
|
|
172
|
-
|
|
390
|
+
expect(failure).toBeInstanceOf(EntityMutationError)
|
|
391
|
+
// This throws *after* a successful write, so the message has to stop a caller reading it as a
|
|
392
|
+
// failure and retrying — which would create the entities a second time.
|
|
393
|
+
expect(failure.message).toContain("do not retry")
|
|
394
|
+
expect(failure.message).toContain("0xdeadbeef")
|
|
173
395
|
})
|
|
396
|
+
})
|
|
174
397
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
398
|
+
describe("attribute typing through the write path", () => {
|
|
399
|
+
it("rejects a value that does not fit the type it defaults to", async () => {
|
|
400
|
+
const { client } = makeClient()
|
|
401
|
+
await expect(
|
|
402
|
+
sendArkivTransaction(client, { creates: [{ ...validCreate, attributes: { n: 1.5 } }] }),
|
|
403
|
+
).rejects.toThrow(InvalidValueError)
|
|
404
|
+
})
|
|
178
405
|
|
|
179
|
-
|
|
180
|
-
|
|
406
|
+
it("rejects an attribute name outside the grammar", async () => {
|
|
407
|
+
const { client } = makeClient()
|
|
408
|
+
await expect(
|
|
409
|
+
sendArkivTransaction(client, { creates: [{ ...validCreate, attributes: { "1bad": 1 } }] }),
|
|
410
|
+
).rejects.toThrow(InvalidAttributeNameError)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("rejects an attribute whose value is absent, rather than writing a typeless one", async () => {
|
|
414
|
+
const { client } = makeClient()
|
|
415
|
+
await expect(
|
|
416
|
+
sendArkivTransaction(client, {
|
|
417
|
+
creates: [
|
|
418
|
+
{
|
|
419
|
+
...validCreate,
|
|
420
|
+
// What a spread of an optional-shaped object produces at runtime.
|
|
421
|
+
attributes: { category: "docs", score: undefined } as unknown as AttributeInputs,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
}),
|
|
425
|
+
).rejects.toThrow(MissingValueError)
|
|
181
426
|
})
|
|
182
427
|
|
|
183
|
-
it("
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
428
|
+
it("carries only the system cells when there are no user attributes", async () => {
|
|
429
|
+
const { client, writeContract } = makeClient()
|
|
430
|
+
await sendArkivTransaction(client, { creates: [validCreate] })
|
|
431
|
+
expect(sentCreate(writeContract).attributes.map((a) => a.name)).toEqual([
|
|
432
|
+
toHex("$contentType", { size: 32 }),
|
|
433
|
+
toHex("$payload", { size: 32 }),
|
|
189
434
|
])
|
|
435
|
+
})
|
|
190
436
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
437
|
+
it("writes an empty payload rather than dropping it", async () => {
|
|
438
|
+
const { client, writeContract } = makeClient()
|
|
439
|
+
await sendArkivTransaction(client, {
|
|
440
|
+
creates: [{ ...validCreate, payload: new Uint8Array() }],
|
|
441
|
+
})
|
|
442
|
+
const byName = Object.fromEntries(sentCreate(writeContract).attributes.map((a) => [a.name, a]))
|
|
443
|
+
expect(byName[toHex("$payload", { size: 32 })]).toMatchObject({ typeId: 7, value: "0x" })
|
|
195
444
|
})
|
|
196
445
|
|
|
197
|
-
it("
|
|
198
|
-
const
|
|
199
|
-
|
|
446
|
+
it("carries the protocol typeId of whichever type the value names", async () => {
|
|
447
|
+
const { client, writeContract } = makeClient()
|
|
448
|
+
await sendArkivTransaction(client, {
|
|
449
|
+
creates: [
|
|
450
|
+
{ ...validCreate, attributes: { count: 42, balance: u256(1n), score: dec("3.5") } },
|
|
451
|
+
],
|
|
452
|
+
})
|
|
453
|
+
const byName = Object.fromEntries(
|
|
454
|
+
sentCreate(writeContract).attributes.map((a) => [a.name, a.typeId]),
|
|
455
|
+
)
|
|
456
|
+
expect(byName[toHex("count", { size: 32 })]).toBe(2)
|
|
457
|
+
expect(byName[toHex("balance", { size: 32 })]).toBe(4)
|
|
458
|
+
expect(byName[toHex("score", { size: 32 })]).toBe(5)
|
|
459
|
+
})
|
|
200
460
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
461
|
+
it("sends the payload and content type as system cells", async () => {
|
|
462
|
+
const { client, writeContract } = makeClient()
|
|
463
|
+
await sendArkivTransaction(client, {
|
|
464
|
+
creates: [
|
|
465
|
+
{
|
|
466
|
+
...validCreate,
|
|
467
|
+
payload: new Uint8Array([1, 2, 3]),
|
|
468
|
+
contentType: "application/json",
|
|
469
|
+
attributes: { level: i32(1) },
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
})
|
|
473
|
+
const byName = Object.fromEntries(sentCreate(writeContract).attributes.map((a) => [a.name, a]))
|
|
474
|
+
expect(byName[toHex("$payload", { size: 32 })]).toMatchObject({ typeId: 7, value: "0x010203" })
|
|
475
|
+
expect(byName[toHex("$contentType", { size: 32 })].typeId).toBe(8)
|
|
205
476
|
})
|
|
206
|
-
})
|
|
207
477
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
])
|
|
478
|
+
it("rejects a content type that is not a MIME token", async () => {
|
|
479
|
+
const { client } = makeClient()
|
|
480
|
+
await expect(
|
|
481
|
+
sendArkivTransaction(client, { creates: [{ ...validCreate, contentType: "Not A Mime" }] }),
|
|
482
|
+
).rejects.toThrow(/Invalid content type/)
|
|
483
|
+
})
|
|
215
484
|
|
|
216
|
-
|
|
485
|
+
it("sorts attributes ascending by their bytes32 name", async () => {
|
|
486
|
+
const { client, writeContract } = makeClient()
|
|
487
|
+
await sendArkivTransaction(client, {
|
|
488
|
+
creates: [{ ...validCreate, attributes: { tag: "zz", status: "active", score: 30 } }],
|
|
489
|
+
})
|
|
490
|
+
const names = sentCreate(writeContract).attributes.map((a) => a.name)
|
|
491
|
+
expect(names).toEqual([...names].sort())
|
|
492
|
+
// The "$" system cells lead; the user attributes follow in name order.
|
|
493
|
+
expect(names.slice(2)).toEqual([
|
|
217
494
|
toHex("score", { size: 32 }),
|
|
218
495
|
toHex("status", { size: 32 }),
|
|
219
496
|
toHex("tag", { size: 32 }),
|
|
220
497
|
])
|
|
221
498
|
})
|
|
499
|
+
})
|
|
222
500
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
501
|
+
describe("patch", () => {
|
|
502
|
+
/** The decoded Patch struct of the batch's only patch. */
|
|
503
|
+
function sentPatch(writeContract: ReturnType<typeof vi.fn>) {
|
|
504
|
+
const op = sentOperations(writeContract).find((o) => o.operation === OperationType.Patch)
|
|
505
|
+
if (!op) throw new Error("no Patch operation was sent")
|
|
506
|
+
return decodeAbiParameters(PATCH_PARAMS, op.operationData)[0]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
it("carries only what the patch named", async () => {
|
|
510
|
+
const { client, writeContract } = makeClient()
|
|
511
|
+
await sendArkivTransaction(client, {
|
|
512
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(11) } }],
|
|
513
|
+
})
|
|
514
|
+
const patch = sentPatch(writeContract)
|
|
515
|
+
expect(patch.entityKey).toBe(ENTITY_KEY)
|
|
516
|
+
// No $payload, no $contentType, no tombstones: everything unnamed is left alone.
|
|
517
|
+
expect(patch.mutations).toEqual([
|
|
518
|
+
{ name: toHex("level", { size: 32 }), typeId: 2, value: expect.any(String) },
|
|
227
519
|
])
|
|
520
|
+
})
|
|
228
521
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
522
|
+
it("writes the system cells only when the patch gives them", async () => {
|
|
523
|
+
const { client, writeContract } = makeClient()
|
|
524
|
+
await sendArkivTransaction(client, {
|
|
525
|
+
patches: [{ entityKey: ENTITY_KEY, payload: new Uint8Array([9]) }],
|
|
526
|
+
})
|
|
527
|
+
const names = sentPatch(writeContract).mutations.map((m) => m.name)
|
|
528
|
+
expect(names).toEqual([toHex("$payload", { size: 32 })])
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it("tombstones an unset attribute with typeId 0 and an empty value", async () => {
|
|
532
|
+
const { client, writeContract } = makeClient()
|
|
533
|
+
await sendArkivTransaction(client, {
|
|
534
|
+
patches: [{ entityKey: ENTITY_KEY, unset: ["draft"] }],
|
|
535
|
+
})
|
|
536
|
+
expect(sentPatch(writeContract).mutations).toEqual([
|
|
537
|
+
{ name: toHex("draft", { size: 32 }), typeId: 0, value: "0x" },
|
|
538
|
+
])
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it("sorts writes and tombstones together, not each half on its own", async () => {
|
|
542
|
+
const { client, writeContract } = makeClient()
|
|
543
|
+
await sendArkivTransaction(client, {
|
|
544
|
+
patches: [
|
|
545
|
+
{
|
|
546
|
+
entityKey: ENTITY_KEY,
|
|
547
|
+
set: { zeta: "z", beta: "b" },
|
|
548
|
+
unset: ["alpha", "gamma"],
|
|
549
|
+
payload: new Uint8Array([1]),
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
})
|
|
553
|
+
const names = sentPatch(writeContract).mutations.map((m) => m.name)
|
|
554
|
+
expect(names).toEqual([
|
|
555
|
+
toHex("$payload", { size: 32 }),
|
|
556
|
+
toHex("alpha", { size: 32 }),
|
|
557
|
+
toHex("beta", { size: 32 }),
|
|
558
|
+
toHex("gamma", { size: 32 }),
|
|
559
|
+
toHex("zeta", { size: 32 }),
|
|
232
560
|
])
|
|
233
561
|
})
|
|
234
562
|
|
|
235
|
-
it("
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
expect(
|
|
563
|
+
it("collapses a name repeated in unset, since it is the same intent twice", async () => {
|
|
564
|
+
const { client, writeContract } = makeClient()
|
|
565
|
+
await sendArkivTransaction(client, {
|
|
566
|
+
patches: [{ entityKey: ENTITY_KEY, unset: ["draft", "draft"] }],
|
|
567
|
+
})
|
|
568
|
+
// Two identical tombstones would break the engine's strict-ascending uniqueness rule.
|
|
569
|
+
expect(sentPatch(writeContract).mutations).toHaveLength(1)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it("rejects a name that is both set and unset", async () => {
|
|
573
|
+
const { client, writeContract } = makeClient()
|
|
574
|
+
await expect(
|
|
575
|
+
sendArkivTransaction(client, {
|
|
576
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) }, unset: ["level"] }],
|
|
577
|
+
}),
|
|
578
|
+
).rejects.toThrow(ConflictingMutationError)
|
|
579
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it("rejects a patch with nothing to apply", async () => {
|
|
583
|
+
const { client, writeContract } = makeClient()
|
|
584
|
+
await expect(
|
|
585
|
+
sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY }] }),
|
|
586
|
+
).rejects.toThrow(EmptyPatchError)
|
|
587
|
+
// Present but empty is just as empty.
|
|
588
|
+
await expect(
|
|
589
|
+
sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY, set: {}, unset: [] }] }),
|
|
590
|
+
).rejects.toThrow(EmptyPatchError)
|
|
591
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
242
592
|
})
|
|
243
593
|
|
|
244
|
-
it("
|
|
594
|
+
it("still validates a content type when one is given", async () => {
|
|
245
595
|
const { client } = makeClient()
|
|
246
596
|
await expect(
|
|
247
597
|
sendArkivTransaction(client, {
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
...BASE_CREATE,
|
|
251
|
-
attributes: [
|
|
252
|
-
{ key: "tag", value: "one" },
|
|
253
|
-
{ key: "tag", value: "two" },
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
],
|
|
598
|
+
patches: [{ entityKey: ENTITY_KEY, payload: new Uint8Array(), contentType: "Not A Mime" }],
|
|
257
599
|
}),
|
|
258
|
-
).rejects.toThrow(
|
|
600
|
+
).rejects.toThrow(/Invalid content type/)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it("rejects a bare string as unset, rather than tombstoning its letters", async () => {
|
|
604
|
+
const { client, writeContract } = makeClient()
|
|
605
|
+
// A Set is built from any iterable, so "draft" would become tombstones for d, r, a, f and t —
|
|
606
|
+
// five valid attribute names, none of them the one meant, and the real "draft" left untouched.
|
|
607
|
+
await expect(
|
|
608
|
+
sendArkivTransaction(client, {
|
|
609
|
+
patches: [{ entityKey: ENTITY_KEY, unset: "draft" as unknown as string[] }],
|
|
610
|
+
}),
|
|
611
|
+
).rejects.toThrow(/must be an array of attribute names/)
|
|
612
|
+
expect(writeContract).not.toHaveBeenCalled()
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it("rejects unsetting a system cell, which belongs to the engine", async () => {
|
|
616
|
+
const { client } = makeClient()
|
|
617
|
+
await expect(
|
|
618
|
+
sendArkivTransaction(client, { patches: [{ entityKey: ENTITY_KEY, unset: ["$payload"] }] }),
|
|
619
|
+
).rejects.toThrow(InvalidAttributeNameError)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("does not fetch a block height for a patch-only batch", async () => {
|
|
623
|
+
const { client } = makeClient()
|
|
624
|
+
await sendArkivTransaction(client, {
|
|
625
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
|
|
626
|
+
})
|
|
627
|
+
// A patch carries no expiry, so there is nothing to resolve against the chain.
|
|
628
|
+
expect(client.getBlockNumber).not.toHaveBeenCalled()
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
describe("failures that are not reverts", () => {
|
|
633
|
+
it("keeps an unrecognised error's message and the error itself as the cause", async () => {
|
|
634
|
+
const { client } = makeClient()
|
|
635
|
+
// The transaction was broadcast; it is waiting for the receipt that times out. What that error
|
|
636
|
+
// knows — not least the hash it was watching — is the only way to find out whether the patch
|
|
637
|
+
// landed, so summarising it away as "Transaction failed" would strand the caller.
|
|
638
|
+
const timeout = new Error("Timed out while waiting for transaction 0xabc to be confirmed.")
|
|
639
|
+
;(client.waitForTransactionReceipt as ReturnType<typeof vi.fn>).mockRejectedValue(timeout)
|
|
640
|
+
|
|
641
|
+
const failure = await sendArkivTransaction(client, {
|
|
642
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
|
|
643
|
+
}).catch((error: Error) => error)
|
|
644
|
+
|
|
645
|
+
expect(failure).toBeInstanceOf(EntityMutationError)
|
|
646
|
+
expect(failure.message).toContain("0xabc")
|
|
647
|
+
expect(failure.cause).toBe(timeout)
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
describe("a revert the node catches before there is a receipt", () => {
|
|
652
|
+
/** A decoded engine revert, wrapped the way viem delivers one out of `writeContract`. */
|
|
653
|
+
function preflightRevert(errorName: string, args: readonly unknown[]) {
|
|
654
|
+
const inner = new ContractFunctionRevertedError({ abi: [], functionName: "execute" })
|
|
655
|
+
Object.assign(inner, { data: { errorName, args } })
|
|
656
|
+
return new BaseError('The contract function "execute" reverted.', { cause: inner })
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
it("explains it, rather than leaving the explainer to the rarer post-receipt path", async () => {
|
|
660
|
+
// Most reverts never reach the receipt: the node rejects them during gas estimation, so
|
|
661
|
+
// `writeContract` throws and there is nothing to replay. Wiring the named-error decoder only
|
|
662
|
+
// into the replay path would explain the rare case and stay silent on the common one.
|
|
663
|
+
const { client } = makeClient()
|
|
664
|
+
;(client.writeContract as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
665
|
+
preflightRevert("ReadOnlyEntity", [ENTITY_KEY]),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
const failure = await sendArkivTransaction(client, {
|
|
669
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
|
|
670
|
+
}).catch((error: Error) => error)
|
|
671
|
+
|
|
672
|
+
expect(failure).toBeInstanceOf(EntityMutationError)
|
|
673
|
+
expect(failure.message).toContain("readonly")
|
|
674
|
+
// viem's own rendering prints the decoded args positionally — the right data, none of the
|
|
675
|
+
// meaning — so the point is that it is *not* what surfaces.
|
|
676
|
+
expect(failure.message).not.toContain('The contract function "execute" reverted')
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it("still names a bad attribute-name byte, which only ever fails at estimation", async () => {
|
|
680
|
+
const { client } = makeClient()
|
|
681
|
+
;(client.writeContract as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
682
|
+
preflightRevert("Ident32InvalidByte", [2n, "0x2f"]),
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
const failure = await sendArkivTransaction(client, {
|
|
686
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
|
|
687
|
+
}).catch((error: Error) => error)
|
|
688
|
+
|
|
689
|
+
expect(failure.message).toContain('"/"')
|
|
690
|
+
expect(failure.message).toContain("byte 2")
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it("leaves an error it cannot decode to the existing handling", async () => {
|
|
694
|
+
const { client } = makeClient()
|
|
695
|
+
const opaque = new BaseError("nonce too low")
|
|
696
|
+
;(client.writeContract as ReturnType<typeof vi.fn>).mockRejectedValue(opaque)
|
|
697
|
+
|
|
698
|
+
const failure = await sendArkivTransaction(client, {
|
|
699
|
+
patches: [{ entityKey: ENTITY_KEY, set: { level: i32(1) } }],
|
|
700
|
+
}).catch((error: Error) => error)
|
|
701
|
+
|
|
702
|
+
expect(failure.message).toContain("nonce too low")
|
|
703
|
+
expect(failure.cause).toBe(opaque)
|
|
259
704
|
})
|
|
260
705
|
})
|