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
|
@@ -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
|