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
@@ -0,0 +1,210 @@
1
+ /**
2
+ * `Script`: the preset for an on-demand script or agent run.
3
+ *
4
+ * A script reads its configuration from the environment, does one job, prints
5
+ * its answer on stdout and exits with a code a wrapper can act on. This module
6
+ * is the wiring for exactly that: one service holding the two tiers and the
7
+ * signer, one entry point that installs signal handlers and maps the `Exit` to
8
+ * a code, and an exported `exitCode` for consumers who prefer
9
+ * `BunRuntime.runMain`.
10
+ *
11
+ * Nothing here imports a platform package: `process` is all a script needs, and
12
+ * even that is injectable so a test can drive the whole thing without exiting.
13
+ *
14
+ * @since 0.1.0
15
+ */
16
+ import { Config, Context, Effect, Exit, Layer } from "effect";
17
+ import type { NetworkMismatch, TransportError } from "../domain/errors.ts";
18
+ import type { Signer } from "./Signer.ts";
19
+ import type { SuiService } from "./Sui.ts";
20
+ import { Sui } from "./Sui.ts";
21
+ import type { SuiCoreService } from "./SuiCore.ts";
22
+ import { SuiCore } from "./SuiCore.ts";
23
+ /** What a script gets from its one service. */
24
+ export interface ScriptService {
25
+ /** The opinionated tier, already pointed at the configured network. */
26
+ readonly sui: SuiService;
27
+ /** The mechanical tier, for the calls `Sui` does not expose. */
28
+ readonly core: SuiCoreService;
29
+ /** The credential read from `SUI_PRIVATE_KEY`. */
30
+ readonly signer: Signer;
31
+ /** The network this script is running against. */
32
+ readonly network: string;
33
+ }
34
+ /** What a read-only script gets: everything except a way to sign. */
35
+ export interface ScriptReadOnlyService {
36
+ readonly sui: SuiService;
37
+ readonly core: SuiCoreService;
38
+ readonly network: string;
39
+ }
40
+ /**
41
+ * `SUI_NETWORK`, with no default and with the mainnet gate.
42
+ *
43
+ * There is no default network on purpose: a script that runs against whatever
44
+ * happened to be configured is how a test transaction reaches mainnet. And a
45
+ * script that means mainnet has to say so twice, in `SUI_NETWORK` and in
46
+ * `SUI_ALLOW_MAINNET=1`.
47
+ *
48
+ * Fails with: `ConfigError`.
49
+ */
50
+ export declare const readNetwork: Effect.Effect<string, Config.ConfigError>;
51
+ declare const ScriptReadOnly_base: Context.ServiceClass<ScriptReadOnly, "sui-effect/ScriptReadOnly", ScriptReadOnlyService>;
52
+ /** The read-only script service, which has no `signer` in its type. */
53
+ export declare class ScriptReadOnly extends ScriptReadOnly_base {
54
+ }
55
+ declare const Script_base: Context.ServiceClass<Script, "sui-effect/Script", ScriptService>;
56
+ /**
57
+ * The script preset: the two tiers and the signer.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { Console, Effect } from "effect"
62
+ * import { Script } from "sui-effect/script"
63
+ *
64
+ * Script.run(Effect.gen(function*() {
65
+ * const { sui } = yield* Script
66
+ * yield* Console.log((yield* sui.chainTime).toString())
67
+ * }))
68
+ * ```
69
+ */
70
+ export declare class Script extends Script_base {
71
+ /**
72
+ * Everything a script needs, with `Sui` and `SuiCore` merged in so `Tx.*`
73
+ * works inside a script with no further wiring.
74
+ *
75
+ * Reads `SUI_NETWORK` (required, no default; `mainnet` refused unless
76
+ * `SUI_ALLOW_MAINNET=1`), `SUI_RPC_URL` (optional, defaulted per network) and
77
+ * `SUI_PRIVATE_KEY` (Bech32, through `Config.redacted`).
78
+ *
79
+ * Fails with: `ConfigError`, `NetworkMismatch`, `TransportError`.
80
+ */
81
+ static readonly layer: Layer.Layer<Script | Sui | SuiCore, Config.ConfigError | NetworkMismatch | TransportError>;
82
+ /**
83
+ * Like {@link layer}, but over a `Sui` and `SuiCore` the caller already has,
84
+ * which is how a test runs a script against the fake.
85
+ *
86
+ * Fails with: `ConfigError`.
87
+ */
88
+ static readonly layerNoDeps: Layer.Layer<Script, Config.ConfigError, Sui | SuiCore>;
89
+ /**
90
+ * A script with a signer the caller built, for a credential that does not
91
+ * come from `SUI_PRIVATE_KEY` (a KMS, a wallet, a test key).
92
+ *
93
+ * Fails with: `ConfigError`.
94
+ */
95
+ static readonly layerWithSigner: (signer: Signer) => Layer.Layer<Script, Config.ConfigError, Sui | SuiCore>;
96
+ /**
97
+ * The read-only preset, which has no signer at all: a script built on this
98
+ * cannot sign, and the compiler says so.
99
+ *
100
+ * Fails with: `ConfigError`, `NetworkMismatch`, `TransportError`.
101
+ */
102
+ static readonly layerReadOnly: Layer.Layer<ScriptReadOnly | Sui | SuiCore, Config.ConfigError | NetworkMismatch | TransportError>;
103
+ /**
104
+ * {@link layerReadOnly} over a `Sui` and `SuiCore` the caller already has,
105
+ * which is how a test exercises the read-only preset against the fake.
106
+ *
107
+ * Fails with: `ConfigError`.
108
+ */
109
+ static readonly layerReadOnlyNoDeps: Layer.Layer<ScriptReadOnly, Config.ConfigError, Sui | SuiCore>;
110
+ /** See {@link exitCode}. */
111
+ static readonly exitCode: <A, E>(exit: Exit.Exit<A, E>, options?: ExitCodeOptions) => number;
112
+ /** See {@link run}. */
113
+ static readonly run: <A, E>(effect: Effect.Effect<A, E, Script | Sui | SuiCore>, options?: ScriptRunOptions) => Promise<number>;
114
+ }
115
+ /** What {@link exitCode} needs to know beyond the `Exit` itself. */
116
+ export interface ExitCodeOptions {
117
+ /**
118
+ * How many submissions the script's journal still holds unresolved.
119
+ *
120
+ * It is what decides a timeout or an interrupt. `Effect.timeout` around a
121
+ * whole submission interrupts it and produces an outer `Cause.TimeoutError`
122
+ * that never went through `Tx.submit`'s own mapping, so the bytes may well be
123
+ * on the wire; the journal is the only thing that knows. With an unresolved
124
+ * entry the exit is 3, "reconcile before doing anything else"; with none it
125
+ * is the ordinary 4 for a timeout and 130 for an interrupt.
126
+ *
127
+ * `Script.run` fills it in from the journal the script ran with. Left out, it
128
+ * is zero, and a timeout is 4 as before.
129
+ */
130
+ readonly unresolved?: number;
131
+ }
132
+ /**
133
+ * The exit code one failure deserves.
134
+ *
135
+ * The axis is what a wrapper can act on: did the transaction apply (5, gas was
136
+ * charged, do not retry), is the outcome unknown (3, reconcile before doing
137
+ * anything else), or did nothing apply (4, safe to retry)? Configuration
138
+ * problems are 2 because no amount of retrying fixes them, a defect is 1, and
139
+ * an interrupt is 130 the way a shell expects — unless the journal says there
140
+ * is a submission outstanding, in which case it is 3, because a wrapper that
141
+ * sees 130 has no reason to go looking for one.
142
+ *
143
+ * An extension error that declares an `outcome` is honoured, so a downstream
144
+ * SDK's own failures land on the same axis. `SchemaError` — what Effect's own
145
+ * `Config.schema` and `Schema.decodeUnknownEffect` fail with — is exit 2 with
146
+ * `ConfigError`, because in a script it can only mean the input a person gave
147
+ * did not fit the schema, and no retry fixes that.
148
+ *
149
+ * An error with a tag this library has never heard of and no `outcome` is
150
+ * *unclassified* and exits 1, the code that also means defect. It deliberately
151
+ * does not follow `SuiError.outcome`, which answers `"unknown"` for the same
152
+ * value: 3 would tell a wrapper there is a transaction to reconcile, and an
153
+ * unrecognised error is not evidence that anything was ever sent. Extensions
154
+ * are told to declare `outcome` on every error precisely so their failures
155
+ * never land here. Never fails.
156
+ */
157
+ export declare const exitCode: <A, E>(exit: Exit.Exit<A, E>, options?: ExitCodeOptions) => number;
158
+ /** Where a script's diagnostics go, and how it stops. Injectable for tests. */
159
+ export interface ScriptRunOptions {
160
+ /**
161
+ * The layer to run the script over. Defaults to {@link Script.layer}, which
162
+ * reads the environment; a test passes `Script.layerNoDeps` over
163
+ * `layerTest(script)` and never touches the network.
164
+ */
165
+ readonly layer?: Layer.Layer<Script | Sui | SuiCore, Config.ConfigError | NetworkMismatch | TransportError>;
166
+ /** How the process ends. Defaults to `process.exit`. */
167
+ readonly exit?: (code: number) => void;
168
+ /** Where diagnostics go, one line at a time. Defaults to `process.stderr`. */
169
+ readonly stderr?: (line: string) => void;
170
+ /** The signal source. Defaults to `process`. */
171
+ readonly signals?: SignalSource;
172
+ /** Which signals interrupt the script. Defaults to SIGINT and SIGTERM. */
173
+ readonly signalNames?: ReadonlyArray<string>;
174
+ }
175
+ /** The part of `process` {@link run} uses, so a test can stand in for it. */
176
+ export interface SignalSource {
177
+ readonly on: (signal: string, handler: () => void) => unknown;
178
+ readonly off?: (signal: string, handler: () => void) => unknown;
179
+ }
180
+ /**
181
+ * Runs a script: builds `Script.layer`, forks the program, interrupts it on
182
+ * SIGINT or SIGTERM so finalizers run, writes one diagnostic line per failure
183
+ * to stderr, and exits with {@link exitCode}.
184
+ *
185
+ * stdout carries only what the script itself printed: the logger is bound to
186
+ * stderr for the whole run, so `Effect.log` from the script or from anything it
187
+ * calls cannot land in the script's output. A `SubmissionUnknown` additionally
188
+ * prints the base64 of the signed bytes and a line saying to reconcile, because
189
+ * those bytes are the durable record a script has.
190
+ *
191
+ * On **every** non-zero exit it also prints whatever the journal the script ran
192
+ * with still holds unresolved, which is the only record of bytes that may be on
193
+ * the wire when a script is killed — or fails — between signing and the answer.
194
+ * That count is also what decides a timeout (3 rather than 4) and an interrupt
195
+ * (3 rather than 130): an `Effect.timeout` around a submission interrupts it
196
+ * from the outside and never reaches `Tx.submit`'s own mapping.
197
+ *
198
+ * **A second SIGINT does nothing.** The handler interrupts the root fiber once;
199
+ * pressing Ctrl-C again while finalizers run is ignored, because the whole
200
+ * point of the first interrupt is to let those finalizers — the journal write
201
+ * that records what was sent, above all — complete. A script whose finalizers
202
+ * hang has to be killed with SIGKILL, which by construction no process can
203
+ * handle.
204
+ *
205
+ * Returns the exit code as well as passing it to `exit`, so a test can inject
206
+ * `exit` and assert on the number without ending the test process.
207
+ */
208
+ export declare const run: <A, E>(effect: Effect.Effect<A, E, Script | Sui | SuiCore>, options?: ScriptRunOptions) => Promise<number>;
209
+ export {};
210
+ //# sourceMappingURL=Script.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Script.d.ts","sourceRoot":"","sources":["../../src/services/Script.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,441 @@
1
+ /**
2
+ * `Script`: the preset for an on-demand script or agent run.
3
+ *
4
+ * A script reads its configuration from the environment, does one job, prints
5
+ * its answer on stdout and exits with a code a wrapper can act on. This module
6
+ * is the wiring for exactly that: one service holding the two tiers and the
7
+ * signer, one entry point that installs signal handlers and maps the `Exit` to
8
+ * a code, and an exported `exitCode` for consumers who prefer
9
+ * `BunRuntime.runMain`.
10
+ *
11
+ * Nothing here imports a platform package: `process` is all a script needs, and
12
+ * even that is injectable so a test can drive the whole thing without exiting.
13
+ *
14
+ * @since 0.1.0
15
+ */
16
+ import { Cause, Config, ConfigProvider, Context, Effect, Exit, Fiber, Layer, Logger } from "effect";
17
+ import { digestOf, SuiError as SuiErrorHelpers } from "../domain/errors.js";
18
+ import { Journal } from "./Journal.js";
19
+ import { fromConfig } from "./Signer.js";
20
+ import { Sui } from "./Sui.js";
21
+ import { SuiCore } from "./SuiCore.js";
22
+ const MAINNET_GATE = "SUI_ALLOW_MAINNET";
23
+ const configError = (message) => new Config.ConfigError(new ConfigProvider.SourceError({ message }));
24
+ /**
25
+ * `SUI_NETWORK`, with no default and with the mainnet gate.
26
+ *
27
+ * There is no default network on purpose: a script that runs against whatever
28
+ * happened to be configured is how a test transaction reaches mainnet. And a
29
+ * script that means mainnet has to say so twice, in `SUI_NETWORK` and in
30
+ * `SUI_ALLOW_MAINNET=1`.
31
+ *
32
+ * Fails with: `ConfigError`.
33
+ */
34
+ export const readNetwork = Effect.gen(function* () {
35
+ const network = yield* Config.nonEmptyString("SUI_NETWORK");
36
+ if (network !== "mainnet")
37
+ return network;
38
+ const gate = yield* Config.string(MAINNET_GATE).pipe(Config.withDefault(""));
39
+ if (gate === "1" || gate === "true")
40
+ return network;
41
+ return yield* Effect.fail(configError(`SUI_NETWORK is mainnet: set ${MAINNET_GATE}=1 to let this script touch mainnet.`));
42
+ });
43
+ /** The read-only script service, which has no `signer` in its type. */
44
+ export class ScriptReadOnly extends Context.Service()("sui-effect/ScriptReadOnly") {
45
+ }
46
+ /**
47
+ * The script preset: the two tiers and the signer.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { Console, Effect } from "effect"
52
+ * import { Script } from "sui-effect/script"
53
+ *
54
+ * Script.run(Effect.gen(function*() {
55
+ * const { sui } = yield* Script
56
+ * yield* Console.log((yield* sui.chainTime).toString())
57
+ * }))
58
+ * ```
59
+ */
60
+ export class Script extends Context.Service()("sui-effect/Script") {
61
+ /**
62
+ * Everything a script needs, with `Sui` and `SuiCore` merged in so `Tx.*`
63
+ * works inside a script with no further wiring.
64
+ *
65
+ * Reads `SUI_NETWORK` (required, no default; `mainnet` refused unless
66
+ * `SUI_ALLOW_MAINNET=1`), `SUI_RPC_URL` (optional, defaulted per network) and
67
+ * `SUI_PRIVATE_KEY` (Bech32, through `Config.redacted`).
68
+ *
69
+ * Fails with: `ConfigError`, `NetworkMismatch`, `TransportError`.
70
+ */
71
+ static layer = Layer.unwrap(
72
+ // The gate is read *before* the layer below it exists, so a script pointed
73
+ // at mainnet without `SUI_ALLOW_MAINNET=1` fails without a client having
74
+ // been built and without the chain-identifier call having been made. Read
75
+ // inside `Layer.effect` instead, and the gate would be checked after the
76
+ // connection it exists to prevent.
77
+ Effect.gen(function* () {
78
+ const network = yield* readNetwork;
79
+ const signer = yield* fromConfig();
80
+ return Layer.effect(Script, Effect.gen(function* () {
81
+ const sui = yield* Sui;
82
+ const core = yield* SuiCore;
83
+ return { sui, core, signer, network };
84
+ })).pipe(Layer.provideMerge(Sui.layerNoDeps.pipe(Layer.provideMerge(SuiCore.layerConfig))));
85
+ }));
86
+ /**
87
+ * Like {@link layer}, but over a `Sui` and `SuiCore` the caller already has,
88
+ * which is how a test runs a script against the fake.
89
+ *
90
+ * Fails with: `ConfigError`.
91
+ */
92
+ static layerNoDeps = Layer
93
+ .effect(Script, Effect.gen(function* () {
94
+ const network = yield* readNetwork;
95
+ const signer = yield* fromConfig();
96
+ const sui = yield* Sui;
97
+ const core = yield* SuiCore;
98
+ return { sui, core, signer, network };
99
+ }));
100
+ /**
101
+ * A script with a signer the caller built, for a credential that does not
102
+ * come from `SUI_PRIVATE_KEY` (a KMS, a wallet, a test key).
103
+ *
104
+ * Fails with: `ConfigError`.
105
+ */
106
+ static layerWithSigner = (signer) => Layer.effect(Script, Effect.gen(function* () {
107
+ const network = yield* readNetwork;
108
+ const sui = yield* Sui;
109
+ const core = yield* SuiCore;
110
+ return { sui, core, signer, network };
111
+ }));
112
+ /**
113
+ * The read-only preset, which has no signer at all: a script built on this
114
+ * cannot sign, and the compiler says so.
115
+ *
116
+ * Fails with: `ConfigError`, `NetworkMismatch`, `TransportError`.
117
+ */
118
+ static layerReadOnly = Layer.unwrap(
119
+ // As in {@link layer}: the mainnet gate before the client, not after it.
120
+ Effect.gen(function* () {
121
+ const network = yield* readNetwork;
122
+ return Layer.effect(ScriptReadOnly, Effect.gen(function* () {
123
+ const sui = yield* Sui;
124
+ const core = yield* SuiCore;
125
+ return { sui, core, network };
126
+ })).pipe(Layer.provideMerge(Sui.layerNoDeps.pipe(Layer.provideMerge(SuiCore.layerConfig))));
127
+ }));
128
+ /**
129
+ * {@link layerReadOnly} over a `Sui` and `SuiCore` the caller already has,
130
+ * which is how a test exercises the read-only preset against the fake.
131
+ *
132
+ * Fails with: `ConfigError`.
133
+ */
134
+ static layerReadOnlyNoDeps = Layer.effect(ScriptReadOnly, Effect.gen(function* () {
135
+ const network = yield* readNetwork;
136
+ const sui = yield* Sui;
137
+ const core = yield* SuiCore;
138
+ return { sui, core, network };
139
+ }));
140
+ /** See {@link exitCode}. */
141
+ static exitCode = (exit, options) => exitCode(exit, options);
142
+ /** See {@link run}. */
143
+ static run = (effect, options) => run(effect, options);
144
+ }
145
+ /** Exit codes, on the axis a wrapper script acts on. */
146
+ const EXIT = {
147
+ success: 0,
148
+ defect: 1,
149
+ configuration: 2,
150
+ unknown: 3,
151
+ notApplied: 4,
152
+ applied: 5,
153
+ interrupted: 130
154
+ };
155
+ const isConfigError = (error) => typeof error === "object" && error !== null &&
156
+ error._tag === "ConfigError";
157
+ const hasTag = (error) => typeof error === "object" && error !== null &&
158
+ typeof error._tag === "string";
159
+ const hasOutcomeField = (error) => typeof error === "object" && error !== null &&
160
+ typeof error.outcome === "string";
161
+ /**
162
+ * The exit code one failure deserves.
163
+ *
164
+ * The axis is what a wrapper can act on: did the transaction apply (5, gas was
165
+ * charged, do not retry), is the outcome unknown (3, reconcile before doing
166
+ * anything else), or did nothing apply (4, safe to retry)? Configuration
167
+ * problems are 2 because no amount of retrying fixes them, a defect is 1, and
168
+ * an interrupt is 130 the way a shell expects — unless the journal says there
169
+ * is a submission outstanding, in which case it is 3, because a wrapper that
170
+ * sees 130 has no reason to go looking for one.
171
+ *
172
+ * An extension error that declares an `outcome` is honoured, so a downstream
173
+ * SDK's own failures land on the same axis. `SchemaError` — what Effect's own
174
+ * `Config.schema` and `Schema.decodeUnknownEffect` fail with — is exit 2 with
175
+ * `ConfigError`, because in a script it can only mean the input a person gave
176
+ * did not fit the schema, and no retry fixes that.
177
+ *
178
+ * An error with a tag this library has never heard of and no `outcome` is
179
+ * *unclassified* and exits 1, the code that also means defect. It deliberately
180
+ * does not follow `SuiError.outcome`, which answers `"unknown"` for the same
181
+ * value: 3 would tell a wrapper there is a transaction to reconcile, and an
182
+ * unrecognised error is not evidence that anything was ever sent. Extensions
183
+ * are told to declare `outcome` on every error precisely so their failures
184
+ * never land here. Never fails.
185
+ */
186
+ export const exitCode = (exit, options) => {
187
+ if (Exit.isSuccess(exit))
188
+ return EXIT.success;
189
+ const cause = exit.cause;
190
+ const unresolved = options?.unresolved ?? 0;
191
+ if (Cause.hasInterrupts(cause) && !Cause.hasFails(cause) && !Cause.hasDies(cause)) {
192
+ return unresolved > 0 ? EXIT.unknown : EXIT.interrupted;
193
+ }
194
+ if (Cause.hasDies(cause))
195
+ return EXIT.defect;
196
+ const failure = Cause.findErrorOption(cause);
197
+ return failure._tag === "Some" ? codeOfError(failure.value, unresolved) : EXIT.defect;
198
+ };
199
+ /** The exit code of one error value, the same mapping {@link exitCode} uses. */
200
+ const codeOfError = (error, unresolved) => {
201
+ if (isConfigError(error))
202
+ return EXIT.configuration;
203
+ if (hasOutcomeField(error))
204
+ return codeOfOutcome(error.outcome);
205
+ if (!hasTag(error))
206
+ return EXIT.defect;
207
+ switch (error._tag) {
208
+ case "NetworkMismatch":
209
+ return EXIT.configuration;
210
+ case "SubmissionUnknown":
211
+ return EXIT.unknown;
212
+ case "ExecutionFailed":
213
+ // An `UnexpectedEffects` is built from an `Executed`: the transaction
214
+ // applied and gas was charged, and only the receipt is missing. Exit 4
215
+ // would tell a wrapper to run the caller's intent a second time.
216
+ case "UnexpectedEffects":
217
+ return EXIT.applied;
218
+ // `Effect.timeout` puts a `TimeoutError` in the error channel that is not
219
+ // part of the taxonomy. It used to be mapped unconditionally to "not
220
+ // applied" on the theory that `Tx.submit` turns a timed-out submission into
221
+ // `SubmissionUnknown` — but an `Effect.timeout` wrapped *around* a
222
+ // submission interrupts it from the outside and never reaches that mapping,
223
+ // so the bytes may be on the wire. The journal is what knows.
224
+ case "TimeoutError":
225
+ return unresolved > 0 ? EXIT.unknown : EXIT.notApplied;
226
+ case "TransportError":
227
+ case "ObjectNotFound":
228
+ case "ObjectDeleted":
229
+ case "ObjectUnavailable":
230
+ case "TransactionNotFound":
231
+ case "DecodeError":
232
+ case "SimulationFailed":
233
+ case "NotApplied":
234
+ case "SigningError":
235
+ case "BuildError":
236
+ case "PolicyDenied":
237
+ case "JournalError":
238
+ case "GraphQLUnavailable":
239
+ case "ExtensionNotReady":
240
+ return EXIT.notApplied;
241
+ case "SchemaError":
242
+ return EXIT.configuration;
243
+ default:
244
+ return EXIT.defect;
245
+ }
246
+ };
247
+ const codeOfOutcome = (outcome) => {
248
+ switch (outcome) {
249
+ case "applied":
250
+ return EXIT.applied;
251
+ case "unknown":
252
+ return EXIT.unknown;
253
+ default:
254
+ return EXIT.notApplied;
255
+ }
256
+ };
257
+ const describeFailure = (error) => {
258
+ if (!hasTag(error))
259
+ return [String(error)];
260
+ const lines = [];
261
+ if (!isSuiError(error)) {
262
+ const message = "message" in error && typeof error.message === "string"
263
+ ? ` ${error.message}`
264
+ : "";
265
+ return [`${error._tag}${message}`];
266
+ }
267
+ {
268
+ const tagged = error;
269
+ lines.push(SuiErrorHelpers.describe(tagged));
270
+ const digest = digestOf(tagged);
271
+ if (digest !== undefined)
272
+ lines.push(`digest: ${digest}`);
273
+ if (tagged._tag === "SubmissionUnknown") {
274
+ if (tagged.signed !== undefined) {
275
+ lines.push(`bytes: ${toBase64(tagged.signed.bytes)}`);
276
+ }
277
+ lines.push("the outcome is unknown: reconcile this digest before sending anything else from this sender");
278
+ }
279
+ }
280
+ return lines;
281
+ };
282
+ const SUI_ERROR_TAGS = new Set([
283
+ "TransportError",
284
+ "ObjectNotFound",
285
+ "ObjectDeleted",
286
+ "ObjectUnavailable",
287
+ "TransactionNotFound",
288
+ "NetworkMismatch",
289
+ "DecodeError",
290
+ "SimulationFailed",
291
+ "ExecutionFailed",
292
+ "SubmissionUnknown",
293
+ "NotApplied",
294
+ "SigningError",
295
+ "BuildError",
296
+ "PolicyDenied",
297
+ "JournalError",
298
+ "UnexpectedEffects",
299
+ "GraphQLUnavailable",
300
+ "ExtensionNotReady"
301
+ ]);
302
+ const isSuiError = (error) => SUI_ERROR_TAGS.has(error._tag);
303
+ const toBase64 = (bytes) => {
304
+ let binary = "";
305
+ for (const byte of bytes)
306
+ binary += String.fromCharCode(byte);
307
+ return btoa(binary);
308
+ };
309
+ /**
310
+ * The logger a script runs under: every `Effect.log` on the injected stderr,
311
+ * and nothing on stdout.
312
+ *
313
+ * A script's stdout is its answer — a digest, an object id, a line another
314
+ * program parses — and the default Effect logger writes to `console.log`, which
315
+ * is stdout. One `Effect.logInfo` inside a library an extension depends on
316
+ * would then corrupt the output of every script on the platform. `Logger.map`
317
+ * over `Logger.formatLogFmt` keeps the standard rendering and only changes
318
+ * where it lands.
319
+ */
320
+ const stderrLogger = (write) => Logger.map(Logger.formatLogFmt, (line) => {
321
+ write(line);
322
+ });
323
+ /** The lines describing what this process left on the wire, if anything. */
324
+ const unresolvedLines = (entries) => {
325
+ if (entries.length === 0)
326
+ return [];
327
+ const lines = [
328
+ `${entries.length} submission(s) left unresolved; reconcile them before sending anything else:`
329
+ ];
330
+ for (const entry of entries) {
331
+ lines.push(`unresolved ${entry.digest} (${entry._tag})`);
332
+ if (entry._tag === "Signed" || entry._tag === "Unknown") {
333
+ lines.push(`bytes: ${toBase64(entry.signed.bytes)}`);
334
+ }
335
+ }
336
+ return lines;
337
+ };
338
+ /**
339
+ * Reads the journal **the script actually ran with** for entries that never got
340
+ * an answer.
341
+ *
342
+ * A script interrupted mid-submit has a `Signed` entry and nothing else: no
343
+ * digest in an error, no bytes on stderr, and an exit code that says only that
344
+ * someone pressed Ctrl-C. Printing the entry is the difference between a
345
+ * transaction an operator can reconcile and one nobody can account for.
346
+ *
347
+ * The journal is captured inside the script's own runtime rather than read off
348
+ * the bare reference afterwards: a script that provided a durable `Journal` —
349
+ * which is exactly the script with something to lose — would otherwise have its
350
+ * unresolved entries looked for in the process-wide in-memory default, find
351
+ * none, and print nothing.
352
+ */
353
+ const readUnresolved = async (journal) => Effect.runPromise((journal === undefined
354
+ ? Effect.flatMap(Journal, (found) => found.listUnresolved)
355
+ : journal.listUnresolved).pipe(Effect.catchCause(() => Effect.succeed([]))));
356
+ /**
357
+ * Runs a script: builds `Script.layer`, forks the program, interrupts it on
358
+ * SIGINT or SIGTERM so finalizers run, writes one diagnostic line per failure
359
+ * to stderr, and exits with {@link exitCode}.
360
+ *
361
+ * stdout carries only what the script itself printed: the logger is bound to
362
+ * stderr for the whole run, so `Effect.log` from the script or from anything it
363
+ * calls cannot land in the script's output. A `SubmissionUnknown` additionally
364
+ * prints the base64 of the signed bytes and a line saying to reconcile, because
365
+ * those bytes are the durable record a script has.
366
+ *
367
+ * On **every** non-zero exit it also prints whatever the journal the script ran
368
+ * with still holds unresolved, which is the only record of bytes that may be on
369
+ * the wire when a script is killed — or fails — between signing and the answer.
370
+ * That count is also what decides a timeout (3 rather than 4) and an interrupt
371
+ * (3 rather than 130): an `Effect.timeout` around a submission interrupts it
372
+ * from the outside and never reaches `Tx.submit`'s own mapping.
373
+ *
374
+ * **A second SIGINT does nothing.** The handler interrupts the root fiber once;
375
+ * pressing Ctrl-C again while finalizers run is ignored, because the whole
376
+ * point of the first interrupt is to let those finalizers — the journal write
377
+ * that records what was sent, above all — complete. A script whose finalizers
378
+ * hang has to be killed with SIGKILL, which by construction no process can
379
+ * handle.
380
+ *
381
+ * Returns the exit code as well as passing it to `exit`, so a test can inject
382
+ * `exit` and assert on the number without ending the test process.
383
+ */
384
+ export const run = async (effect, options) => {
385
+ const write = options?.stderr ?? ((line) => {
386
+ process.stderr.write(`${line}\n`);
387
+ });
388
+ const stop = options?.exit ?? ((code) => {
389
+ process.exit(code);
390
+ });
391
+ const signals = options?.signals ?? process;
392
+ const names = options?.signalNames ?? ["SIGINT", "SIGTERM"];
393
+ // The journal the script ran with, captured from inside its own context so an
394
+ // interrupt can report what it left on the wire even when the script provided
395
+ // a durable journal of its own.
396
+ let journal;
397
+ const fiber = Effect.runFork(Effect.flatMap(Journal, (found) => {
398
+ journal = found;
399
+ return effect;
400
+ }).pipe(Effect.provide(Layer.merge(options?.layer ?? Script.layer, Logger.layer([stderrLogger(write)])))));
401
+ const handlers = names.map((name) => {
402
+ const handler = () => {
403
+ Effect.runFork(Fiber.interrupt(fiber));
404
+ };
405
+ signals.on(name, handler);
406
+ return [name, handler];
407
+ });
408
+ const exit = await Effect.runPromise(Fiber.await(fiber));
409
+ for (const [name, handler] of handlers)
410
+ signals.off?.(name, handler);
411
+ let unresolved = [];
412
+ if (Exit.isFailure(exit)) {
413
+ if (Cause.hasDies(exit.cause)) {
414
+ write(Cause.pretty(exit.cause));
415
+ }
416
+ else {
417
+ const failure = Cause.findErrorOption(exit.cause);
418
+ if (failure._tag === "Some") {
419
+ for (const line of describeFailure(failure.value))
420
+ write(line);
421
+ }
422
+ else if (Cause.hasInterrupts(exit.cause)) {
423
+ write("interrupted");
424
+ }
425
+ else {
426
+ write(Cause.pretty(exit.cause));
427
+ }
428
+ }
429
+ // On **every** non-zero exit, not only a defect or an interrupt: a typed
430
+ // failure escaping after a submission — an outer timeout, a preflight that
431
+ // ran too late, an extension error raised past `Tx.submit` — leaves the
432
+ // same record on the wire, and the operator needs the same bytes.
433
+ unresolved = await readUnresolved(journal);
434
+ for (const line of unresolvedLines(unresolved))
435
+ write(line);
436
+ }
437
+ const code = exitCode(exit, { unresolved: unresolved.length });
438
+ stop(code);
439
+ return code;
440
+ };
441
+ //# sourceMappingURL=Script.js.map