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.
Files changed (115) hide show
  1. package/AGENTS.md +209 -0
  2. package/LICENSE +21 -0
  3. package/LLMS.md +6707 -0
  4. package/README.md +300 -0
  5. package/dist/domain/bcs.d.ts +101 -0
  6. package/dist/domain/bcs.d.ts.map +1 -0
  7. package/dist/domain/bcs.js +183 -0
  8. package/dist/domain/bcs.js.map +1 -0
  9. package/dist/domain/errors.d.ts +595 -0
  10. package/dist/domain/errors.d.ts.map +1 -0
  11. package/dist/domain/errors.js +476 -0
  12. package/dist/domain/errors.js.map +1 -0
  13. package/dist/domain/executed.d.ts +325 -0
  14. package/dist/domain/executed.d.ts.map +1 -0
  15. package/dist/domain/executed.js +253 -0
  16. package/dist/domain/executed.js.map +1 -0
  17. package/dist/domain/journal-entry.d.ts +468 -0
  18. package/dist/domain/journal-entry.d.ts.map +1 -0
  19. package/dist/domain/journal-entry.js +63 -0
  20. package/dist/domain/journal-entry.js.map +1 -0
  21. package/dist/domain/schemas.d.ts +950 -0
  22. package/dist/domain/schemas.d.ts.map +1 -0
  23. package/dist/domain/schemas.js +571 -0
  24. package/dist/domain/schemas.js.map +1 -0
  25. package/dist/domain/sui-schema.d.ts +12 -0
  26. package/dist/domain/sui-schema.d.ts.map +1 -0
  27. package/dist/domain/sui-schema.js +12 -0
  28. package/dist/domain/sui-schema.js.map +1 -0
  29. package/dist/extension.d.ts +7 -0
  30. package/dist/extension.d.ts.map +1 -0
  31. package/dist/extension.js +7 -0
  32. package/dist/extension.js.map +1 -0
  33. package/dist/index.d.ts +31 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +31 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/internal.d.ts +18 -0
  38. package/dist/internal.d.ts.map +1 -0
  39. package/dist/internal.js +18 -0
  40. package/dist/internal.js.map +1 -0
  41. package/dist/journal.d.ts +51 -0
  42. package/dist/journal.d.ts.map +1 -0
  43. package/dist/journal.js +18 -0
  44. package/dist/journal.js.map +1 -0
  45. package/dist/script.d.ts +7 -0
  46. package/dist/script.d.ts.map +1 -0
  47. package/dist/script.js +7 -0
  48. package/dist/script.js.map +1 -0
  49. package/dist/services/Journal.d.ts +76 -0
  50. package/dist/services/Journal.d.ts.map +1 -0
  51. package/dist/services/Journal.js +70 -0
  52. package/dist/services/Journal.js.map +1 -0
  53. package/dist/services/JournalKeyValueStore.d.ts +73 -0
  54. package/dist/services/JournalKeyValueStore.d.ts.map +1 -0
  55. package/dist/services/JournalKeyValueStore.js +155 -0
  56. package/dist/services/JournalKeyValueStore.js.map +1 -0
  57. package/dist/services/Script.d.ts +210 -0
  58. package/dist/services/Script.d.ts.map +1 -0
  59. package/dist/services/Script.js +441 -0
  60. package/dist/services/Script.js.map +1 -0
  61. package/dist/services/Signer.d.ts +132 -0
  62. package/dist/services/Signer.d.ts.map +1 -0
  63. package/dist/services/Signer.js +158 -0
  64. package/dist/services/Signer.js.map +1 -0
  65. package/dist/services/SubmitConfig.d.ts +175 -0
  66. package/dist/services/SubmitConfig.d.ts.map +1 -0
  67. package/dist/services/SubmitConfig.js +52 -0
  68. package/dist/services/SubmitConfig.js.map +1 -0
  69. package/dist/services/Sui.d.ts +295 -0
  70. package/dist/services/Sui.d.ts.map +1 -0
  71. package/dist/services/Sui.js +414 -0
  72. package/dist/services/Sui.js.map +1 -0
  73. package/dist/services/SuiCore.d.ts +227 -0
  74. package/dist/services/SuiCore.d.ts.map +1 -0
  75. package/dist/services/SuiCore.js +400 -0
  76. package/dist/services/SuiCore.js.map +1 -0
  77. package/dist/services/SuiCoreFake.d.ts +278 -0
  78. package/dist/services/SuiCoreFake.d.ts.map +1 -0
  79. package/dist/services/SuiCoreFake.js +1003 -0
  80. package/dist/services/SuiCoreFake.js.map +1 -0
  81. package/dist/services/SuiExtension.d.ts +208 -0
  82. package/dist/services/SuiExtension.d.ts.map +1 -0
  83. package/dist/services/SuiExtension.js +355 -0
  84. package/dist/services/SuiExtension.js.map +1 -0
  85. package/dist/services/SuiGraphQL.d.ts +98 -0
  86. package/dist/services/SuiGraphQL.d.ts.map +1 -0
  87. package/dist/services/SuiGraphQL.js +118 -0
  88. package/dist/services/SuiGraphQL.js.map +1 -0
  89. package/dist/services/Tx.d.ts +687 -0
  90. package/dist/services/Tx.d.ts.map +1 -0
  91. package/dist/services/Tx.js +1224 -0
  92. package/dist/services/Tx.js.map +1 -0
  93. package/dist/testing.d.ts +88 -0
  94. package/dist/testing.d.ts.map +1 -0
  95. package/dist/testing.js +163 -0
  96. package/dist/testing.js.map +1 -0
  97. package/dist/tx.d.ts +24 -0
  98. package/dist/tx.d.ts.map +1 -0
  99. package/dist/tx.js +24 -0
  100. package/dist/tx.js.map +1 -0
  101. package/docs/extensions.md +1685 -0
  102. package/examples/extension-template/README.md +170 -0
  103. package/examples/extension-template/package.json +44 -0
  104. package/examples/extension-template/scripts/check-package.ts +145 -0
  105. package/examples/extension-template/src/Escrow.ts +445 -0
  106. package/examples/extension-template/src/Platform.ts +136 -0
  107. package/examples/extension-template/src/errors.ts +61 -0
  108. package/examples/extension-template/src/extension.ts +63 -0
  109. package/examples/extension-template/src/index.ts +34 -0
  110. package/examples/extension-template/src/schema.ts +158 -0
  111. package/examples/extension-template/src/upstream.ts +66 -0
  112. package/examples/extension-template/test/escrow.test.ts +559 -0
  113. package/examples/extension-template/tsconfig.build.json +26 -0
  114. package/examples/extension-template/tsconfig.json +35 -0
  115. package/package.json +87 -0
