@unconfirmed/sui-effect 0.1.0 → 0.1.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/AGENTS.md +47 -14
- package/CHANGELOG.md +86 -0
- package/LLMS.md +2889 -1726
- package/README.md +517 -11
- package/dist/domain/bcs.d.ts +44 -0
- package/dist/domain/bcs.d.ts.map +1 -1
- package/dist/domain/bcs.js +64 -0
- package/dist/domain/bcs.js.map +1 -1
- package/dist/domain/errors.d.ts +164 -26
- package/dist/domain/errors.d.ts.map +1 -1
- package/dist/domain/errors.js +193 -15
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/executed.d.ts +123 -18
- package/dist/domain/executed.d.ts.map +1 -1
- package/dist/domain/executed.js +196 -5
- package/dist/domain/executed.js.map +1 -1
- package/dist/domain/journal-entry.d.ts +6 -2
- package/dist/domain/journal-entry.d.ts.map +1 -1
- package/dist/domain/schemas.d.ts +234 -67
- package/dist/domain/schemas.d.ts.map +1 -1
- package/dist/domain/schemas.js +82 -4
- package/dist/domain/schemas.js.map +1 -1
- package/dist/domain/sui-schema.d.ts +3 -2
- package/dist/domain/sui-schema.d.ts.map +1 -1
- package/dist/domain/sui-schema.js +3 -2
- package/dist/domain/sui-schema.js.map +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +1 -1
- package/dist/extension.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/script.d.ts +1 -1
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +1 -1
- package/dist/script.js.map +1 -1
- package/dist/services/Script.d.ts +42 -0
- package/dist/services/Script.d.ts.map +1 -1
- package/dist/services/Script.js +69 -2
- package/dist/services/Script.js.map +1 -1
- package/dist/services/Signer.d.ts +32 -7
- package/dist/services/Signer.d.ts.map +1 -1
- package/dist/services/Signer.js +69 -10
- package/dist/services/Signer.js.map +1 -1
- package/dist/services/Sui.d.ts +42 -1
- package/dist/services/Sui.d.ts.map +1 -1
- package/dist/services/Sui.js +18 -4
- package/dist/services/Sui.js.map +1 -1
- package/dist/services/SuiCore.d.ts +12 -0
- package/dist/services/SuiCore.d.ts.map +1 -1
- package/dist/services/SuiCore.js +124 -0
- package/dist/services/SuiCore.js.map +1 -1
- package/dist/services/SuiCoreFake.d.ts +59 -4
- package/dist/services/SuiCoreFake.d.ts.map +1 -1
- package/dist/services/SuiCoreFake.js +199 -23
- package/dist/services/SuiCoreFake.js.map +1 -1
- package/dist/services/SuiExtension.d.ts +127 -14
- package/dist/services/SuiExtension.d.ts.map +1 -1
- package/dist/services/SuiExtension.js +125 -28
- package/dist/services/SuiExtension.js.map +1 -1
- package/dist/services/SuiGraphQL.d.ts +13 -0
- package/dist/services/SuiGraphQL.d.ts.map +1 -1
- package/dist/services/SuiGraphQL.js +13 -0
- package/dist/services/SuiGraphQL.js.map +1 -1
- package/dist/services/Tx.d.ts +630 -12
- package/dist/services/Tx.d.ts.map +1 -1
- package/dist/services/Tx.js +187 -8
- package/dist/services/Tx.js.map +1 -1
- package/dist/testing.d.ts +22 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +39 -3
- package/dist/testing.js.map +1 -1
- package/dist/tx.d.ts +1 -1
- package/dist/tx.d.ts.map +1 -1
- package/dist/tx.js +1 -1
- package/dist/tx.js.map +1 -1
- package/docs/extensions.md +871 -33
- package/examples/extension-template/src/Escrow.ts +1 -1
- package/examples/extension-template/test/escrow.test.ts +91 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -67,6 +67,341 @@ That is `examples/script-claim.ts`, which a test runs against the in-memory
|
|
|
67
67
|
fake. Every failure it can produce is in the generator's inferred error type and
|
|
68
68
|
has its own exit code, with no handling lines anywhere.
|
|
69
69
|
|
|
70
|
+
## Side by side
|
|
71
|
+
|
|
72
|
+
The same two tasks, once against the canonical `@mysten/sui` 2.30 SDK (a
|
|
73
|
+
gRPC client, the Core API, `Transaction`) and once against sui-effect. Both
|
|
74
|
+
sides are exercised in `test/examples.test.ts`, and this section is asserted
|
|
75
|
+
to be the literal text of the four files it quotes.
|
|
76
|
+
|
|
77
|
+
### Read
|
|
78
|
+
|
|
79
|
+
`examples/compare-read.sdk.ts`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
/**
|
|
83
|
+
* Reads one Escrow object and an optional dynamic field on it, written
|
|
84
|
+
* against the canonical 2.30 SDK: a gRPC client, the Core API, manual BCS.
|
|
85
|
+
*
|
|
86
|
+
* Compare with `examples/compare-read.effect.ts`. Both print identical
|
|
87
|
+
* output for the same id.
|
|
88
|
+
*
|
|
89
|
+
* Run with `SUI_NETWORK=testnet ESCROW_ID=0x… bun examples/compare-read.sdk.ts`.
|
|
90
|
+
*/
|
|
91
|
+
import { bcs } from "@mysten/sui/bcs"
|
|
92
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client"
|
|
93
|
+
import { ObjectError } from "@mysten/sui/client"
|
|
94
|
+
import { SuiGrpcClient } from "@mysten/sui/grpc"
|
|
95
|
+
|
|
96
|
+
const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
|
|
97
|
+
const ESCROW_TYPE = `${PKG}::escrow::Escrow`
|
|
98
|
+
const EscrowBcs = bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() })
|
|
99
|
+
// A note some escrows carry, keyed by the literal index 0. The key is a
|
|
100
|
+
// primitive `u64`, not a struct tag, so nothing here parses it as one.
|
|
101
|
+
const NOTE_NAME = { type: "u64", bcs: bcs.u64().serialize(0).toBytes() }
|
|
102
|
+
|
|
103
|
+
/** Reads the escrow at `id` plus its optional note. */
|
|
104
|
+
export const readEscrow = async (
|
|
105
|
+
client: ClientWithCoreApi,
|
|
106
|
+
id: string
|
|
107
|
+
): Promise<{ id: string; amount: string; note: string | undefined }> => {
|
|
108
|
+
let object
|
|
109
|
+
try {
|
|
110
|
+
;({ object } = await client.core.getObject({ objectId: id, include: { content: true } }))
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// `getObject` throws rather than returning a value that says which of
|
|
113
|
+
// "missing", "deleted" or "unreachable" happened; that three-way split
|
|
114
|
+
// is `ObjectError.reason`, read by hand.
|
|
115
|
+
if (error instanceof ObjectError) {
|
|
116
|
+
if (error.reason === "notFound") throw new Error(`escrow ${id} does not exist`)
|
|
117
|
+
if (error.reason === "deleted") throw new Error(`escrow ${id} was deleted`)
|
|
118
|
+
throw new Error(`escrow ${id}: node could not say what happened to it`, { cause: error })
|
|
119
|
+
}
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
// No type parameters on this tag, so a plain string comparison is honest;
|
|
123
|
+
// a generic type would need to be parsed and compared piece by piece,
|
|
124
|
+
// which is what sui-effect's bridge does for every caller.
|
|
125
|
+
if (object.type !== ESCROW_TYPE) {
|
|
126
|
+
throw new Error(`${id} is a ${object.type}, not ${ESCROW_TYPE}`)
|
|
127
|
+
}
|
|
128
|
+
const { id: escrowId, amount } = EscrowBcs.parse(object.content)
|
|
129
|
+
|
|
130
|
+
let note: string | undefined
|
|
131
|
+
try {
|
|
132
|
+
const { dynamicField } = await client.core.getDynamicField({ parentId: id, name: NOTE_NAME })
|
|
133
|
+
note = bcs.u64().parse(dynamicField.value.bcs)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Absence is normal here; only a reason other than "notFound"/"deleted"
|
|
136
|
+
// is a real problem.
|
|
137
|
+
if (!(error instanceof ObjectError) || error.reason === "unknown") throw error
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { id: escrowId, amount, note }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (import.meta.main) {
|
|
144
|
+
const id = process.env.ESCROW_ID
|
|
145
|
+
if (!id) throw new Error("ESCROW_ID is required")
|
|
146
|
+
const client = new SuiGrpcClient({
|
|
147
|
+
network: (process.env.SUI_NETWORK ?? "testnet") as "testnet",
|
|
148
|
+
baseUrl: process.env.SUI_RPC_URL ?? "https://fullnode.testnet.sui.io:443"
|
|
149
|
+
})
|
|
150
|
+
const escrow = await readEscrow(client, id)
|
|
151
|
+
console.log(`${escrow.id} holds ${escrow.amount}, note: ${escrow.note ?? "none"}`)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`examples/compare-read.effect.ts`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
/**
|
|
159
|
+
* Reads one Escrow object and its optional note through sui-effect.
|
|
160
|
+
*
|
|
161
|
+
* Compare with `examples/compare-read.sdk.ts`, which reads the same two
|
|
162
|
+
* things with the canonical SDK client. Both print identical output.
|
|
163
|
+
*
|
|
164
|
+
* Run with `SUI_NETWORK=testnet ESCROW_ID=0x… bun examples/compare-read.effect.ts`.
|
|
165
|
+
* Every failure this program can produce is in the generator's inferred error
|
|
166
|
+
* type: `ObjectNotFound`, `ObjectDeleted`, `ObjectUnavailable` and
|
|
167
|
+
* `DecodeError` (the last one is what a wrong Move type becomes, because
|
|
168
|
+
* `getObject`'s tag check runs before a byte is parsed), plus `TransportError`
|
|
169
|
+
* from both reads and, from the layer, `ConfigError` and `NetworkMismatch`.
|
|
170
|
+
*/
|
|
171
|
+
import { bcs } from "@mysten/sui/bcs"
|
|
172
|
+
import { Config, Console, Effect, Option } from "effect"
|
|
173
|
+
import { ObjectId, Sui, SuiSchema } from "../src/index.ts"
|
|
174
|
+
|
|
175
|
+
const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
|
|
176
|
+
|
|
177
|
+
const Escrow = SuiSchema.bcs(
|
|
178
|
+
bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() }),
|
|
179
|
+
`${PKG}::escrow::Escrow`
|
|
180
|
+
)
|
|
181
|
+
// The note's value has no struct tag of its own, so its codec carries none;
|
|
182
|
+
// the key is a primitive `u64`, which is why nothing here reaches for
|
|
183
|
+
// `normalizeStructTag` on it.
|
|
184
|
+
const Note = SuiSchema.bcs(bcs.u64())
|
|
185
|
+
const NOTE_NAME = { type: "u64", bcs: bcs.u64().serialize(0).toBytes() }
|
|
186
|
+
|
|
187
|
+
/** The program itself, exported so a test can run it against the fake. */
|
|
188
|
+
export const program = Effect.gen(function*() {
|
|
189
|
+
const sui = yield* Sui
|
|
190
|
+
const id = yield* Config.schema(ObjectId, "ESCROW_ID")
|
|
191
|
+
// No `ObjectError.reason` switch and no manual type comparison: a missing
|
|
192
|
+
// or deleted escrow and a wrong Move type are already three different tags
|
|
193
|
+
// in the type this line returns.
|
|
194
|
+
const escrow = yield* sui.getObject(id, { schema: Escrow })
|
|
195
|
+
const field = yield* sui.getDynamicFieldOption(id, NOTE_NAME)
|
|
196
|
+
const note = yield* Option.match(field, {
|
|
197
|
+
onNone: () => Effect.void,
|
|
198
|
+
onSome: (entry) => SuiSchema.decode(Note, entry.value.bcs)
|
|
199
|
+
})
|
|
200
|
+
yield* Console.log(`${escrow.id} holds ${escrow.content.amount}, note: ${note ?? "none"}`)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (import.meta.main) {
|
|
204
|
+
// Imported here rather than at the top so that a test can import `program`
|
|
205
|
+
// without pulling a platform package into the test process.
|
|
206
|
+
const { BunRuntime } = await import("@effect/platform-bun")
|
|
207
|
+
BunRuntime.runMain(program.pipe(Effect.provide(Sui.layerConfig)))
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Write
|
|
212
|
+
|
|
213
|
+
`examples/compare-write.sdk.ts`:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
/**
|
|
217
|
+
* Claims an escrow through a PTB, written against the canonical 2.30 SDK.
|
|
218
|
+
*
|
|
219
|
+
* Compare with `examples/compare-write.effect.ts`, which does the same thing
|
|
220
|
+
* through `Tx.run` and notes what that adds over this file. Both sign with
|
|
221
|
+
* `SUI_PRIVATE_KEY` and print the created receipt's object id.
|
|
222
|
+
*
|
|
223
|
+
* Run with:
|
|
224
|
+
* SUI_NETWORK=testnet SUI_PRIVATE_KEY=suiprivkey1… ESCROW_ID=0x… bun examples/compare-write.sdk.ts
|
|
225
|
+
*/
|
|
226
|
+
import { bcs } from "@mysten/sui/bcs"
|
|
227
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client"
|
|
228
|
+
import type { Keypair } from "@mysten/sui/cryptography"
|
|
229
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography"
|
|
230
|
+
import { SuiGrpcClient } from "@mysten/sui/grpc"
|
|
231
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
|
|
232
|
+
import { Secp256k1Keypair } from "@mysten/sui/keypairs/secp256k1"
|
|
233
|
+
import { Secp256r1Keypair } from "@mysten/sui/keypairs/secp256r1"
|
|
234
|
+
import { Transaction } from "@mysten/sui/transactions"
|
|
235
|
+
|
|
236
|
+
const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
|
|
237
|
+
const RECEIPT_TYPE = `${PKG}::escrow::Receipt`
|
|
238
|
+
const EscrowBcs = bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() })
|
|
239
|
+
|
|
240
|
+
// The scheme flag on a Bech32 key names one of three keypair classes;
|
|
241
|
+
// nothing else decodes it, and a fourth scheme has no keypair class at all.
|
|
242
|
+
const KEYPAIR_FOR: Record<string, undefined | ((secretKey: Uint8Array) => Keypair)> = {
|
|
243
|
+
ED25519: Ed25519Keypair.fromSecretKey,
|
|
244
|
+
Secp256k1: Secp256k1Keypair.fromSecretKey,
|
|
245
|
+
Secp256r1: Secp256r1Keypair.fromSecretKey
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reads the escrow, claims it, and returns the created receipt's object id.
|
|
250
|
+
*
|
|
251
|
+
* Builds, signs and executes as three explicit steps rather than the
|
|
252
|
+
* one-call `signAndExecuteTransaction`, so this can run against an in-memory
|
|
253
|
+
* client with no live network or signer behind it.
|
|
254
|
+
*/
|
|
255
|
+
export const claimEscrow = async (
|
|
256
|
+
client: ClientWithCoreApi,
|
|
257
|
+
escrowId: string,
|
|
258
|
+
keypair: Keypair
|
|
259
|
+
): Promise<string> => {
|
|
260
|
+
const { object } = await client.core.getObject({ objectId: escrowId, include: { content: true } })
|
|
261
|
+
const { amount } = EscrowBcs.parse(object.content)
|
|
262
|
+
|
|
263
|
+
const tx = new Transaction()
|
|
264
|
+
tx.setSenderIfNotSet(keypair.toSuiAddress())
|
|
265
|
+
tx.moveCall({ target: `${PKG}::escrow::claim`, arguments: [tx.object(escrowId), tx.pure.u64(amount)] })
|
|
266
|
+
const bytes = await tx.build({ client })
|
|
267
|
+
const { signature } = await keypair.signTransaction(bytes)
|
|
268
|
+
|
|
269
|
+
const result = await client.core.executeTransaction({
|
|
270
|
+
transaction: bytes,
|
|
271
|
+
signatures: [signature],
|
|
272
|
+
include: { effects: true, objectTypes: true }
|
|
273
|
+
})
|
|
274
|
+
const digest = (result.Transaction ?? result.FailedTransaction).digest
|
|
275
|
+
await client.core.waitForTransaction({ digest })
|
|
276
|
+
|
|
277
|
+
if (result.$kind === "FailedTransaction") {
|
|
278
|
+
const { error } = result.FailedTransaction.status
|
|
279
|
+
if (error?.$kind === "MoveAbort") {
|
|
280
|
+
const { abortCode, cleverError } = error.MoveAbort
|
|
281
|
+
throw new Error(`claim aborted: code ${abortCode}${cleverError?.constantName ? ` (${cleverError.constantName})` : ""}`)
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`claim failed: ${error?.message}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Effects list every changed object by id; only the `objectTypes` join
|
|
287
|
+
// says which one is the receipt. sui-effect's `expectCreated` is this join,
|
|
288
|
+
// plus a check that exactly one match exists.
|
|
289
|
+
const { changedObjects } = result.Transaction.effects
|
|
290
|
+
const created = changedObjects.find(
|
|
291
|
+
(change) => change.idOperation === "Created" && result.Transaction.objectTypes[change.objectId] === RECEIPT_TYPE
|
|
292
|
+
)
|
|
293
|
+
if (created === undefined) throw new Error(`claim applied (${digest}) but created no ${RECEIPT_TYPE}`)
|
|
294
|
+
return created.objectId
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (import.meta.main) {
|
|
298
|
+
const escrowId = process.env.ESCROW_ID
|
|
299
|
+
const key = process.env.SUI_PRIVATE_KEY
|
|
300
|
+
if (!escrowId || !key) throw new Error("ESCROW_ID and SUI_PRIVATE_KEY are required")
|
|
301
|
+
const parsed = decodeSuiPrivateKey(key)
|
|
302
|
+
const fromSecretKey = KEYPAIR_FOR[parsed.scheme]
|
|
303
|
+
if (!fromSecretKey) throw new Error(`${parsed.scheme} has no keypair class here (use a remote signer)`)
|
|
304
|
+
const client = new SuiGrpcClient({
|
|
305
|
+
network: (process.env.SUI_NETWORK ?? "testnet") as "testnet",
|
|
306
|
+
baseUrl: process.env.SUI_RPC_URL ?? "https://fullnode.testnet.sui.io:443"
|
|
307
|
+
})
|
|
308
|
+
console.log(await claimEscrow(client, escrowId, fromSecretKey(parsed.secretKey)))
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`examples/compare-write.effect.ts`:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
/**
|
|
316
|
+
* Claims an escrow through `Tx.run`, sui-effect's write path.
|
|
317
|
+
*
|
|
318
|
+
* Compare with `examples/compare-write.sdk.ts`, which does the same thing
|
|
319
|
+
* against the canonical SDK client. Both sign with `SUI_PRIVATE_KEY` and
|
|
320
|
+
* print the created receipt's object id.
|
|
321
|
+
*
|
|
322
|
+
* What `Tx.run` adds over the SDK file: a default expiration bounded to the
|
|
323
|
+
* current epoch, a journal entry written before the first execute so a crash
|
|
324
|
+
* mid-flight leaves a record, resending the identical signed bytes rather
|
|
325
|
+
* than rebuilding on a transient failure, `reconcile` when a submission's
|
|
326
|
+
* outcome is unknown, and a sender lock so two concurrent claims from one
|
|
327
|
+
* address cannot pick the same gas coin. What it costs is the Effect
|
|
328
|
+
* vocabulary below.
|
|
329
|
+
*
|
|
330
|
+
* Run with:
|
|
331
|
+
* SUI_NETWORK=testnet SUI_PRIVATE_KEY=suiprivkey1… ESCROW_ID=0x… bun examples/compare-write.effect.ts
|
|
332
|
+
*/
|
|
333
|
+
import { bcs } from "@mysten/sui/bcs"
|
|
334
|
+
import { Config, Console, Effect } from "effect"
|
|
335
|
+
import { ObjectId, SuiSchema } from "../src/index.ts"
|
|
336
|
+
import { Script } from "../src/script.ts"
|
|
337
|
+
import { Tx } from "../src/tx.ts"
|
|
338
|
+
|
|
339
|
+
const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
|
|
340
|
+
|
|
341
|
+
const Escrow = SuiSchema.bcs(
|
|
342
|
+
bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() }),
|
|
343
|
+
`${PKG}::escrow::Escrow`
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The program itself, exported so a test can run it against the fake. Every
|
|
348
|
+
* failure it can produce is in the generator's inferred error type, with no
|
|
349
|
+
* handling lines anywhere in this file: `ObjectNotFound`, `ObjectDeleted`,
|
|
350
|
+
* `ObjectUnavailable`, `DecodeError`, `TransportError`, `BuildError`,
|
|
351
|
+
* `SimulationFailed`, `SigningError`, `PolicyDenied`, `NotApplied`,
|
|
352
|
+
* `JournalError`, `UnexpectedEffects`, `ExecutionFailed`, `SubmissionUnknown`,
|
|
353
|
+
* plus `ConfigError` and `NetworkMismatch` from the layer.
|
|
354
|
+
*/
|
|
355
|
+
export const program = Effect.gen(function*() {
|
|
356
|
+
const { signer, sui } = yield* Script
|
|
357
|
+
const id = yield* Config.schema(ObjectId, "ESCROW_ID")
|
|
358
|
+
const escrow = yield* sui.getObject(id, { schema: Escrow })
|
|
359
|
+
const executed = yield* Tx.run((tx) => {
|
|
360
|
+
tx.moveCall({
|
|
361
|
+
target: `${PKG}::escrow::claim`,
|
|
362
|
+
arguments: [tx.object(id), tx.pure.u64(escrow.content.amount)]
|
|
363
|
+
})
|
|
364
|
+
}, { signer })
|
|
365
|
+
const receipt = yield* executed.expectCreated(`${PKG}::escrow::Receipt`)
|
|
366
|
+
yield* Console.log(receipt.id)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
if (import.meta.main) {
|
|
370
|
+
await Script.run(program)
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### What differs
|
|
375
|
+
|
|
376
|
+
- **Absence.** The SDK throws `ObjectError` and the reason (`notFound`,
|
|
377
|
+
`deleted`, `unknown`) is read off the caught value by hand; sui-effect's
|
|
378
|
+
`getObject` fails with `ObjectNotFound` / `ObjectDeleted` / `ObjectUnavailable`,
|
|
379
|
+
three separate tags a caller can `catchTag` without an `if` chain.
|
|
380
|
+
- **Wrong type.** The SDK compares `object.type` itself, and only gets away
|
|
381
|
+
with a plain string equality because this particular type has no type
|
|
382
|
+
parameters; a generic Move type would need the same piecewise comparison
|
|
383
|
+
sui-effect's bridge (`typeMatches`) already runs inside every `getObject`.
|
|
384
|
+
- **BCS.** Both sides hand a `BcsType` to a struct; the SDK calls `.parse`
|
|
385
|
+
itself, sui-effect calls it once, inside `getObject`, after the type check
|
|
386
|
+
above has already passed.
|
|
387
|
+
- **Signing.** Both decode `SUI_PRIVATE_KEY` and dispatch on the scheme flag
|
|
388
|
+
to the matching keypair class; sui-effect's version is `Signer.fromConfig`,
|
|
389
|
+
one call, done once in `src/services/Signer.ts` rather than in every script.
|
|
390
|
+
- **Effects.** The SDK inspects `$kind`, `status.error` and joins
|
|
391
|
+
`changedObjects` against `objectTypes` by hand to find the created object;
|
|
392
|
+
sui-effect's `expectCreated(type)` is that join, plus a check that exactly
|
|
393
|
+
one match exists, as a single call on `Executed`.
|
|
394
|
+
- **What sui-effect adds that neither SDK file has:** a default expiration
|
|
395
|
+
bounded to the current epoch, a journal entry written before the first
|
|
396
|
+
execute, resending identical signed bytes instead of rebuilding on a
|
|
397
|
+
transient failure, `reconcile` when a submission's outcome is unknown, and
|
|
398
|
+
a sender lock so two concurrent writes from one address cannot pick the
|
|
399
|
+
same gas coin.
|
|
400
|
+
- **What it costs.** The sui-effect files import `effect` and run inside
|
|
401
|
+
`Effect.gen`; a reader who does not already know Effect's vocabulary
|
|
402
|
+
(`Effect.fn`, `catchTag`, `Option.match`, `yield*`) has that to learn before
|
|
403
|
+
either file is legible.
|
|
404
|
+
|
|
70
405
|
## The two tiers
|
|
71
406
|
|
|
72
407
|
`SuiCore` is the mechanical tier: a one-to-one Effect wrap of the SDK's
|
|
@@ -81,7 +416,8 @@ need a field or a method the tier above does not expose, and through
|
|
|
81
416
|
code reads through: fixed include sets, BCS content decoded through `Schema`
|
|
82
417
|
with the object's Move type checked first, `Option` where absence is normal,
|
|
83
418
|
batch reads chunked to 50 and checked for missing or duplicated ids — per-item
|
|
84
|
-
`Result` from `getObjects`, first-error-wins from `
|
|
419
|
+
`Result` from `getObjects`, first-error-wins from `getObjectsStrict` (the same
|
|
420
|
+
function as the deprecated `getObjectsOrFail`) — pagination
|
|
85
421
|
as `Stream`, one lock per sender so two transactions from one address cannot
|
|
86
422
|
pick the same gas coin, and the chain's own clock. A Move type with no type
|
|
87
423
|
arguments matches every instantiation of it, wherever a type is compared, so one
|
|
@@ -92,7 +428,8 @@ was built over as `sui.core`, which is why every `Tx.*` function needs only
|
|
|
92
428
|
## The transaction lifecycle
|
|
93
429
|
|
|
94
430
|
`Tx` is the lifecycle as functions — `build`, `sign`, `cosign`, `sponsored`,
|
|
95
|
-
`submit`, `reconcile`, `run`, `reconcileAll` — each
|
|
431
|
+
`submit`, `submitVia`, `reconcile`, `recorded`, `run`, `reconcileAll` — each
|
|
432
|
+
with a closed error union
|
|
96
433
|
and `R = Sui`. `Tx.run` holds the sender lock from build through submit, builds
|
|
97
434
|
(which always simulates before anything is signed — the SDK's resolver does it
|
|
98
435
|
when there is anything to resolve, and `Tx.build` runs one explicitly when
|
|
@@ -103,7 +440,12 @@ timeout, waits for the execution to be visible to reads before it releases the
|
|
|
103
440
|
lock, and if it still does not know what happened, reconciles: `Executed`,
|
|
104
441
|
`ExecutionFailed`, `NotApplied { evidence }`, or `SubmissionUnknown` carrying
|
|
105
442
|
the bytes. A `TransportError` never escapes once bytes may have been sent —
|
|
106
|
-
from `submit`, and from `reconcile` and `reconcileAll` too
|
|
443
|
+
from `submit`, and from `reconcile` and `reconcileAll` too — with one exception:
|
|
444
|
+
a gRPC **`INVALID_ARGUMENT`**, which is the node refusing the request outright
|
|
445
|
+
(malformed bytes, or a sponsored transaction carrying one signature). Nothing
|
|
446
|
+
was executed, so `Tx.submit` reports that error as it is rather than
|
|
447
|
+
reconciling, because a reconcile would go on to ask whether that digest is on
|
|
448
|
+
chain — a question about a transaction that was never sent. `Signer` is a
|
|
107
449
|
value, not a service, so one process can hold two credentials; `SubmitConfig`
|
|
108
450
|
and `Journal` are `Context.Reference`s with working defaults, so none of this
|
|
109
451
|
needs wiring, and `@unconfirmed/sui-effect/journal` swaps the memory journal for a durable
|
|
@@ -134,6 +476,30 @@ do not build a retry loop that expects `NotApplied { inputConsumed }`. Before
|
|
|
134
476
|
any of this, reconcile compares the chain the bytes were built for with
|
|
135
477
|
`sui.chainId` and refuses to reason across chains.
|
|
136
478
|
|
|
479
|
+
**`Tx.run` has a hook between signing and sending.** `onSigned(signed)` runs
|
|
480
|
+
inside the sender lock, after the last signature and before the first
|
|
481
|
+
`executeTransaction`, for the record the journal does not hold — a domain row
|
|
482
|
+
joining the digest to a batch, an outbox, an idempotency key. Failing it fails
|
|
483
|
+
the run with nothing sent; its error is `JournalError`, which is already in
|
|
484
|
+
`Tx.run`'s union and already means "the record could not be written and nothing
|
|
485
|
+
has gone out".
|
|
486
|
+
|
|
487
|
+
**`Tx.submitVia(signed, send)` is the lifecycle when someone else submits.** A
|
|
488
|
+
relay or a sponsorship service that holds the only key allowed to talk to the
|
|
489
|
+
node gets the bytes from you; this keeps everything around that. It journals
|
|
490
|
+
`Signed` before calling `send`, calls it exactly once, turns the reply into an
|
|
491
|
+
`Executed` when it carries one (an SDK `TransactionResult`, a reduced envelope,
|
|
492
|
+
or a bare digest, in which case it asks the chain), reconciles an ambiguous
|
|
493
|
+
`send` failure with the full evidence rules, and journals the terminal answer.
|
|
494
|
+
A `send` error whose instance declares `outcome: "not_applied"` is taken at its
|
|
495
|
+
word and fails straight through.
|
|
496
|
+
|
|
497
|
+
**`Tx.reconcileAll()` returns only what was unresolved**, as a tagged union —
|
|
498
|
+
`{ _tag: "Executed", executed }`, `{ _tag: "ExecutionFailed", error }`,
|
|
499
|
+
`{ _tag: "NotApplied", error }`, `{ _tag: "SubmissionUnknown", error }`. A
|
|
500
|
+
digest that had already settled is not in it; `Tx.recorded(digest)` answers with
|
|
501
|
+
that entry (`Option<JournalEntry>`).
|
|
502
|
+
|
|
137
503
|
**A sponsored `Tx.run` needs both signatures.** When the gas owner is not the
|
|
138
504
|
sender, pass `sponsor`: `Tx.run(recipe, { signer, gasOwner, sponsor })`. Without
|
|
139
505
|
it the run fails with `SigningError` before anything is built, because one
|
|
@@ -172,7 +538,12 @@ pattern. A Promise face's **synchronous** members (recipe builders, a package
|
|
|
172
538
|
id) are real only once the runtime exists, so either `await client.<name>.$ready()`
|
|
173
539
|
once or register with `warm`; calling one before that fails with
|
|
174
540
|
`ExtensionNotReady` rather than returning a Promise the type does not mention
|
|
175
|
-
(`Effect` and `Stream` members work cold
|
|
541
|
+
(`Effect` and `Stream` members work cold: a cold call is a real `Promise` that
|
|
542
|
+
is also an `AsyncIterable`, and its rejection is pre-handled so an un-awaited
|
|
543
|
+
one cannot abort the process). A namespace member may be an `interface` — the
|
|
544
|
+
face maps it by type, not by declaration style — while a class instance with
|
|
545
|
+
`Effect`-returning methods is declared `SuiExtension.Leaf<T>` and built with
|
|
546
|
+
`SuiExtension.leaf(value)`.
|
|
176
547
|
Every registration on one client **shares one `Sui`**, and therefore one
|
|
177
548
|
sender-lock map, so two extensions never select gas for the same address at
|
|
178
549
|
once; `$dispose()` releases that shared base only when the last registration on
|
|
@@ -192,7 +563,7 @@ no error inheritance to match on.
|
|
|
192
563
|
| `ObjectNotFound` / `ObjectDeleted` / `ObjectUnavailable` | `objectId`, `version?` | The three `ObjectError.reason` values |
|
|
193
564
|
| `TransactionNotFound` | `digest` | No transaction with that digest is known |
|
|
194
565
|
| `NetworkMismatch` | `expected`, `actual` | The node is on another chain than the layer was built for |
|
|
195
|
-
| `DecodeError` | `objectId?`, `expectedType?`, `issue` | BCS content or a schema boundary did not decode |
|
|
566
|
+
| `DecodeError` | `objectId?`, `expectedType?`, `kind`, `issue` | BCS content or a schema boundary did not decode. `kind` is `"type"` (the Move type was not the one expected — nothing was parsed, and the one a caller may answer with a 404), `"bytes"` (the BCS parse failed: a layout mismatch, never safe to swallow) or `"shape"` (a domain schema refused a parsed or JSON value). Branch on `kind`, never on `issue`, whose wording changes between releases |
|
|
196
567
|
| `SimulationFailed` | `reason`, `message` | Simulation reported an execution failure. No gas charged |
|
|
197
568
|
| `ExecutionFailed` | `digest`, `reason`, `command?`, `effects` | Applied on chain and failed. Gas charged |
|
|
198
569
|
| `SubmissionUnknown` | `digest`, `signed?`, `cause` | Bytes may have been sent; the outcome is unknown. Carries them, unless it came from reconciling a bare digest |
|
|
@@ -207,14 +578,39 @@ no error inheritance to match on.
|
|
|
207
578
|
|
|
208
579
|
`ExecutionReason` mirrors the SDK's `ExecutionError` variant for variant, with
|
|
209
580
|
`MoveAbort.abortCode` as a `bigint` and clever-error constant names decoded.
|
|
210
|
-
|
|
211
|
-
`
|
|
581
|
+
**Every one of them has a readable `.message`**: for the classes that carry no
|
|
582
|
+
`message` field of their own it is a getter returning `SuiError.describe(this)`,
|
|
583
|
+
so anything that surfaces `error.message` — a log line, a UI, another library's
|
|
584
|
+
formatter — shows the actionable line instead of an empty string. It is not a
|
|
585
|
+
schema field and does not appear in `SuiError.toJson`'s output.
|
|
586
|
+
|
|
587
|
+
`SuiError.isRetryable`, `SuiError.outcome`, `SuiError.isTaxonomy`,
|
|
588
|
+
`SuiError.describe` and `SuiError.toJson` are the helpers every repo otherwise
|
|
589
|
+
hand-rolls;
|
|
212
590
|
`outcome` puts every failure on the `"applied" | "not_applied" | "unknown"`
|
|
213
591
|
axis, and an extension error may declare its own. An error that is neither a tag
|
|
214
592
|
above nor declares an `outcome` is *unclassified*: `outcome` answers `"unknown"`,
|
|
215
593
|
because an unrecognised tag is no evidence that nothing happened, and
|
|
216
594
|
`Script.exitCode` exits 1 rather than 3, because it is no evidence that anything
|
|
217
|
-
was sent either. Declare `outcome` on every error your extension defines
|
|
595
|
+
was sent either. Declare `outcome` on every error your extension defines — as a
|
|
596
|
+
class field is fine, `toJson` reads it off the instance and serializes it either
|
|
597
|
+
way, and the check duck-types the field, so an existing `Error` subclass with an
|
|
598
|
+
`outcome` property lands on the axis without becoming a `Schema.TaggedError`
|
|
599
|
+
first.
|
|
600
|
+
|
|
601
|
+
`SuiError.outcome(error, { phase: "pre-submit" })` changes exactly one answer:
|
|
602
|
+
an unrecognised tag caught **before** anything could have been sent — in a
|
|
603
|
+
`catchAll` around a build, a simulate or a signature — is `"not_applied"`
|
|
604
|
+
rather than `"unknown"`, which is true there by construction. The default stays
|
|
605
|
+
`"post-submit"`. `SuiError.isTaxonomy(error)` answers the question underneath
|
|
606
|
+
it, and `SuiError.describe` accepts a foreign error too, falling back to its tag
|
|
607
|
+
and message rather than returning nothing.
|
|
608
|
+
|
|
609
|
+
**A wrapper error must carry what it wrapped.** `Script.exitCode` honours a
|
|
610
|
+
declared `outcome` before the tag, so a `catchAll` that re-raises its own error
|
|
611
|
+
type around a `Tx.run` must copy `outcome: SuiError.outcome(cause)` and the
|
|
612
|
+
digest, or a charged `ExecutionFailed` becomes exit 1 or, worse, exit 4 — and
|
|
613
|
+
the wrapper retries a transaction that already ran.
|
|
218
614
|
|
|
219
615
|
## Scripts
|
|
220
616
|
|
|
@@ -225,7 +621,7 @@ was sent either. Declare `outcome` on every error your extension defines.
|
|
|
225
621
|
| `SUI_NETWORK` | yes, no default | `mainnet`, `testnet`, `devnet`, `localnet` or your own |
|
|
226
622
|
| `SUI_ALLOW_MAINNET` | only for mainnet | `1` or `true`; a script that means mainnet has to say so twice |
|
|
227
623
|
| `SUI_RPC_URL` | no | The gRPC endpoint; defaulted per known network |
|
|
228
|
-
| `SUI_PRIVATE_KEY` | `Script` only | A Bech32 `suiprivkey1…` key, read through `Config.redacted`. A key that does not decode fails with one fixed sentence and no cause: the Bech32 decoder quotes the whole input it rejected, so nothing derived from it is ever printed |
|
|
624
|
+
| `SUI_PRIVATE_KEY` | `Script` only | A Bech32 `suiprivkey1…` key **or a 32-byte hex seed** (64 hex characters, `0x` optional, read as Ed25519), through `Config.redacted`. A key that does not decode fails with one fixed sentence and no cause: the Bech32 decoder quotes the whole input it rejected, so nothing derived from it is ever printed |
|
|
229
625
|
|
|
230
626
|
`Script.layerReadOnly` provides `ScriptReadOnly`, which has no signer at all —
|
|
231
627
|
a separate service key, so a script written to sign cannot silently build over a
|
|
@@ -249,18 +645,128 @@ bytes finish.
|
|
|
249
645
|
| 5 | applied on chain: `ExecutionFailed` (gas charged) or `UnexpectedEffects` (it ran; the receipt is missing) |
|
|
250
646
|
| 130 | interrupted, with nothing outstanding in the journal |
|
|
251
647
|
|
|
648
|
+
**`Script.report(exit, { stderr?, journal? })`** is those last two steps on
|
|
649
|
+
their own — the diagnostic lines and the unresolved entries, with the exit code
|
|
650
|
+
as the answer — for a CLI that owns its own argv parsing and process. Assign it
|
|
651
|
+
to `process.exitCode` rather than calling `process.exit`.
|
|
652
|
+
|
|
252
653
|
A timeout or an interrupt asks the journal: with an unresolved submission in it
|
|
253
654
|
the exit is 3, not 4 or 130, because an `Effect.timeout` wrapped around a
|
|
254
|
-
submission interrupts it from the outside and the bytes may be on the wire.
|
|
655
|
+
submission interrupts it from the outside and the bytes may be on the wire. Each
|
|
656
|
+
unresolved entry is printed three ways: the digest and tag, the raw base64
|
|
657
|
+
bytes, and the whole entry encoded through the `JournalEntry` schema — the same
|
|
658
|
+
JSON a durable journal stores — so a wrapper can hand stderr to a recovery
|
|
659
|
+
process verbatim.
|
|
660
|
+
|
|
661
|
+
**Every `SubmitConfig` field is overridable, and one of them usually should be.**
|
|
662
|
+
`SubmitConfig` is a `Context.Reference` with the spec's defaults, including a
|
|
663
|
+
`maxGasBudget` of **50 SUI** — a ceiling, not a budget, but far above what a
|
|
664
|
+
sponsor policy typically allows. Narrow it once, where the runtime is built:
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
Effect.provideService(program, SubmitConfig, {
|
|
668
|
+
...SubmitConfig.defaults,
|
|
669
|
+
maxGasBudget: 1_000_000_000n, // 1 SUI
|
|
670
|
+
lockSender: false // see below
|
|
671
|
+
})
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`lockSender` is the other one worth a decision. The lock exists because two
|
|
675
|
+
concurrent builds from one address can pick the same gas coin; with
|
|
676
|
+
`tx.setGasPayment([])` there is no coin to pick — the node pays from the address
|
|
677
|
+
balance — and the lock buys nothing. That is the case for every transaction
|
|
678
|
+
built with `Tx.sponsored`, whose empty gas payment `Tx.build` preserves through
|
|
679
|
+
the resolver, and it is the only setting that makes sense across isolates
|
|
680
|
+
(Workers, Durable Objects), where a per-runtime semaphore is not a lock at all.
|
|
681
|
+
|
|
682
|
+
## Relay and sponsor envelopes
|
|
683
|
+
|
|
684
|
+
`Executed` describes the SDK's own execute include set. A relay, a sponsor or
|
|
685
|
+
any service that submitted on your behalf returns whatever *it* asked the node
|
|
686
|
+
for, which is usually less: `changedObjects` with an `objectId` and an
|
|
687
|
+
`idOperation` and nothing else, no `objectTypes`, no `balanceChanges`, no
|
|
688
|
+
checkpoint, events as JSON with no BCS.
|
|
689
|
+
|
|
690
|
+
`Executed.fromPartial(envelope)` decodes exactly that, and what it was not told
|
|
691
|
+
stays "not told": input and output states are `Unknown` rather than a guessed
|
|
692
|
+
`ObjectWrite`, versions and digests are `null`, and the accessors read `Unknown`
|
|
693
|
+
as "the envelope did not say", so `created()` and `deleted()` still classify
|
|
694
|
+
from the id operation alone. JSON spellings are accepted where the SDK's types
|
|
695
|
+
are not JSON — `bcs` as base64 or a byte array, every `u64` as a number or a
|
|
696
|
+
`bigint` as well as the decimal string the wire uses. Two things it cannot
|
|
697
|
+
invent: the **types** (`created(type)` and `expectCreated(type)` match against
|
|
698
|
+
the `objectTypes` join, so without one they match nothing — use `created()` or
|
|
699
|
+
`createdWhere(predicate)`), and the **gas** (`gasUsedTotal` is `0n` for an
|
|
700
|
+
envelope that reported none, which means "not reported", not "free").
|
|
701
|
+
|
|
702
|
+
`Executed.fromTransactionResult(result)` is the strict constructor, for an SDK
|
|
703
|
+
`TransactionResult` read with the full include set.
|
|
704
|
+
|
|
705
|
+
The whole sequence, for a service that co-signs and submits: `Tx.build` with
|
|
706
|
+
`Tx.sponsored`, `Tx.sign`, hand `toBase64(signed.bytes)` and `signed.signatures[0]`
|
|
707
|
+
to the service, and then either `Executed.fromPartial(reply)` or — through
|
|
708
|
+
`Tx.submitVia`, which keeps the journal — `Tx.reconcile(signed)`. The digest
|
|
709
|
+
does not change when the sponsor adds its signature, so `signed.digest` is what
|
|
710
|
+
to record. `docs/extensions.md` has the section.
|
|
711
|
+
|
|
712
|
+
**`Executed.events` is `ReadonlyArray<Event>`, not `SuiClientTypes.Event[]`**:
|
|
713
|
+
the same fields with branded ids, and `json` present only when whatever produced
|
|
714
|
+
the `Executed` carried one (a relay envelope, never a gRPC execute). Decode an
|
|
715
|
+
event with `SuiSchema.decode(codec, event.bcs)`. Code typed against the SDK's
|
|
716
|
+
`Event[]` does not accept these.
|
|
717
|
+
|
|
718
|
+
**`bigint` throws in `JSON.stringify`.** Gas, balances, versions and
|
|
719
|
+
`checkpoint` are all `bigint`; anything that logs, persists or returns one over
|
|
720
|
+
HTTP needs `.toString()` or a replacer. Decimal strings are what the wire uses
|
|
721
|
+
and what every schema here decodes from.
|
|
722
|
+
|
|
723
|
+
## Applications, Workers and Durable Objects
|
|
724
|
+
|
|
725
|
+
An application that consumes this library — an SPA, a Worker, a Durable Object —
|
|
726
|
+
wants one `ManagedRuntime` at module scope over
|
|
727
|
+
`Sui.layerNoDepsWith({ chainId })` and `SuiCore.layerFromClient(client)`, and
|
|
728
|
+
should know four things:
|
|
729
|
+
|
|
730
|
+
- **a `ManagedRuntime` memoizes its layer build, failure included.** One flaky
|
|
731
|
+
`getChainIdentifier` at boot and every later use of that runtime fails with the
|
|
732
|
+
same stale `TransportError`. Pass `SuiLayerOptions.retry` (a `Schedule` for
|
|
733
|
+
that one read), dispose and rebuild on a build failure, or use
|
|
734
|
+
`Sui.layerNoDepsPinned(chainId)`, which makes no call at all;
|
|
735
|
+
- **the default `Journal` is process-wide memory.** In a browser tab a refresh
|
|
736
|
+
is a new process and two tabs are two journals and two sender locks; what an
|
|
737
|
+
app actually holds is the `SubmissionUnknown` and its bytes, so persist that
|
|
738
|
+
and reconcile it on the next boot, or provide the durable journal;
|
|
739
|
+
- **the sender lock does not cross isolates**, so correctness there rests on
|
|
740
|
+
address-balance gas (`tx.setGasPayment([])`, `lockSender: false`) or on
|
|
741
|
+
serializing an address through one Durable Object;
|
|
742
|
+
- **there is no `process`** in a Worker: `Script` is a Node entrypoint, and
|
|
743
|
+
configuration comes from `ConfigProvider.fromEnvRecord(env)`.
|
|
744
|
+
|
|
745
|
+
`docs/extensions.md` sections 17 and 18 have the runtime module, the HMR
|
|
746
|
+
dispose, the vitest double and the `KeyValueStore.makeStringOnly` adapter over
|
|
747
|
+
Durable Object storage.
|
|
255
748
|
|
|
256
749
|
## Testing
|
|
257
750
|
|
|
258
751
|
`@unconfirmed/sui-effect/testing` ships the in-memory `SuiCoreFake`, `layerTest(script)` (the
|
|
259
752
|
real `Sui` over the fake, so tests exercise the production high tier),
|
|
260
|
-
`layerExtensionTest(layer, script)` for an extension's own tests
|
|
753
|
+
`layerExtensionTest(layer, script, { extra })` for an extension's own tests
|
|
754
|
+
(`SuiGraphQL.layerUnavailable` is provided by default; `extra` is for anything
|
|
755
|
+
else the extension's layer requires), and `SuiTest`
|
|
261
756
|
for driving the fake's state and reading back what it was sent. No test in this
|
|
262
757
|
repository touches the network, and neither should yours.
|
|
263
758
|
|
|
759
|
+
Four things a test on the fake has to know. `layerTest` is the production layer,
|
|
760
|
+
so a script naming `mainnet` or `testnet` must report that network's real chain
|
|
761
|
+
identifier — use `localnet` (the default) in fixtures. `Tx.submit` reconciles
|
|
762
|
+
through `getTransaction`, so a submit test that does not mean "this landed"
|
|
763
|
+
scripts `getTransaction: [FakeOutcome.notFound()]`. A **sponsored** submit needs
|
|
764
|
+
`Tx.cosign` first: the fake refuses an under-signed or wrongly-signed submission
|
|
765
|
+
with a gRPC `INVALID_ARGUMENT`, the way a validator does. And the build's
|
|
766
|
+
simulate is the resolver's — recorded, so `SuiTest.calls("simulateTransaction")`
|
|
767
|
+
sees it, and answered by `FakeScript.buildSimulate` if there is one and by the
|
|
768
|
+
ordered `simulate` script otherwise.
|
|
769
|
+
|
|
264
770
|
## Versions
|
|
265
771
|
|
|
266
772
|
| Package | Range | Tested against |
|
package/dist/domain/bcs.d.ts
CHANGED
|
@@ -32,6 +32,50 @@ import type { ObjectId } from "./schemas.ts";
|
|
|
32
32
|
* from an object, which is every `getObject(id, { schema })` read.
|
|
33
33
|
*/
|
|
34
34
|
export declare const bcs: <T extends Input, Input>(bcsType: BcsType<T, Input>, expectedType?: string) => Schema.Codec<T, Uint8Array>;
|
|
35
|
+
/**
|
|
36
|
+
* A BCS layout plus the mapping into a domain value, as one codec.
|
|
37
|
+
*
|
|
38
|
+
* This is the shape every extension writes by hand and writes slightly
|
|
39
|
+
* differently: parse the bytes with a layout, then hand the raw struct to a
|
|
40
|
+
* constructor or a mapping function that may throw (an id that has to be
|
|
41
|
+
* branded, a `bigint` that has to be range-checked, a discriminant that has to
|
|
42
|
+
* become a union). Written out it is `SuiSchema.bcs(...)` piped into a
|
|
43
|
+
* `Schema.decodeTo` with a `transformOrFail` and an `Effect.try`, and the part
|
|
44
|
+
* that gets forgotten is turning the thrown value into a schema issue, so the
|
|
45
|
+
* failure arrives as a defect instead of a `DecodeError`.
|
|
46
|
+
*
|
|
47
|
+
* `map` is called with whatever the layout parsed. Returning a value decodes
|
|
48
|
+
* it; **throwing** fails the decode, and the thrown value's message becomes the
|
|
49
|
+
* `DecodeError.issue` the caller sees, with the expected type already on it.
|
|
50
|
+
* The result is a `Schema.Codec<A, Uint8Array>` like any other: pass it as
|
|
51
|
+
* `sui.getObject(id, { schema })`, and the Move type check still runs first
|
|
52
|
+
* because the annotation {@link bcs} leaves behind survives the composition.
|
|
53
|
+
*
|
|
54
|
+
* Encoding is not supported: a mapping function has no inverse, and inventing
|
|
55
|
+
* one silently is worse than saying so. Encode with the layout itself when you
|
|
56
|
+
* need bytes back.
|
|
57
|
+
*
|
|
58
|
+
* Fails with: `DecodeError` (through the schema), when the bytes do not parse
|
|
59
|
+
* or `map` throws.
|
|
60
|
+
*
|
|
61
|
+
* @since 0.1.1
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { ObjectId, SuiSchema } from "@unconfirmed/sui-effect"
|
|
66
|
+
*
|
|
67
|
+
* class Escrow {
|
|
68
|
+
* constructor(readonly id: ObjectId, readonly amount: bigint) {}
|
|
69
|
+
* }
|
|
70
|
+
*
|
|
71
|
+
* const EscrowContent = SuiSchema.decodeWith(
|
|
72
|
+
* EscrowLayout,
|
|
73
|
+
* `${packageId}::escrow::Escrow`,
|
|
74
|
+
* (raw) => new Escrow(ObjectId.normalize(raw.id), BigInt(raw.amount))
|
|
75
|
+
* )
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare const decodeWith: <T extends Input, Input, A>(bcsType: BcsType<T, Input>, expectedType: string | undefined, map: (parsed: T) => A) => Schema.Codec<A, Uint8Array>;
|
|
35
79
|
/**
|
|
36
80
|
* The normalized Move type a codec built by {@link bcs} expects, or `undefined`
|
|
37
81
|
* for any other codec. `Sui.getObject` compares it with the object's own type
|
package/dist/domain/bcs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bcs.d.ts","sourceRoot":"","sources":["../../src/domain/bcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAgD,MAAM,QAAQ,CAAA;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAY5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,KAAK,EAAE,KAAK,EACxC,SAAS,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,EAC1B,eAAe,MAAM,KACpB,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAgD5B,CAAA;AAuBD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,GAAG,SAC/C,CAAA;AAU5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,QAAQ,MAAM,KAAG,OAU9D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,QAAQ,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EACnC,SAAS,UAAU,EACnB,UAAU;IACR,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,KACA,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"bcs.d.ts","sourceRoot":"","sources":["../../src/domain/bcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAgD,MAAM,QAAQ,CAAA;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAY5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,KAAK,EAAE,KAAK,EACxC,SAAS,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,EAC1B,eAAe,MAAM,KACpB,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAgD5B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,KAAK,EAAE,KAAK,EAAE,CAAC,EAClD,SAAS,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,EAC1B,cAAc,MAAM,GAAG,SAAS,EAChC,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,KACpB,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAmC5B,CAAA;AAuBD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,GAAG,SAC/C,CAAA;AAU5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,QAAQ,MAAM,KAAG,OAU9D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,QAAQ,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EACnC,SAAS,UAAU,EACnB,UAAU;IACR,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,KACA,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAiC9B,CAAA"}
|