@arkiv-network/sdk 0.7.0-dev.1 → 0.8.0-advanced.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -16
- package/dist/actions/advanced/index.cjs +13 -0
- package/dist/actions/advanced/index.d.cts +8 -0
- package/dist/actions/advanced/index.d.ts +9 -0
- package/dist/actions/advanced/index.js +9 -0
- package/dist/advanced-8ZPTmmaN.js +598 -0
- package/dist/advanced-8ZPTmmaN.js.map +1 -0
- package/dist/advanced-fPImL6lW.cjs +656 -0
- package/dist/arkivTransactions-D74gERHG.d.cts +571 -0
- package/dist/arkivTransactions-D74gERHG.d.cts.map +1 -0
- package/dist/arkivTransactions-NtGD6uk5.d.ts +571 -0
- package/dist/arkivTransactions-NtGD6uk5.d.ts.map +1 -0
- package/dist/attr/index.cjs +35 -0
- package/dist/attr/index.d.cts +2 -0
- package/dist/attr/index.d.ts +3 -0
- package/dist/attr/index.js +4 -0
- package/dist/attributes-BhY5DwJp.js +809 -0
- package/dist/attributes-BhY5DwJp.js.map +1 -0
- package/dist/attributes-Df9IsCVs.cjs +1029 -0
- package/dist/chains/index.cjs +2 -3
- package/dist/chains/index.d.cts +22 -60
- package/dist/chains/index.d.cts.map +1 -1
- package/dist/chains/index.d.ts +22 -60
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js +2 -2
- package/dist/chains-BJrZs6sG.js +57 -0
- package/dist/chains-BJrZs6sG.js.map +1 -0
- package/dist/chains-BYRTTipa.cjs +73 -0
- package/dist/codec-Car3KkFF.d.ts +102 -0
- package/dist/codec-Car3KkFF.d.ts.map +1 -0
- package/dist/entity/index.cjs +20 -0
- package/dist/entity/index.d.cts +4 -0
- package/dist/entity/index.d.ts +5 -0
- package/dist/entity/index.js +7 -0
- package/dist/entity-B00Li04j.cjs +71 -0
- package/dist/entity-B7G8q_Eo.cjs +0 -0
- package/dist/entity-BYQR1uoe.js +67 -0
- package/dist/entity-BYQR1uoe.js.map +1 -0
- package/dist/entity-CD_qFF2M.js +0 -0
- package/dist/errors-BVbjKVMN.cjs +56 -0
- package/dist/errors-LTe7bOYS.js +33 -0
- package/dist/errors-LTe7bOYS.js.map +1 -0
- package/dist/expiry-BELY2Kpw.cjs +135 -0
- package/dist/expiry-BjpxZZQU.js +95 -0
- package/dist/expiry-BjpxZZQU.js.map +1 -0
- package/dist/expiry-DRV29F1r.d.cts +79 -0
- package/dist/expiry-DRV29F1r.d.cts.map +1 -0
- package/dist/expiry-nDbCgZX4.d.ts +79 -0
- package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
- package/dist/flags-5dTNRnow.d.ts +41 -0
- package/dist/flags-5dTNRnow.d.ts.map +1 -0
- package/dist/flags-CRjPpoh3.d.cts +41 -0
- package/dist/flags-CRjPpoh3.d.cts.map +1 -0
- package/dist/flags-DFBs2J94.js +33 -0
- package/dist/flags-DFBs2J94.js.map +1 -0
- package/dist/flags-kr276q6i.cjs +43 -0
- package/dist/index-B5j76711.d.ts +184 -0
- package/dist/index-B5j76711.d.ts.map +1 -0
- package/dist/index-BF7Mq9Is.d.cts +280 -0
- package/dist/index-BF7Mq9Is.d.cts.map +1 -0
- package/dist/index-CA4V7Pdh.d.cts +167 -0
- package/dist/index-CA4V7Pdh.d.cts.map +1 -0
- package/dist/index-CV5zCdpa.d.cts +417 -0
- package/dist/index-CV5zCdpa.d.cts.map +1 -0
- package/dist/index-DHDud2hC.d.ts +280 -0
- package/dist/index-DHDud2hC.d.ts.map +1 -0
- package/dist/index-DvYvrkdk.d.ts +320 -0
- package/dist/index-DvYvrkdk.d.ts.map +1 -0
- package/dist/index-EeUO0nnJ.d.cts +184 -0
- package/dist/index-EeUO0nnJ.d.cts.map +1 -0
- package/dist/index-fX_XyqLp.d.ts +167 -0
- package/dist/index-fX_XyqLp.d.ts.map +1 -0
- package/dist/index.cjs +411 -405
- package/dist/index.d.cts +360 -209
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +361 -209
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +342 -387
- package/dist/index.js.map +1 -1
- package/dist/key-D1Bh7MF0.js +70 -0
- package/dist/key-D1Bh7MF0.js.map +1 -0
- package/dist/key-WKhXnXuU.cjs +98 -0
- package/dist/logger-CVpvU8ik.js +153 -0
- package/dist/logger-CVpvU8ik.js.map +1 -0
- package/dist/logger-Cr15lPAQ.cjs +188 -0
- package/dist/payload-BNoud2KZ.d.cts +94 -0
- package/dist/payload-BNoud2KZ.d.cts.map +1 -0
- package/dist/payload-Bz6651ax.js +67 -0
- package/dist/payload-Bz6651ax.js.map +1 -0
- package/dist/payload-CcUj85ys.d.ts +94 -0
- package/dist/payload-CcUj85ys.d.ts.map +1 -0
- package/dist/payload-KC14SDlN.cjs +83 -0
- package/dist/query/index.cjs +26 -150
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +6 -3
- package/dist/query/index.js +8 -138
- package/dist/queryBuilder-DM6AWCBa.js +820 -0
- package/dist/queryBuilder-DM6AWCBa.js.map +1 -0
- package/dist/queryBuilder-DbUMEZln.cjs +944 -0
- package/dist/selection-C4UhsMOO.d.ts +434 -0
- package/dist/selection-C4UhsMOO.d.ts.map +1 -0
- package/dist/selection-DPg0uyJf.d.cts +433 -0
- package/dist/selection-DPg0uyJf.d.cts.map +1 -0
- package/dist/types/index.cjs +3 -3
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +8 -2
- package/dist/types/index.js +3 -3
- package/dist/types-Bu_ZwMbZ.js +0 -0
- package/dist/types-NRqymi6S.cjs +0 -0
- package/dist/utils/index.cjs +4 -17
- package/dist/utils/index.d.cts +3 -17
- package/dist/utils/index.d.ts +3 -17
- package/dist/utils/index.js +4 -18
- package/package.json +26 -5
- package/src/actions/advanced/advanced.test.ts +258 -0
- package/src/actions/advanced/buildMutation.ts +104 -0
- package/src/actions/advanced/getMutationResult.ts +112 -0
- package/src/actions/advanced/index.ts +21 -0
- package/src/actions/advanced/pingTransaction.ts +43 -0
- package/src/actions/advanced/sendMutation.ts +59 -0
- package/src/actions/public/getEntity.ts +27 -43
- package/src/actions/public/getEntityNonce.ts +30 -0
- package/src/actions/public/predictEntityKeys.test.ts +143 -0
- package/src/actions/public/predictEntityKeys.ts +138 -0
- package/src/actions/public/query.ts +35 -67
- package/src/actions/public/watchEntityEvents.test.ts +414 -0
- package/src/actions/public/watchEntityEvents.ts +225 -0
- package/src/actions/wallet/changeOwnership.ts +2 -2
- package/src/actions/wallet/createEntity.ts +99 -24
- package/src/actions/wallet/extendEntity.ts +37 -13
- package/src/actions/wallet/mutateEntities.test.ts +76 -0
- package/src/actions/wallet/mutateEntities.ts +16 -33
- package/src/actions/wallet/patchEntity.ts +90 -0
- package/src/attr/attributes.test.ts +120 -0
- package/src/attr/attributes.ts +190 -0
- package/src/attr/codec.test.ts +162 -0
- package/src/attr/codec.ts +220 -0
- package/src/attr/decimal.ts +86 -0
- package/src/attr/errors.ts +145 -0
- package/src/attr/index.ts +50 -0
- package/src/attr/names.ts +106 -0
- package/src/attr/types.ts +150 -0
- package/src/attr/values.test.ts +271 -0
- package/src/attr/values.ts +502 -0
- package/src/chains/cheesecake.ts +22 -0
- package/src/chains/index.ts +1 -2
- package/src/clients/createPublicClient.test.ts +3 -5
- package/src/clients/createPublicClient.ts +5 -4
- package/src/clients/createWalletClient.test.ts +2 -2
- package/src/clients/createWalletClient.ts +13 -7
- package/src/clients/decorators/arkivAdvanced.ts +144 -0
- package/src/clients/decorators/arkivPublic.ts +172 -139
- package/src/clients/decorators/arkivWallet.ts +101 -94
- package/src/entity/create.test.ts +184 -0
- package/src/entity/errors.ts +38 -0
- package/src/entity/events.ts +16 -0
- package/src/entity/expiry.test.ts +195 -0
- package/src/entity/expiry.ts +181 -0
- package/src/entity/flags.ts +65 -0
- package/src/entity/index.ts +15 -0
- package/src/entity/key.ts +68 -0
- package/src/entity/operations.ts +134 -0
- package/src/entity/params.ts +15 -0
- package/src/errors.ts +26 -49
- package/src/index.ts +19 -0
- package/src/query/engine.ts +68 -137
- package/src/query/errors.ts +151 -0
- package/src/query/expression.test.ts +286 -0
- package/src/query/expression.ts +493 -0
- package/src/query/index.ts +40 -4
- package/src/query/injection.test.ts +151 -0
- package/src/query/queryBuilder.test.ts +342 -637
- package/src/query/queryBuilder.ts +133 -359
- package/src/query/queryResult.ts +54 -51
- package/src/query/selection.test.ts +48 -98
- package/src/query/selection.ts +77 -65
- package/src/types/entity.test.ts +44 -155
- package/src/types/entity.ts +60 -52
- package/src/types/events.ts +59 -16
- package/src/types/index.ts +20 -17
- package/src/types/rpcSchema.ts +69 -37
- package/src/utils/arkivTransactions.test.ts +600 -155
- package/src/utils/arkivTransactions.ts +350 -219
- package/src/utils/chains.test.ts +21 -24
- package/src/utils/compression.ts +1 -1
- package/src/utils/entities.test.ts +62 -0
- package/src/utils/entities.ts +109 -37
- package/src/utils/expirationTime.ts +114 -10
- package/src/utils/logger.ts +2 -2
- package/src/utils/revert.test.ts +99 -0
- package/src/utils/revert.ts +145 -0
- package/dist/chains-BifaY04V.cjs +0 -108
- package/dist/chains-CrtkVbBh.js +0 -86
- package/dist/chains-CrtkVbBh.js.map +0 -1
- package/dist/consts-Bz0LZQZY.js +0 -7
- package/dist/consts-Bz0LZQZY.js.map +0 -1
- package/dist/consts-CxT1-RXU.cjs +0 -18
- package/dist/entity-BhSXaB23.cjs +0 -81
- package/dist/entity-CN41IOrq.d.cts +0 -401
- package/dist/entity-CN41IOrq.d.cts.map +0 -1
- package/dist/entity-DGfVyB-a.js +0 -71
- package/dist/entity-DGfVyB-a.js.map +0 -1
- package/dist/entity-NnEVlePN.d.ts +0 -401
- package/dist/entity-NnEVlePN.d.ts.map +0 -1
- package/dist/index-ClkpS6s_.d.ts +0 -481
- package/dist/index-ClkpS6s_.d.ts.map +0 -1
- package/dist/index-w4qop0Pe.d.cts +0 -481
- package/dist/index-w4qop0Pe.d.cts.map +0 -1
- package/dist/payload-BpNUPOnY.js +0 -13
- package/dist/payload-BpNUPOnY.js.map +0 -1
- package/dist/payload-C2KHOUoj.d.ts +0 -6
- package/dist/payload-C2KHOUoj.d.ts.map +0 -1
- package/dist/payload-CJN8fTvX.d.cts +0 -6
- package/dist/payload-CJN8fTvX.d.cts.map +0 -1
- package/dist/payload-D8YLKcJj.cjs +0 -23
- package/dist/query/index.js.map +0 -1
- package/dist/queryBuilder-BE_XhLLa.js +0 -735
- package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
- package/dist/queryBuilder-J37buGCi.cjs +0 -865
- package/dist/utils/index.d.cts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/src/actions/public/buildQuery.ts +0 -10
- package/src/actions/public/subscribeEntityEvents.ts +0 -93
- package/src/actions/wallet/updateEntity.ts +0 -59
- package/src/chains/braga.ts +0 -26
- package/src/chains/kaolin.ts +0 -29
- package/src/query/engine.test.ts +0 -571
- package/src/query/predicate.test.ts +0 -461
- package/src/query/predicate.ts +0 -144
- package/src/query/queryResult.test.ts +0 -270
- package/src/query/selectQueryBuilder.test.ts +0 -213
- package/src/types/attributes.ts +0 -28
- package/src/utils/validation.test.ts +0 -106
- package/src/utils/validation.ts +0 -80
- /package/dist/{types-Ci4BRClw.js → attr-DlmErkvR.cjs} +0 -0
- /package/dist/{types-a8gm_IaQ.cjs → attr-GcQKatub.js} +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
encodeAbiParameters,
|
|
4
|
+
encodeEventTopics,
|
|
5
|
+
type Hex,
|
|
6
|
+
parseAbiParameters,
|
|
7
|
+
type TransactionReceipt,
|
|
8
|
+
TransactionReceiptNotFoundError,
|
|
9
|
+
} from "viem"
|
|
10
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
11
|
+
import { ARKIV_ADDRESS } from "../../consts"
|
|
12
|
+
import { ENTITY_EVENTS_ABI } from "../../entity/events"
|
|
13
|
+
import { ExpirationTime } from "../../utils/expirationTime"
|
|
14
|
+
import { buildMutation } from "./buildMutation"
|
|
15
|
+
import { decodeMutationResult, getMutationResult } from "./getMutationResult"
|
|
16
|
+
import { pingTransaction } from "./pingTransaction"
|
|
17
|
+
import { sendMutation } from "./sendMutation"
|
|
18
|
+
|
|
19
|
+
const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
|
|
20
|
+
const OWNER = "0x1111111111111111111111111111111111111111"
|
|
21
|
+
const NEW_OWNER = "0x2222222222222222222222222222222222222222"
|
|
22
|
+
const TX_HASH = `0x${"cd".repeat(32)}` as const
|
|
23
|
+
|
|
24
|
+
const CREATE = {
|
|
25
|
+
payload: new Uint8Array([1, 2, 3]),
|
|
26
|
+
contentType: "application/json",
|
|
27
|
+
expires: ExpirationTime.atBlock(1_000_000n),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeWalletClient() {
|
|
31
|
+
const writeContract = vi.fn().mockResolvedValue(TX_HASH)
|
|
32
|
+
const getBlockNumber = vi.fn().mockResolvedValue(100n)
|
|
33
|
+
const waitForTransactionReceipt = vi.fn()
|
|
34
|
+
const simulateContract = vi.fn()
|
|
35
|
+
return {
|
|
36
|
+
client: {
|
|
37
|
+
account: { address: OWNER },
|
|
38
|
+
chain: { id: 1 },
|
|
39
|
+
getBlockNumber,
|
|
40
|
+
writeContract,
|
|
41
|
+
waitForTransactionReceipt,
|
|
42
|
+
simulateContract,
|
|
43
|
+
} as unknown as ArkivClient,
|
|
44
|
+
writeContract,
|
|
45
|
+
getBlockNumber,
|
|
46
|
+
waitForTransactionReceipt,
|
|
47
|
+
simulateContract,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeReceiptClient(receiptOrError: TransactionReceipt | Error) {
|
|
52
|
+
const getTransactionReceipt =
|
|
53
|
+
receiptOrError instanceof Error
|
|
54
|
+
? vi.fn().mockRejectedValue(receiptOrError)
|
|
55
|
+
: vi.fn().mockResolvedValue(receiptOrError)
|
|
56
|
+
return {
|
|
57
|
+
client: { getTransactionReceipt } as unknown as ArkivClient,
|
|
58
|
+
getTransactionReceipt,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A receipt log as the engine emits it: topics for the indexed args, data for the rest. */
|
|
63
|
+
function entityLog(
|
|
64
|
+
eventName: (typeof ENTITY_EVENTS_ABI)[number]["name"],
|
|
65
|
+
args: Record<string, unknown>,
|
|
66
|
+
data: Hex = "0x",
|
|
67
|
+
address: string = ARKIV_ADDRESS,
|
|
68
|
+
) {
|
|
69
|
+
return {
|
|
70
|
+
address,
|
|
71
|
+
topics: encodeEventTopics({ abi: ENTITY_EVENTS_ABI, eventName, args } as never),
|
|
72
|
+
data,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function receiptWith(logs: unknown[], status: "success" | "reverted" = "success") {
|
|
77
|
+
return {
|
|
78
|
+
status,
|
|
79
|
+
transactionHash: TX_HASH,
|
|
80
|
+
blockNumber: 123n,
|
|
81
|
+
logs,
|
|
82
|
+
} as unknown as TransactionReceipt
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("buildMutation", () => {
|
|
86
|
+
it("spends no RPC call when every expiry is an absolute block", async () => {
|
|
87
|
+
const { client, getBlockNumber } = makeWalletClient()
|
|
88
|
+
const built = await buildMutation(client, {
|
|
89
|
+
creates: [CREATE],
|
|
90
|
+
extensions: [{ entityKey: ENTITY_KEY, expires: ExpirationTime.atBlock(2_000_000n) }],
|
|
91
|
+
})
|
|
92
|
+
expect(getBlockNumber).not.toHaveBeenCalled()
|
|
93
|
+
expect(built.operations).toHaveLength(2)
|
|
94
|
+
expect(built.to).toBe(ARKIV_ADDRESS)
|
|
95
|
+
expect(built.data.startsWith("0x")).toBe(true)
|
|
96
|
+
expect(built.expected).toEqual({
|
|
97
|
+
creates: 1,
|
|
98
|
+
patches: 0,
|
|
99
|
+
deletes: 0,
|
|
100
|
+
extensions: 1,
|
|
101
|
+
ownershipChanges: 0,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("fetches the head once for a relative lifetime, and not at all when it is supplied", async () => {
|
|
106
|
+
const { client, getBlockNumber } = makeWalletClient()
|
|
107
|
+
const relative = { ...CREATE, expires: ExpirationTime.fromDays(1) }
|
|
108
|
+
|
|
109
|
+
await buildMutation(client, { creates: [relative] })
|
|
110
|
+
expect(getBlockNumber).toHaveBeenCalledTimes(1)
|
|
111
|
+
|
|
112
|
+
getBlockNumber.mockClear()
|
|
113
|
+
await buildMutation(client, { creates: [relative] }, { currentBlock: 100n })
|
|
114
|
+
expect(getBlockNumber).not.toHaveBeenCalled()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("needs no head for batches without expiries at all", async () => {
|
|
118
|
+
const { client, getBlockNumber } = makeWalletClient()
|
|
119
|
+
const built = await buildMutation(client, {
|
|
120
|
+
deletes: [{ entityKey: ENTITY_KEY }],
|
|
121
|
+
ownershipChanges: [{ entityKey: ENTITY_KEY, newOwner: NEW_OWNER }],
|
|
122
|
+
})
|
|
123
|
+
expect(getBlockNumber).not.toHaveBeenCalled()
|
|
124
|
+
expect(built.operations).toHaveLength(2)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("still rejects an empty batch locally", async () => {
|
|
128
|
+
const { client } = makeWalletClient()
|
|
129
|
+
await expect(buildMutation(client, {})).rejects.toThrow(/No operations/)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("sendMutation", () => {
|
|
134
|
+
it("submits once and never waits, polls or simulates", async () => {
|
|
135
|
+
const { client, writeContract, waitForTransactionReceipt, simulateContract, getBlockNumber } =
|
|
136
|
+
makeWalletClient()
|
|
137
|
+
const { txHash, expected } = await sendMutation(client, { creates: [CREATE] })
|
|
138
|
+
|
|
139
|
+
expect(txHash).toBe(TX_HASH)
|
|
140
|
+
expect(expected.creates).toBe(1)
|
|
141
|
+
expect(writeContract).toHaveBeenCalledTimes(1)
|
|
142
|
+
// The whole point of the advanced path: nothing beyond the submission itself.
|
|
143
|
+
expect(getBlockNumber).not.toHaveBeenCalled()
|
|
144
|
+
expect(waitForTransactionReceipt).not.toHaveBeenCalled()
|
|
145
|
+
expect(simulateContract).not.toHaveBeenCalled()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("forwards txParams so viem has nothing left to look up", async () => {
|
|
149
|
+
const { client, writeContract } = makeWalletClient()
|
|
150
|
+
await sendMutation(
|
|
151
|
+
client,
|
|
152
|
+
{ deletes: [{ entityKey: ENTITY_KEY }] },
|
|
153
|
+
{ txParams: { nonce: 7, gas: 1_000_000n, gasPrice: 2n } },
|
|
154
|
+
)
|
|
155
|
+
const call = writeContract.mock.calls[0][0]
|
|
156
|
+
expect(call.nonce).toBe(7)
|
|
157
|
+
expect(call.gas).toBe(1_000_000n)
|
|
158
|
+
expect(call.gasPrice).toBe(2n)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("requires an account and a chain", async () => {
|
|
162
|
+
const { client } = makeWalletClient()
|
|
163
|
+
const noAccount = { ...(client as object), account: undefined } as unknown as ArkivClient
|
|
164
|
+
await expect(sendMutation(noAccount, { creates: [CREATE] })).rejects.toThrow(/Account/)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe("pingTransaction", () => {
|
|
169
|
+
it("reports pending instead of throwing when the receipt is not there yet", async () => {
|
|
170
|
+
const { client, getTransactionReceipt } = makeReceiptClient(
|
|
171
|
+
new TransactionReceiptNotFoundError({ hash: TX_HASH }),
|
|
172
|
+
)
|
|
173
|
+
expect(await pingTransaction(client, TX_HASH)).toEqual({ status: "pending" })
|
|
174
|
+
expect(getTransactionReceipt).toHaveBeenCalledTimes(1)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("reports a mined transaction with its block", async () => {
|
|
178
|
+
const { client } = makeReceiptClient(receiptWith([]))
|
|
179
|
+
expect(await pingTransaction(client, TX_HASH)).toEqual({ status: "success", blockNumber: 123n })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("reports a reverted transaction as reverted, not as an error", async () => {
|
|
183
|
+
const { client } = makeReceiptClient(receiptWith([], "reverted"))
|
|
184
|
+
expect(await pingTransaction(client, TX_HASH)).toEqual({
|
|
185
|
+
status: "reverted",
|
|
186
|
+
blockNumber: 123n,
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("lets transport errors through untouched", async () => {
|
|
191
|
+
const { client } = makeReceiptClient(new Error("connection refused"))
|
|
192
|
+
await expect(pingTransaction(client, TX_HASH)).rejects.toThrow(/connection refused/)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe("getMutationResult and decodeMutationResult", () => {
|
|
197
|
+
const createdData = encodeAbiParameters(parseAbiParameters("uint64, uint8"), [555n, 0])
|
|
198
|
+
const extendedData = encodeAbiParameters(parseAbiParameters("uint64"), [999n])
|
|
199
|
+
const fullReceipt = receiptWith([
|
|
200
|
+
entityLog("EntityCreated", { entityKey: ENTITY_KEY, owner: OWNER }, createdData),
|
|
201
|
+
entityLog("EntityPatched", { entityKey: ENTITY_KEY, owner: OWNER }),
|
|
202
|
+
entityLog("EntityDeleted", { entityKey: ENTITY_KEY, owner: OWNER }),
|
|
203
|
+
entityLog("ExpiryExtended", { entityKey: ENTITY_KEY, owner: OWNER }, extendedData),
|
|
204
|
+
entityLog("OwnershipTransferred", {
|
|
205
|
+
entityKey: ENTITY_KEY,
|
|
206
|
+
previousOwner: OWNER,
|
|
207
|
+
newOwner: NEW_OWNER,
|
|
208
|
+
}),
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
it("turns one receipt fetch into the full decoded result", async () => {
|
|
212
|
+
const { client, getTransactionReceipt } = makeReceiptClient(fullReceipt)
|
|
213
|
+
const result = await getMutationResult(client, TX_HASH)
|
|
214
|
+
|
|
215
|
+
expect(getTransactionReceipt).toHaveBeenCalledTimes(1)
|
|
216
|
+
if (result.status === "pending") throw new Error("expected a mined result")
|
|
217
|
+
expect(result.txHash).toBe(TX_HASH)
|
|
218
|
+
expect(result.blockNumber).toBe(123n)
|
|
219
|
+
expect(result.createdEntities).toEqual([ENTITY_KEY])
|
|
220
|
+
expect(result.createdExpiries).toEqual([555n])
|
|
221
|
+
expect(result.patchedEntities).toEqual([ENTITY_KEY])
|
|
222
|
+
expect(result.deletedEntities).toEqual([ENTITY_KEY])
|
|
223
|
+
expect(result.extendedEntities).toEqual([ENTITY_KEY])
|
|
224
|
+
expect(result.extendedExpiries).toEqual([999n])
|
|
225
|
+
expect(result.ownershipChanges).toEqual([ENTITY_KEY])
|
|
226
|
+
expect(result.receipt).toBe(fullReceipt)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("reports pending before the transaction is mined", async () => {
|
|
230
|
+
const { client } = makeReceiptClient(new TransactionReceiptNotFoundError({ hash: TX_HASH }))
|
|
231
|
+
expect(await getMutationResult(client, TX_HASH)).toEqual({ status: "pending" })
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("decodes a held receipt with zero RPC calls", () => {
|
|
235
|
+
const result = decodeMutationResult(fullReceipt)
|
|
236
|
+
expect(result.status).toBe("success")
|
|
237
|
+
expect(result.createdEntities).toEqual([ENTITY_KEY])
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("skips logs from other contracts and events it has no name for", () => {
|
|
241
|
+
const foreign = entityLog(
|
|
242
|
+
"EntityCreated",
|
|
243
|
+
{ entityKey: ENTITY_KEY, owner: OWNER },
|
|
244
|
+
createdData,
|
|
245
|
+
"0x9999999999999999999999999999999999999999",
|
|
246
|
+
)
|
|
247
|
+
const unknown = { address: ARKIV_ADDRESS, topics: [`0x${"ff".repeat(32)}`], data: "0x" }
|
|
248
|
+
const result = decodeMutationResult(receiptWith([foreign, unknown]))
|
|
249
|
+
expect(result.createdEntities).toEqual([])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("is lenient: a reverted receipt decodes to empty arrays, never an error", () => {
|
|
253
|
+
const result = decodeMutationResult(receiptWith([], "reverted"))
|
|
254
|
+
expect(result.status).toBe("reverted")
|
|
255
|
+
expect(result.createdEntities).toEqual([])
|
|
256
|
+
expect(result.deletedEntities).toEqual([])
|
|
257
|
+
})
|
|
258
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Address, encodeFunctionData, type Hex } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import type { PublicArkivClient } from "../../clients/createPublicClient"
|
|
4
|
+
import { ARKIV_ADDRESS } from "../../consts"
|
|
5
|
+
import { EXECUTE_ABI, type Operation } from "../../entity/operations"
|
|
6
|
+
import {
|
|
7
|
+
buildEntityOperations,
|
|
8
|
+
type EntityMutationOps,
|
|
9
|
+
mutationNeedsBlockNumber,
|
|
10
|
+
} from "../../utils/arkivTransactions"
|
|
11
|
+
import { getLogger } from "../../utils/logger"
|
|
12
|
+
|
|
13
|
+
const logger = getLogger("actions:advanced:build-mutation")
|
|
14
|
+
|
|
15
|
+
/** Options for the advanced-path builders ({@link buildMutation}, {@link sendMutation}). */
|
|
16
|
+
export type BuildMutationOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* The chain head to resolve relative expiries against.
|
|
19
|
+
*
|
|
20
|
+
* Pass a block number you already hold — from a previous call, `getBlockTiming()`, or your own
|
|
21
|
+
* tracking — and the build spends no RPC call at all. Without it, one `eth_blockNumber` is made,
|
|
22
|
+
* and only when the batch actually needs the head (a relative lifetime or a `Date` deadline);
|
|
23
|
+
* purely absolute `atBlock(n)` expiries never trigger it.
|
|
24
|
+
*
|
|
25
|
+
* Supplying it also buys back a check: absolute expiries are validated against it, so an
|
|
26
|
+
* `atBlock(n)` already in the past throws {@link InvalidExpiryError} locally instead of
|
|
27
|
+
* reverting on-chain. When the head is neither supplied nor fetched, that dead-on-arrival
|
|
28
|
+
* check is skipped — the one local validation of the everyday path this path trades away.
|
|
29
|
+
*/
|
|
30
|
+
currentBlock?: bigint
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What {@link buildMutation} hands back: the batch in every form a sender might want. */
|
|
34
|
+
export type BuildMutationReturnType = {
|
|
35
|
+
/** The encoded batch, in engine order, for `writeContract`-style sends. */
|
|
36
|
+
operations: Operation[]
|
|
37
|
+
/** The Arkiv engine address the transaction must be sent to. */
|
|
38
|
+
to: Address
|
|
39
|
+
/**
|
|
40
|
+
* The full `execute(operations)` calldata. Sign a transaction carrying it offline (with your own
|
|
41
|
+
* nonce, gas and fees) and the whole mutation costs exactly one `eth_sendRawTransaction`.
|
|
42
|
+
*/
|
|
43
|
+
data: Hex
|
|
44
|
+
/**
|
|
45
|
+
* How many operations of each kind went into the batch — which is how many events of each kind a
|
|
46
|
+
* successful transaction emits, in batch order, for cross-checking a decoded result.
|
|
47
|
+
*/
|
|
48
|
+
expected: {
|
|
49
|
+
creates: number
|
|
50
|
+
patches: number
|
|
51
|
+
deletes: number
|
|
52
|
+
extensions: number
|
|
53
|
+
ownershipChanges: number
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Encodes a mutation batch without sending anything.
|
|
59
|
+
*
|
|
60
|
+
* This is the first step of the advanced path: it performs only local validation and encoding, so
|
|
61
|
+
* it costs **zero RPC calls** when `options.currentBlock` is provided (or no expiry is relative),
|
|
62
|
+
* and at most one `eth_blockNumber` otherwise. Send the result however you like: pass `operations`
|
|
63
|
+
* to `writeContract`, or sign `{ to, data }` offline and push it with `eth_sendRawTransaction`.
|
|
64
|
+
*
|
|
65
|
+
* One check rides on knowing the head: an absolute `atBlock(n)` already in the past is rejected
|
|
66
|
+
* locally only when the head is supplied or was fetched anyway. With neither, a dead-on-arrival
|
|
67
|
+
* expiry encodes cleanly and the engine reverts it on-chain — see
|
|
68
|
+
* {@link BuildMutationOptions.currentBlock}.
|
|
69
|
+
*
|
|
70
|
+
* @throws {InvalidExpiryError} If an expiry exceeds a protocol bound.
|
|
71
|
+
* @throws {InvalidValueError} If an attribute value does not fit the type it names.
|
|
72
|
+
* @throws {EmptyPatchError} If a patch has nothing to apply.
|
|
73
|
+
*/
|
|
74
|
+
export async function buildMutation(
|
|
75
|
+
client: ArkivClient,
|
|
76
|
+
data: EntityMutationOps,
|
|
77
|
+
options: BuildMutationOptions = {},
|
|
78
|
+
): Promise<BuildMutationReturnType> {
|
|
79
|
+
// The head is fetched only when an expiry is actually measured from it and the caller did not
|
|
80
|
+
// supply one — the advanced path never spends an RPC call it can avoid.
|
|
81
|
+
const currentBlock =
|
|
82
|
+
options.currentBlock ??
|
|
83
|
+
(mutationNeedsBlockNumber(data)
|
|
84
|
+
? // Every SDK-built client carries viem's `publicActions`; `PublicArkivClient` is the type
|
|
85
|
+
// that says so, the same narrowing `sendArkivTransaction` does to `WalletArkivClient`.
|
|
86
|
+
await (client as PublicArkivClient).getBlockNumber()
|
|
87
|
+
: 0n)
|
|
88
|
+
|
|
89
|
+
const operations = buildEntityOperations(data, { currentBlock })
|
|
90
|
+
logger("Built %d operations at block %d", operations.length, currentBlock)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
operations,
|
|
94
|
+
to: ARKIV_ADDRESS,
|
|
95
|
+
data: encodeFunctionData({ abi: EXECUTE_ABI, functionName: "execute", args: [operations] }),
|
|
96
|
+
expected: {
|
|
97
|
+
creates: data.creates?.length ?? 0,
|
|
98
|
+
patches: data.patches?.length ?? 0,
|
|
99
|
+
deletes: data.deletes?.length ?? 0,
|
|
100
|
+
extensions: data.extensions?.length ?? 0,
|
|
101
|
+
ownershipChanges: data.ownershipChanges?.length ?? 0,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type Hash, type Hex, type TransactionReceipt, TransactionReceiptNotFoundError } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import type { PublicArkivClient } from "../../clients/createPublicClient"
|
|
4
|
+
import { collectMutationEvents } from "../../utils/arkivTransactions"
|
|
5
|
+
import { getLogger } from "../../utils/logger"
|
|
6
|
+
|
|
7
|
+
const logger = getLogger("actions:advanced:get-mutation-result")
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A mined mutation, decoded from its receipt alone.
|
|
11
|
+
*
|
|
12
|
+
* Everything here comes out of the events the batch emitted, so no request context is needed to
|
|
13
|
+
* read it — a process that holds nothing but the transaction hash gets the full picture. Arrays
|
|
14
|
+
* are in batch order, which is the order the operations were applied in.
|
|
15
|
+
*/
|
|
16
|
+
export type MutationResult = {
|
|
17
|
+
/** `success` means the whole batch applied; `reverted` means none of it did (arrays are empty). */
|
|
18
|
+
status: "success" | "reverted"
|
|
19
|
+
/** The transaction hash. */
|
|
20
|
+
txHash: Hash
|
|
21
|
+
/** The block the transaction landed in. */
|
|
22
|
+
blockNumber: bigint
|
|
23
|
+
/** The keys the engine minted for the created entities, in batch order. */
|
|
24
|
+
createdEntities: Hex[]
|
|
25
|
+
/** The block each created entity expires at, as the engine recorded it, in the same order. */
|
|
26
|
+
createdExpiries: bigint[]
|
|
27
|
+
/** The keys of the patched entities. */
|
|
28
|
+
patchedEntities: Hex[]
|
|
29
|
+
/** The keys of the deleted entities. */
|
|
30
|
+
deletedEntities: Hex[]
|
|
31
|
+
/** The keys of the extended entities. */
|
|
32
|
+
extendedEntities: Hex[]
|
|
33
|
+
/** The block each extended entity now expires at, in the same order. */
|
|
34
|
+
extendedExpiries: bigint[]
|
|
35
|
+
/** The keys of the entities handed to a new owner. */
|
|
36
|
+
ownershipChanges: Hex[]
|
|
37
|
+
/** The raw receipt, so nothing the chain said is lost. */
|
|
38
|
+
receipt: TransactionReceipt
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Return type for {@link getMutationResult}: the decoded result, or still pending. */
|
|
42
|
+
export type GetMutationResultReturnType = MutationResult | { status: "pending" }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decodes a receipt you already hold into a {@link MutationResult}. **Zero RPC calls.**
|
|
46
|
+
*
|
|
47
|
+
* The pure tail of the advanced path: if a receipt reached you some other way — your own
|
|
48
|
+
* `waitForTransactionReceipt`, a webhook, a block watcher — this turns it into entity keys and
|
|
49
|
+
* expiries without touching the network. Lenient by design: it reports what the events say,
|
|
50
|
+
* without counting them against a request it never saw.
|
|
51
|
+
*/
|
|
52
|
+
export function decodeMutationResult(receipt: TransactionReceipt): MutationResult {
|
|
53
|
+
const events = collectMutationEvents(receipt.logs)
|
|
54
|
+
return {
|
|
55
|
+
status: receipt.status,
|
|
56
|
+
txHash: receipt.transactionHash,
|
|
57
|
+
blockNumber: receipt.blockNumber,
|
|
58
|
+
createdEntities: events.created.map((e) => e.entityKey),
|
|
59
|
+
createdExpiries: events.created.map((e) => e.expiresAt),
|
|
60
|
+
patchedEntities: events.patched.map((e) => e.entityKey),
|
|
61
|
+
deletedEntities: events.deleted.map((e) => e.entityKey),
|
|
62
|
+
extendedEntities: events.extended.map((e) => e.entityKey),
|
|
63
|
+
extendedExpiries: events.extended.map((e) => e.expiresAt),
|
|
64
|
+
ownershipChanges: events.ownershipTransferred.map((e) => e.entityKey),
|
|
65
|
+
receipt,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetches a transaction's receipt — once — and decodes what the mutation did.
|
|
71
|
+
*
|
|
72
|
+
* Exactly one `eth_getTransactionReceipt`, like `pingTransaction`, but the answer carries the
|
|
73
|
+
* decoded entity events as well: created keys, recorded expiries, and the keys of everything
|
|
74
|
+
* patched, deleted, extended or handed over. A transaction that is not mined yet comes back as
|
|
75
|
+
* `{ status: "pending" }` — check `status` before reading the rest.
|
|
76
|
+
*
|
|
77
|
+
* No revert diagnosis is run: a `reverted` result reports the fact and the raw receipt, and
|
|
78
|
+
* spending a `simulateContract` on the why is your call, not the SDK's.
|
|
79
|
+
*/
|
|
80
|
+
export async function getMutationResult(
|
|
81
|
+
client: ArkivClient,
|
|
82
|
+
txHash: Hash,
|
|
83
|
+
): Promise<GetMutationResultReturnType> {
|
|
84
|
+
const receipt = await fetchReceipt(client, txHash)
|
|
85
|
+
if (receipt === undefined) {
|
|
86
|
+
logger("Tx %s still pending", txHash)
|
|
87
|
+
return { status: "pending" }
|
|
88
|
+
}
|
|
89
|
+
return decodeMutationResult(receipt)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One `eth_getTransactionReceipt`, with "not there yet" as a value instead of an exception.
|
|
94
|
+
*
|
|
95
|
+
* Internal seam shared with `pingTransaction`; not part of the package's public surface.
|
|
96
|
+
*/
|
|
97
|
+
export async function fetchReceipt(
|
|
98
|
+
client: ArkivClient,
|
|
99
|
+
txHash: Hash,
|
|
100
|
+
): Promise<TransactionReceipt | undefined> {
|
|
101
|
+
// The one action this needs is on every SDK-built client — both factories extend viem's
|
|
102
|
+
// `publicActions` — and `PublicArkivClient` is the type that says so, the same way
|
|
103
|
+
// `sendArkivTransaction` narrows to `WalletArkivClient`.
|
|
104
|
+
const reader = client as PublicArkivClient
|
|
105
|
+
try {
|
|
106
|
+
return await reader.getTransactionReceipt({ hash: txHash })
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// viem models an unmined transaction as a throw; the advanced path models it as a state.
|
|
109
|
+
if (error instanceof TransactionReceiptNotFoundError) return undefined
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advanced path: send, ping and read results as separate, single-RPC-call steps.
|
|
3
|
+
*
|
|
4
|
+
* The everyday actions (`createEntity`, `mutateEntities`, …) bundle send + wait + decode into one
|
|
5
|
+
* call, which is convenient and spends several RPC requests. These actions unbundle that so a
|
|
6
|
+
* caller can hit the theoretical minimum:
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link buildMutation} — encode locally (0 RPC calls with `currentBlock` supplied)
|
|
9
|
+
* 2. {@link sendMutation} — submit and return the hash (1 call with full `txParams`)
|
|
10
|
+
* 3. {@link pingTransaction} — one `eth_getTransactionReceipt`, on your schedule
|
|
11
|
+
* 4. {@link getMutationResult} / {@link decodeMutationResult} — the same single call (or none)
|
|
12
|
+
* turned into entity keys and expiries
|
|
13
|
+
*/
|
|
14
|
+
export type { BuildMutationOptions, BuildMutationReturnType } from "./buildMutation"
|
|
15
|
+
export { buildMutation } from "./buildMutation"
|
|
16
|
+
export type { GetMutationResultReturnType, MutationResult } from "./getMutationResult"
|
|
17
|
+
export { decodeMutationResult, getMutationResult } from "./getMutationResult"
|
|
18
|
+
export type { PingTransactionReturnType } from "./pingTransaction"
|
|
19
|
+
export { pingTransaction } from "./pingTransaction"
|
|
20
|
+
export type { SendMutationOptions, SendMutationReturnType } from "./sendMutation"
|
|
21
|
+
export { sendMutation } from "./sendMutation"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Hash } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import { getLogger } from "../../utils/logger"
|
|
4
|
+
import { fetchReceipt } from "./getMutationResult"
|
|
5
|
+
|
|
6
|
+
const logger = getLogger("actions:advanced:ping-transaction")
|
|
7
|
+
|
|
8
|
+
/** Return type for {@link pingTransaction}. */
|
|
9
|
+
export type PingTransactionReturnType =
|
|
10
|
+
| {
|
|
11
|
+
/** Not mined yet (or unknown to this node). Ping again later. */
|
|
12
|
+
status: "pending"
|
|
13
|
+
blockNumber?: undefined
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
/** Mined. `success` means the whole batch applied; `reverted` means none of it did. */
|
|
17
|
+
status: "success" | "reverted"
|
|
18
|
+
/** The block the transaction landed in. */
|
|
19
|
+
blockNumber: bigint
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Asks the chain — once — whether a transaction has landed.
|
|
24
|
+
*
|
|
25
|
+
* Exactly one `eth_getTransactionReceipt`, never a poll: a transaction that is not mined yet comes
|
|
26
|
+
* back as `{ status: "pending" }` rather than being waited on. This is the "ping" of the advanced
|
|
27
|
+
* path — you own the schedule, the SDK owns nothing.
|
|
28
|
+
*
|
|
29
|
+
* When it reports `success` and you need the created keys or recorded expiries, spend the next
|
|
30
|
+
* call on `getMutationResult` — or none at all, if you predicted the keys up front with
|
|
31
|
+
* `predictEntityKeys`.
|
|
32
|
+
*/
|
|
33
|
+
export async function pingTransaction(
|
|
34
|
+
client: ArkivClient,
|
|
35
|
+
txHash: Hash,
|
|
36
|
+
): Promise<PingTransactionReturnType> {
|
|
37
|
+
const receipt = await fetchReceipt(client, txHash)
|
|
38
|
+
if (receipt === undefined) {
|
|
39
|
+
logger("Tx %s still pending", txHash)
|
|
40
|
+
return { status: "pending" }
|
|
41
|
+
}
|
|
42
|
+
return { status: receipt.status, blockNumber: receipt.blockNumber }
|
|
43
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Hash } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
|
+
import type { TxParams } from "../../types"
|
|
4
|
+
import { type EntityMutationOps, submitMutation } from "../../utils/arkivTransactions"
|
|
5
|
+
import { getLogger } from "../../utils/logger"
|
|
6
|
+
import {
|
|
7
|
+
type BuildMutationOptions,
|
|
8
|
+
type BuildMutationReturnType,
|
|
9
|
+
buildMutation,
|
|
10
|
+
} from "./buildMutation"
|
|
11
|
+
|
|
12
|
+
const logger = getLogger("actions:advanced:send-mutation")
|
|
13
|
+
|
|
14
|
+
/** Options for {@link sendMutation}. */
|
|
15
|
+
export type SendMutationOptions = BuildMutationOptions & {
|
|
16
|
+
/**
|
|
17
|
+
* Transaction parameters. Supply `nonce`, `gas` and the fee fields and viem has nothing left to
|
|
18
|
+
* look up — the send collapses to a single `eth_sendRawTransaction`.
|
|
19
|
+
*/
|
|
20
|
+
txParams?: TxParams
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Return type for {@link sendMutation}. */
|
|
24
|
+
export type SendMutationReturnType = {
|
|
25
|
+
/** The transaction hash. Poll it with `pingTransaction` / fetch it with `getMutationResult`. */
|
|
26
|
+
txHash: Hash
|
|
27
|
+
/** The per-kind operation counts of the batch that was sent, for cross-checking the result. */
|
|
28
|
+
expected: BuildMutationReturnType["expected"]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Signs and sends a mutation batch, returning as soon as the transaction is submitted.
|
|
33
|
+
*
|
|
34
|
+
* The fire-and-forget half of the advanced path. Unlike `mutateEntities`, this does **not** wait
|
|
35
|
+
* for a receipt, does not poll, and runs no revert diagnosis — it spends only what submitting
|
|
36
|
+
* costs. With `options.currentBlock` and full `txParams` (nonce, gas, fees) that is exactly one
|
|
37
|
+
* RPC call; each omitted piece adds only the lookup viem needs to fill it.
|
|
38
|
+
*
|
|
39
|
+
* Follow up separately, on your own schedule:
|
|
40
|
+
* - `pingTransaction(txHash)` — one `eth_getTransactionReceipt`, tells you pending/success/reverted.
|
|
41
|
+
* - `getMutationResult(txHash)` — the same single call, plus the decoded entity keys and expiries.
|
|
42
|
+
*
|
|
43
|
+
* @throws {EntityMutationError} If the node rejects the submission (the batch never made it into
|
|
44
|
+
* the mempool) — in engine terms when the rejection decodes, with the node's message otherwise.
|
|
45
|
+
* Anything after that — inclusion, success — is yours to check.
|
|
46
|
+
*/
|
|
47
|
+
export async function sendMutation(
|
|
48
|
+
client: ArkivClient,
|
|
49
|
+
data: EntityMutationOps,
|
|
50
|
+
options: SendMutationOptions = {},
|
|
51
|
+
): Promise<SendMutationReturnType> {
|
|
52
|
+
const { operations, expected } = await buildMutation(client, data, options)
|
|
53
|
+
|
|
54
|
+
logger("Sending execute with %d operations (no wait)", operations.length)
|
|
55
|
+
|
|
56
|
+
const txHash = await submitMutation(client, operations, options.txParams)
|
|
57
|
+
|
|
58
|
+
return { txHash, expected }
|
|
59
|
+
}
|
|
@@ -1,50 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Hex } from "viem"
|
|
2
|
+
import { key } from "../../attr"
|
|
2
3
|
import type { ArkivClient } from "../../clients/baseClient"
|
|
3
4
|
import { NoEntityFoundError } from "../../errors"
|
|
4
|
-
import {
|
|
5
|
+
import { runQuery } from "../../query/engine"
|
|
6
|
+
import { eq } from "../../query/expression"
|
|
7
|
+
import { type FullEntity, toRpcSelect } from "../../query/selection"
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const result = await client.request({
|
|
24
|
-
method: "arkiv_query",
|
|
25
|
-
params: [
|
|
26
|
-
`$key = ${hexKey}`,
|
|
27
|
-
{
|
|
28
|
-
includeData: {
|
|
29
|
-
key: true,
|
|
30
|
-
attributes: true,
|
|
31
|
-
payload: true,
|
|
32
|
-
contentType: true,
|
|
33
|
-
expiration: true,
|
|
34
|
-
owner: true,
|
|
35
|
-
creator: true,
|
|
36
|
-
createdAtBlock: true,
|
|
37
|
-
lastModifiedAtBlock: true,
|
|
38
|
-
transactionIndexInBlock: true,
|
|
39
|
-
operationIndexInTransaction: true,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
],
|
|
9
|
+
/**
|
|
10
|
+
* Reads one entity by key, with every field selected.
|
|
11
|
+
*
|
|
12
|
+
* Returns a {@link FullEntity} rather than an {@link Entity}: the selection is fixed at `"*"`, so
|
|
13
|
+
* every field is populated and none of them needs a `?` at the call site. The bare `Entity` type
|
|
14
|
+
* has every field optional because a `select()` result carries only what it asked for.
|
|
15
|
+
*
|
|
16
|
+
* @throws {InvalidValueError} If the key is not 32 bytes.
|
|
17
|
+
* @throws {NoEntityFoundError} If no live entity has that key — it never existed, or it was
|
|
18
|
+
* deleted or has expired.
|
|
19
|
+
*/
|
|
20
|
+
export async function getEntity(client: ArkivClient, entityKey: Hex): Promise<FullEntity> {
|
|
21
|
+
const { entities } = await runQuery(client, {
|
|
22
|
+
query: eq("$key", key(entityKey)).toString(),
|
|
23
|
+
select: toRpcSelect("*"),
|
|
24
|
+
limit: 1,
|
|
43
25
|
})
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
const entity = entities[0]
|
|
28
|
+
if (entity === undefined) {
|
|
29
|
+
throw new NoEntityFoundError(entityKey)
|
|
47
30
|
}
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
// Every field was selected, so the runtime Entity carries them all; the cast is the same
|
|
32
|
+
// narrowing `client.select("*")` gets from its own inference.
|
|
33
|
+
return entity as unknown as FullEntity
|
|
50
34
|
}
|