sui-effect 0.1.0
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 +209 -0
- package/LICENSE +21 -0
- package/LLMS.md +6707 -0
- package/README.md +300 -0
- package/dist/domain/bcs.d.ts +101 -0
- package/dist/domain/bcs.d.ts.map +1 -0
- package/dist/domain/bcs.js +183 -0
- package/dist/domain/bcs.js.map +1 -0
- package/dist/domain/errors.d.ts +595 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +476 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/executed.d.ts +325 -0
- package/dist/domain/executed.d.ts.map +1 -0
- package/dist/domain/executed.js +253 -0
- package/dist/domain/executed.js.map +1 -0
- package/dist/domain/journal-entry.d.ts +468 -0
- package/dist/domain/journal-entry.d.ts.map +1 -0
- package/dist/domain/journal-entry.js +63 -0
- package/dist/domain/journal-entry.js.map +1 -0
- package/dist/domain/schemas.d.ts +950 -0
- package/dist/domain/schemas.d.ts.map +1 -0
- package/dist/domain/schemas.js +571 -0
- package/dist/domain/schemas.js.map +1 -0
- package/dist/domain/sui-schema.d.ts +12 -0
- package/dist/domain/sui-schema.d.ts.map +1 -0
- package/dist/domain/sui-schema.js +12 -0
- package/dist/domain/sui-schema.js.map +1 -0
- package/dist/extension.d.ts +7 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +7 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +18 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +18 -0
- package/dist/internal.js.map +1 -0
- package/dist/journal.d.ts +51 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +18 -0
- package/dist/journal.js.map +1 -0
- package/dist/script.d.ts +7 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +7 -0
- package/dist/script.js.map +1 -0
- package/dist/services/Journal.d.ts +76 -0
- package/dist/services/Journal.d.ts.map +1 -0
- package/dist/services/Journal.js +70 -0
- package/dist/services/Journal.js.map +1 -0
- package/dist/services/JournalKeyValueStore.d.ts +73 -0
- package/dist/services/JournalKeyValueStore.d.ts.map +1 -0
- package/dist/services/JournalKeyValueStore.js +155 -0
- package/dist/services/JournalKeyValueStore.js.map +1 -0
- package/dist/services/Script.d.ts +210 -0
- package/dist/services/Script.d.ts.map +1 -0
- package/dist/services/Script.js +441 -0
- package/dist/services/Script.js.map +1 -0
- package/dist/services/Signer.d.ts +132 -0
- package/dist/services/Signer.d.ts.map +1 -0
- package/dist/services/Signer.js +158 -0
- package/dist/services/Signer.js.map +1 -0
- package/dist/services/SubmitConfig.d.ts +175 -0
- package/dist/services/SubmitConfig.d.ts.map +1 -0
- package/dist/services/SubmitConfig.js +52 -0
- package/dist/services/SubmitConfig.js.map +1 -0
- package/dist/services/Sui.d.ts +295 -0
- package/dist/services/Sui.d.ts.map +1 -0
- package/dist/services/Sui.js +414 -0
- package/dist/services/Sui.js.map +1 -0
- package/dist/services/SuiCore.d.ts +227 -0
- package/dist/services/SuiCore.d.ts.map +1 -0
- package/dist/services/SuiCore.js +400 -0
- package/dist/services/SuiCore.js.map +1 -0
- package/dist/services/SuiCoreFake.d.ts +278 -0
- package/dist/services/SuiCoreFake.d.ts.map +1 -0
- package/dist/services/SuiCoreFake.js +1003 -0
- package/dist/services/SuiCoreFake.js.map +1 -0
- package/dist/services/SuiExtension.d.ts +208 -0
- package/dist/services/SuiExtension.d.ts.map +1 -0
- package/dist/services/SuiExtension.js +355 -0
- package/dist/services/SuiExtension.js.map +1 -0
- package/dist/services/SuiGraphQL.d.ts +98 -0
- package/dist/services/SuiGraphQL.d.ts.map +1 -0
- package/dist/services/SuiGraphQL.js +118 -0
- package/dist/services/SuiGraphQL.js.map +1 -0
- package/dist/services/Tx.d.ts +687 -0
- package/dist/services/Tx.d.ts.map +1 -0
- package/dist/services/Tx.js +1224 -0
- package/dist/services/Tx.js.map +1 -0
- package/dist/testing.d.ts +88 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +163 -0
- package/dist/testing.js.map +1 -0
- package/dist/tx.d.ts +24 -0
- package/dist/tx.d.ts.map +1 -0
- package/dist/tx.js +24 -0
- package/dist/tx.js.map +1 -0
- package/docs/extensions.md +1685 -0
- package/examples/extension-template/README.md +170 -0
- package/examples/extension-template/package.json +44 -0
- package/examples/extension-template/scripts/check-package.ts +145 -0
- package/examples/extension-template/src/Escrow.ts +445 -0
- package/examples/extension-template/src/Platform.ts +136 -0
- package/examples/extension-template/src/errors.ts +61 -0
- package/examples/extension-template/src/extension.ts +63 -0
- package/examples/extension-template/src/index.ts +34 -0
- package/examples/extension-template/src/schema.ts +158 -0
- package/examples/extension-template/src/upstream.ts +66 -0
- package/examples/extension-template/test/escrow.test.ts +559 -0
- package/examples/extension-template/tsconfig.build.json +26 -0
- package/examples/extension-template/tsconfig.json +35 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# sui-effect
|
|
2
|
+
|
|
3
|
+
An opinionated [Effect](https://effect.website) v4 layer over
|
|
4
|
+
[`@mysten/sui`](https://www.npmjs.com/package/@mysten/sui) for building safe
|
|
5
|
+
TypeScript applications and on-demand agent scripts on Sui.
|
|
6
|
+
|
|
7
|
+
The SDK keeps doing BCS, transaction building, signing and transport.
|
|
8
|
+
sui-effect owns the shape of the program around it: two client tiers, closed
|
|
9
|
+
error unions on every function, the transaction lifecycle as functions with
|
|
10
|
+
typed outcomes, crash-safe submission, and time, retry and interruption through
|
|
11
|
+
Effect so tests can drive them.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add sui-effect
|
|
17
|
+
bun add -d effect@4.0.0-rc.112 @mysten/sui@2.30.0
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`effect` and `@mysten/sui` are peer dependencies: one copy of each per process,
|
|
21
|
+
or `Context.Service` identities and `instanceof` checks stop matching. The
|
|
22
|
+
`effect` peer is pinned **exactly** to `4.0.0-rc.112`, because rc.113 renamed
|
|
23
|
+
`Config.nonEmptyString`, `Config.string` and `Config.redacted` to
|
|
24
|
+
`Config.NonEmptyString`, `Config.String` and `Config.Redacted`, and neighbouring
|
|
25
|
+
release candidates are not interchangeable.
|
|
26
|
+
|
|
27
|
+
A **library** built on sui-effect puts all three in its own
|
|
28
|
+
`peerDependencies` **and** `devDependencies`, never in `dependencies`; see
|
|
29
|
+
`examples/extension-template/README.md`.
|
|
30
|
+
|
|
31
|
+
## A complete script
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { bcs } from "@mysten/sui/bcs"
|
|
35
|
+
import { Config, Console, Effect } from "effect"
|
|
36
|
+
import { ObjectId, SuiSchema } from "sui-effect"
|
|
37
|
+
import { Script } from "sui-effect/script"
|
|
38
|
+
import { Tx } from "sui-effect/tx"
|
|
39
|
+
|
|
40
|
+
const PKG = "0x…"
|
|
41
|
+
|
|
42
|
+
const Escrow = SuiSchema.bcs(
|
|
43
|
+
bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() }),
|
|
44
|
+
`${PKG}::escrow::Escrow`
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export const program = Effect.gen(function*() {
|
|
48
|
+
const { signer, sui } = yield* Script
|
|
49
|
+
const id = yield* Config.schema(ObjectId, "ESCROW_ID")
|
|
50
|
+
const escrow = yield* sui.getObject(id, { schema: Escrow })
|
|
51
|
+
const executed = yield* Tx.run((tx) => {
|
|
52
|
+
tx.moveCall({
|
|
53
|
+
target: `${PKG}::escrow::claim`,
|
|
54
|
+
arguments: [tx.object(id), tx.pure.u64(escrow.content.amount)]
|
|
55
|
+
})
|
|
56
|
+
}, { signer })
|
|
57
|
+
const receipt = yield* executed.expectCreated(`${PKG}::escrow::Receipt`)
|
|
58
|
+
yield* Console.log(receipt.id)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (import.meta.main) {
|
|
62
|
+
await Script.run(program)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That is `examples/script-claim.ts`, which a test runs against the in-memory
|
|
67
|
+
fake. Every failure it can produce is in the generator's inferred error type and
|
|
68
|
+
has its own exit code, with no handling lines anywhere.
|
|
69
|
+
|
|
70
|
+
## The two tiers
|
|
71
|
+
|
|
72
|
+
`SuiCore` is the mechanical tier: a one-to-one Effect wrap of the SDK's
|
|
73
|
+
`ClientWithCoreApi`, with every transport method present, the SDK's `Include`
|
|
74
|
+
generics preserved, every call cancellable through the Effect's `AbortSignal`,
|
|
75
|
+
every failure mapped into one tagged error by one mapper, a span per method, and
|
|
76
|
+
retries on transient transport failures for reads only. Reach for it when you
|
|
77
|
+
need a field or a method the tier above does not expose, and through
|
|
78
|
+
`core.use(client => …)` when an upstream package wants the client object itself.
|
|
79
|
+
|
|
80
|
+
`Sui` is the opinionated tier over it, and is what application and extension
|
|
81
|
+
code reads through: fixed include sets, BCS content decoded through `Schema`
|
|
82
|
+
with the object's Move type checked first, `Option` where absence is normal,
|
|
83
|
+
batch reads chunked to 50 and checked for missing or duplicated ids — per-item
|
|
84
|
+
`Result` from `getObjects`, first-error-wins from `getObjectsOrFail` — pagination
|
|
85
|
+
as `Stream`, one lock per sender so two transactions from one address cannot
|
|
86
|
+
pick the same gas coin, and the chain's own clock. A Move type with no type
|
|
87
|
+
arguments matches every instantiation of it, wherever a type is compared, so one
|
|
88
|
+
codec covers a generic Move type and the object keeps its own instantiated type. It carries the `SuiCore` it
|
|
89
|
+
was built over as `sui.core`, which is why every `Tx.*` function needs only
|
|
90
|
+
`Sui`.
|
|
91
|
+
|
|
92
|
+
## The transaction lifecycle
|
|
93
|
+
|
|
94
|
+
`Tx` is the lifecycle as functions — `build`, `sign`, `cosign`, `sponsored`,
|
|
95
|
+
`submit`, `reconcile`, `run`, `reconcileAll` — each with a closed error union
|
|
96
|
+
and `R = Sui`. `Tx.run` holds the sender lock from build through submit, builds
|
|
97
|
+
(which always simulates before anything is signed — the SDK's resolver does it
|
|
98
|
+
when there is anything to resolve, and `Tx.build` runs one explicitly when
|
|
99
|
+
there is not, so a transaction that would abort never gets signed),
|
|
100
|
+
signs, journals the signed bytes before the first execute, re-sends the
|
|
101
|
+
identical bytes — never a rebuild — on a retryable transport failure or a
|
|
102
|
+
timeout, waits for the execution to be visible to reads before it releases the
|
|
103
|
+
lock, and if it still does not know what happened, reconciles: `Executed`,
|
|
104
|
+
`ExecutionFailed`, `NotApplied { evidence }`, or `SubmissionUnknown` carrying
|
|
105
|
+
the bytes. A `TransportError` never escapes once bytes may have been sent —
|
|
106
|
+
from `submit`, and from `reconcile` and `reconcileAll` too. `Signer` is a
|
|
107
|
+
value, not a service, so one process can hold two credentials; `SubmitConfig`
|
|
108
|
+
and `Journal` are `Context.Reference`s with working defaults, so none of this
|
|
109
|
+
needs wiring, and `sui-effect/journal` swaps the memory journal for a durable
|
|
110
|
+
one over `KeyValueStore`.
|
|
111
|
+
|
|
112
|
+
**`NotApplied` is hard to earn, on purpose.** Saying a transaction never applied
|
|
113
|
+
tells the documented retry idiom to send the caller's intent again, so there are
|
|
114
|
+
exactly two kinds of evidence and both are checked twice over. `"expired"`
|
|
115
|
+
requires the epoch (or timestamp) bound to be observed as passed, then a
|
|
116
|
+
`getTransaction` miss, then — after `SubmitConfig.reconcileRecheck` — both
|
|
117
|
+
again; a single observation is `SubmissionUnknown`, and
|
|
118
|
+
`SubmitConfig.expiryEvidence: "never"` turns the rule off for a deployment
|
|
119
|
+
behind a mixed-node load balancer. `"inputConsumed"` requires a *different*
|
|
120
|
+
transaction's **own effects** to report that it took a pinned object at exactly
|
|
121
|
+
the version the bytes pinned: reconcile follows the live object's
|
|
122
|
+
`previousTransaction` — which names the latest mutation, not the consumer of the
|
|
123
|
+
version in question — to that transaction and reads `inputVersion` off its
|
|
124
|
+
`changedObjects`. There is no "the object at version `v + 1`" rule, because Sui
|
|
125
|
+
stamps every output with the transaction's **Lamport version**, `max(input
|
|
126
|
+
versions) + 1`, so a coin read alongside a newer gas object jumps from version 4
|
|
127
|
+
to 6,436,928 and version 5 never existed. Every pinned reference — owned inputs
|
|
128
|
+
and gas coins alike — is tried before reconcile gives up.
|
|
129
|
+
|
|
130
|
+
**In practice that means `SubmissionUnknown`, not `NotApplied`, for almost
|
|
131
|
+
every stuck submission** whose PTB touched a shared object or an owned object
|
|
132
|
+
older than the gas coin. Plan an operator path or a `reconcileAll` at startup;
|
|
133
|
+
do not build a retry loop that expects `NotApplied { inputConsumed }`. Before
|
|
134
|
+
any of this, reconcile compares the chain the bytes were built for with
|
|
135
|
+
`sui.chainId` and refuses to reason across chains.
|
|
136
|
+
|
|
137
|
+
**A sponsored `Tx.run` needs both signatures.** When the gas owner is not the
|
|
138
|
+
sender, pass `sponsor`: `Tx.run(recipe, { signer, gasOwner, sponsor })`. Without
|
|
139
|
+
it the run fails with `SigningError` before anything is built, because one
|
|
140
|
+
signature on sponsored bytes is something a validator rejects outright. Two
|
|
141
|
+
parties that cannot both sign in one process use `build`, `sign`, `cosign` and
|
|
142
|
+
`submit` directly.
|
|
143
|
+
|
|
144
|
+
When the recipe sets no expiration, `Tx.build` sets one: `ValidDuring` bounded
|
|
145
|
+
to the current epoch and the next, carrying the chain identifier as a replay
|
|
146
|
+
guard. Two epochs is what the validator rule allows for a transaction with no
|
|
147
|
+
address-owned inputs — a PTB over shared objects, or any sponsored transaction —
|
|
148
|
+
and the chain field is enforced, so bytes signed for testnet cannot land on
|
|
149
|
+
mainnet. There is deliberately no wall-clock bound: every Sui network refuses a
|
|
150
|
+
transaction that carries one today. `SubmitConfig.validFor` adds one for the day
|
|
151
|
+
that changes.
|
|
152
|
+
|
|
153
|
+
## Extensions
|
|
154
|
+
|
|
155
|
+
A downstream SDK is an extension: an Effect service built on `Sui` and `Tx`
|
|
156
|
+
whose layer requires `Sui` and nothing it could have built itself, whose
|
|
157
|
+
contributions to a transaction are recipe fragments consumers compose into one
|
|
158
|
+
programmable transaction, whose signers are parameters, and whose Promise face
|
|
159
|
+
is derived — not hand-maintained — by `SuiExtension.fromService`, so a consumer
|
|
160
|
+
with an SDK client writes `client.$extend(escrow(options))` and then plain
|
|
161
|
+
`await`s, with the same tagged error instances on rejection.
|
|
162
|
+
**[`docs/extensions.md`](docs/extensions.md) is the contract**, and
|
|
163
|
+
`examples/extension-template/` is a copyable package that implements it — it
|
|
164
|
+
ships inside the published package, so
|
|
165
|
+
`node_modules/sui-effect/examples/extension-template/` is there to copy without
|
|
166
|
+
a checkout.
|
|
167
|
+
|
|
168
|
+
Four things an extension author should know before reading the guide. A layer
|
|
169
|
+
may require `Sui | SuiCore` and must provide everything else itself, including
|
|
170
|
+
another extension's service — the guide's "composing extensions" section is that
|
|
171
|
+
pattern. A Promise face's **synchronous** members (recipe builders, a package
|
|
172
|
+
id) are real only once the runtime exists, so either `await client.<name>.$ready()`
|
|
173
|
+
once or register with `warm`; calling one before that fails with
|
|
174
|
+
`ExtensionNotReady` rather than returning a Promise the type does not mention
|
|
175
|
+
(`Effect` and `Stream` members work cold, as Promises and as async iterables).
|
|
176
|
+
Every registration on one client **shares one `Sui`**, and therefore one
|
|
177
|
+
sender-lock map, so two extensions never select gas for the same address at
|
|
178
|
+
once; `$dispose()` releases that shared base only when the last registration on
|
|
179
|
+
the client is disposed, and it is not final — the next call builds a fresh one.
|
|
180
|
+
And `SuiGraphQL` is sui-effect's tag over the SDK's `SuiGraphQLClient` — one
|
|
181
|
+
client shared by every extension that reads GraphQL; sui-effect wraps no GraphQL
|
|
182
|
+
API of its own.
|
|
183
|
+
|
|
184
|
+
## Errors
|
|
185
|
+
|
|
186
|
+
Every failure is one of these, every one is a `Schema.TaggedError`, and there is
|
|
187
|
+
no error inheritance to match on.
|
|
188
|
+
|
|
189
|
+
| Tag | Fields | Means |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `TransportError` | `method`, `retryable`, `status?`, `cause` | The request did not reach a usable answer. Inside `Tx.submit` a timed-out `executeTransaction` lands here with `retryable: true` and `status: "DEADLINE_EXCEEDED"`; anywhere else `Effect.timeout` produces Effect's own `TimeoutError`, which is not part of this taxonomy |
|
|
192
|
+
| `ObjectNotFound` / `ObjectDeleted` / `ObjectUnavailable` | `objectId`, `version?` | The three `ObjectError.reason` values |
|
|
193
|
+
| `TransactionNotFound` | `digest` | No transaction with that digest is known |
|
|
194
|
+
| `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 |
|
|
196
|
+
| `SimulationFailed` | `reason`, `message` | Simulation reported an execution failure. No gas charged |
|
|
197
|
+
| `ExecutionFailed` | `digest`, `reason`, `command?`, `effects` | Applied on chain and failed. Gas charged |
|
|
198
|
+
| `SubmissionUnknown` | `digest`, `signed?`, `cause` | Bytes may have been sent; the outcome is unknown. Carries them, unless it came from reconciling a bare digest |
|
|
199
|
+
| `NotApplied` | `digest`, `evidence: "expired" \| "inputConsumed"` | Provably never applied, and never will be |
|
|
200
|
+
| `SigningError` | `cause` | A signer refused or failed |
|
|
201
|
+
| `BuildError` | `message`, `cause` | The transaction could not be built |
|
|
202
|
+
| `PolicyDenied` | `rule`, `message` | A preflight policy refused it before it was signed |
|
|
203
|
+
| `JournalError` | `cause` | The journal could not be read or written. It escapes `Tx.submit` only from the write that happens **before** the first send; after the network has answered, a failed write is logged and the answer stands |
|
|
204
|
+
| `UnexpectedEffects` | `digest`, `expected`, `found` | The effects did not contain what the caller expected. Outcome `applied` and exit 5: it comes from an `Executed`, so the transaction ran and gas was charged; only the receipt is missing |
|
|
205
|
+
| `GraphQLUnavailable` | `method`, `reason` | The GraphQL endpoint an extension needs is not usable. What `SuiGraphQL.layerUnavailable` rejects every call with |
|
|
206
|
+
| `ExtensionNotReady` | `extension`, `member` | A synchronous member of a Promise face was called before its runtime existed: `await client.<name>.$ready()`, or register with `warm` |
|
|
207
|
+
|
|
208
|
+
`ExecutionReason` mirrors the SDK's `ExecutionError` variant for variant, with
|
|
209
|
+
`MoveAbort.abortCode` as a `bigint` and clever-error constant names decoded.
|
|
210
|
+
`SuiError.isRetryable`, `SuiError.outcome`, `SuiError.describe` and
|
|
211
|
+
`SuiError.toJson` are the four helpers every repo otherwise hand-rolls;
|
|
212
|
+
`outcome` puts every failure on the `"applied" | "not_applied" | "unknown"`
|
|
213
|
+
axis, and an extension error may declare its own. An error that is neither a tag
|
|
214
|
+
above nor declares an `outcome` is *unclassified*: `outcome` answers `"unknown"`,
|
|
215
|
+
because an unrecognised tag is no evidence that nothing happened, and
|
|
216
|
+
`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.
|
|
218
|
+
|
|
219
|
+
## Scripts
|
|
220
|
+
|
|
221
|
+
`Script.layer` reads the environment:
|
|
222
|
+
|
|
223
|
+
| Variable | Required | Meaning |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `SUI_NETWORK` | yes, no default | `mainnet`, `testnet`, `devnet`, `localnet` or your own |
|
|
226
|
+
| `SUI_ALLOW_MAINNET` | only for mainnet | `1` or `true`; a script that means mainnet has to say so twice |
|
|
227
|
+
| `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 |
|
|
229
|
+
|
|
230
|
+
`Script.layerReadOnly` provides `ScriptReadOnly`, which has no signer at all —
|
|
231
|
+
a separate service key, so a script written to sign cannot silently build over a
|
|
232
|
+
layer that cannot. `Script.run` installs SIGINT and SIGTERM handlers, interrupts
|
|
233
|
+
the root fiber so finalizers run, writes one diagnostic line per failure to
|
|
234
|
+
stderr, and exits. stdout carries only what the script itself printed: the
|
|
235
|
+
logger is bound to stderr for the whole run, so an `Effect.log` anywhere in the
|
|
236
|
+
call tree cannot corrupt the output. On every non-zero exit the unresolved
|
|
237
|
+
entries of the journal the script ran with are printed too, so a script killed
|
|
238
|
+
or failed mid-submit still leaves the digest and the bytes. A second SIGINT is
|
|
239
|
+
ignored on purpose — the first one is what lets the finalizers that record those
|
|
240
|
+
bytes finish.
|
|
241
|
+
|
|
242
|
+
| Code | Means |
|
|
243
|
+
|---|---|
|
|
244
|
+
| 0 | success |
|
|
245
|
+
| 1 | a defect, or an unclassified failure |
|
|
246
|
+
| 2 | configuration: `ConfigError`, `NetworkMismatch`, `SchemaError`, the mainnet gate |
|
|
247
|
+
| 3 | unknown outcome: reconcile before sending anything else |
|
|
248
|
+
| 4 | nothing applied: safe to retry |
|
|
249
|
+
| 5 | applied on chain: `ExecutionFailed` (gas charged) or `UnexpectedEffects` (it ran; the receipt is missing) |
|
|
250
|
+
| 130 | interrupted, with nothing outstanding in the journal |
|
|
251
|
+
|
|
252
|
+
A timeout or an interrupt asks the journal: with an unresolved submission in it
|
|
253
|
+
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.
|
|
255
|
+
|
|
256
|
+
## Testing
|
|
257
|
+
|
|
258
|
+
`sui-effect/testing` ships the in-memory `SuiCoreFake`, `layerTest(script)` (the
|
|
259
|
+
real `Sui` over the fake, so tests exercise the production high tier),
|
|
260
|
+
`layerExtensionTest(layer, script)` for an extension's own tests, and `SuiTest`
|
|
261
|
+
for driving the fake's state and reading back what it was sent. No test in this
|
|
262
|
+
repository touches the network, and neither should yours.
|
|
263
|
+
|
|
264
|
+
## Versions
|
|
265
|
+
|
|
266
|
+
| Package | Range | Tested against |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `effect` | `4.0.0-rc.112` (exact) | `4.0.0-rc.112`, in CI |
|
|
269
|
+
| `@mysten/sui` | `^2.28` (the first version whose BCS and gRPC round-trip `ValidDuring` and `Validity` expirations) | `2.29.0` and `2.30.0`, both in CI; `2.30.0` is the pinned devDependency |
|
|
270
|
+
| `@mysten/bcs` | `^2.1.1` | `2.1.1` |
|
|
271
|
+
| TypeScript | `5.9.x` to build | `5.9.3` |
|
|
272
|
+
| Bun | `1.4.x` | `1.4.2` |
|
|
273
|
+
|
|
274
|
+
The SDK row is a matrix, not a hope: CI builds, typechecks and runs the suite
|
|
275
|
+
against `@mysten/sui` 2.29.0 and 2.30.0, which are the versions consumers pin
|
|
276
|
+
today. The `effect` row is a matrix of one, and it is exact on purpose: rc.113
|
|
277
|
+
renamed three `Config` constructors this package calls at four sites
|
|
278
|
+
(`Script.ts`, `Signer.ts`, `SuiCore.ts`, `SuiGraphQL.ts`), so widening the range
|
|
279
|
+
means supporting both spellings and proving the wider one in CI.
|
|
280
|
+
|
|
281
|
+
**Consumers on TypeScript 7 (`tsgo`) are supported.** The shipped `.d.ts` needs
|
|
282
|
+
nothing from the old compiler. The `prepare` script here
|
|
283
|
+
(`effect-language-service patch`) is a library concern — it patches the checker
|
|
284
|
+
this repository develops against — and should not be copied into a consumer or
|
|
285
|
+
an extension package.
|
|
286
|
+
|
|
287
|
+
`bun run check` is typecheck, build, tests and the extension template's own
|
|
288
|
+
check.
|
|
289
|
+
|
|
290
|
+
## For agents
|
|
291
|
+
|
|
292
|
+
`LLMS.md` ships in the package: every public export with its signature and the
|
|
293
|
+
error union its JSDoc states, plus every example verbatim. It is generated by
|
|
294
|
+
`bun run llms` and a test fails when it is stale. `AGENTS.md` is the short list
|
|
295
|
+
of invariants to read before the code, `DESIGN.md` the specification, and
|
|
296
|
+
`docs/extensions.md` the extension contract.
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BCS bridge: a `@mysten/bcs` layout becomes an Effect `Schema.Codec` whose
|
|
3
|
+
* encoded side is the `content` bytes of an object.
|
|
4
|
+
*
|
|
5
|
+
* Decoding never reads the transport-varying `json` field of an object, only
|
|
6
|
+
* `content`, so the same program gives the same answer on gRPC, GraphQL and
|
|
7
|
+
* JSON-RPC.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
import type { BcsType } from "@mysten/bcs";
|
|
12
|
+
import { Effect, Schema } from "effect";
|
|
13
|
+
import { DecodeError } from "./errors.ts";
|
|
14
|
+
import type { ObjectId } from "./schemas.ts";
|
|
15
|
+
/**
|
|
16
|
+
* Turns a BCS layout into a `Schema.Codec<T, Uint8Array>` tagged with the Move
|
|
17
|
+
* type it belongs to.
|
|
18
|
+
*
|
|
19
|
+
* Decoding fails with a `SchemaError` (which `Sui` maps to `DecodeError`) when
|
|
20
|
+
* the bytes do not parse; encoding fails the same way when the value does not
|
|
21
|
+
* serialize. The expected type is normalized with `normalizeStructTag`, so
|
|
22
|
+
* `Coin<0x2::sui::SUI>` and its padded spelling are the same type, and a tag
|
|
23
|
+
* with no type arguments matches every instantiation of it (see
|
|
24
|
+
* {@link typeMatches}).
|
|
25
|
+
*
|
|
26
|
+
* **`expectedType` is optional.** A Move *return value* has no struct tag —
|
|
27
|
+
* `sui.view(recipe, bcs.Address())` reads a `vector<u8>` off a command result —
|
|
28
|
+
* and inventing one so the bridge has something to compare is worse than saying
|
|
29
|
+
* there is nothing to compare. A codec built without an expected type carries
|
|
30
|
+
* none, so nothing checks a tag before it parses: the re-serialize length check
|
|
31
|
+
* is still what rejects mis-shaped bytes. Give the type whenever the bytes come
|
|
32
|
+
* from an object, which is every `getObject(id, { schema })` read.
|
|
33
|
+
*/
|
|
34
|
+
export declare const bcs: <T extends Input, Input>(bcsType: BcsType<T, Input>, expectedType?: string) => Schema.Codec<T, Uint8Array>;
|
|
35
|
+
/**
|
|
36
|
+
* The normalized Move type a codec built by {@link bcs} expects, or `undefined`
|
|
37
|
+
* for any other codec. `Sui.getObject` compares it with the object's own type
|
|
38
|
+
* tag before decoding. Composition preserves it: a codec piped into
|
|
39
|
+
* `Schema.decodeTo(DomainClass, ...)` still reports the type the bytes came
|
|
40
|
+
* from. Never fails.
|
|
41
|
+
*/
|
|
42
|
+
export declare const expectedTypeOf: <T, E>(schema: Schema.Codec<T, E>) => string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Whether an object's type tag satisfies an expected type.
|
|
45
|
+
*
|
|
46
|
+
* One rule, used everywhere a Move type is checked — the BCS bridge, the
|
|
47
|
+
* `expectedType` option of `Sui.getObject`, `getObjectOption` and `getObjects`,
|
|
48
|
+
* `SuiSchema.decode`, and the fake's owned-object filter:
|
|
49
|
+
*
|
|
50
|
+
* - **An expected tag with no type arguments names the generic itself**, so
|
|
51
|
+
* `pkg::m::Composition` matches every instantiation:
|
|
52
|
+
* `pkg::m::Composition<0x…::share::Share>` included. Only
|
|
53
|
+
* `address::module::name` is compared, with the address normalized. This is
|
|
54
|
+
* what a node does when a bare type is used as a filter, and it is what makes
|
|
55
|
+
* a generic Move type usable with the bridge at all: a codec is written once
|
|
56
|
+
* for `Composition<T>`, and the instantiation lives on the object.
|
|
57
|
+
* - **An expected tag that carries type arguments is compared in full**, after
|
|
58
|
+
* `normalizeStructTag`, so `Coin<0x2::sui::SUI>` and its padded spelling are
|
|
59
|
+
* the same type and `Coin<0x2::sui::SUI>` does not match `Coin<0x…::usdc::USDC>`.
|
|
60
|
+
*
|
|
61
|
+
* Anything that is not a struct tag — the literal `package`, a primitive — is
|
|
62
|
+
* compared as a normalized string. The object keeps its own instantiated type
|
|
63
|
+
* on `SuiObject.type`; this only decides whether the bytes may be decoded.
|
|
64
|
+
*
|
|
65
|
+
* **It never throws, which is the reason it is public** as
|
|
66
|
+
* `SuiSchema.matchesType`. A dynamic field's `name.type` is legally a primitive
|
|
67
|
+
* — `u64`, `bool`, `address`, `vector<u8>` — and the SDK's own
|
|
68
|
+
* `normalizeStructTag` throws on every one of them, so the obvious way to
|
|
69
|
+
* filter a `streamDynamicFields` by key type dies on the first `u64` key.
|
|
70
|
+
* Comparing tags with this is safe on any string at all.
|
|
71
|
+
*
|
|
72
|
+
* Never fails.
|
|
73
|
+
*/
|
|
74
|
+
export declare const typeMatches: (expected: string, actual: string) => boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Decodes BCS `content` bytes with a codec, turning any schema failure into a
|
|
77
|
+
* `DecodeError` that names the object and the type that was expected.
|
|
78
|
+
*
|
|
79
|
+
* This is what `Sui.getObject(id, { schema })` does after it has checked the
|
|
80
|
+
* object's type tag, exposed as `SuiSchema.decode` for the places that already
|
|
81
|
+
* have bytes: a dynamic field's value, an event payload, a `Stream` of
|
|
82
|
+
* envelopes an extension decodes itself. Without it every caller reinvents the
|
|
83
|
+
* same `Schema.decodeUnknownEffect(...).pipe(Effect.mapError(...))`, and the
|
|
84
|
+
* `DecodeError` it produces is worse than this one.
|
|
85
|
+
*
|
|
86
|
+
* `expectedType` defaults to the Move type the codec was built with, so passing
|
|
87
|
+
* it is only needed for a codec that carries none, or to override the recorded
|
|
88
|
+
* one. `actualType` is the Move type the bytes actually came from, when the
|
|
89
|
+
* caller knows it — a dynamic field's `name.type`, a stream envelope's `type`:
|
|
90
|
+
* give it and the same tag check `getObject` does runs here, under the
|
|
91
|
+
* {@link typeMatches} rule, before a byte is parsed. `objectId` is recorded on
|
|
92
|
+
* the error so an operator knows which object did not decode.
|
|
93
|
+
*
|
|
94
|
+
* Fails with: `DecodeError`.
|
|
95
|
+
*/
|
|
96
|
+
export declare const decodeContent: <T>(schema: Schema.Codec<T, Uint8Array>, content: Uint8Array, context?: {
|
|
97
|
+
readonly objectId?: ObjectId;
|
|
98
|
+
readonly expectedType?: string;
|
|
99
|
+
readonly actualType?: string;
|
|
100
|
+
}) => Effect.Effect<T, DecodeError>;
|
|
101
|
+
//# sourceMappingURL=bcs.d.ts.map
|
|
@@ -0,0 +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,CA0B9B,CAAA"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { normalizeStructTag, parseStructTag } from "@mysten/sui/utils";
|
|
2
|
+
import { Effect, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
|
|
3
|
+
import { DecodeError } from "./errors.js";
|
|
4
|
+
const SUI_TYPE_ANNOTATION = "sui-effect/suiType";
|
|
5
|
+
const normalizeSafe = (value) => {
|
|
6
|
+
try {
|
|
7
|
+
return normalizeStructTag(value);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Turns a BCS layout into a `Schema.Codec<T, Uint8Array>` tagged with the Move
|
|
15
|
+
* type it belongs to.
|
|
16
|
+
*
|
|
17
|
+
* Decoding fails with a `SchemaError` (which `Sui` maps to `DecodeError`) when
|
|
18
|
+
* the bytes do not parse; encoding fails the same way when the value does not
|
|
19
|
+
* serialize. The expected type is normalized with `normalizeStructTag`, so
|
|
20
|
+
* `Coin<0x2::sui::SUI>` and its padded spelling are the same type, and a tag
|
|
21
|
+
* with no type arguments matches every instantiation of it (see
|
|
22
|
+
* {@link typeMatches}).
|
|
23
|
+
*
|
|
24
|
+
* **`expectedType` is optional.** A Move *return value* has no struct tag —
|
|
25
|
+
* `sui.view(recipe, bcs.Address())` reads a `vector<u8>` off a command result —
|
|
26
|
+
* and inventing one so the bridge has something to compare is worse than saying
|
|
27
|
+
* there is nothing to compare. A codec built without an expected type carries
|
|
28
|
+
* none, so nothing checks a tag before it parses: the re-serialize length check
|
|
29
|
+
* is still what rejects mis-shaped bytes. Give the type whenever the bytes come
|
|
30
|
+
* from an object, which is every `getObject(id, { schema })` read.
|
|
31
|
+
*/
|
|
32
|
+
export const bcs = (bcsType, expectedType) => {
|
|
33
|
+
const normalized = expectedType === undefined ? undefined : normalizeSafe(expectedType);
|
|
34
|
+
// What the failure messages call the layout when it has no Move type of its
|
|
35
|
+
// own: `bcsType.name` is what `@mysten/bcs` named it (`"Escrow"`, `"vector"`).
|
|
36
|
+
const label = normalized ?? bcsType.name;
|
|
37
|
+
// A BCS layout carries no runtime type to test a decoded value against: the
|
|
38
|
+
// parse below is the validation, so the target schema accepts whatever the
|
|
39
|
+
// layout produced.
|
|
40
|
+
const target = Schema.declare((_u) => true);
|
|
41
|
+
return Schema.Uint8Array.pipe(Schema.decodeTo(target, SchemaTransformation.transformOrFail({
|
|
42
|
+
decode: (bytes, options) => Effect.try({
|
|
43
|
+
try: () => {
|
|
44
|
+
const parsed = bcsType.parse(bytes);
|
|
45
|
+
// `parse` stops at the end of the layout and ignores whatever
|
|
46
|
+
// follows, which would silently accept an `objectBcs` envelope
|
|
47
|
+
// where `content` was expected. Re-serializing costs one pass and
|
|
48
|
+
// catches both trailing and mis-shaped bytes.
|
|
49
|
+
if (bcsType.serialize(parsed).toBytes().length !== bytes.length) {
|
|
50
|
+
throw new Error(`expected ${bytes.length} bytes of ${label}`);
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
},
|
|
54
|
+
catch: (cause) => new SchemaIssue.InvalidValue({ message: `Could not parse ${label} from BCS content: ${String(cause)}` }, bytes, options)
|
|
55
|
+
}),
|
|
56
|
+
encode: (value, options) => Effect.try({
|
|
57
|
+
try: () => bcsType.serialize(value).toBytes(),
|
|
58
|
+
catch: (cause) => new SchemaIssue.InvalidValue({ message: `Could not serialize ${label} to BCS: ${String(cause)}` }, value, options)
|
|
59
|
+
})
|
|
60
|
+
}))).annotate(normalized === undefined ? {} : { [SUI_TYPE_ANNOTATION]: normalized });
|
|
61
|
+
};
|
|
62
|
+
const MAX_ENCODING_DEPTH = 32;
|
|
63
|
+
/**
|
|
64
|
+
* Walks the encoding chain looking for the annotation {@link bcs} leaves
|
|
65
|
+
* behind. `SuiSchema.bcs(...).pipe(Schema.decodeTo(DomainClass, ...))` puts a
|
|
66
|
+
* new node on top and keeps the annotated node as the source side of the
|
|
67
|
+
* transformation, so the tag survives composition.
|
|
68
|
+
*/
|
|
69
|
+
const findSuiType = (ast, depth) => {
|
|
70
|
+
const annotation = ast.annotations?.[SUI_TYPE_ANNOTATION];
|
|
71
|
+
if (typeof annotation === "string")
|
|
72
|
+
return annotation;
|
|
73
|
+
if (depth >= MAX_ENCODING_DEPTH)
|
|
74
|
+
return undefined;
|
|
75
|
+
const encoding = ast.encoding;
|
|
76
|
+
if (encoding === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
for (const link of encoding) {
|
|
79
|
+
const found = findSuiType(link.to, depth + 1);
|
|
80
|
+
if (found !== undefined)
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The normalized Move type a codec built by {@link bcs} expects, or `undefined`
|
|
87
|
+
* for any other codec. `Sui.getObject` compares it with the object's own type
|
|
88
|
+
* tag before decoding. Composition preserves it: a codec piped into
|
|
89
|
+
* `Schema.decodeTo(DomainClass, ...)` still reports the type the bytes came
|
|
90
|
+
* from. Never fails.
|
|
91
|
+
*/
|
|
92
|
+
export const expectedTypeOf = (schema) => findSuiType(schema.ast, 0);
|
|
93
|
+
const parseSafe = (value) => {
|
|
94
|
+
try {
|
|
95
|
+
return parseStructTag(value);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Whether an object's type tag satisfies an expected type.
|
|
103
|
+
*
|
|
104
|
+
* One rule, used everywhere a Move type is checked — the BCS bridge, the
|
|
105
|
+
* `expectedType` option of `Sui.getObject`, `getObjectOption` and `getObjects`,
|
|
106
|
+
* `SuiSchema.decode`, and the fake's owned-object filter:
|
|
107
|
+
*
|
|
108
|
+
* - **An expected tag with no type arguments names the generic itself**, so
|
|
109
|
+
* `pkg::m::Composition` matches every instantiation:
|
|
110
|
+
* `pkg::m::Composition<0x…::share::Share>` included. Only
|
|
111
|
+
* `address::module::name` is compared, with the address normalized. This is
|
|
112
|
+
* what a node does when a bare type is used as a filter, and it is what makes
|
|
113
|
+
* a generic Move type usable with the bridge at all: a codec is written once
|
|
114
|
+
* for `Composition<T>`, and the instantiation lives on the object.
|
|
115
|
+
* - **An expected tag that carries type arguments is compared in full**, after
|
|
116
|
+
* `normalizeStructTag`, so `Coin<0x2::sui::SUI>` and its padded spelling are
|
|
117
|
+
* the same type and `Coin<0x2::sui::SUI>` does not match `Coin<0x…::usdc::USDC>`.
|
|
118
|
+
*
|
|
119
|
+
* Anything that is not a struct tag — the literal `package`, a primitive — is
|
|
120
|
+
* compared as a normalized string. The object keeps its own instantiated type
|
|
121
|
+
* on `SuiObject.type`; this only decides whether the bytes may be decoded.
|
|
122
|
+
*
|
|
123
|
+
* **It never throws, which is the reason it is public** as
|
|
124
|
+
* `SuiSchema.matchesType`. A dynamic field's `name.type` is legally a primitive
|
|
125
|
+
* — `u64`, `bool`, `address`, `vector<u8>` — and the SDK's own
|
|
126
|
+
* `normalizeStructTag` throws on every one of them, so the obvious way to
|
|
127
|
+
* filter a `streamDynamicFields` by key type dies on the first `u64` key.
|
|
128
|
+
* Comparing tags with this is safe on any string at all.
|
|
129
|
+
*
|
|
130
|
+
* Never fails.
|
|
131
|
+
*/
|
|
132
|
+
export const typeMatches = (expected, actual) => {
|
|
133
|
+
const expectedTag = parseSafe(expected);
|
|
134
|
+
if (expectedTag === undefined || expectedTag.typeParams.length > 0) {
|
|
135
|
+
return normalizeSafe(expected) === normalizeSafe(actual);
|
|
136
|
+
}
|
|
137
|
+
const actualTag = parseSafe(actual);
|
|
138
|
+
if (actualTag === undefined)
|
|
139
|
+
return normalizeSafe(expected) === normalizeSafe(actual);
|
|
140
|
+
return expectedTag.address === actualTag.address &&
|
|
141
|
+
expectedTag.module === actualTag.module &&
|
|
142
|
+
expectedTag.name === actualTag.name;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Decodes BCS `content` bytes with a codec, turning any schema failure into a
|
|
146
|
+
* `DecodeError` that names the object and the type that was expected.
|
|
147
|
+
*
|
|
148
|
+
* This is what `Sui.getObject(id, { schema })` does after it has checked the
|
|
149
|
+
* object's type tag, exposed as `SuiSchema.decode` for the places that already
|
|
150
|
+
* have bytes: a dynamic field's value, an event payload, a `Stream` of
|
|
151
|
+
* envelopes an extension decodes itself. Without it every caller reinvents the
|
|
152
|
+
* same `Schema.decodeUnknownEffect(...).pipe(Effect.mapError(...))`, and the
|
|
153
|
+
* `DecodeError` it produces is worse than this one.
|
|
154
|
+
*
|
|
155
|
+
* `expectedType` defaults to the Move type the codec was built with, so passing
|
|
156
|
+
* it is only needed for a codec that carries none, or to override the recorded
|
|
157
|
+
* one. `actualType` is the Move type the bytes actually came from, when the
|
|
158
|
+
* caller knows it — a dynamic field's `name.type`, a stream envelope's `type`:
|
|
159
|
+
* give it and the same tag check `getObject` does runs here, under the
|
|
160
|
+
* {@link typeMatches} rule, before a byte is parsed. `objectId` is recorded on
|
|
161
|
+
* the error so an operator knows which object did not decode.
|
|
162
|
+
*
|
|
163
|
+
* Fails with: `DecodeError`.
|
|
164
|
+
*/
|
|
165
|
+
export const decodeContent = (schema, content, context) => {
|
|
166
|
+
const expected = context?.expectedType ?? expectedTypeOf(schema);
|
|
167
|
+
const withExpected = expected === undefined ? {} : { expectedType: expected };
|
|
168
|
+
const withObject = context?.objectId === undefined ? {} : { objectId: context.objectId };
|
|
169
|
+
const actual = context?.actualType;
|
|
170
|
+
if (expected !== undefined && actual !== undefined && !typeMatches(expected, actual)) {
|
|
171
|
+
return Effect.fail(new DecodeError({
|
|
172
|
+
...withObject,
|
|
173
|
+
...withExpected,
|
|
174
|
+
issue: `${context?.objectId === undefined ? "the bytes have" : `object ${context.objectId} has`} type ${actual}`
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
return Schema.decodeUnknownEffect(schema)(content).pipe(Effect.mapError((error) => new DecodeError({
|
|
178
|
+
...withObject,
|
|
179
|
+
...withExpected,
|
|
180
|
+
issue: error.message
|
|
181
|
+
})));
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=bcs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bcs.js","sourceRoot":"","sources":["../../src/domain/bcs.ts"],"names":[],"mappings":"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"}
|