package/AGENTS.md ADDED
@@ -0,0 +1,209 @@
1
+ # Agent guidance
2
+
3
+ `DESIGN.md` is the specification and wins over everything else; `docs/PLAN.md` is
4
+ the work plan. Where this file and the spec disagree, fix this file.
5
+
6
+ ## Ground rules
7
+
8
+ - **Effect v4, pinned to exactly `4.0.0-rc.112`.** The peer range is the exact
9
+ version, not a range: rc.113 renamed `Config.nonEmptyString`, `Config.string`
10
+ and `Config.redacted` to `Config.NonEmptyString`, `Config.String` and
11
+ `Config.Redacted`, which this package calls at four sites (`Script.ts:64,66`,
12
+ `Signer.ts:150,154`, `SuiCore.ts:458-462`, `SuiGraphQL.ts:73-77`). v3 names
13
+ are compile errors. Never
14
+ write an Effect name from memory: verify it in `node_modules/effect/dist/*.d.ts`.
15
+ The house skill `effect-ts` and its review checklist are binding.
16
+ - **SDK names are the source of truth.** Read
17
+ `node_modules/@mysten/sui/docs/llms-index.md` first, then the version-matched
18
+ page, then confirm the signature in `node_modules/@mysten/sui/dist/**/*.d.mts`.
19
+ - Everything that is not public API lives in `src/internal.ts`, which is not in
20
+ the `exports` map. `src/index.ts` is exactly the public surface.
21
+ - **Every public name mirrors the SDK name it wraps**, so an agent that knows
22
+ `@mysten/sui` can guess sui-effect.
23
+ - Nothing under `src/` imports `@effect/platform-bun` or `bun:*`. `@effect/platform-bun`
24
+ is a devDependency for tests and examples only.
25
+ - No `Effect.runPromise` / `runSync` under `src/`, except at the two documented
26
+ edges: the Promise facade of `SuiExtension.fromService`, and `Script.run`,
27
+ which is a process entrypoint and whose whole job is to fork the root fiber,
28
+ await its `Exit` and exit.
29
+ - No `any`. No `unknown` in an error channel. No `console.log` in `src/`.
30
+ - **One Move type rule.** `typeMatches(expected, actual)` (`src/domain/bcs.ts`)
31
+ is the only comparison: a bare expected tag matches every instantiation of the
32
+ generic, a parameterized one is compared in full after normalization. The
33
+ bridge, `expectedType`, `SuiSchema.decode`'s `actualType` and the fake's
34
+ owned-object filter all go through it, and so must anything new.
35
+ - **A `TransportError` for your own transport** comes from
36
+ `TransportError.fromUnknown(method, cause, retryable?)`, never from
37
+ hand-building the three fields.
38
+ - Every public function carries a JSDoc block that **states its error union in
39
+ words** ("Fails with: `ObjectNotFound`, `TransportError`."), so `LLMS.md` can be
40
+ generated from the source.
41
+ - Every dependency is a `Context.Service` with static layers (`layer`,
42
+ `layerNoDeps`, `layerConfig`, a test layer). Every error is a
43
+ `Schema.TaggedError`. Schema decodes at every boundary. Time comes from
44
+ `DateTime`/`Clock`, randomness from `Random`, config from `Config`.
45
+ - Every service method is `Effect.fn("Service.method")`, which names its span.
46
+ The only exceptions are `Sui.getObject`, `getObjectOption` and `getObjects`,
47
+ whose declared types are overload sets `Effect.fn` cannot express; their
48
+ implementations are still `Effect.fn`.
49
+ - Every SDK call forwards the Effect's `AbortSignal` into the SDK's `signal`
50
+ option, so `Effect.timeout` and interruption cancel the request.
51
+ - Tests run on `bun test` with `effect/testing` (`TestClock`, `TestSchema`).
52
+ Tests provide their own layers and never touch the network, with one gated
53
+ exception: `test/live.devnet.test.ts` runs against devnet behind `SUI_LIVE=1`
54
+ and is the only proof that a default-built transaction is accepted by a real
55
+ validator. Localnet tests sit behind `SUI_LOCALNET=1`. A test that runs
56
+ `Tx.submit` or `Tx.run` provides `Journal.layerMemory`, because the default
57
+ journal is process-wide.
58
+ - Done means `bun run check` (typecheck, build, tests, and the extension
59
+ template's own check) is green. `LLMS.md` is generated — `bun run llms` after
60
+ any public signature or example change, and a test fails if it is stale.
61
+ `docs/extensions.md` is generated from `docs/extensions.tpl.md` by
62
+ `bun run docs:extensions`, and its code blocks must stay verbatim copies of
63
+ `examples/extension-template/`.
64
+
65
+ ## The two tiers
66
+
67
+ | Tier | What it is | When to use it |
68
+ |---|---|---|
69
+ | `SuiCore` | A 1:1 Effect wrap of `ClientWithCoreApi`. One member per `SuiClientTypes.TransportMethods` key plus `getObject`, `getDynamicObjectField`, `waitForTransaction`, `signAndExecuteTransaction` and `use`. `Include` generics preserved. | Reaching a field or method `Sui` does not expose, and inside extension implementations. |
70
+ | `Sui` | The opinionated tier over `SuiCore`: fixed include sets, decoded BCS content, `Option` where absence is normal, chunked and integrity-checked batch reads (`getObjects` per-item `Result`, `getObjectsOrFail` first-error-wins), `Stream` pagination, one sender lock. | Almost all application and extension code. |
71
+
72
+ Reads go through `Sui`; writes go through `Tx`. An extension never calls
73
+ `SuiCore.executeTransaction` directly. `Sui` exposes the `SuiCore` it was built
74
+ over as `sui.core`, which is what lets every `Tx.*` function declare `R = Sui`
75
+ and nothing else.
76
+
77
+ ## The lifecycle
78
+
79
+ | Name | What it is |
80
+ |---|---|
81
+ | `Signer` | A credential as a **value**, never a service: `{ address, scheme, signTransaction, signPersonalMessage }`. One process may hold two. Secret material never reaches the value. |
82
+ | `Tx.build/sign/cosign/sponsored/submit/reconcile/run/reconcileAll` | The lifecycle as functions, each with a closed error union, all `R = Sui`. |
83
+ | `SubmitConfig` | A `Context.Reference` holding expiration policy, the optional `validFor` wall-clock bound, the gas-budget ceiling, `preflight`, the sender lock, the resubmit schedule, attempts, timeout and expiry margin, plus `expiryEvidence`, `reconcileRecheck`, `awaitVisibility`, `visibilityTimeout` and `nonce`. |
84
+ | `Journal` | A `Context.Reference` with an in-memory default. `sui-effect/journal` swaps in a durable one over `KeyValueStore`; `Tx.reconcileAll()` is the explicit startup call. |
85
+ | `Script` | `{ sui, core, signer, network }` plus `Script.run` and `Script.exitCode`. `ScriptReadOnly` is the signer-less variant, a separate key on purpose. |
86
+ | `SuiExtension.fromService` | The Promise face of an Effect service, and the only place in `src/` allowed to run Effects. Options: `sui` (chain pinning), `warm` (build the runtime synchronously in `register`). The face carries `$ready()` and `$dispose()`; a synchronous member called before the runtime exists fails with `ExtensionNotReady`, while `Effect` and `Stream` members work cold. Every registration on one client shares one base `Sui`/`SuiCore` — one chain-id read and one sender-lock map — reference counted, so `$dispose()` releases it only when the last registration does. |
87
+ | `SuiGraphQL` | A bare tag over the SDK's `SuiGraphQLClient` (`layer`, `layerConfig`, `layerUnavailable`). sui-effect wraps no GraphQL API; the tag exists so extensions share one client. |
88
+
89
+ ## Extensions
90
+
91
+ Downstream SDKs are extensions: one `Context.Service` on `Sui` and `Tx`, layers
92
+ that require `Sui` and never build a client, recipe fragments rather than
93
+ submissions, signers as parameters, errors that declare an `outcome`, and a
94
+ Promise face derived by `SuiExtension.fromService`. `docs/extensions.md` is the
95
+ contract and its review checklist; `examples/extension-template/` is the
96
+ copyable package every block of that guide is quoted from, and
97
+ `bun run check:template` is its own check.
98
+
99
+ `Tx.submit` journals `Signed` before the first execute, re-sends the identical
100
+ bytes (never a rebuild) on a retryable `TransportError` or a timeout, and
101
+ reconciles when the retries run out. A `TransportError` never escapes once bytes
102
+ may have been sent: it becomes `SubmissionUnknown`, which carries them.
103
+ `JournalError` escapes only from the `Signed` write, before anything is sent;
104
+ after the network has answered, a failed journal write is logged and the answer
105
+ stands.
106
+
107
+ `Tx.build` bounds a transaction to the current epoch and the next and names the
108
+ chain, records `sui.chainId` on `Built`/`Signed`, always simulates before
109
+ anything is signed (the SDK's resolver does it whenever there is anything to
110
+ resolve — a client with a base resolver and a preset gas budget resolves without
111
+ simulating, and `willResolve` sees that and runs one explicitly), and cancels
112
+ its in-flight request when interrupted, on **every** transport: the gRPC
113
+ resolver's simulate is reached by rebuilding the SDK's own
114
+ `GrpcCoreClient.resolveTransactionPlugin` over a client that carries the
115
+ signal. It sets no `maxTimestamp`: no Sui network accepts a timestamp
116
+ expiration yet (`test/live.devnet.test.ts`, behind `SUI_LIVE=1`, is the proof).
117
+ `Tx.run` takes `sponsor?: Signer` and requires it whenever the bytes name a gas
118
+ owner that is not the sender. `Tx.submit` waits for visibility before releasing
119
+ the sender lock.
120
+
121
+ `NotApplied` needs evidence that is checked twice: `"expired"` is
122
+ epoch-or-timestamp closed, then a `getTransaction` miss, then both again after
123
+ `SubmitConfig.reconcileRecheck`; `"inputConsumed"` is a **different**
124
+ transaction's own effects reporting `inputVersion` equal to the version the
125
+ bytes pinned. Reconcile follows the live object's `previousTransaction` — which
126
+ names the latest mutation, and is never evidence by itself — to that
127
+ transaction and reads `changedObjects[].inputVersion` off it. There is no
128
+ `v + 1` rule: Sui stamps every output with the transaction's Lamport version
129
+ (`max(input versions) + 1`), so the object "one version on" from a pinned one
130
+ usually never existed. `SuiCore.getObjectAtVersion` is still a public primitive;
131
+ it is no longer part of the rule. Every pinned reference is tried before the
132
+ answer is `SubmissionUnknown`, which is what almost every stuck submission gets.
133
+ Chain identity is compared before any recovery query, and
134
+ `reconcile`/`reconcileAll` turn every recovery read failure into
135
+ `SubmissionUnknown`.
136
+
137
+ `SuiCore` retries retryable `TransportError`s on reads only
138
+ (`Schedule.min([exponential("250 millis"), spaced("10 seconds")])` jittered, five
139
+ attempts). Retryable means gRPC `UNAVAILABLE`, `DEADLINE_EXCEEDED`,
140
+ `RESOURCE_EXHAUSTED`, `INTERNAL` or `UNKNOWN`, HTTP 5xx or 429, or a timeout;
141
+ `INTERNAL` and `UNKNOWN` are how grpc-web reports that the request never reached
142
+ a node at all. `executeTransaction` is never retried at this tier.
143
+
144
+ ## The error taxonomy
145
+
146
+ Every failure is one flat tag; there is no error inheritance.
147
+
148
+ | Tag | Means |
149
+ |---|---|
150
+ | `TransportError` | The request did not reach a usable answer. `retryable` says whether a read may try again. |
151
+ | `ObjectNotFound` / `ObjectDeleted` / `ObjectUnavailable` | The three `ObjectError.reason` values. |
152
+ | `TransactionNotFound` | No transaction with that digest is known. |
153
+ | `NetworkMismatch` | The node is on another chain than the layer was built for. |
154
+ | `DecodeError` | BCS content or a schema boundary did not decode. |
155
+ | `SimulationFailed` | Simulation reported an execution failure. No gas charged. |
156
+ | `ExecutionFailed` | Applied on chain and failed. Gas charged. |
157
+ | `SubmissionUnknown` | Bytes may have been sent; the outcome is unknown. Carries the signed bytes, unless it came from reconciling a bare digest. |
158
+ | `NotApplied` | Provably never applied. `expired`: the expiry window was observed closed and the transaction missing, twice, `reconcileRecheck` apart. `inputConsumed`: the object at the version **after** a pinned one names a **different** transaction. An input that merely moved on is not evidence. |
159
+ | `SigningError` / `BuildError` / `PolicyDenied` / `JournalError` / `UnexpectedEffects` | Signing, building, preflight policy, journal, and effects that did not contain what was expected. |
160
+ | `GraphQLUnavailable` / `ExtensionNotReady` | No usable GraphQL endpoint; a synchronous Promise-face member used before its runtime existed. Both `not_applied`. |
161
+
162
+ `SuiError.outcome(e)` puts every failure on the axis a wrapper script acts on:
163
+ `"applied"` for `ExecutionFailed` and `UnexpectedEffects`, `"unknown"` for `SubmissionUnknown`,
164
+ `"not_applied"` for every other tag in the taxonomy, and `"unknown"` for
165
+ anything that is neither one of those tags nor declares an `outcome`.
166
+ `Script.exitCode` exits 1 for that last case rather than 3. An extension error
167
+ may declare its own `outcome`, and should. `SuiError.isRetryable`, `SuiError.describe` (one actionable line) and
168
+ `SuiError.toJson` round it out.
169
+
170
+ ## Testing
171
+
172
+ `sui-effect/testing` ships `SuiCoreFake.layer(script)`, `layerTest(script)`
173
+ (the real `Sui` over the fake `SuiCore`), `layerExtensionTest(layer, script)`
174
+ (an extension's own layer over that) and `SuiTest` (`putObject`, `bumpVersion`,
175
+ `recordTransaction`, `deleteObject`, `setClock`, `setEpoch`, `scriptExecute`,
176
+ `scriptSimulate`, `scriptGetTransaction`, `calls`), which is the whole harness
177
+ an extension's tests need. Call recording is reached through `SuiTest.calls`,
178
+ not off the fake handle. The fake serves in-memory objects with
179
+ BCS content, the Clock object `0x6`, and scripted outcomes
180
+ (`FakeOutcome.succeed`, `failWith`, `transportError`, `notFound`, `timeoutThen`)
181
+ for simulate, execute, `getTransaction` and the resolver's budget simulation
182
+ (`buildSimulate`, which is how a test makes `Tx.build` fail with
183
+ `SimulationFailed`). It records every call so a test can
184
+ assert the include set that was sent. It also implements
185
+ `resolveTransactionPlugin` and `listCoins`, so `transaction.build({ client })`
186
+ against the fake's `client` resolves gas and object inputs from the script with
187
+ no network, and it keys transactions by
188
+ `TransactionDataBuilder.getDigestFromBytes` of the bytes it was handed. Any
189
+ method the script does not cover dies with a message naming it: a test never
190
+ silently passes against a stub. Its `client` implements `$extend`, so a derived
191
+ Promise face is testable the way a consumer writes it; `listOwnedObjects`
192
+ filters through `typeMatches` rather than string equality, and `getDynamicField`
193
+ matches an entry on **both** `name.type` and `name.bcs` — an entry scripted
194
+ without `bcs` still matches any key of its type, so two same-typed keys on one
195
+ parent can be told apart and a test can prove which key bytes a lookup used.
196
+ `FakeScript.transactions` (and `SuiTest.recordTransaction`) answers
197
+ `getTransaction` **by digest**, before the ordered script, which is what a
198
+ `NotApplied { inputConsumed }` test needs: the rule reads
199
+ `changedObjects[].inputVersion` off the consuming transaction, and
200
+ `FakeChange.inputVersion` is how a test says which version that was. A scripted
201
+ `commandResults` entry may give either array; the missing one defaults to `[]`
202
+ rather than failing a `Simulation` decode.
203
+
204
+ The fake enforces the invariants the lifecycle depends on: a known digest
205
+ executes idempotently, gas selection excludes object inputs, the coin set
206
+ evolves (deleted, mutated with `FakeChange.balance`, gas-bumped, created), a
207
+ submission with fewer signatures than the bytes name signers is refused, and a
208
+ version history is served through `tryGetPastObject`, which is what
209
+ `SuiCore.getObjectAtVersion` reads.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Unconfirmed Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.