@unconfirmed/sui-effect 0.1.0 → 0.1.1

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 (53) hide show
  1. package/AGENTS.md +16 -6
  2. package/CHANGELOG.md +40 -0
  3. package/LLMS.md +1609 -1098
  4. package/README.md +347 -3
  5. package/dist/domain/bcs.d.ts +44 -0
  6. package/dist/domain/bcs.d.ts.map +1 -1
  7. package/dist/domain/bcs.js +57 -0
  8. package/dist/domain/bcs.js.map +1 -1
  9. package/dist/domain/errors.d.ts +63 -21
  10. package/dist/domain/errors.d.ts.map +1 -1
  11. package/dist/domain/errors.js +35 -7
  12. package/dist/domain/errors.js.map +1 -1
  13. package/dist/domain/executed.d.ts +54 -18
  14. package/dist/domain/executed.d.ts.map +1 -1
  15. package/dist/domain/journal-entry.d.ts +6 -2
  16. package/dist/domain/journal-entry.d.ts.map +1 -1
  17. package/dist/domain/schemas.d.ts +207 -66
  18. package/dist/domain/schemas.d.ts.map +1 -1
  19. package/dist/domain/schemas.js +62 -2
  20. package/dist/domain/schemas.js.map +1 -1
  21. package/dist/domain/sui-schema.d.ts +3 -2
  22. package/dist/domain/sui-schema.d.ts.map +1 -1
  23. package/dist/domain/sui-schema.js +3 -2
  24. package/dist/domain/sui-schema.js.map +1 -1
  25. package/dist/extension.d.ts +1 -1
  26. package/dist/extension.d.ts.map +1 -1
  27. package/dist/extension.js +1 -1
  28. package/dist/extension.js.map +1 -1
  29. package/dist/services/SuiCore.d.ts +12 -0
  30. package/dist/services/SuiCore.d.ts.map +1 -1
  31. package/dist/services/SuiCore.js +124 -0
  32. package/dist/services/SuiCore.js.map +1 -1
  33. package/dist/services/SuiCoreFake.d.ts +12 -0
  34. package/dist/services/SuiCoreFake.d.ts.map +1 -1
  35. package/dist/services/SuiCoreFake.js +6 -1
  36. package/dist/services/SuiCoreFake.js.map +1 -1
  37. package/dist/services/SuiExtension.d.ts +127 -14
  38. package/dist/services/SuiExtension.d.ts.map +1 -1
  39. package/dist/services/SuiExtension.js +125 -28
  40. package/dist/services/SuiExtension.js.map +1 -1
  41. package/dist/services/SuiGraphQL.d.ts +13 -0
  42. package/dist/services/SuiGraphQL.d.ts.map +1 -1
  43. package/dist/services/SuiGraphQL.js +13 -0
  44. package/dist/services/SuiGraphQL.js.map +1 -1
  45. package/dist/services/Tx.d.ts +3 -1
  46. package/dist/services/Tx.d.ts.map +1 -1
  47. package/dist/testing.d.ts +21 -3
  48. package/dist/testing.d.ts.map +1 -1
  49. package/dist/testing.js +30 -3
  50. package/dist/testing.js.map +1 -1
  51. package/docs/extensions.md +338 -25
  52. package/examples/extension-template/test/escrow.test.ts +91 -2
  53. package/package.json +3 -2
package/LLMS.md CHANGED
@@ -105,23 +105,6 @@ A transaction built into bytes and ready to sign, with the expiration the
105
105
  builder settled on recorded so `Tx.reconcile` can decide, later and without
106
106
  the builder, whether the transaction can still land.
107
107
 
108
- ### `chainOf` (const)
109
-
110
- ```ts
111
- declare const chainOf: (expiration: TransactionExpiration | undefined) => string | undefined
112
- ```
113
-
114
- The chain identifier an expiration names, or `undefined` for the variants
115
- that name none (`None`, `Epoch`).
116
-
117
- This is the first half of the chain-identity guard `Tx.reconcile` applies
118
- before it asks a node anything: bytes built for one chain must not be
119
- declared expired by another chain's epoch. The second half is
120
- `SignedTransaction.chain`, which `Tx.build` records for the variants that
121
- carry no chain of their own.
122
-
123
- **Never fails.**
124
-
125
108
  ### `ChangedObject` (const)
126
109
 
127
110
  ```ts
@@ -254,18 +237,6 @@ export declare class DecodeError extends DecodeError_base {
254
237
 
255
238
  BCS content or a schema boundary did not decode.
256
239
 
257
- ### `defaultGrpcUrl` (const)
258
-
259
- ```ts
260
- declare const defaultGrpcUrl: (network: string) => string | undefined
261
- ```
262
-
263
- The gRPC endpoint sui-effect uses when `SUI_RPC_URL` is not set. The SDK
264
- ships no such table; these are the URLs its own documentation uses.
265
- Returns `undefined` for any other network.
266
-
267
- **Never fails.**
268
-
269
240
  ### `Digest` (const)
270
241
 
271
242
  ```ts
@@ -842,42 +813,6 @@ declare const KnownNetwork: Schema
842
813
 
843
814
  The four networks with a built-in default gRPC endpoint.
844
815
 
845
- ### `maxEpochOf` (const)
846
-
847
- ```ts
848
- declare const maxEpochOf: (expiration: TransactionExpiration | undefined) => bigint | undefined
849
- ```
850
-
851
- The last epoch in which a transaction can still be applied, or `undefined`
852
- when its expiration sets no such bound (`None`, or a `ValidDuring` with no
853
- `maxEpoch`).
854
-
855
- An `Epoch` expiration is that epoch: the SDK's `Epoch` variant means "valid
856
- until the end of this epoch", so it is its own upper bound.
857
-
858
- This is the bound that matters in practice. Epochs are what the default
859
- expiration carries and what the validator rule is written in, and unlike a
860
- wall clock an epoch is a consensus fact, so `Tx.reconcile` needs no skew
861
- margin to reason about it.
862
-
863
- **Never fails.**
864
-
865
- ### `maxTimestampMsOf` (const)
866
-
867
- ```ts
868
- declare const maxTimestampMsOf: (expiration: TransactionExpiration | undefined) => bigint | undefined
869
- ```
870
-
871
- The wall-clock bound after which a transaction can no longer be applied, in
872
- milliseconds, or `undefined` when its expiration sets no such bound — which
873
- is `None`, `Epoch`, and the default `ValidDuring`, whose `maxTimestamp` is
874
- `null` unless `SubmitConfig.validFor` asks for one.
875
-
876
- `Tx.reconcile` compares it against `chainTime` with
877
- `SubmitConfig.expiryMargin` for skew, which the epoch bound needs none of.
878
-
879
- **Never fails.**
880
-
881
816
  ### `Mist` (const)
882
817
 
883
818
  ```ts
@@ -1018,8 +953,7 @@ declare const ObjectId: Schema
1018
953
  // decodes to: ObjectId
1019
954
  ```
1020
955
 
1021
- A 32-byte object id. Same encoding rules as `SuiAddress`; the brand is
1022
- separate so an address cannot be passed where an object id is expected.
956
+ The schema, plus `normalizeObjectIdValue` as `ObjectId.normalize`.
1023
957
 
1024
958
  ### `ObjectLookupError` (type)
1025
959
 
@@ -1079,17 +1013,6 @@ declare const ObjectRef: Schema
1079
1013
  Everything the transaction builder needs to consume an object again, plus the
1080
1014
  type and owner so a caller can tell a shared object from an owned one.
1081
1015
 
1082
- ### `objectRefOf` (const)
1083
-
1084
- ```ts
1085
- declare const objectRefOf: (ref: ChangedRef) => ObjectRef | undefined
1086
- ```
1087
-
1088
- The full builder reference of a changed object, when the effects carried
1089
- every field the builder needs.
1090
-
1091
- **Never fails.**
1092
-
1093
1016
  ### `ObjectType` (const)
1094
1017
 
1095
1018
  ```ts
@@ -1186,34 +1109,6 @@ export interface SdkObjectRef {
1186
1109
 
1187
1110
  The SDK's own object reference shape, as `Transaction#objectRef` takes it.
1188
1111
 
1189
- ### `sdkRefOf` (const)
1190
-
1191
- ```ts
1192
- declare const sdkRefOf: (ref: ChangedRef | ObjectRef) => SdkObjectRef | undefined
1193
- ```
1194
-
1195
- The shape `Transaction#objectRef` and `Inputs.ObjectRef` want: `objectId`
1196
- rather than `id`, and a decimal **string** version rather than a `bigint`.
1197
-
1198
- sui-effect's own `ObjectRef` carries the branded `id` and a `bigint`
1199
- `version`, because that is what a domain model wants and what a `Version`
1200
- check can be run on; the SDK builder wants neither. This is the one
1201
- conversion, so nobody writes `{ objectId: ref.id, version: String(ref.version) }`
1202
- by hand and gets the field name wrong.
1203
-
1204
- **Only for an address-owned (or immutable) object.** `tx.objectRef` pins a
1205
- version, which is exactly right for an owned input and exactly wrong for a
1206
- **shared** object: a shared object is passed with `tx.sharedObjectRef({
1207
- objectId, initialSharedVersion, mutable })` (the initial shared version lives
1208
- on `ref.owner.Shared.initialSharedVersion`), and a **receiving** object with
1209
- `tx.receivingRef(...)`, which takes the same three fields this returns.
1210
- Passing a shared object by `objectRef` produces bytes a validator rejects.
1211
-
1212
- `undefined` when the effects did not carry a version or a digest — a deleted
1213
- object has no output version, and nothing can be consumed without both.
1214
-
1215
- **Never fails.**
1216
-
1217
1112
  ### `Signature` (const)
1218
1113
 
1219
1114
  ```ts
@@ -1675,8 +1570,7 @@ declare const SuiAddress: Schema
1675
1570
  // decodes to: SuiAddress
1676
1571
  ```
1677
1572
 
1678
- A 32-byte Sui account address, normalized to the padded lowercase `0x` form on
1679
- decode. Rejects anything `isValidSuiAddress` rejects with a `SchemaError`.
1573
+ The schema, plus `normalizeSuiAddressValue` as `SuiAddress.normalize`.
1680
1574
 
1681
1575
  ### `SuiCore` (class)
1682
1576
 
@@ -1892,6 +1786,19 @@ export declare class SuiGraphQL extends SuiGraphQL_base {
1892
1786
  The SDK's GraphQL client as a service, so an extension can require it
1893
1787
  without constructing one.
1894
1788
 
1789
+ **The service value *is* the client.** `yield* SuiGraphQL` hands back the
1790
+ `SuiGraphQLClient` that was passed to `SuiGraphQL.layer(client)` — not a
1791
+ wrapper with `client` on it, not an interface of methods this package
1792
+ defines. `SuiGraphQL["Service"]` is therefore `SuiGraphQLClient`, which is
1793
+ the type to write when a function takes the service as a parameter:
1794
+
1795
+ ```ts
1796
+ const names = (graphql: SuiGraphQL["Service"]) => graphql.query({ query, variables })
1797
+ ```
1798
+
1799
+ The requirement is spelled `SuiGraphQL` in an `Effect`'s `R`, as with every
1800
+ other tag here.
1801
+
1895
1802
  ### `SuiGrpcLayerOptions` (interface)
1896
1803
 
1897
1804
  ```ts
@@ -1997,6 +1904,36 @@ the error so an operator knows which object did not decode.
1997
1904
 
1998
1905
  **Fails with: `DecodeError`.**
1999
1906
 
1907
+ #### `decodeWith` (const)
1908
+
1909
+ ```ts
1910
+ declare const decodeWith: <T extends Input, Input, A>(bcsType: BcsType<T, Input>, expectedType: string | undefined, map: (parsed: T) => A) => Schema.Codec<A, Uint8Array>
1911
+ ```
1912
+
1913
+ A BCS layout plus the mapping into a domain value, as one codec.
1914
+
1915
+ This is the shape every extension writes by hand and writes slightly
1916
+ differently: parse the bytes with a layout, then hand the raw struct to a
1917
+ constructor or a mapping function that may throw (an id that has to be
1918
+ branded, a `bigint` that has to be range-checked, a discriminant that has to
1919
+ become a union). Written out it is `SuiSchema.bcs(...)` piped into a
1920
+ `Schema.decodeTo` with a `transformOrFail` and an `Effect.try`, and the part
1921
+ that gets forgotten is turning the thrown value into a schema issue, so the
1922
+ failure arrives as a defect instead of a `DecodeError`.
1923
+
1924
+ `map` is called with whatever the layout parsed. Returning a value decodes
1925
+ it; **throwing** fails the decode, and the thrown value's message becomes the
1926
+ `DecodeError.issue` the caller sees, with the expected type already on it.
1927
+ The result is a `Schema.Codec<A, Uint8Array>` like any other: pass it as
1928
+ `sui.getObject(id, { schema })`, and the Move type check still runs first
1929
+ because the annotation `bcs` leaves behind survives the composition.
1930
+
1931
+ Encoding is not supported: a mapping function has no inverse, and inventing
1932
+ one silently is worse than saying so. Encode with the layout itself when you
1933
+ need bytes back.
1934
+
1935
+ **Fails with: `DecodeError` (through the schema), when the bytes do not parse or `map` throws.**
1936
+
2000
1937
  #### `matchesType` (const)
2001
1938
 
2002
1939
  ```ts
@@ -2533,203 +2470,119 @@ What a versioned object read found: the object as it was at that exact
2533
2470
  version, with the digest of the transaction that produced it, or nothing at
2534
2471
  all with a reason a log can print.
2535
2472
 
2536
- ## `@unconfirmed/sui-effect/tx`
2473
+ ### `chainOf` (const)
2537
2474
 
2475
+ ```ts
2476
+ declare const chainOf: (expiration: TransactionExpiration | undefined) => string | undefined
2477
+ ```
2538
2478
 
2539
- 37 exported symbols.
2479
+ The chain identifier an expiration names, or `undefined` for the variants
2480
+ that name none (`None`, `Epoch`).
2540
2481
 
2541
- ### `build` (const)
2482
+ This is the first half of the chain-identity guard `Tx.reconcile` applies
2483
+ before it asks a node anything: bytes built for one chain must not be
2484
+ declared expired by another chain's epoch. The second half is
2485
+ `SignedTransaction.chain`, which `Tx.build` records for the variants that
2486
+ carry no chain of their own.
2487
+
2488
+ **Never fails.**
2489
+
2490
+ ### `defaultGrpcUrl` (const)
2542
2491
 
2543
2492
  ```ts
2544
- declare const build: (input: Transaction | Recipe, opts: {
2545
- readonly sender: SuiAddress;
2546
- readonly gasOwner?: SuiAddress;
2547
- }) => Effect.Effect<{
2548
- readonly digest: Digest;
2549
- readonly sender: SuiAddress;
2550
- readonly bytes: Uint8Array<ArrayBufferLike>;
2551
- readonly chain?: string | undefined;
2552
- readonly expiration?: {
2553
- readonly $kind: "None";
2554
- readonly None: true;
2555
- } | {
2556
- readonly $kind: "Epoch";
2557
- readonly Epoch: bigint;
2558
- } | {
2559
- readonly $kind: "ValidDuring";
2560
- readonly ValidDuring: {
2561
- readonly minEpoch: bigint | null;
2562
- readonly maxEpoch: bigint | null;
2563
- readonly minTimestamp: bigint | null;
2564
- readonly maxTimestamp: bigint | null;
2565
- readonly chain: string;
2566
- readonly nonce: number;
2567
- };
2568
- } | {
2569
- readonly $kind: "Validity";
2570
- readonly Validity: {
2571
- readonly allowedProposers: {
2572
- readonly epoch: bigint;
2573
- readonly proposers: readonly number[];
2574
- } | null;
2575
- readonly minEpoch: bigint | null;
2576
- readonly maxEpoch: bigint | null;
2577
- readonly minTimestamp: bigint | null;
2578
- readonly maxTimestamp: bigint | null;
2579
- readonly chain: string;
2580
- readonly nonce: number;
2581
- };
2582
- } | undefined;
2583
- readonly gasOwner?: SuiAddress | undefined;
2584
- }, TransportError | SimulationFailed | BuildError, Sui>
2493
+ declare const defaultGrpcUrl: (network: string) => string | undefined
2585
2494
  ```
2586
2495
 
2587
- Builds a transaction into signable bytes.
2496
+ The gRPC endpoint sui-effect uses when `SUI_RPC_URL` is not set. The SDK
2497
+ ships no such table; these are the URLs its own documentation uses.
2498
+ Returns `undefined` for any other network.
2588
2499
 
2589
- **Building always simulates.** On gRPC the SDK's resolve plugin simulates
2590
- with checks enabled to choose the gas budget, and an execution failure there
2591
- arrives as `SimulationFailed`. That costs nothing extra — but the resolver
2592
- returns early for a transaction that was **already fully resolved** (every
2593
- input resolved, gas price, budget and payment set), and then nothing
2594
- simulates at all. `Tx.build` detects that case and runs one explicit
2595
- `simulateTransaction` with checks enabled, so simulate-before-submit holds
2596
- for every transaction: it costs nothing extra when the SDK had to resolve,
2597
- and one call otherwise.
2500
+ **Never fails.**
2598
2501
 
2599
- An interrupted build cancels the request it started: the SDK is handed a
2600
- client whose Core calls carry the Effect's `AbortSignal`.
2502
+ ### `maxEpochOf` (const)
2601
2503
 
2602
- When the recipe set no expiration, `SubmitConfig.expiration` decides one.
2603
- The default, `ValidDuring`, bounds the transaction to the **current epoch and
2604
- the next**, names the chain (bytes signed for testnet cannot land on mainnet)
2605
- and carries a `u32` nonce from `SubmitConfig.nonce`. There is **no
2606
- wall-clock bound** unless `SubmitConfig.validFor` asks for one: no Sui
2607
- network accepts a timestamp expiration yet, and a node refuses any
2608
- transaction carrying one. The expiration that ends up in the bytes is
2609
- recorded on the result, because it is what `Tx.reconcile` needs later to
2610
- prove a transaction can no longer land.
2504
+ ```ts
2505
+ declare const maxEpochOf: (expiration: TransactionExpiration | undefined) => bigint | undefined
2506
+ ```
2611
2507
 
2612
- `Tx.build` takes no sender lock; `Tx.run` is what holds one from build
2613
- through submit. Called on its own, two concurrent builds for one address can
2614
- pick the same gas coin.
2508
+ The last epoch in which a transaction can still be applied, or `undefined`
2509
+ when its expiration sets no such bound (`None`, or a `ValidDuring` with no
2510
+ `maxEpoch`).
2615
2511
 
2616
- **Fails with: `BuildError` (the recipe threw, an input could not be resolved, or the budget the node chose is over `SubmitConfig.maxGasBudget`), `SimulationFailed` (the transaction would abort on chain), `TransportError`.**
2512
+ An `Epoch` expiration is that epoch: the SDK's `Epoch` variant means "valid
2513
+ until the end of this epoch", so it is its own upper bound.
2617
2514
 
2618
- ### `Built` (const)
2515
+ This is the bound that matters in practice. Epochs are what the default
2516
+ expiration carries and what the validator rule is written in, and unlike a
2517
+ wall clock an epoch is a consensus fact, so `Tx.reconcile` needs no skew
2518
+ margin to reason about it.
2619
2519
 
2620
- Re-exported from `@unconfirmed/sui-effect`.
2520
+ **Never fails.**
2621
2521
 
2622
- ### `chainOf` (const)
2522
+ ### `maxTimestampMsOf` (const)
2623
2523
 
2624
- Re-exported from `@unconfirmed/sui-effect`.
2524
+ ```ts
2525
+ declare const maxTimestampMsOf: (expiration: TransactionExpiration | undefined) => bigint | undefined
2526
+ ```
2625
2527
 
2626
- ### `cosign` (const)
2528
+ The wall-clock bound after which a transaction can no longer be applied, in
2529
+ milliseconds, or `undefined` when its expiration sets no such bound — which
2530
+ is `None`, `Epoch`, and the default `ValidDuring`, whose `maxTimestamp` is
2531
+ `null` unless `SubmitConfig.validFor` asks for one.
2532
+
2533
+ `Tx.reconcile` compares it against `chainTime` with
2534
+ `SubmitConfig.expiryMargin` for skew, which the epoch bound needs none of.
2535
+
2536
+ **Never fails.**
2537
+
2538
+ ### `objectRefOf` (const)
2627
2539
 
2628
2540
  ```ts
2629
- declare const cosign: (signed: {
2630
- readonly digest: Digest;
2631
- readonly sender: SuiAddress;
2632
- readonly signatures: readonly Signature[];
2633
- readonly bytes: Uint8Array<ArrayBufferLike>;
2634
- readonly chain?: string | undefined;
2635
- readonly expiration?: {
2636
- readonly $kind: "None";
2637
- readonly None: true;
2638
- } | {
2639
- readonly $kind: "Epoch";
2640
- readonly Epoch: bigint;
2641
- } | {
2642
- readonly $kind: "ValidDuring";
2643
- readonly ValidDuring: {
2644
- readonly minEpoch: bigint | null;
2645
- readonly maxEpoch: bigint | null;
2646
- readonly minTimestamp: bigint | null;
2647
- readonly maxTimestamp: bigint | null;
2648
- readonly chain: string;
2649
- readonly nonce: number;
2650
- };
2651
- } | {
2652
- readonly $kind: "Validity";
2653
- readonly Validity: {
2654
- readonly allowedProposers: {
2655
- readonly epoch: bigint;
2656
- readonly proposers: readonly number[];
2657
- } | null;
2658
- readonly minEpoch: bigint | null;
2659
- readonly maxEpoch: bigint | null;
2660
- readonly minTimestamp: bigint | null;
2661
- readonly maxTimestamp: bigint | null;
2662
- readonly chain: string;
2663
- readonly nonce: number;
2664
- };
2665
- } | undefined;
2666
- }, signer: Signer) => Effect.Effect<{
2667
- readonly digest: Digest;
2668
- readonly sender: SuiAddress;
2669
- readonly signatures: readonly Signature[];
2670
- readonly bytes: Uint8Array<ArrayBufferLike>;
2671
- readonly chain?: string | undefined;
2672
- readonly expiration?: {
2673
- readonly $kind: "None";
2674
- readonly None: true;
2675
- } | {
2676
- readonly $kind: "Epoch";
2677
- readonly Epoch: bigint;
2678
- } | {
2679
- readonly $kind: "ValidDuring";
2680
- readonly ValidDuring: {
2681
- readonly minEpoch: bigint | null;
2682
- readonly maxEpoch: bigint | null;
2683
- readonly minTimestamp: bigint | null;
2684
- readonly maxTimestamp: bigint | null;
2685
- readonly chain: string;
2686
- readonly nonce: number;
2687
- };
2688
- } | {
2689
- readonly $kind: "Validity";
2690
- readonly Validity: {
2691
- readonly allowedProposers: {
2692
- readonly epoch: bigint;
2693
- readonly proposers: readonly number[];
2694
- } | null;
2695
- readonly minEpoch: bigint | null;
2696
- readonly maxEpoch: bigint | null;
2697
- readonly minTimestamp: bigint | null;
2698
- readonly maxTimestamp: bigint | null;
2699
- readonly chain: string;
2700
- readonly nonce: number;
2701
- };
2702
- } | undefined;
2703
- }, SigningError, never>
2541
+ declare const objectRefOf: (ref: ChangedRef) => ObjectRef | undefined
2704
2542
  ```
2705
2543
 
2706
- Adds one more signature to already signed bytes, for a sponsored or
2707
- multi-party transaction. The bytes are untouched, so both parties sign
2708
- exactly the same transaction.
2709
-
2710
- As in `sign`, the co-signer's address must be the sender or the gas
2711
- owner named in the bytes.
2544
+ The full builder reference of a changed object, when the effects carried
2545
+ every field the builder needs.
2712
2546
 
2713
- **Fails with: `SigningError`.**
2547
+ **Never fails.**
2714
2548
 
2715
- ### `ephemeral` (const)
2549
+ ### `sdkRefOf` (const)
2716
2550
 
2717
2551
  ```ts
2718
- declare const ephemeral: Effect.Effect<Signer>
2552
+ declare const sdkRefOf: (ref: ChangedRef | ObjectRef) => SdkObjectRef | undefined
2719
2553
  ```
2720
2554
 
2721
- A fresh Ed25519 credential that exists only for this process. For tests,
2722
- localnet and throwaway addresses.
2555
+ The shape `Transaction#objectRef` and `Inputs.ObjectRef` want: `objectId`
2556
+ rather than `id`, and a decimal **string** version rather than a `bigint`.
2723
2557
 
2724
- This is the one place sui-effect does not take randomness from Effect's
2725
- `Random`: key generation must come from a cryptographically secure source,
2726
- and `Random` is a seeded, test-controllable PRNG whose whole purpose is to be
2727
- reproducible. `new Ed25519Keypair()` uses the SDK's CSPRNG (`@noble/curves`
2728
- over `crypto.getRandomValues`). A `TestClock`-style deterministic key would
2729
- be a security bug, not a convenience.
2558
+ sui-effect's own `ObjectRef` carries the branded `id` and a `bigint`
2559
+ `version`, because that is what a domain model wants and what a `Version`
2560
+ check can be run on; the SDK builder wants neither. This is the one
2561
+ conversion, so nobody writes `{ objectId: ref.id, version: String(ref.version) }`
2562
+ by hand and gets the field name wrong.
2563
+
2564
+ **Only for an address-owned (or immutable) object.** `tx.objectRef` pins a
2565
+ version, which is exactly right for an owned input and exactly wrong for a
2566
+ **shared** object: a shared object is passed with `tx.sharedObjectRef({
2567
+ objectId, initialSharedVersion, mutable })` (the initial shared version lives
2568
+ on `ref.owner.Shared.initialSharedVersion`), and a **receiving** object with
2569
+ `tx.receivingRef(...)`, which takes the same three fields this returns.
2570
+ Passing a shared object by `objectRef` produces bytes a validator rejects.
2571
+
2572
+ `undefined` when the effects did not carry a version or a digest — a deleted
2573
+ object has no output version, and nothing can be consumed without both.
2730
2574
 
2731
2575
  **Never fails.**
2732
2576
 
2577
+ ## `@unconfirmed/sui-effect/tx`
2578
+
2579
+
2580
+ 37 exported symbols.
2581
+
2582
+ ### `Built` (const)
2583
+
2584
+ Re-exported from `@unconfirmed/sui-effect`.
2585
+
2733
2586
  ### `ExpirationPolicy` (type)
2734
2587
 
2735
2588
  ```ts
@@ -2766,351 +2619,31 @@ fact executed, twice in a row. The recheck delay makes that unlikely, not
2766
2619
  impossible; a deployment that cannot tolerate it sets `"never"` and settles
2767
2620
  unknown submissions by hand.
2768
2621
 
2769
- ### `fromConfig` (const)
2770
-
2771
- ```ts
2772
- declare const fromConfig: (name?: string) => Effect.Effect<Signer, Config.ConfigError>
2773
- ```
2774
-
2775
- Reads a Bech32 `suiprivkey1…` secret key from configuration and builds the
2776
- signer for whichever of the three schemes its flag names.
2777
-
2778
- The key is read with `Config.redacted`, and the decoded bytes never leave
2779
- this function. Neither does anything derived from them: the failure carries
2780
- one fixed sentence and no `cause`, because the decoder's own message quotes
2781
- the input it rejected.
2782
-
2783
- **Fails with: `ConfigError` when the variable is missing, is not a Bech32 Sui private key, or names a scheme that has no keypair class (`MultiSig`, `ZkLogin`, `Passkey` — use {@link remote} for those).**
2784
-
2785
- ### `fromKeypair` (const)
2786
-
2787
- ```ts
2788
- declare const fromKeypair: (keypair: Keypair) => Signer
2789
- ```
2790
-
2791
- `fromSdkSigner` under the name the spec gave it when a keypair was the
2792
- only thing it took. A `Keypair` **is** an SDK `Signer`, so this is a thin
2793
- alias kept for callers who hold one.
2794
-
2795
- **Never fails.**
2796
-
2797
- ### `fromSdkSigner` (const)
2622
+ ### `Journal` (const)
2798
2623
 
2799
2624
  ```ts
2800
- declare const fromSdkSigner: (keypair: SdkSigner) => Signer
2625
+ declare const Journal: Context.Reference<JournalService> & {
2626
+ /** A fresh in-memory journal, for a test or a process that wants its own. */
2627
+ layerMemory: Layer.Layer<never, never, never>;
2628
+ /** The in-memory implementation, for building one directly. */
2629
+ makeMemoryUnsafe: () => JournalService;
2630
+ }
2801
2631
  ```
2802
2632
 
2803
- Wraps any `@mysten/sui/cryptography` `Signer`.
2804
-
2805
- The SDK's `Signer` is the base class every credential extends: `Keypair` and
2806
- its three schemes, but also a Ledger signer, a wallet adapter's signer, a KMS
2807
- signer — anything that can `toSuiAddress`, `getKeyScheme`, `signTransaction`
2808
- and `signPersonalMessage`. Nothing here needs the secret, so nothing here
2809
- needs a keypair, and the `Signer` this returns exposes no secret material
2810
- either.
2633
+ The submission journal.
2811
2634
 
2812
- For a credential that is not an SDK `Signer` at all — a remote service, a
2813
- hardware device behind your own protocol use `remote`, which takes
2814
- Effects and the address to sign as.
2635
+ Being a `Context.Reference`, it is never in an `R`: `Tx.submit` reads it from
2636
+ context and finds the in-memory default unless something provided another.
2815
2637
 
2816
- Never fails: a bad address or signature surfaces as a `SigningError` from the
2817
- member that produced it, not from construction.
2638
+ **The default is process-wide.** A `Context.Reference`'s default value is
2639
+ computed once and cached on the reference itself, so every fiber that does
2640
+ not provide one shares a single `Map` for the life of the process. That is
2641
+ what makes a one-shot script work with zero wiring, and it is also why a
2642
+ test that runs `Tx.submit` or `Tx.run` should provide
2643
+ `Journal.layerMemory`: without it, entries from one test are visible to
2644
+ the next, and `listUnresolved` returns other tests' submissions.
2818
2645
 
2819
- ### `isUnresolved` (const)
2820
-
2821
- ```ts
2822
- declare const isUnresolved: (value: {
2823
- readonly _tag: "Unknown";
2824
- readonly digest: Digest;
2825
- readonly signed: {
2826
- readonly digest: Digest;
2827
- readonly sender: SuiAddress;
2828
- readonly signatures: readonly Signature[];
2829
- readonly bytes: Uint8Array<ArrayBufferLike>;
2830
- readonly chain?: string | undefined;
2831
- readonly expiration?: {
2832
- readonly $kind: "None";
2833
- readonly None: true;
2834
- } | {
2835
- readonly $kind: "Epoch";
2836
- readonly Epoch: bigint;
2837
- } | {
2838
- readonly $kind: "ValidDuring";
2839
- readonly ValidDuring: {
2840
- readonly minEpoch: bigint | null;
2841
- readonly maxEpoch: bigint | null;
2842
- readonly minTimestamp: bigint | null;
2843
- readonly maxTimestamp: bigint | null;
2844
- readonly chain: string;
2845
- readonly nonce: number;
2846
- };
2847
- } | {
2848
- readonly $kind: "Validity";
2849
- readonly Validity: {
2850
- readonly allowedProposers: {
2851
- readonly epoch: bigint;
2852
- readonly proposers: readonly number[];
2853
- } | null;
2854
- readonly minEpoch: bigint | null;
2855
- readonly maxEpoch: bigint | null;
2856
- readonly minTimestamp: bigint | null;
2857
- readonly maxTimestamp: bigint | null;
2858
- readonly chain: string;
2859
- readonly nonce: number;
2860
- };
2861
- } | undefined;
2862
- };
2863
- readonly lastError: string;
2864
- readonly attempts: number;
2865
- readonly at: import("effect/DateTime").Utc;
2866
- } | {
2867
- readonly _tag: "NotApplied";
2868
- readonly digest: Digest;
2869
- readonly evidence: "expired" | "inputConsumed";
2870
- readonly at: import("effect/DateTime").Utc;
2871
- } | {
2872
- readonly _tag: "Signed";
2873
- readonly digest: Digest;
2874
- readonly signed: {
2875
- readonly digest: Digest;
2876
- readonly sender: SuiAddress;
2877
- readonly signatures: readonly Signature[];
2878
- readonly bytes: Uint8Array<ArrayBufferLike>;
2879
- readonly chain?: string | undefined;
2880
- readonly expiration?: {
2881
- readonly $kind: "None";
2882
- readonly None: true;
2883
- } | {
2884
- readonly $kind: "Epoch";
2885
- readonly Epoch: bigint;
2886
- } | {
2887
- readonly $kind: "ValidDuring";
2888
- readonly ValidDuring: {
2889
- readonly minEpoch: bigint | null;
2890
- readonly maxEpoch: bigint | null;
2891
- readonly minTimestamp: bigint | null;
2892
- readonly maxTimestamp: bigint | null;
2893
- readonly chain: string;
2894
- readonly nonce: number;
2895
- };
2896
- } | {
2897
- readonly $kind: "Validity";
2898
- readonly Validity: {
2899
- readonly allowedProposers: {
2900
- readonly epoch: bigint;
2901
- readonly proposers: readonly number[];
2902
- } | null;
2903
- readonly minEpoch: bigint | null;
2904
- readonly maxEpoch: bigint | null;
2905
- readonly minTimestamp: bigint | null;
2906
- readonly maxTimestamp: bigint | null;
2907
- readonly chain: string;
2908
- readonly nonce: number;
2909
- };
2910
- } | undefined;
2911
- };
2912
- readonly signedAt: import("effect/DateTime").Utc;
2913
- } | {
2914
- readonly at: import("effect/DateTime").Utc;
2915
- readonly digest: Digest;
2916
- readonly _tag: "Executed";
2917
- readonly checkpoint?: bigint | undefined;
2918
- } | {
2919
- readonly _tag: "Failed";
2920
- readonly digest: Digest;
2921
- readonly reason: {
2922
- readonly $kind: "MoveAbort";
2923
- readonly MoveAbort: {
2924
- readonly abortCode: bigint;
2925
- readonly location?: {
2926
- readonly function?: number | undefined;
2927
- readonly package?: string | undefined;
2928
- readonly module?: string | undefined;
2929
- readonly functionName?: string | undefined;
2930
- readonly instruction?: number | undefined;
2931
- } | undefined;
2932
- readonly cleverError?: {
2933
- readonly value?: string | undefined;
2934
- readonly errorCode?: number | undefined;
2935
- readonly lineNumber?: number | undefined;
2936
- readonly constantName?: string | undefined;
2937
- readonly constantType?: string | undefined;
2938
- } | undefined;
2939
- };
2940
- } | {
2941
- readonly $kind: "SizeError";
2942
- readonly SizeError: {
2943
- readonly name: string;
2944
- readonly size: number;
2945
- readonly maxSize: number;
2946
- };
2947
- } | {
2948
- readonly $kind: "CommandArgumentError";
2949
- readonly CommandArgumentError: {
2950
- readonly argument: number;
2951
- readonly name: string;
2952
- };
2953
- } | {
2954
- readonly $kind: "TypeArgumentError";
2955
- readonly TypeArgumentError: {
2956
- readonly typeArgument: number;
2957
- readonly name: string;
2958
- };
2959
- } | {
2960
- readonly $kind: "PackageUpgradeError";
2961
- readonly PackageUpgradeError: {
2962
- readonly name: string;
2963
- readonly digest?: string | undefined;
2964
- readonly packageId?: string | undefined;
2965
- };
2966
- } | {
2967
- readonly $kind: "IndexError";
2968
- readonly IndexError: {
2969
- readonly index?: number | undefined;
2970
- readonly subresult?: number | undefined;
2971
- };
2972
- } | {
2973
- readonly $kind: "CoinDenyListError";
2974
- readonly CoinDenyListError: {
2975
- readonly coinType: string;
2976
- readonly name: string;
2977
- readonly address?: string | undefined;
2978
- };
2979
- } | {
2980
- readonly $kind: "CongestedObjects";
2981
- readonly CongestedObjects: {
2982
- readonly name: string;
2983
- readonly objects: readonly string[];
2984
- };
2985
- } | {
2986
- readonly $kind: "ObjectIdError";
2987
- readonly ObjectIdError: {
2988
- readonly objectId: string;
2989
- readonly name?: string | undefined;
2990
- };
2991
- } | {
2992
- readonly $kind: "Unknown";
2993
- };
2994
- readonly at: import("effect/DateTime").Utc;
2995
- }) => value is {
2996
- readonly _tag: "Unknown";
2997
- readonly digest: Digest;
2998
- readonly signed: {
2999
- readonly digest: Digest;
3000
- readonly sender: SuiAddress;
3001
- readonly signatures: readonly Signature[];
3002
- readonly bytes: Uint8Array<ArrayBufferLike>;
3003
- readonly chain?: string | undefined;
3004
- readonly expiration?: {
3005
- readonly $kind: "None";
3006
- readonly None: true;
3007
- } | {
3008
- readonly $kind: "Epoch";
3009
- readonly Epoch: bigint;
3010
- } | {
3011
- readonly $kind: "ValidDuring";
3012
- readonly ValidDuring: {
3013
- readonly minEpoch: bigint | null;
3014
- readonly maxEpoch: bigint | null;
3015
- readonly minTimestamp: bigint | null;
3016
- readonly maxTimestamp: bigint | null;
3017
- readonly chain: string;
3018
- readonly nonce: number;
3019
- };
3020
- } | {
3021
- readonly $kind: "Validity";
3022
- readonly Validity: {
3023
- readonly allowedProposers: {
3024
- readonly epoch: bigint;
3025
- readonly proposers: readonly number[];
3026
- } | null;
3027
- readonly minEpoch: bigint | null;
3028
- readonly maxEpoch: bigint | null;
3029
- readonly minTimestamp: bigint | null;
3030
- readonly maxTimestamp: bigint | null;
3031
- readonly chain: string;
3032
- readonly nonce: number;
3033
- };
3034
- } | undefined;
3035
- };
3036
- readonly lastError: string;
3037
- readonly attempts: number;
3038
- readonly at: import("effect/DateTime").Utc;
3039
- } | {
3040
- readonly _tag: "Signed";
3041
- readonly digest: Digest;
3042
- readonly signed: {
3043
- readonly digest: Digest;
3044
- readonly sender: SuiAddress;
3045
- readonly signatures: readonly Signature[];
3046
- readonly bytes: Uint8Array<ArrayBufferLike>;
3047
- readonly chain?: string | undefined;
3048
- readonly expiration?: {
3049
- readonly $kind: "None";
3050
- readonly None: true;
3051
- } | {
3052
- readonly $kind: "Epoch";
3053
- readonly Epoch: bigint;
3054
- } | {
3055
- readonly $kind: "ValidDuring";
3056
- readonly ValidDuring: {
3057
- readonly minEpoch: bigint | null;
3058
- readonly maxEpoch: bigint | null;
3059
- readonly minTimestamp: bigint | null;
3060
- readonly maxTimestamp: bigint | null;
3061
- readonly chain: string;
3062
- readonly nonce: number;
3063
- };
3064
- } | {
3065
- readonly $kind: "Validity";
3066
- readonly Validity: {
3067
- readonly allowedProposers: {
3068
- readonly epoch: bigint;
3069
- readonly proposers: readonly number[];
3070
- } | null;
3071
- readonly minEpoch: bigint | null;
3072
- readonly maxEpoch: bigint | null;
3073
- readonly minTimestamp: bigint | null;
3074
- readonly maxTimestamp: bigint | null;
3075
- readonly chain: string;
3076
- readonly nonce: number;
3077
- };
3078
- } | undefined;
3079
- };
3080
- readonly signedAt: import("effect/DateTime").Utc;
3081
- }
3082
- ```
3083
-
3084
- Whether this entry is still waiting for an answer, and therefore something
3085
- `Tx.reconcileAll` has work to do about.
3086
-
3087
- **Never fails.**
3088
-
3089
- ### `Journal` (const)
3090
-
3091
- ```ts
3092
- declare const Journal: Context.Reference<JournalService> & {
3093
- /** A fresh in-memory journal, for a test or a process that wants its own. */
3094
- layerMemory: Layer.Layer<never, never, never>;
3095
- /** The in-memory implementation, for building one directly. */
3096
- makeMemoryUnsafe: () => JournalService;
3097
- }
3098
- ```
3099
-
3100
- The submission journal.
3101
-
3102
- Being a `Context.Reference`, it is never in an `R`: `Tx.submit` reads it from
3103
- context and finds the in-memory default unless something provided another.
3104
-
3105
- **The default is process-wide.** A `Context.Reference`'s default value is
3106
- computed once and cached on the reference itself, so every fiber that does
3107
- not provide one shares a single `Map` for the life of the process. That is
3108
- what makes a one-shot script work with zero wiring, and it is also why a
3109
- test that runs `Tx.submit` or `Tx.run` should provide
3110
- `Journal.layerMemory`: without it, entries from one test are visible to
3111
- the next, and `listUnresolved` returns other tests' submissions.
3112
-
3113
- ### `JournalEntry` (const)
2646
+ ### `JournalEntry` (const)
3114
2647
 
3115
2648
  ```ts
3116
2649
  declare const JournalEntry: Schema
@@ -3332,138 +2865,32 @@ export interface JournalService {
3332
2865
 
3333
2866
  Reading and writing the submission journal.
3334
2867
 
3335
- ### `maxEpochOf` (const)
3336
-
3337
- Re-exported from `@unconfirmed/sui-effect`.
2868
+ ### `ReconcileInput` (type)
3338
2869
 
3339
- ### `maxTimestampMsOf` (const)
2870
+ ```ts
2871
+ export type ReconcileInput = Digest | Signed | SubmissionUnknown;
2872
+ ```
3340
2873
 
3341
- Re-exported from `@unconfirmed/sui-effect`.
2874
+ What `Tx.reconcile` can be asked about.
3342
2875
 
3343
- ### `reconcile` (const)
2876
+ ### `Reconciled` (type)
3344
2877
 
3345
2878
  ```ts
3346
- declare const reconcile: (input: ReconcileInput) => Effect.Effect<Executed, TransportError | ExecutionFailed | SubmissionUnknown | NotApplied, Sui>
2879
+ export type Reconciled = Executed | ExecutionFailed | NotApplied | SubmissionUnknown;
3347
2880
  ```
3348
2881
 
3349
- Finds out what happened to a transaction that was sent but never answered
3350
- for.
2882
+ What one entry of `Tx.reconcileAll` settled to.
3351
2883
 
3352
- A transaction the node knows is `Executed`, or `ExecutionFailed` when it
3353
- applied and aborted. A transaction the node does not know is only ever
3354
- `NotApplied` on evidence, and there are exactly two kinds:
2884
+ ### `RemoteSigner` (interface)
3355
2885
 
3356
- - `"expired"`, under an **ordered and repeated** rule, because a closed
3357
- expiration window proves only that the bytes cannot execute *later*, not
3358
- that they did not execute *earlier*, and a transaction can execute between
3359
- a lookup and an expiry check. So: the window must be observed closed, then
3360
- `getTransaction` must miss, then after `SubmitConfig.reconcileRecheck`
3361
- (two seconds by default, through the `Clock`) both must hold again. Any
3362
- other order, or a single observation, is `SubmissionUnknown`. Set
3363
- `SubmitConfig.expiryEvidence: "never"` to disable the rule entirely, which
3364
- is what a deployment behind a mixed-node load balancer wants. The residual
3365
- risk is a node whose transaction index lags its epoch view;
3366
- - `"inputConsumed"` when the transaction that last mutated a pinned object is
3367
- a **different** one **and its own effects report `inputVersion` equal to
3368
- the version these bytes pinned**, so those exact bytes can never execute
3369
- again.
3370
-
3371
- An input that merely advanced is not evidence: the transaction being
3372
- reconciled is itself the likeliest thing to have advanced it, and calling
3373
- that `NotApplied` would tell the documented retry idiom to execute the
3374
- caller's intent a second time. When the live object names *our* digest the
3375
- transaction applied and `getTransaction` is asked again; when the consuming
3376
- transaction took a **later** version than ours — which is the common case,
3377
- because Sui stamps every output with the transaction's Lamport version and
3378
- the object "one version on" from ours usually never existed — nothing is
3379
- proven and the answer is `SubmissionUnknown`, which carries the bytes so a
3380
- later process, or a person, can settle it. Every pinned reference is tried
3381
- before that answer is given.
3382
-
3383
- **Chain identity is checked before anything is asked.** Bytes built for one
3384
- chain must never be declared expired by another chain's epoch, which a
3385
- process-wide journal holding two networks' submissions makes easy to do. A
3386
- mismatch is `SubmissionUnknown` naming both chains.
3387
-
3388
- **No `TransportError` escapes.** A recovery read that fails says nothing
3389
- about whether the transaction applied, and `SuiError.outcome` puts
3390
- `TransportError` on `"not_applied"` — which would tell a wrapper to retry a
3391
- submission whose outcome is genuinely unknown. Every read failure here
3392
- becomes `SubmissionUnknown` carrying the digest, the bytes and the cause. The
3393
- tag stays in the signature so the union does not shrink under callers.
3394
-
3395
- Given only a `Digest` there can be no evidence, so an unknown transaction is
3396
- always `SubmissionUnknown`. Pass the `Signed` bytes (or the
3397
- `SubmissionUnknown` that carries them) to get the evidence rules.
3398
-
3399
- **Fails with: `ExecutionFailed`, `NotApplied`, `SubmissionUnknown`, `TransportError`.**
3400
-
3401
- ### `reconcileAll` (const)
3402
-
3403
- ```ts
3404
- declare const reconcileAll: () => Effect.Effect<readonly Reconciled[], TransportError | JournalError, Sui>
3405
- ```
3406
-
3407
- Settles every unresolved entry in the journal: the explicit startup call a
3408
- long-lived application makes after building a durable `Journal`.
3409
-
3410
- Nothing here fails per entry: each one settles to an `Executed`, an
3411
- `ExecutionFailed`, a `NotApplied` or a `SubmissionUnknown`, in the order the
3412
- journal listed them, and the journal is updated to match. Every settled entry
3413
- gets the same evidence rules `Tx.reconcile` applies — the ordered, repeated
3414
- expiry rule, the chain-identity guard and the versioned consumer check — so a
3415
- startup never reports a transaction that applied as `NotApplied`, and a
3416
- recovery read that fails becomes that entry's `SubmissionUnknown` rather than
3417
- escaping as a `TransportError` the taxonomy would call "not applied".
3418
-
3419
- The whole call fails only if the journal itself cannot be **read**: a write
3420
- that fails after an entry has been settled is logged and the answer stands,
3421
- the same rule `Tx.submit` follows.
3422
-
3423
- **Fails with: `JournalError`, `TransportError`.**
3424
-
3425
- ### `Reconciled` (type)
3426
-
3427
- ```ts
3428
- export type Reconciled = Executed | ExecutionFailed | NotApplied | SubmissionUnknown;
3429
- ```
3430
-
3431
- What one entry of `Tx.reconcileAll` settled to.
3432
-
3433
- ### `ReconcileInput` (type)
3434
-
3435
- ```ts
3436
- export type ReconcileInput = Digest | Signed | SubmissionUnknown;
3437
- ```
3438
-
3439
- What `Tx.reconcile` can be asked about.
3440
-
3441
- ### `remote` (const)
3442
-
3443
- ```ts
3444
- declare const remote: (signer: RemoteSigner) => Signer
3445
- ```
3446
-
3447
- Builds a signer around something that signs elsewhere: a KMS, a wallet, a
3448
- hardware device, another process.
3449
-
3450
- The returned signer decodes whatever the remote produced, so a malformed
3451
- signature is a `SigningError` rather than a surprise at execution. When
3452
- `signPersonalMessage` is not given, asking for one fails with `SigningError`
3453
- instead of pretending.
3454
-
3455
- **Never fails.**
3456
-
3457
- ### `RemoteSigner` (interface)
3458
-
3459
- ```ts
3460
- export interface RemoteSigner {
3461
- readonly address: SuiAddress;
3462
- readonly scheme: SignatureScheme;
3463
- readonly signTransaction: (bytes: Uint8Array) => Effect.Effect<string, SigningError>;
3464
- readonly signPersonalMessage?: (bytes: Uint8Array) => Effect.Effect<string, SigningError>;
3465
- }
3466
- ```
2886
+ ```ts
2887
+ export interface RemoteSigner {
2888
+ readonly address: SuiAddress;
2889
+ readonly scheme: SignatureScheme;
2890
+ readonly signTransaction: (bytes: Uint8Array) => Effect.Effect<string, SigningError>;
2891
+ readonly signPersonalMessage?: (bytes: Uint8Array) => Effect.Effect<string, SigningError>;
2892
+ }
2893
+ ```
3467
2894
 
3468
2895
  What `remote` needs to know about a credential it does not hold.
3469
2896
 
@@ -3473,51 +2900,6 @@ transaction's sender and gas owner and refuse a mismatch, which is the only
3473
2900
  thing standing between a misconfigured KMS key and an `INVALID_ARGUMENT`
3474
2901
  rejection that `Tx.submit` can only report as `SubmissionUnknown`.
3475
2902
 
3476
- ### `run` (const)
3477
-
3478
- ```ts
3479
- declare const run: (recipe: Transaction | Recipe, opts: {
3480
- readonly signer: Signer;
3481
- readonly gasOwner?: SuiAddress;
3482
- /**
3483
- * The gas owner's signer, for a sponsored transaction. Required whenever
3484
- * the bytes name a gas owner that is not the sender.
3485
- */
3486
- readonly sponsor?: Signer;
3487
- }) => Effect.Effect<Executed, TransportError | SimulationFailed | ExecutionFailed | SubmissionUnknown | NotApplied | SigningError | BuildError | PolicyDenied | JournalError, Sui>
3488
- ```
3489
-
3490
- Build, preflight, sign and submit, with the sender lock held throughout.
3491
-
3492
- Gas coins are chosen during build, so two transactions from one address that
3493
- overlap can pick the same coin and one of them will fail on chain. `Tx.run`
3494
- holds the sender lock from build through submit, which is the whole reason to
3495
- prefer it over calling the steps separately.
3496
-
3497
- The address that matters is the one whose coins are being spent, which is the
3498
- **gas owner** when there is one: two sponsored runs for different senders
3499
- paid by one sponsor are exactly the case that picks the same coin twice. When
3500
- sender and gas owner differ, both locks are held, in ascending address order
3501
- — a fixed order, so two runs that each need the same pair cannot deadlock by
3502
- taking them the other way round.
3503
-
3504
- When `SubmitConfig.preflight` is set it costs one extra simulate, and is
3505
- where spend limits and target policies refuse a transaction before anything
3506
- is signed.
3507
-
3508
- **A sponsored run needs both signatures.** A transaction whose gas owner is
3509
- not its sender is signed by *both* parties; one signature is bytes a
3510
- validator rejects. So when `opts.gasOwner` differs from the signer's address
3511
- — or when the recipe itself set a different gas owner, which `Tx.sponsored`
3512
- does — `opts.sponsor` is required and co-signs the same bytes. Without it
3513
- `Tx.run` fails with `SigningError` naming the address whose signature is
3514
- missing, before anything is built when the gas owner was given as an option
3515
- and immediately after the build when it came out of the recipe. Use the
3516
- explicit lifecycle (`Tx.build`, `Tx.sign`, `Tx.cosign`, `Tx.submit`) when the
3517
- two parties cannot both sign in one process.
3518
-
3519
- **Fails with: `BuildError`, `SimulationFailed`, `PolicyDenied`, `SigningError`, `ExecutionFailed`, `NotApplied`, `SubmissionUnknown`, `JournalError`, `TransportError` (from the build reads; once bytes are sent, transport failures become `SubmissionUnknown`).**
3520
-
3521
2903
  ### `RunError` (type)
3522
2904
 
3523
2905
  ```ts
@@ -3527,95 +2909,6 @@ export type RunError = BuildError | SimulationFailed | PolicyDenied | SigningErr
3527
2909
  Everything `run` can fail with, as one name: `SubmitError` plus
3528
2910
  what building, preflighting and signing can produce.
3529
2911
 
3530
- ### `sign` (const)
3531
-
3532
- ```ts
3533
- declare const sign: (built: {
3534
- readonly digest: Digest;
3535
- readonly sender: SuiAddress;
3536
- readonly bytes: Uint8Array<ArrayBufferLike>;
3537
- readonly chain?: string | undefined;
3538
- readonly expiration?: {
3539
- readonly $kind: "None";
3540
- readonly None: true;
3541
- } | {
3542
- readonly $kind: "Epoch";
3543
- readonly Epoch: bigint;
3544
- } | {
3545
- readonly $kind: "ValidDuring";
3546
- readonly ValidDuring: {
3547
- readonly minEpoch: bigint | null;
3548
- readonly maxEpoch: bigint | null;
3549
- readonly minTimestamp: bigint | null;
3550
- readonly maxTimestamp: bigint | null;
3551
- readonly chain: string;
3552
- readonly nonce: number;
3553
- };
3554
- } | {
3555
- readonly $kind: "Validity";
3556
- readonly Validity: {
3557
- readonly allowedProposers: {
3558
- readonly epoch: bigint;
3559
- readonly proposers: readonly number[];
3560
- } | null;
3561
- readonly minEpoch: bigint | null;
3562
- readonly maxEpoch: bigint | null;
3563
- readonly minTimestamp: bigint | null;
3564
- readonly maxTimestamp: bigint | null;
3565
- readonly chain: string;
3566
- readonly nonce: number;
3567
- };
3568
- } | undefined;
3569
- readonly gasOwner?: SuiAddress | undefined;
3570
- }, signer: Signer) => Effect.Effect<{
3571
- readonly digest: Digest;
3572
- readonly sender: SuiAddress;
3573
- readonly signatures: readonly Signature[];
3574
- readonly bytes: Uint8Array<ArrayBufferLike>;
3575
- readonly chain?: string | undefined;
3576
- readonly expiration?: {
3577
- readonly $kind: "None";
3578
- readonly None: true;
3579
- } | {
3580
- readonly $kind: "Epoch";
3581
- readonly Epoch: bigint;
3582
- } | {
3583
- readonly $kind: "ValidDuring";
3584
- readonly ValidDuring: {
3585
- readonly minEpoch: bigint | null;
3586
- readonly maxEpoch: bigint | null;
3587
- readonly minTimestamp: bigint | null;
3588
- readonly maxTimestamp: bigint | null;
3589
- readonly chain: string;
3590
- readonly nonce: number;
3591
- };
3592
- } | {
3593
- readonly $kind: "Validity";
3594
- readonly Validity: {
3595
- readonly allowedProposers: {
3596
- readonly epoch: bigint;
3597
- readonly proposers: readonly number[];
3598
- } | null;
3599
- readonly minEpoch: bigint | null;
3600
- readonly maxEpoch: bigint | null;
3601
- readonly minTimestamp: bigint | null;
3602
- readonly maxTimestamp: bigint | null;
3603
- readonly chain: string;
3604
- readonly nonce: number;
3605
- };
3606
- } | undefined;
3607
- }, SigningError, never>
3608
- ```
3609
-
3610
- Signs built bytes.
3611
-
3612
- The signer's address must be the transaction's sender or, for a sponsored
3613
- transaction, its gas owner; anything else is a `SigningError` rather than a
3614
- rejection at execution time. A `Signer.remote` therefore has to report the
3615
- address it signs as truthfully.
3616
-
3617
- **Fails with: `SigningError`.**
3618
-
3619
2912
  ### `Signature` (const)
3620
2913
 
3621
2914
  Re-exported from `@unconfirmed/sui-effect`.
@@ -3665,67 +2958,22 @@ Both members fail with `SigningError` and nothing else: a signer that has to
3665
2958
  reach a KMS or a wallet wraps its own transport failure in the `cause`, so a
3666
2959
  caller's error union does not grow a branch per credential kind.
3667
2960
 
3668
- ### `sponsored` (const)
2961
+ ### `SubmitConfig` (const)
3669
2962
 
3670
2963
  ```ts
3671
- declare const sponsored: (opts: {
3672
- readonly sender: SuiAddress;
3673
- readonly gasOwner: SuiAddress;
3674
- }) => (recipe: Recipe) => Recipe
2964
+ declare const SubmitConfig: Context.Reference<SubmitConfigService> & {
2965
+ defaults: SubmitConfigService;
2966
+ }
3675
2967
  ```
3676
2968
 
3677
- Turns a recipe into a sponsored one: the sender owns the transaction, the
3678
- gas owner pays, and gas comes from the sponsor's address balance rather than
3679
- from coin objects, so the two parties never have to agree on a gas coin and
3680
- can sign in either order.
2969
+ The lifecycle settings.
3681
2970
 
3682
- Never fails; the recipe it returns throws only if the recipe it wraps does,
3683
- which `Tx.build` reports as `BuildError`.
2971
+ Because this is a `Context.Reference` and not a service, it never appears in
2972
+ an `R`: a one-shot script gets the defaults with no wiring, and an
2973
+ application overrides what it cares about with
2974
+ `Effect.provideService(effect, SubmitConfig, { ...SubmitConfig.defaults, validFor: "30 seconds" })`.
3684
2975
 
3685
- ### `submit` (const)
3686
-
3687
- ```ts
3688
- declare const submit: (signed: Signed) => Effect.Effect<Executed, SubmitError, Sui>
3689
- ```
3690
-
3691
- Sends signed bytes, and does not stop caring until it knows what happened.
3692
-
3693
- Before the first `executeTransaction` it writes a `Signed` journal entry, so
3694
- a process that dies mid-flight leaves a record of bytes that may be on the
3695
- wire. A retryable transport failure or a timeout re-sends the identical bytes
3696
- on `SubmitConfig.resubmit`; nothing is ever rebuilt, so a retry can only land
3697
- the transaction that was already signed. When the retries run out it runs
3698
- `Tx.reconcile`, which either finds the transaction, proves it never applied,
3699
- or says it does not know.
3700
-
3701
- `TransportError` never escapes: once bytes may have been sent, "the network
3702
- was unreachable" is not an answer a caller can act on, so it becomes
3703
- `SubmissionUnknown` carrying the signed bytes.
3704
-
3705
- `JournalError` can only come from the `Signed` write, before anything has
3706
- been sent. Once the network has answered, a journal write that fails is
3707
- logged with `Effect.logError` and the answer stands, because "the journal is
3708
- broken" is not a thing a caller can act on and reporting it in place of a
3709
- charged `ExecutionFailed` would invite a second submission.
3710
-
3711
- **Fails with: `ExecutionFailed` (applied on chain and failed; gas was charged), `NotApplied` (provably never applied), `SubmissionUnknown` (the outcome is not known and the bytes are in the error), `JournalError` (only before the first send).**
3712
-
3713
- ### `SubmitConfig` (const)
3714
-
3715
- ```ts
3716
- declare const SubmitConfig: Context.Reference<SubmitConfigService> & {
3717
- defaults: SubmitConfigService;
3718
- }
3719
- ```
3720
-
3721
- The lifecycle settings.
3722
-
3723
- Because this is a `Context.Reference` and not a service, it never appears in
3724
- an `R`: a one-shot script gets the defaults with no wiring, and an
3725
- application overrides what it cares about with
3726
- `Effect.provideService(effect, SubmitConfig, { ...SubmitConfig.defaults, validFor: "30 seconds" })`.
3727
-
3728
- ### `SubmitConfigService` (interface)
2976
+ ### `SubmitConfigService` (interface)
3729
2977
 
3730
2978
  ```ts
3731
2979
  export interface SubmitConfigService {
@@ -4076,13 +3324,806 @@ The lifecycle, namespaced the way the spec spells it: `Tx.build`, `Tx.sign`,
4076
3324
  `Tx.cosign`, `Tx.sponsored`, `Tx.submit`, `Tx.reconcile`, `Tx.run`,
4077
3325
  `Tx.reconcileAll`.
4078
3326
 
4079
- ### `UNRESOLVED_TAGS` (const)
3327
+ ### `UNRESOLVED_TAGS` (const)
3328
+
3329
+ ```ts
3330
+ declare const UNRESOLVED_TAGS: readonly ["Signed", "Unknown"]
3331
+ ```
3332
+
3333
+ The tags whose entries still need an answer from the network.
3334
+
3335
+ ### `build` (const)
3336
+
3337
+ ```ts
3338
+ declare const build: (input: Transaction | Recipe, opts: {
3339
+ readonly sender: SuiAddress;
3340
+ readonly gasOwner?: SuiAddress;
3341
+ }) => Effect.Effect<{
3342
+ readonly digest: Digest;
3343
+ readonly sender: SuiAddress;
3344
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3345
+ readonly chain?: string | undefined;
3346
+ readonly expiration?: {
3347
+ readonly $kind: "None";
3348
+ readonly None: true;
3349
+ } | {
3350
+ readonly $kind: "Epoch";
3351
+ readonly Epoch: bigint;
3352
+ } | {
3353
+ readonly $kind: "ValidDuring";
3354
+ readonly ValidDuring: {
3355
+ readonly minEpoch: bigint | null;
3356
+ readonly maxEpoch: bigint | null;
3357
+ readonly minTimestamp: bigint | null;
3358
+ readonly maxTimestamp: bigint | null;
3359
+ readonly chain: string;
3360
+ readonly nonce: number;
3361
+ };
3362
+ } | {
3363
+ readonly $kind: "Validity";
3364
+ readonly Validity: {
3365
+ readonly allowedProposers: {
3366
+ readonly epoch: bigint;
3367
+ readonly proposers: readonly number[];
3368
+ } | null;
3369
+ readonly minEpoch: bigint | null;
3370
+ readonly maxEpoch: bigint | null;
3371
+ readonly minTimestamp: bigint | null;
3372
+ readonly maxTimestamp: bigint | null;
3373
+ readonly chain: string;
3374
+ readonly nonce: number;
3375
+ };
3376
+ } | undefined;
3377
+ readonly gasOwner?: SuiAddress | undefined;
3378
+ }, TransportError | SimulationFailed | BuildError, Sui>
3379
+ ```
3380
+
3381
+ Builds a transaction into signable bytes.
3382
+
3383
+ **Building always simulates.** On gRPC the SDK's resolve plugin simulates
3384
+ with checks enabled to choose the gas budget, and an execution failure there
3385
+ arrives as `SimulationFailed`. That costs nothing extra — but the resolver
3386
+ returns early for a transaction that was **already fully resolved** (every
3387
+ input resolved, gas price, budget and payment set), and then nothing
3388
+ simulates at all. `Tx.build` detects that case and runs one explicit
3389
+ `simulateTransaction` with checks enabled, so simulate-before-submit holds
3390
+ for every transaction: it costs nothing extra when the SDK had to resolve,
3391
+ and one call otherwise.
3392
+
3393
+ An interrupted build cancels the request it started: the SDK is handed a
3394
+ client whose Core calls carry the Effect's `AbortSignal`.
3395
+
3396
+ When the recipe set no expiration, `SubmitConfig.expiration` decides one.
3397
+ The default, `ValidDuring`, bounds the transaction to the **current epoch and
3398
+ the next**, names the chain (bytes signed for testnet cannot land on mainnet)
3399
+ and carries a `u32` nonce from `SubmitConfig.nonce`. There is **no
3400
+ wall-clock bound** unless `SubmitConfig.validFor` asks for one: no Sui
3401
+ network accepts a timestamp expiration yet, and a node refuses any
3402
+ transaction carrying one. The expiration that ends up in the bytes is
3403
+ recorded on the result, because it is what `Tx.reconcile` needs later to
3404
+ prove a transaction can no longer land.
3405
+
3406
+ `Tx.build` takes no sender lock; `Tx.run` is what holds one from build
3407
+ through submit. Called on its own, two concurrent builds for one address can
3408
+ pick the same gas coin.
3409
+
3410
+ **Fails with: `BuildError` (the recipe threw, an input could not be resolved, or the budget the node chose is over `SubmitConfig.maxGasBudget`), `SimulationFailed` (the transaction would abort on chain), `TransportError`.**
3411
+
3412
+ ### `chainOf` (const)
3413
+
3414
+ Re-exported from `@unconfirmed/sui-effect`.
3415
+
3416
+ ### `cosign` (const)
3417
+
3418
+ ```ts
3419
+ declare const cosign: (signed: {
3420
+ readonly digest: Digest;
3421
+ readonly sender: SuiAddress;
3422
+ readonly signatures: readonly Signature[];
3423
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3424
+ readonly chain?: string | undefined;
3425
+ readonly expiration?: {
3426
+ readonly $kind: "None";
3427
+ readonly None: true;
3428
+ } | {
3429
+ readonly $kind: "Epoch";
3430
+ readonly Epoch: bigint;
3431
+ } | {
3432
+ readonly $kind: "ValidDuring";
3433
+ readonly ValidDuring: {
3434
+ readonly minEpoch: bigint | null;
3435
+ readonly maxEpoch: bigint | null;
3436
+ readonly minTimestamp: bigint | null;
3437
+ readonly maxTimestamp: bigint | null;
3438
+ readonly chain: string;
3439
+ readonly nonce: number;
3440
+ };
3441
+ } | {
3442
+ readonly $kind: "Validity";
3443
+ readonly Validity: {
3444
+ readonly allowedProposers: {
3445
+ readonly epoch: bigint;
3446
+ readonly proposers: readonly number[];
3447
+ } | null;
3448
+ readonly minEpoch: bigint | null;
3449
+ readonly maxEpoch: bigint | null;
3450
+ readonly minTimestamp: bigint | null;
3451
+ readonly maxTimestamp: bigint | null;
3452
+ readonly chain: string;
3453
+ readonly nonce: number;
3454
+ };
3455
+ } | undefined;
3456
+ }, signer: Signer) => Effect.Effect<{
3457
+ readonly digest: Digest;
3458
+ readonly sender: SuiAddress;
3459
+ readonly signatures: readonly Signature[];
3460
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3461
+ readonly chain?: string | undefined;
3462
+ readonly expiration?: {
3463
+ readonly $kind: "None";
3464
+ readonly None: true;
3465
+ } | {
3466
+ readonly $kind: "Epoch";
3467
+ readonly Epoch: bigint;
3468
+ } | {
3469
+ readonly $kind: "ValidDuring";
3470
+ readonly ValidDuring: {
3471
+ readonly minEpoch: bigint | null;
3472
+ readonly maxEpoch: bigint | null;
3473
+ readonly minTimestamp: bigint | null;
3474
+ readonly maxTimestamp: bigint | null;
3475
+ readonly chain: string;
3476
+ readonly nonce: number;
3477
+ };
3478
+ } | {
3479
+ readonly $kind: "Validity";
3480
+ readonly Validity: {
3481
+ readonly allowedProposers: {
3482
+ readonly epoch: bigint;
3483
+ readonly proposers: readonly number[];
3484
+ } | null;
3485
+ readonly minEpoch: bigint | null;
3486
+ readonly maxEpoch: bigint | null;
3487
+ readonly minTimestamp: bigint | null;
3488
+ readonly maxTimestamp: bigint | null;
3489
+ readonly chain: string;
3490
+ readonly nonce: number;
3491
+ };
3492
+ } | undefined;
3493
+ }, SigningError, never>
3494
+ ```
3495
+
3496
+ Adds one more signature to already signed bytes, for a sponsored or
3497
+ multi-party transaction. The bytes are untouched, so both parties sign
3498
+ exactly the same transaction.
3499
+
3500
+ As in `sign`, the co-signer's address must be the sender or the gas
3501
+ owner named in the bytes.
3502
+
3503
+ **Fails with: `SigningError`.**
3504
+
3505
+ ### `ephemeral` (const)
3506
+
3507
+ ```ts
3508
+ declare const ephemeral: Effect.Effect<Signer>
3509
+ ```
3510
+
3511
+ A fresh Ed25519 credential that exists only for this process. For tests,
3512
+ localnet and throwaway addresses.
3513
+
3514
+ This is the one place sui-effect does not take randomness from Effect's
3515
+ `Random`: key generation must come from a cryptographically secure source,
3516
+ and `Random` is a seeded, test-controllable PRNG whose whole purpose is to be
3517
+ reproducible. `new Ed25519Keypair()` uses the SDK's CSPRNG (`@noble/curves`
3518
+ over `crypto.getRandomValues`). A `TestClock`-style deterministic key would
3519
+ be a security bug, not a convenience.
3520
+
3521
+ **Never fails.**
3522
+
3523
+ ### `fromConfig` (const)
3524
+
3525
+ ```ts
3526
+ declare const fromConfig: (name?: string) => Effect.Effect<Signer, Config.ConfigError>
3527
+ ```
3528
+
3529
+ Reads a Bech32 `suiprivkey1…` secret key from configuration and builds the
3530
+ signer for whichever of the three schemes its flag names.
3531
+
3532
+ The key is read with `Config.redacted`, and the decoded bytes never leave
3533
+ this function. Neither does anything derived from them: the failure carries
3534
+ one fixed sentence and no `cause`, because the decoder's own message quotes
3535
+ the input it rejected.
3536
+
3537
+ **Fails with: `ConfigError` when the variable is missing, is not a Bech32 Sui private key, or names a scheme that has no keypair class (`MultiSig`, `ZkLogin`, `Passkey` — use {@link remote} for those).**
3538
+
3539
+ ### `fromKeypair` (const)
3540
+
3541
+ ```ts
3542
+ declare const fromKeypair: (keypair: Keypair) => Signer
3543
+ ```
3544
+
3545
+ `fromSdkSigner` under the name the spec gave it when a keypair was the
3546
+ only thing it took. A `Keypair` **is** an SDK `Signer`, so this is a thin
3547
+ alias kept for callers who hold one.
3548
+
3549
+ **Never fails.**
3550
+
3551
+ ### `fromSdkSigner` (const)
3552
+
3553
+ ```ts
3554
+ declare const fromSdkSigner: (keypair: SdkSigner) => Signer
3555
+ ```
3556
+
3557
+ Wraps any `@mysten/sui/cryptography` `Signer`.
3558
+
3559
+ The SDK's `Signer` is the base class every credential extends: `Keypair` and
3560
+ its three schemes, but also a Ledger signer, a wallet adapter's signer, a KMS
3561
+ signer — anything that can `toSuiAddress`, `getKeyScheme`, `signTransaction`
3562
+ and `signPersonalMessage`. Nothing here needs the secret, so nothing here
3563
+ needs a keypair, and the `Signer` this returns exposes no secret material
3564
+ either.
3565
+
3566
+ For a credential that is not an SDK `Signer` at all — a remote service, a
3567
+ hardware device behind your own protocol — use `remote`, which takes
3568
+ Effects and the address to sign as.
3569
+
3570
+ Never fails: a bad address or signature surfaces as a `SigningError` from the
3571
+ member that produced it, not from construction.
3572
+
3573
+ ### `isUnresolved` (const)
3574
+
3575
+ ```ts
3576
+ declare const isUnresolved: (value: {
3577
+ readonly _tag: "Unknown";
3578
+ readonly digest: Digest;
3579
+ readonly signed: {
3580
+ readonly digest: Digest;
3581
+ readonly sender: SuiAddress;
3582
+ readonly signatures: readonly Signature[];
3583
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3584
+ readonly chain?: string | undefined;
3585
+ readonly expiration?: {
3586
+ readonly $kind: "None";
3587
+ readonly None: true;
3588
+ } | {
3589
+ readonly $kind: "Epoch";
3590
+ readonly Epoch: bigint;
3591
+ } | {
3592
+ readonly $kind: "ValidDuring";
3593
+ readonly ValidDuring: {
3594
+ readonly minEpoch: bigint | null;
3595
+ readonly maxEpoch: bigint | null;
3596
+ readonly minTimestamp: bigint | null;
3597
+ readonly maxTimestamp: bigint | null;
3598
+ readonly chain: string;
3599
+ readonly nonce: number;
3600
+ };
3601
+ } | {
3602
+ readonly $kind: "Validity";
3603
+ readonly Validity: {
3604
+ readonly allowedProposers: {
3605
+ readonly epoch: bigint;
3606
+ readonly proposers: readonly number[];
3607
+ } | null;
3608
+ readonly minEpoch: bigint | null;
3609
+ readonly maxEpoch: bigint | null;
3610
+ readonly minTimestamp: bigint | null;
3611
+ readonly maxTimestamp: bigint | null;
3612
+ readonly chain: string;
3613
+ readonly nonce: number;
3614
+ };
3615
+ } | undefined;
3616
+ };
3617
+ readonly lastError: string;
3618
+ readonly attempts: number;
3619
+ readonly at: import("effect/DateTime").Utc;
3620
+ } | {
3621
+ readonly _tag: "NotApplied";
3622
+ readonly digest: Digest;
3623
+ readonly evidence: "expired" | "inputConsumed";
3624
+ readonly at: import("effect/DateTime").Utc;
3625
+ } | {
3626
+ readonly _tag: "Signed";
3627
+ readonly digest: Digest;
3628
+ readonly signed: {
3629
+ readonly digest: Digest;
3630
+ readonly sender: SuiAddress;
3631
+ readonly signatures: readonly Signature[];
3632
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3633
+ readonly chain?: string | undefined;
3634
+ readonly expiration?: {
3635
+ readonly $kind: "None";
3636
+ readonly None: true;
3637
+ } | {
3638
+ readonly $kind: "Epoch";
3639
+ readonly Epoch: bigint;
3640
+ } | {
3641
+ readonly $kind: "ValidDuring";
3642
+ readonly ValidDuring: {
3643
+ readonly minEpoch: bigint | null;
3644
+ readonly maxEpoch: bigint | null;
3645
+ readonly minTimestamp: bigint | null;
3646
+ readonly maxTimestamp: bigint | null;
3647
+ readonly chain: string;
3648
+ readonly nonce: number;
3649
+ };
3650
+ } | {
3651
+ readonly $kind: "Validity";
3652
+ readonly Validity: {
3653
+ readonly allowedProposers: {
3654
+ readonly epoch: bigint;
3655
+ readonly proposers: readonly number[];
3656
+ } | null;
3657
+ readonly minEpoch: bigint | null;
3658
+ readonly maxEpoch: bigint | null;
3659
+ readonly minTimestamp: bigint | null;
3660
+ readonly maxTimestamp: bigint | null;
3661
+ readonly chain: string;
3662
+ readonly nonce: number;
3663
+ };
3664
+ } | undefined;
3665
+ };
3666
+ readonly signedAt: import("effect/DateTime").Utc;
3667
+ } | {
3668
+ readonly at: import("effect/DateTime").Utc;
3669
+ readonly digest: Digest;
3670
+ readonly _tag: "Executed";
3671
+ readonly checkpoint?: bigint | undefined;
3672
+ } | {
3673
+ readonly _tag: "Failed";
3674
+ readonly digest: Digest;
3675
+ readonly reason: {
3676
+ readonly $kind: "MoveAbort";
3677
+ readonly MoveAbort: {
3678
+ readonly abortCode: bigint;
3679
+ readonly location?: {
3680
+ readonly function?: number | undefined;
3681
+ readonly package?: string | undefined;
3682
+ readonly module?: string | undefined;
3683
+ readonly functionName?: string | undefined;
3684
+ readonly instruction?: number | undefined;
3685
+ } | undefined;
3686
+ readonly cleverError?: {
3687
+ readonly value?: string | undefined;
3688
+ readonly errorCode?: number | undefined;
3689
+ readonly lineNumber?: number | undefined;
3690
+ readonly constantName?: string | undefined;
3691
+ readonly constantType?: string | undefined;
3692
+ } | undefined;
3693
+ };
3694
+ } | {
3695
+ readonly $kind: "SizeError";
3696
+ readonly SizeError: {
3697
+ readonly name: string;
3698
+ readonly size: number;
3699
+ readonly maxSize: number;
3700
+ };
3701
+ } | {
3702
+ readonly $kind: "CommandArgumentError";
3703
+ readonly CommandArgumentError: {
3704
+ readonly argument: number;
3705
+ readonly name: string;
3706
+ };
3707
+ } | {
3708
+ readonly $kind: "TypeArgumentError";
3709
+ readonly TypeArgumentError: {
3710
+ readonly typeArgument: number;
3711
+ readonly name: string;
3712
+ };
3713
+ } | {
3714
+ readonly $kind: "PackageUpgradeError";
3715
+ readonly PackageUpgradeError: {
3716
+ readonly name: string;
3717
+ readonly digest?: string | undefined;
3718
+ readonly packageId?: string | undefined;
3719
+ };
3720
+ } | {
3721
+ readonly $kind: "IndexError";
3722
+ readonly IndexError: {
3723
+ readonly index?: number | undefined;
3724
+ readonly subresult?: number | undefined;
3725
+ };
3726
+ } | {
3727
+ readonly $kind: "CoinDenyListError";
3728
+ readonly CoinDenyListError: {
3729
+ readonly coinType: string;
3730
+ readonly name: string;
3731
+ readonly address?: string | undefined;
3732
+ };
3733
+ } | {
3734
+ readonly $kind: "CongestedObjects";
3735
+ readonly CongestedObjects: {
3736
+ readonly name: string;
3737
+ readonly objects: readonly string[];
3738
+ };
3739
+ } | {
3740
+ readonly $kind: "ObjectIdError";
3741
+ readonly ObjectIdError: {
3742
+ readonly objectId: string;
3743
+ readonly name?: string | undefined;
3744
+ };
3745
+ } | {
3746
+ readonly $kind: "Unknown";
3747
+ };
3748
+ readonly at: import("effect/DateTime").Utc;
3749
+ }) => value is {
3750
+ readonly _tag: "Unknown";
3751
+ readonly digest: Digest;
3752
+ readonly signed: {
3753
+ readonly digest: Digest;
3754
+ readonly sender: SuiAddress;
3755
+ readonly signatures: readonly Signature[];
3756
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3757
+ readonly chain?: string | undefined;
3758
+ readonly expiration?: {
3759
+ readonly $kind: "None";
3760
+ readonly None: true;
3761
+ } | {
3762
+ readonly $kind: "Epoch";
3763
+ readonly Epoch: bigint;
3764
+ } | {
3765
+ readonly $kind: "ValidDuring";
3766
+ readonly ValidDuring: {
3767
+ readonly minEpoch: bigint | null;
3768
+ readonly maxEpoch: bigint | null;
3769
+ readonly minTimestamp: bigint | null;
3770
+ readonly maxTimestamp: bigint | null;
3771
+ readonly chain: string;
3772
+ readonly nonce: number;
3773
+ };
3774
+ } | {
3775
+ readonly $kind: "Validity";
3776
+ readonly Validity: {
3777
+ readonly allowedProposers: {
3778
+ readonly epoch: bigint;
3779
+ readonly proposers: readonly number[];
3780
+ } | null;
3781
+ readonly minEpoch: bigint | null;
3782
+ readonly maxEpoch: bigint | null;
3783
+ readonly minTimestamp: bigint | null;
3784
+ readonly maxTimestamp: bigint | null;
3785
+ readonly chain: string;
3786
+ readonly nonce: number;
3787
+ };
3788
+ } | undefined;
3789
+ };
3790
+ readonly lastError: string;
3791
+ readonly attempts: number;
3792
+ readonly at: import("effect/DateTime").Utc;
3793
+ } | {
3794
+ readonly _tag: "Signed";
3795
+ readonly digest: Digest;
3796
+ readonly signed: {
3797
+ readonly digest: Digest;
3798
+ readonly sender: SuiAddress;
3799
+ readonly signatures: readonly Signature[];
3800
+ readonly bytes: Uint8Array<ArrayBufferLike>;
3801
+ readonly chain?: string | undefined;
3802
+ readonly expiration?: {
3803
+ readonly $kind: "None";
3804
+ readonly None: true;
3805
+ } | {
3806
+ readonly $kind: "Epoch";
3807
+ readonly Epoch: bigint;
3808
+ } | {
3809
+ readonly $kind: "ValidDuring";
3810
+ readonly ValidDuring: {
3811
+ readonly minEpoch: bigint | null;
3812
+ readonly maxEpoch: bigint | null;
3813
+ readonly minTimestamp: bigint | null;
3814
+ readonly maxTimestamp: bigint | null;
3815
+ readonly chain: string;
3816
+ readonly nonce: number;
3817
+ };
3818
+ } | {
3819
+ readonly $kind: "Validity";
3820
+ readonly Validity: {
3821
+ readonly allowedProposers: {
3822
+ readonly epoch: bigint;
3823
+ readonly proposers: readonly number[];
3824
+ } | null;
3825
+ readonly minEpoch: bigint | null;
3826
+ readonly maxEpoch: bigint | null;
3827
+ readonly minTimestamp: bigint | null;
3828
+ readonly maxTimestamp: bigint | null;
3829
+ readonly chain: string;
3830
+ readonly nonce: number;
3831
+ };
3832
+ } | undefined;
3833
+ };
3834
+ readonly signedAt: import("effect/DateTime").Utc;
3835
+ }
3836
+ ```
3837
+
3838
+ Whether this entry is still waiting for an answer, and therefore something
3839
+ `Tx.reconcileAll` has work to do about.
3840
+
3841
+ **Never fails.**
3842
+
3843
+ ### `maxEpochOf` (const)
3844
+
3845
+ Re-exported from `@unconfirmed/sui-effect`.
3846
+
3847
+ ### `maxTimestampMsOf` (const)
3848
+
3849
+ Re-exported from `@unconfirmed/sui-effect`.
3850
+
3851
+ ### `reconcile` (const)
3852
+
3853
+ ```ts
3854
+ declare const reconcile: (input: ReconcileInput) => Effect.Effect<Executed, TransportError | ExecutionFailed | SubmissionUnknown | NotApplied, Sui>
3855
+ ```
3856
+
3857
+ Finds out what happened to a transaction that was sent but never answered
3858
+ for.
3859
+
3860
+ A transaction the node knows is `Executed`, or `ExecutionFailed` when it
3861
+ applied and aborted. A transaction the node does not know is only ever
3862
+ `NotApplied` on evidence, and there are exactly two kinds:
3863
+
3864
+ - `"expired"`, under an **ordered and repeated** rule, because a closed
3865
+ expiration window proves only that the bytes cannot execute *later*, not
3866
+ that they did not execute *earlier*, and a transaction can execute between
3867
+ a lookup and an expiry check. So: the window must be observed closed, then
3868
+ `getTransaction` must miss, then — after `SubmitConfig.reconcileRecheck`
3869
+ (two seconds by default, through the `Clock`) — both must hold again. Any
3870
+ other order, or a single observation, is `SubmissionUnknown`. Set
3871
+ `SubmitConfig.expiryEvidence: "never"` to disable the rule entirely, which
3872
+ is what a deployment behind a mixed-node load balancer wants. The residual
3873
+ risk is a node whose transaction index lags its epoch view;
3874
+ - `"inputConsumed"` when the transaction that last mutated a pinned object is
3875
+ a **different** one **and its own effects report `inputVersion` equal to
3876
+ the version these bytes pinned**, so those exact bytes can never execute
3877
+ again.
3878
+
3879
+ An input that merely advanced is not evidence: the transaction being
3880
+ reconciled is itself the likeliest thing to have advanced it, and calling
3881
+ that `NotApplied` would tell the documented retry idiom to execute the
3882
+ caller's intent a second time. When the live object names *our* digest the
3883
+ transaction applied and `getTransaction` is asked again; when the consuming
3884
+ transaction took a **later** version than ours — which is the common case,
3885
+ because Sui stamps every output with the transaction's Lamport version and
3886
+ the object "one version on" from ours usually never existed — nothing is
3887
+ proven and the answer is `SubmissionUnknown`, which carries the bytes so a
3888
+ later process, or a person, can settle it. Every pinned reference is tried
3889
+ before that answer is given.
3890
+
3891
+ **Chain identity is checked before anything is asked.** Bytes built for one
3892
+ chain must never be declared expired by another chain's epoch, which a
3893
+ process-wide journal holding two networks' submissions makes easy to do. A
3894
+ mismatch is `SubmissionUnknown` naming both chains.
3895
+
3896
+ **No `TransportError` escapes.** A recovery read that fails says nothing
3897
+ about whether the transaction applied, and `SuiError.outcome` puts
3898
+ `TransportError` on `"not_applied"` — which would tell a wrapper to retry a
3899
+ submission whose outcome is genuinely unknown. Every read failure here
3900
+ becomes `SubmissionUnknown` carrying the digest, the bytes and the cause. The
3901
+ tag stays in the signature so the union does not shrink under callers.
3902
+
3903
+ Given only a `Digest` there can be no evidence, so an unknown transaction is
3904
+ always `SubmissionUnknown`. Pass the `Signed` bytes (or the
3905
+ `SubmissionUnknown` that carries them) to get the evidence rules.
3906
+
3907
+ **Fails with: `ExecutionFailed`, `NotApplied`, `SubmissionUnknown`, `TransportError`.**
3908
+
3909
+ ### `reconcileAll` (const)
3910
+
3911
+ ```ts
3912
+ declare const reconcileAll: () => Effect.Effect<readonly Reconciled[], TransportError | JournalError, Sui>
3913
+ ```
3914
+
3915
+ Settles every unresolved entry in the journal: the explicit startup call a
3916
+ long-lived application makes after building a durable `Journal`.
3917
+
3918
+ Nothing here fails per entry: each one settles to an `Executed`, an
3919
+ `ExecutionFailed`, a `NotApplied` or a `SubmissionUnknown`, in the order the
3920
+ journal listed them, and the journal is updated to match. Every settled entry
3921
+ gets the same evidence rules `Tx.reconcile` applies — the ordered, repeated
3922
+ expiry rule, the chain-identity guard and the versioned consumer check — so a
3923
+ startup never reports a transaction that applied as `NotApplied`, and a
3924
+ recovery read that fails becomes that entry's `SubmissionUnknown` rather than
3925
+ escaping as a `TransportError` the taxonomy would call "not applied".
3926
+
3927
+ The whole call fails only if the journal itself cannot be **read**: a write
3928
+ that fails after an entry has been settled is logged and the answer stands,
3929
+ the same rule `Tx.submit` follows.
3930
+
3931
+ **Fails with: `JournalError`, `TransportError`.**
3932
+
3933
+ ### `remote` (const)
3934
+
3935
+ ```ts
3936
+ declare const remote: (signer: RemoteSigner) => Signer
3937
+ ```
3938
+
3939
+ Builds a signer around something that signs elsewhere: a KMS, a wallet, a
3940
+ hardware device, another process.
3941
+
3942
+ The returned signer decodes whatever the remote produced, so a malformed
3943
+ signature is a `SigningError` rather than a surprise at execution. When
3944
+ `signPersonalMessage` is not given, asking for one fails with `SigningError`
3945
+ instead of pretending.
3946
+
3947
+ **Never fails.**
3948
+
3949
+ ### `run` (const)
3950
+
3951
+ ```ts
3952
+ declare const run: (recipe: Transaction | Recipe, opts: {
3953
+ readonly signer: Signer;
3954
+ readonly gasOwner?: SuiAddress;
3955
+ /**
3956
+ * The gas owner's signer, for a sponsored transaction. Required whenever
3957
+ * the bytes name a gas owner that is not the sender.
3958
+ */
3959
+ readonly sponsor?: Signer;
3960
+ }) => Effect.Effect<Executed, TransportError | SimulationFailed | ExecutionFailed | SubmissionUnknown | NotApplied | SigningError | BuildError | PolicyDenied | JournalError, Sui>
3961
+ ```
3962
+
3963
+ Build, preflight, sign and submit, with the sender lock held throughout.
3964
+
3965
+ Gas coins are chosen during build, so two transactions from one address that
3966
+ overlap can pick the same coin and one of them will fail on chain. `Tx.run`
3967
+ holds the sender lock from build through submit, which is the whole reason to
3968
+ prefer it over calling the steps separately.
3969
+
3970
+ The address that matters is the one whose coins are being spent, which is the
3971
+ **gas owner** when there is one: two sponsored runs for different senders
3972
+ paid by one sponsor are exactly the case that picks the same coin twice. When
3973
+ sender and gas owner differ, both locks are held, in ascending address order
3974
+ — a fixed order, so two runs that each need the same pair cannot deadlock by
3975
+ taking them the other way round.
3976
+
3977
+ When `SubmitConfig.preflight` is set it costs one extra simulate, and is
3978
+ where spend limits and target policies refuse a transaction before anything
3979
+ is signed.
3980
+
3981
+ **A sponsored run needs both signatures.** A transaction whose gas owner is
3982
+ not its sender is signed by *both* parties; one signature is bytes a
3983
+ validator rejects. So when `opts.gasOwner` differs from the signer's address
3984
+ — or when the recipe itself set a different gas owner, which `Tx.sponsored`
3985
+ does — `opts.sponsor` is required and co-signs the same bytes. Without it
3986
+ `Tx.run` fails with `SigningError` naming the address whose signature is
3987
+ missing, before anything is built when the gas owner was given as an option
3988
+ and immediately after the build when it came out of the recipe. Use the
3989
+ explicit lifecycle (`Tx.build`, `Tx.sign`, `Tx.cosign`, `Tx.submit`) when the
3990
+ two parties cannot both sign in one process.
3991
+
3992
+ **Fails with: `BuildError`, `SimulationFailed`, `PolicyDenied`, `SigningError`, `ExecutionFailed`, `NotApplied`, `SubmissionUnknown`, `JournalError`, `TransportError` (from the build reads; once bytes are sent, transport failures become `SubmissionUnknown`).**
3993
+
3994
+ ### `sign` (const)
3995
+
3996
+ ```ts
3997
+ declare const sign: (built: {
3998
+ readonly digest: Digest;
3999
+ readonly sender: SuiAddress;
4000
+ readonly bytes: Uint8Array<ArrayBufferLike>;
4001
+ readonly chain?: string | undefined;
4002
+ readonly expiration?: {
4003
+ readonly $kind: "None";
4004
+ readonly None: true;
4005
+ } | {
4006
+ readonly $kind: "Epoch";
4007
+ readonly Epoch: bigint;
4008
+ } | {
4009
+ readonly $kind: "ValidDuring";
4010
+ readonly ValidDuring: {
4011
+ readonly minEpoch: bigint | null;
4012
+ readonly maxEpoch: bigint | null;
4013
+ readonly minTimestamp: bigint | null;
4014
+ readonly maxTimestamp: bigint | null;
4015
+ readonly chain: string;
4016
+ readonly nonce: number;
4017
+ };
4018
+ } | {
4019
+ readonly $kind: "Validity";
4020
+ readonly Validity: {
4021
+ readonly allowedProposers: {
4022
+ readonly epoch: bigint;
4023
+ readonly proposers: readonly number[];
4024
+ } | null;
4025
+ readonly minEpoch: bigint | null;
4026
+ readonly maxEpoch: bigint | null;
4027
+ readonly minTimestamp: bigint | null;
4028
+ readonly maxTimestamp: bigint | null;
4029
+ readonly chain: string;
4030
+ readonly nonce: number;
4031
+ };
4032
+ } | undefined;
4033
+ readonly gasOwner?: SuiAddress | undefined;
4034
+ }, signer: Signer) => Effect.Effect<{
4035
+ readonly digest: Digest;
4036
+ readonly sender: SuiAddress;
4037
+ readonly signatures: readonly Signature[];
4038
+ readonly bytes: Uint8Array<ArrayBufferLike>;
4039
+ readonly chain?: string | undefined;
4040
+ readonly expiration?: {
4041
+ readonly $kind: "None";
4042
+ readonly None: true;
4043
+ } | {
4044
+ readonly $kind: "Epoch";
4045
+ readonly Epoch: bigint;
4046
+ } | {
4047
+ readonly $kind: "ValidDuring";
4048
+ readonly ValidDuring: {
4049
+ readonly minEpoch: bigint | null;
4050
+ readonly maxEpoch: bigint | null;
4051
+ readonly minTimestamp: bigint | null;
4052
+ readonly maxTimestamp: bigint | null;
4053
+ readonly chain: string;
4054
+ readonly nonce: number;
4055
+ };
4056
+ } | {
4057
+ readonly $kind: "Validity";
4058
+ readonly Validity: {
4059
+ readonly allowedProposers: {
4060
+ readonly epoch: bigint;
4061
+ readonly proposers: readonly number[];
4062
+ } | null;
4063
+ readonly minEpoch: bigint | null;
4064
+ readonly maxEpoch: bigint | null;
4065
+ readonly minTimestamp: bigint | null;
4066
+ readonly maxTimestamp: bigint | null;
4067
+ readonly chain: string;
4068
+ readonly nonce: number;
4069
+ };
4070
+ } | undefined;
4071
+ }, SigningError, never>
4072
+ ```
4073
+
4074
+ Signs built bytes.
4075
+
4076
+ The signer's address must be the transaction's sender or, for a sponsored
4077
+ transaction, its gas owner; anything else is a `SigningError` rather than a
4078
+ rejection at execution time. A `Signer.remote` therefore has to report the
4079
+ address it signs as truthfully.
4080
+
4081
+ **Fails with: `SigningError`.**
4082
+
4083
+ ### `sponsored` (const)
4084
+
4085
+ ```ts
4086
+ declare const sponsored: (opts: {
4087
+ readonly sender: SuiAddress;
4088
+ readonly gasOwner: SuiAddress;
4089
+ }) => (recipe: Recipe) => Recipe
4090
+ ```
4091
+
4092
+ Turns a recipe into a sponsored one: the sender owns the transaction, the
4093
+ gas owner pays, and gas comes from the sponsor's address balance rather than
4094
+ from coin objects, so the two parties never have to agree on a gas coin and
4095
+ can sign in either order.
4096
+
4097
+ Never fails; the recipe it returns throws only if the recipe it wraps does,
4098
+ which `Tx.build` reports as `BuildError`.
4099
+
4100
+ ### `submit` (const)
4080
4101
 
4081
4102
  ```ts
4082
- declare const UNRESOLVED_TAGS: readonly ["Signed", "Unknown"]
4103
+ declare const submit: (signed: Signed) => Effect.Effect<Executed, SubmitError, Sui>
4083
4104
  ```
4084
4105
 
4085
- The tags whose entries still need an answer from the network.
4106
+ Sends signed bytes, and does not stop caring until it knows what happened.
4107
+
4108
+ Before the first `executeTransaction` it writes a `Signed` journal entry, so
4109
+ a process that dies mid-flight leaves a record of bytes that may be on the
4110
+ wire. A retryable transport failure or a timeout re-sends the identical bytes
4111
+ on `SubmitConfig.resubmit`; nothing is ever rebuilt, so a retry can only land
4112
+ the transaction that was already signed. When the retries run out it runs
4113
+ `Tx.reconcile`, which either finds the transaction, proves it never applied,
4114
+ or says it does not know.
4115
+
4116
+ `TransportError` never escapes: once bytes may have been sent, "the network
4117
+ was unreachable" is not an answer a caller can act on, so it becomes
4118
+ `SubmissionUnknown` carrying the signed bytes.
4119
+
4120
+ `JournalError` can only come from the `Signed` write, before anything has
4121
+ been sent. Once the network has answered, a journal write that fails is
4122
+ logged with `Effect.logError` and the answer stands, because "the journal is
4123
+ broken" is not a thing a caller can act on and reporting it in place of a
4124
+ charged `ExecutionFailed` would invite a second submission.
4125
+
4126
+ **Fails with: `ExecutionFailed` (applied on chain and failed; gas was charged), `NotApplied` (provably never applied), `SubmissionUnknown` (the outcome is not known and the bytes are in the error), `JournalError` (only before the first send).**
4086
4127
 
4087
4128
  ## `@unconfirmed/sui-effect/journal`
4088
4129
 
@@ -4167,7 +4208,7 @@ Every member fails with `JournalError` and nothing else.
4167
4208
  ## `@unconfirmed/sui-effect/extension`
4168
4209
 
4169
4210
 
4170
- 5 exported symbols.
4211
+ 7 exported symbols.
4171
4212
 
4172
4213
  ### `ExtensionFace` (interface)
4173
4214
 
@@ -4196,68 +4237,23 @@ What every registration carries besides the service's own members, under
4196
4237
  `$`-prefixed names so an extension is free to call a member `ready` or
4197
4238
  `dispose` itself.
4198
4239
 
4199
- ### `fromService` (const)
4240
+ ### `Leaf` (type)
4200
4241
 
4201
4242
  ```ts
4202
- declare const fromService: <Self, Shape, E, const Name extends string>(service: Context.Key<Self, Shape>, options: SuiExtensionOptions<Self, E, Name>) => SuiClientRegistration<ClientWithCoreApi, Name, PromiseFace<Shape> & ExtensionFace>
4243
+ export type Leaf<T> = T & LeafBrand;
4203
4244
  ```
4204
4245
 
4205
- Turns an Effect service into a `SuiClientRegistration` a Promise consumer
4206
- passes to `client.$extend(...)`.
4207
-
4208
- `register(client)` does no work by default: the `ManagedRuntime` over
4209
- `SuiCore.layerFromClient(client)`, a `Sui` for the client's effective chain
4210
- id and the extension's own layer is built on the first call and shared by
4211
- every call after it. The `Sui` and `SuiCore` underneath are shared with every
4212
- other registration on that client that names the same chain id — one
4213
- transport, one chain identity and one sender-lock map. A rejection carries
4214
- the original tagged error instance, so a Promise consumer can still switch on
4215
- `_tag`.
4216
-
4217
- `name` is generic in a string literal, so `client.escrow` is a property of
4218
- the extended client's type and not an index lookup: no cast, and no
4219
- `| undefined` under `noUncheckedIndexedAccess`.
4220
-
4221
- **The window before the runtime exists.** Until then nothing knows what a
4222
- member *is*, so a member read off the face is a placeholder. An `Effect` or
4223
- `Stream` member behaves exactly as its type says — the call returns a
4224
- Promise, the iteration works — because that is what the face promises for
4225
- them anyway. A **synchronous** member does not: `PromiseFace` types a recipe
4226
- builder as returning a `Recipe` and a plain value as that value, and a
4227
- placeholder has neither. So a synchronous member used in that window fails
4228
- with `ExtensionNotReady` naming itself, rather than quietly handing back a
4229
- Promise where the type says `Recipe` — which is a bug that only shows up on
4230
- the *second* call, when the member has become real. Two cures:
4231
-
4232
- - `await client.<name>.$ready()` once after `$extend`, which builds the
4233
- runtime and resolves the service; every member is real from then on;
4234
- - register with `warm`, which does the same synchronously inside `register`,
4235
- for a layer that needs no network.
4236
-
4237
- Two lifetimes worth knowing:
4238
-
4239
- - **`$dispose()` is not final.** It releases everything the layer acquired
4240
- and forgets the runtime; the next call builds a fresh one. That is what a
4241
- long-lived page wants (a disposed extension is usable again after a
4242
- reconnect) and it does mean a `$dispose()` that races an in-flight call can
4243
- leave the caller's Promise rejected while a new runtime starts behind it.
4244
- Dispose when the consumer is done, not between calls. `dispose()` is the
4245
- same function under the name it had first.
4246
- - **Each `register` is independent.** Registering the same extension on two
4247
- clients — or twice on one — gives two runtimes, two layer builds and two
4248
- copies of whatever the layer holds (a cache, a connection). Register once
4249
- per client and keep the extended client.
4246
+ `T`, marked as a leaf of the Promise face: it is handed to a Promise consumer
4247
+ exactly as the Effect service holds it.
4250
4248
 
4251
- Never fails, except a `warm` registration, which throws out of `register`
4252
- when the layer needs an asynchronous step or when the network has no known
4253
- chain identifier and none was given. Otherwise the layer's own failures
4254
- surface as rejections of the first call that needs it.
4249
+ Declare the member as `Leaf<MyClass>` and build it with `leaf`. A
4250
+ `Leaf<T>` is still a `T` every consumer of the Effect face is unaffected.
4255
4251
 
4256
4252
  ### `PromiseFace` (type)
4257
4253
 
4258
4254
  ```ts
4259
4255
  export type PromiseFace<S> = {
4260
- readonly [K in keyof S]: S[K] extends Stream.Stream<infer A, infer _E, infer _R> ? AsyncIterable<A> : S[K] extends Effect.Effect<infer A, infer _E2, infer _R2> ? () => Promise<A> : S[K] extends (...args: infer Args) => Stream.Stream<infer A, infer _E3, infer _R3> ? (...args: Args) => AsyncIterable<A> : S[K] extends (...args: infer Args) => Effect.Effect<infer A, infer _E4, infer _R4> ? (...args: Args) => Promise<A> : S[K] extends Record<string, unknown> ? PromiseFace<S[K]> : S[K];
4256
+ readonly [K in keyof S]: S[K] extends Stream.Stream<infer A, infer _E, infer _R> ? AsyncIterable<A> : S[K] extends Effect.Effect<infer A, infer _E2, infer _R2> ? () => Promise<A> : S[K] extends (...args: infer Args) => Stream.Stream<infer A, infer _E3, infer _R3> ? (...args: Args) => AsyncIterable<A> : S[K] extends (...args: infer Args) => Effect.Effect<infer A, infer _E4, infer _R4> ? (...args: Args) => Promise<A> : S[K] extends FaceLeaf ? S[K] : S[K] extends (...args: ReadonlyArray<never>) => unknown ? S[K] : S[K] extends object ? PromiseFace<S[K]> : S[K];
4261
4257
  };
4262
4258
  ```
4263
4259
 
@@ -4265,8 +4261,8 @@ The Promise face of a service interface.
4265
4261
 
4266
4262
  An `Effect` member becomes a zero-argument method returning a Promise, a
4267
4263
  function returning an `Effect` keeps its arguments and returns a Promise, a
4268
- `Stream` member becomes an `AsyncIterable`, a nested plain object of members
4269
- is mapped the same way (platform SDKs namespace their surface as
4264
+ `Stream` member becomes an `AsyncIterable`, a nested object of members is
4265
+ mapped the same way (platform SDKs namespace their surface as
4270
4266
  `client.miso.protocol.*`), and anything else passes through untouched.
4271
4267
 
4272
4268
  **A synchronous member stays synchronous**: a recipe builder
@@ -4275,11 +4271,22 @@ value is still that value. The type says so and, once the runtime exists, the
4275
4271
  runtime agrees — see `warm` and `$ready` on `fromService` for the
4276
4272
  window before it does.
4277
4273
 
4278
- **A class instance is a leaf.** The recursion is into plain object literals
4279
- only, which is what the runtime maps; a `BcsType`, a `Schema.Class` instance,
4280
- a `Date`, anything with a prototype of its own passes through whole, in the
4281
- type and at runtime alike. (An interface or class type is not assignable to
4282
- `Record<string, unknown>`, which is what keeps the two in step.)
4274
+ **The recursion is by type, not by declaration style.** Every object-typed
4275
+ member that is not a function, an array, an `Effect`, a `Stream`, a
4276
+ `Uint8Array`, a `Date`, a `Promise`, a BCS codec (both `parse` and
4277
+ `serialize`) or a `Leaf` is mapped as a namespace an `interface`
4278
+ exactly like a type alias. Before 0.1.1 the bound was
4279
+ `Record<string, unknown>`, which an interface is not assignable to, so an
4280
+ interface-typed namespace (`readonly protocol: ProtocolService`) kept its
4281
+ `Effect` members **in the type** while the runtime mapped them to Promises.
4282
+ The type lied; it no longer does.
4283
+
4284
+ **A class instance that is a value, not a namespace, is marked.** The runtime
4285
+ maps plain-prototype objects and passes class instances through, so a class
4286
+ whose methods return `Effect`s would be typed as mapped and arrive unmapped.
4287
+ Declare such a member `Leaf<T>` and build it with `leaf`: the type and
4288
+ the runtime then agree that it is a value. `Uint8Array`, `Date`, `Promise`,
4289
+ arrays and BCS codecs need no marker.
4283
4290
 
4284
4291
  **A plain-object *value* member is the one place the two faces cannot agree.**
4285
4292
  `{ packageId: "0x…" }` is indistinguishable from a namespace of members, so
@@ -4296,10 +4303,13 @@ through an `Effect` member instead.
4296
4303
  ```ts
4297
4304
  declare const SuiExtension: {
4298
4305
  readonly fromService: <Self, Shape, E, const Name extends string>(service: Context.Key<Self, Shape>, options: SuiExtensionOptions<Self, E, Name>) => SuiClientRegistration<ClientWithCoreApi, Name, PromiseFace<Shape> & ExtensionFace>;
4306
+ readonly leaf: <T>(value: T) => Leaf<T>;
4299
4307
  }
4300
4308
  ```
4301
4309
 
4302
- The namespace the spec spells: `SuiExtension.fromService(...)`.
4310
+ The namespace the spec spells: `SuiExtension.fromService(...)`, plus the
4311
+ leaf marker a service uses to say "this member is a value, not a namespace"
4312
+ (`SuiExtension.leaf(value)`, typed `SuiExtension.Leaf<T>`).
4303
4313
 
4304
4314
  ### `SuiExtensionOptions` (interface)
4305
4315
 
@@ -4344,6 +4354,16 @@ export interface SuiExtensionOptions<Self, E, Name extends string = string> {
4344
4354
  * registers. Every member is then the real thing immediately, and
4345
4355
  * `$ready()` has nothing left to do.
4346
4356
  *
4357
+ * **Every failure of the layer becomes a synchronous throw.** The whole
4358
+ * layer is built inside `register`, so a missing deployment, a bad
4359
+ * configuration, a `NetworkMismatch` — anything the layer can fail with —
4360
+ * comes out of `client.$extend(...)` as a thrown value rather than as the
4361
+ * rejection of a first call. That is the trade `warm` makes, and a consumer
4362
+ * has to catch it where it registers.
4363
+ *
4364
+ * After `$dispose()`, the next use re-runs this same build rather than
4365
+ * degrading to a cold registration.
4366
+ *
4347
4367
  * Two conditions, both enforced:
4348
4368
  *
4349
4369
  * - **The layer must not perform an asynchronous step.** A layer that reads
@@ -4379,90 +4399,96 @@ export interface SuiExtensionOptions<Self, E, Name extends string = string> {
4379
4399
 
4380
4400
  What `fromService` needs to know beyond the service key itself.
4381
4401
 
4382
- ## `@unconfirmed/sui-effect/script`
4383
-
4384
-
4385
- 9 exported symbols.
4386
-
4387
- ### `exitCode` (const)
4402
+ ### `fromService` (const)
4388
4403
 
4389
4404
  ```ts
4390
- declare const exitCode: <A, E>(exit: Exit.Exit<A, E>, options?: ExitCodeOptions) => number
4405
+ declare const fromService: <Self, Shape, E, const Name extends string>(service: Context.Key<Self, Shape>, options: SuiExtensionOptions<Self, E, Name>) => SuiClientRegistration<ClientWithCoreApi, Name, PromiseFace<Shape> & ExtensionFace>
4391
4406
  ```
4392
4407
 
4393
- The exit code one failure deserves.
4394
-
4395
- The axis is what a wrapper can act on: did the transaction apply (5, gas was
4396
- charged, do not retry), is the outcome unknown (3, reconcile before doing
4397
- anything else), or did nothing apply (4, safe to retry)? Configuration
4398
- problems are 2 because no amount of retrying fixes them, a defect is 1, and
4399
- an interrupt is 130 the way a shell expects — unless the journal says there
4400
- is a submission outstanding, in which case it is 3, because a wrapper that
4401
- sees 130 has no reason to go looking for one.
4408
+ Turns an Effect service into a `SuiClientRegistration` a Promise consumer
4409
+ passes to `client.$extend(...)`.
4402
4410
 
4403
- An extension error that declares an `outcome` is honoured, so a downstream
4404
- SDK's own failures land on the same axis. `SchemaError` what Effect's own
4405
- `Config.schema` and `Schema.decodeUnknownEffect` fail with is exit 2 with
4406
- `ConfigError`, because in a script it can only mean the input a person gave
4407
- did not fit the schema, and no retry fixes that.
4411
+ `register(client)` does no work by default: the `ManagedRuntime` over
4412
+ `SuiCore.layerFromClient(client)`, a `Sui` for the client's effective chain
4413
+ id and the extension's own layer is built on the first call and shared by
4414
+ every call after it. The `Sui` and `SuiCore` underneath are shared with every
4415
+ other registration on that client that names the same chain id one
4416
+ transport, one chain identity and one sender-lock map. A rejection carries
4417
+ the original tagged error instance, so a Promise consumer can still switch on
4418
+ `_tag`.
4408
4419
 
4409
- An error with a tag this library has never heard of and no `outcome` is
4410
- *unclassified* and exits 1, the code that also means defect. It deliberately
4411
- does not follow `SuiError.outcome`, which answers `"unknown"` for the same
4412
- value: 3 would tell a wrapper there is a transaction to reconcile, and an
4413
- unrecognised error is not evidence that anything was ever sent. Extensions
4414
- are told to declare `outcome` on every error precisely so their failures
4415
- never land here.
4420
+ `name` is generic in a string literal, so `client.escrow` is a property of
4421
+ the extended client's type and not an index lookup: no cast, and no
4422
+ `| undefined` under `noUncheckedIndexedAccess`.
4416
4423
 
4417
- **Never fails.**
4424
+ **The window before the runtime exists.** Until then nothing knows what a
4425
+ member *is*, so a member read off the face is a placeholder. An `Effect` or
4426
+ `Stream` member behaves exactly as its type says — the call returns a
4427
+ Promise, the iteration works — because that is what the face promises for
4428
+ them anyway. A **synchronous** member does not: `PromiseFace` types a recipe
4429
+ builder as returning a `Recipe` and a plain value as that value, and a
4430
+ placeholder has neither. So a synchronous member used in that window fails
4431
+ with `ExtensionNotReady` naming itself, rather than quietly handing back a
4432
+ Promise where the type says `Recipe` — which is a bug that only shows up on
4433
+ the *second* call, when the member has become real. Two cures:
4418
4434
 
4419
- ### `readNetwork` (const)
4435
+ - `await client.<name>.$ready()` once after `$extend`, which builds the
4436
+ runtime and resolves the service; every member is real from then on;
4437
+ - register with `warm`, which does the same synchronously inside `register`,
4438
+ for a layer that needs no network.
4420
4439
 
4421
- ```ts
4422
- declare const readNetwork: Effect.Effect<string, Config.ConfigError>
4423
- ```
4440
+ Two lifetimes worth knowing:
4424
4441
 
4425
- `SUI_NETWORK`, with no default and with the mainnet gate.
4442
+ - **`$dispose()` is not final.** It releases everything the layer acquired
4443
+ and forgets the runtime; the next call builds a fresh one — and a `warm`
4444
+ registration re-runs its **warm** build, with the same options, on the next
4445
+ use, so it does not silently degrade to cold with every synchronous member
4446
+ throwing `ExtensionNotReady`. That is what a
4447
+ long-lived page wants (a disposed extension is usable again after a
4448
+ reconnect) and it does mean a `$dispose()` that races an in-flight call can
4449
+ leave the caller's Promise rejected while a new runtime starts behind it.
4450
+ Dispose when the consumer is done, not between calls. `dispose()` is the
4451
+ same function under the name it had first.
4452
+ - **Each `register` is independent.** Registering the same extension on two
4453
+ clients — or twice on one — gives two runtimes, two layer builds and two
4454
+ copies of whatever the layer holds (a cache, a connection). Register once
4455
+ per client and keep the extended client.
4426
4456
 
4427
- There is no default network on purpose: a script that runs against whatever
4428
- happened to be configured is how a test transaction reaches mainnet. And a
4429
- script that means mainnet has to say so twice, in `SUI_NETWORK` and in
4430
- `SUI_ALLOW_MAINNET=1`.
4457
+ **A cold call is a real `Promise`.** The placeholder a member call returns
4458
+ before the runtime exists is a `Promise` subclass that also implements
4459
+ `Symbol.asyncIterator`, so `instanceof Promise` holds and `bun:test`'s
4460
+ `expect(...).rejects` recognises it. Its rejection is **pre-handled** (a
4461
+ no-op `catch` is attached at creation), so a cold call nobody awaits —
4462
+ `client.ext.doThing()` as a statement — cannot abort the process with an
4463
+ unhandled rejection; an `await` of it still throws `ExtensionNotReady`.
4431
4464
 
4432
- **Fails with: `ConfigError`.**
4465
+ Never fails, except a `warm` registration. A warm registration builds the
4466
+ **whole layer** synchronously inside `register`, so **any** failure of that
4467
+ layer — a missing deployment for the network, a configuration error, a
4468
+ `NetworkMismatch`, not only an asynchronous step or an unknown chain
4469
+ identifier — is thrown synchronously out of `register`, which means out of
4470
+ `client.$extend(...)`. Catch it where you register. A lazy registration has
4471
+ nowhere to put such a failure at registration time, so it surfaces as the
4472
+ rejection of the first call that needs the layer.
4433
4473
 
4434
- ### `run` (const)
4474
+ ### `leaf` (const)
4435
4475
 
4436
4476
  ```ts
4437
- declare const run: <A, E>(effect: Effect.Effect<A, E, Script | Sui | SuiCore>, options?: ScriptRunOptions) => Promise<number>
4477
+ declare const leaf: <T>(value: T) => Leaf<T>
4438
4478
  ```
4439
4479
 
4440
- Runs a script: builds `Script.layer`, forks the program, interrupts it on
4441
- SIGINT or SIGTERM so finalizers run, writes one diagnostic line per failure
4442
- to stderr, and exits with `exitCode`.
4480
+ Marks a value as a `Leaf`: the Promise face passes it through untouched
4481
+ instead of walking into it.
4443
4482
 
4444
- stdout carries only what the script itself printed: the logger is bound to
4445
- stderr for the whole run, so `Effect.log` from the script or from anything it
4446
- calls cannot land in the script's output. A `SubmissionUnknown` additionally
4447
- prints the base64 of the signed bytes and a line saying to reconcile, because
4448
- those bytes are the durable record a script has.
4483
+ The registry is a `WeakSet`, so a frozen value can be marked and nothing is
4484
+ added to the value itself.
4449
4485
 
4450
- On **every** non-zero exit it also prints whatever the journal the script ran
4451
- with still holds unresolved, which is the only record of bytes that may be on
4452
- the wire when a script is killed — or fails — between signing and the answer.
4453
- That count is also what decides a timeout (3 rather than 4) and an interrupt
4454
- (3 rather than 130): an `Effect.timeout` around a submission interrupts it
4455
- from the outside and never reaches `Tx.submit`'s own mapping.
4486
+ **Never fails.**
4456
4487
 
4457
- **A second SIGINT does nothing.** The handler interrupts the root fiber once;
4458
- pressing Ctrl-C again while finalizers run is ignored, because the whole
4459
- point of the first interrupt is to let those finalizers — the journal write
4460
- that records what was sent, above all — complete. A script whose finalizers
4461
- hang has to be killed with SIGKILL, which by construction no process can
4462
- handle.
4488
+ ## `@unconfirmed/sui-effect/script`
4463
4489
 
4464
- Returns the exit code as well as passing it to `exit`, so a test can inject
4465
- `exit` and assert on the number without ending the test process.
4490
+
4491
+ 9 exported symbols.
4466
4492
 
4467
4493
  ### `Script` (class)
4468
4494
 
@@ -4588,7 +4614,87 @@ export interface SignalSource {
4588
4614
  }
4589
4615
  ```
4590
4616
 
4591
- The part of `process` `run` uses, so a test can stand in for it.
4617
+ The part of `process` `run` uses, so a test can stand in for it.
4618
+
4619
+ ### `exitCode` (const)
4620
+
4621
+ ```ts
4622
+ declare const exitCode: <A, E>(exit: Exit.Exit<A, E>, options?: ExitCodeOptions) => number
4623
+ ```
4624
+
4625
+ The exit code one failure deserves.
4626
+
4627
+ The axis is what a wrapper can act on: did the transaction apply (5, gas was
4628
+ charged, do not retry), is the outcome unknown (3, reconcile before doing
4629
+ anything else), or did nothing apply (4, safe to retry)? Configuration
4630
+ problems are 2 because no amount of retrying fixes them, a defect is 1, and
4631
+ an interrupt is 130 the way a shell expects — unless the journal says there
4632
+ is a submission outstanding, in which case it is 3, because a wrapper that
4633
+ sees 130 has no reason to go looking for one.
4634
+
4635
+ An extension error that declares an `outcome` is honoured, so a downstream
4636
+ SDK's own failures land on the same axis. `SchemaError` — what Effect's own
4637
+ `Config.schema` and `Schema.decodeUnknownEffect` fail with — is exit 2 with
4638
+ `ConfigError`, because in a script it can only mean the input a person gave
4639
+ did not fit the schema, and no retry fixes that.
4640
+
4641
+ An error with a tag this library has never heard of and no `outcome` is
4642
+ *unclassified* and exits 1, the code that also means defect. It deliberately
4643
+ does not follow `SuiError.outcome`, which answers `"unknown"` for the same
4644
+ value: 3 would tell a wrapper there is a transaction to reconcile, and an
4645
+ unrecognised error is not evidence that anything was ever sent. Extensions
4646
+ are told to declare `outcome` on every error precisely so their failures
4647
+ never land here.
4648
+
4649
+ **Never fails.**
4650
+
4651
+ ### `readNetwork` (const)
4652
+
4653
+ ```ts
4654
+ declare const readNetwork: Effect.Effect<string, Config.ConfigError>
4655
+ ```
4656
+
4657
+ `SUI_NETWORK`, with no default and with the mainnet gate.
4658
+
4659
+ There is no default network on purpose: a script that runs against whatever
4660
+ happened to be configured is how a test transaction reaches mainnet. And a
4661
+ script that means mainnet has to say so twice, in `SUI_NETWORK` and in
4662
+ `SUI_ALLOW_MAINNET=1`.
4663
+
4664
+ **Fails with: `ConfigError`.**
4665
+
4666
+ ### `run` (const)
4667
+
4668
+ ```ts
4669
+ declare const run: <A, E>(effect: Effect.Effect<A, E, Script | Sui | SuiCore>, options?: ScriptRunOptions) => Promise<number>
4670
+ ```
4671
+
4672
+ Runs a script: builds `Script.layer`, forks the program, interrupts it on
4673
+ SIGINT or SIGTERM so finalizers run, writes one diagnostic line per failure
4674
+ to stderr, and exits with `exitCode`.
4675
+
4676
+ stdout carries only what the script itself printed: the logger is bound to
4677
+ stderr for the whole run, so `Effect.log` from the script or from anything it
4678
+ calls cannot land in the script's output. A `SubmissionUnknown` additionally
4679
+ prints the base64 of the signed bytes and a line saying to reconcile, because
4680
+ those bytes are the durable record a script has.
4681
+
4682
+ On **every** non-zero exit it also prints whatever the journal the script ran
4683
+ with still holds unresolved, which is the only record of bytes that may be on
4684
+ the wire when a script is killed — or fails — between signing and the answer.
4685
+ That count is also what decides a timeout (3 rather than 4) and an interrupt
4686
+ (3 rather than 130): an `Effect.timeout` around a submission interrupts it
4687
+ from the outside and never reaches `Tx.submit`'s own mapping.
4688
+
4689
+ **A second SIGINT does nothing.** The handler interrupts the root fiber once;
4690
+ pressing Ctrl-C again while finalizers run is ignored, because the whole
4691
+ point of the first interrupt is to let those finalizers — the journal write
4692
+ that records what was sent, above all — complete. A script whose finalizers
4693
+ hang has to be killed with SIGKILL, which by construction no process can
4694
+ handle.
4695
+
4696
+ Returns the exit code as well as passing it to `exit`, so a test can inject
4697
+ `exit` and assert on the number without ending the test process.
4592
4698
 
4593
4699
  ## `@unconfirmed/sui-effect/testing`
4594
4700
 
@@ -4660,14 +4766,6 @@ export interface FakeChange {
4660
4766
 
4661
4767
  A created, mutated or deleted object in a scripted execution.
4662
4768
 
4663
- ### `fakeDigest` (const)
4664
-
4665
- ```ts
4666
- declare const fakeDigest: (seed: number) => string
4667
- ```
4668
-
4669
- A deterministic base58 32-byte digest, for fixtures.
4670
-
4671
4769
  ### `FakeExecution` (interface)
4672
4770
 
4673
4771
  ```ts
@@ -4763,6 +4861,18 @@ export interface FakeScript {
4763
4861
  /** The gas budget the resolve plugin sets when a transaction has none. */
4764
4862
  readonly gasBudget?: bigint;
4765
4863
  readonly balances?: ReadonlyArray<SuiClientTypes.Balance>;
4864
+ /**
4865
+ * What `getCoinMetadata` answers, keyed by coin type.
4866
+ *
4867
+ * Unscripted, the method dies naming itself the way every uncovered method
4868
+ * does. Scripted, a coin type the record does not name answers
4869
+ * `{ coinMetadata: null }` — which is what a node says about a type that has
4870
+ * no metadata object, and the case an extension that formats balances has to
4871
+ * handle.
4872
+ *
4873
+ * @since 0.1.1
4874
+ */
4875
+ readonly coinMetadata?: Readonly<Record<string, SuiClientTypes.CoinMetadata>>;
4766
4876
  readonly dynamicFields?: Readonly<Record<string, ReadonlyArray<SuiClientTypes.DynamicFieldEntry>>>;
4767
4877
  readonly dynamicFieldValues?: Readonly<Record<string, SuiClientTypes.DynamicFieldValue>>;
4768
4878
  /** How many items a list method returns per page. Defaults to 50. */
@@ -4822,42 +4932,6 @@ export declare class FakeUnimplemented extends Error {
4822
4932
 
4823
4933
  Thrown, and re-thrown as a defect, when a test reaches an unscripted method.
4824
4934
 
4825
- ### `layerExtensionTest` (const)
4826
-
4827
- ```ts
4828
- declare const layerExtensionTest: <Self, E>(layer: Layer.Layer<Self, E, Sui | SuiCore>, script?: FakeScript) => Layer.Layer<Self | Sui | SuiCore | SuiCoreFake, E | NetworkMismatch | TransportError>
4829
- ```
4830
-
4831
- An extension's own layer over `layerTest`, which is the whole wiring an
4832
- extension test needs.
4833
-
4834
- `layerTest` provides `Sui`, `SuiCore` and `SuiCoreFake`, and an extension's
4835
- `layerTest` (or `layer`, for an extension with no fake of its own) may
4836
- require `Sui | SuiCore` and nothing else — the same requirement
4837
- `SuiExtension.fromService` satisfies in production. So composing the two is
4838
- the recipe, and everything the extension's layer provides plus the fake's
4839
- handle comes out the other side.
4840
-
4841
- **Fails with: whatever the extension's layer fails with, plus `NetworkMismatch` and `TransportError` when the script asks for them.**
4842
-
4843
- ### `layerTest` (const)
4844
-
4845
- ```ts
4846
- declare const layerTest: (script?: FakeScript) => Layer.Layer<Sui | SuiCore | SuiCoreFake, NetworkMismatch | TransportError>
4847
- ```
4848
-
4849
- The real `Sui` over the fake `SuiCore`, plus the fake's own handle so a test
4850
- can script outcomes and inspect what the fake received.
4851
-
4852
- This is `Sui.layerNoDeps`, the production layer, so the chain-id rules are
4853
- the production rules: a script whose `network` is `mainnet` or `testnet` must
4854
- report that network's identifier, and any other network asserts nothing. A
4855
- test that wants the assertion on a custom network builds
4856
- `Sui.layerNoDepsWith({ chainId })` over `SuiCoreFake.layer(script)` itself,
4857
- rather than having the fake compared against its own script.
4858
-
4859
- **Fails with: `NetworkMismatch`, `TransportError` — both only when the script asks for them.**
4860
-
4861
4935
  ### `RecordedCall` (interface)
4862
4936
 
4863
4937
  ```ts
@@ -4979,11 +5053,359 @@ inside an Effect, without reaching for the `SuiCoreFake` handle by hand.
4979
5053
  Every member requires `SuiCoreFake`, which `layerTest` and
4980
5054
  `layerExtensionTest` provide.
4981
5055
 
5056
+ ### `fakeDigest` (const)
5057
+
5058
+ ```ts
5059
+ declare const fakeDigest: (seed: number) => string
5060
+ ```
5061
+
5062
+ A deterministic base58 32-byte digest, for fixtures.
5063
+
5064
+ ### `layerExtensionTest` (const)
5065
+
5066
+ ```ts
5067
+ declare const layerExtensionTest: <Self, E, RExtra = never, EExtra = never>(layer: Layer.Layer<Self, E, Sui | SuiCore | SuiGraphQL | RExtra>, script?: FakeScript, options?: {
5068
+ readonly extra?: Layer.Layer<RExtra, EExtra>;
5069
+ }) => Layer.Layer<Self | Sui | SuiCore | SuiCoreFake, E | EExtra | NetworkMismatch | TransportError>
5070
+ ```
5071
+
5072
+ An extension's own layer over `layerTest`, which is the whole wiring an
5073
+ extension test needs.
5074
+
5075
+ `layerTest` provides `Sui`, `SuiCore` and `SuiCoreFake`, and an extension's
5076
+ `layerTest` (or `layer`, for an extension with no fake of its own) may
5077
+ require `Sui | SuiCore` and nothing else — the same requirement
5078
+ `SuiExtension.fromService` satisfies in production. So composing the two is
5079
+ the recipe, and everything the extension's layer provides plus the fake's
5080
+ handle comes out the other side.
5081
+
5082
+ **`SuiGraphQL` is provided too**, as `SuiGraphQL.layerUnavailable`: an
5083
+ extension that reads GraphQL requires the tag, its layer would otherwise not
5084
+ build in a test, and "there is no endpoint" is the answer a test wants by
5085
+ default — every GraphQL call fails with `GraphQLUnavailable`, which is a
5086
+ failure the extension already handles. Pass `extra` to override it with a
5087
+ real or scripted client, or to provide anything else the extension's layer
5088
+ requires that the client could not have given it (an `HttpClient`, an
5089
+ operator service, a sibling extension's test layer).
5090
+
5091
+ **Fails with: whatever the extension's layer fails with, whatever `extra` fails with, plus `NetworkMismatch` and `TransportError` when the script asks for them.**
5092
+
5093
+ ### `layerTest` (const)
5094
+
5095
+ ```ts
5096
+ declare const layerTest: (script?: FakeScript) => Layer.Layer<Sui | SuiCore | SuiCoreFake, NetworkMismatch | TransportError>
5097
+ ```
5098
+
5099
+ The real `Sui` over the fake `SuiCore`, plus the fake's own handle so a test
5100
+ can script outcomes and inspect what the fake received.
5101
+
5102
+ This is `Sui.layerNoDeps`, the production layer, so the chain-id rules are
5103
+ the production rules: a script whose `network` is `mainnet` or `testnet` must
5104
+ report that network's identifier, and any other network asserts nothing. A
5105
+ test that wants the assertion on a custom network builds
5106
+ `Sui.layerNoDepsWith({ chainId })` over `SuiCoreFake.layer(script)` itself,
5107
+ rather than having the fake compared against its own script.
5108
+
5109
+ **Fails with: `NetworkMismatch`, `TransportError` — both only when the script asks for them.**
5110
+
4982
5111
  # Examples
4983
5112
 
4984
5113
  Every file below is typechecked, and each one is exercised by a test against
4985
5114
  the in-memory fake.
4986
5115
 
5116
+ ### `examples/compare-read.effect.ts`
5117
+
5118
+ ```ts
5119
+ /**
5120
+ * Reads one Escrow object and its optional note through sui-effect.
5121
+ *
5122
+ * Compare with `examples/compare-read.sdk.ts`, which reads the same two
5123
+ * things with the canonical SDK client. Both print identical output.
5124
+ *
5125
+ * Run with `SUI_NETWORK=testnet ESCROW_ID=0x… bun examples/compare-read.effect.ts`.
5126
+ * Every failure this program can produce is in the generator's inferred error
5127
+ * type: `ObjectNotFound`, `ObjectDeleted`, `ObjectUnavailable` and
5128
+ * `DecodeError` (the last one is what a wrong Move type becomes, because
5129
+ * `getObject`'s tag check runs before a byte is parsed), plus `TransportError`
5130
+ * from both reads and, from the layer, `ConfigError` and `NetworkMismatch`.
5131
+ */
5132
+ import { bcs } from "@mysten/sui/bcs"
5133
+ import { Config, Console, Effect, Option } from "effect"
5134
+ import { ObjectId, Sui, SuiSchema } from "../src/index.ts"
5135
+
5136
+ const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
5137
+
5138
+ const Escrow = SuiSchema.bcs(
5139
+ bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() }),
5140
+ `${PKG}::escrow::Escrow`
5141
+ )
5142
+ // The note's value has no struct tag of its own, so its codec carries none;
5143
+ // the key is a primitive `u64`, which is why nothing here reaches for
5144
+ // `normalizeStructTag` on it.
5145
+ const Note = SuiSchema.bcs(bcs.u64())
5146
+ const NOTE_NAME = { type: "u64", bcs: bcs.u64().serialize(0).toBytes() }
5147
+
5148
+ /** The program itself, exported so a test can run it against the fake. */
5149
+ export const program = Effect.gen(function*() {
5150
+ const sui = yield* Sui
5151
+ const id = yield* Config.schema(ObjectId, "ESCROW_ID")
5152
+ // No `ObjectError.reason` switch and no manual type comparison: a missing
5153
+ // or deleted escrow and a wrong Move type are already three different tags
5154
+ // in the type this line returns.
5155
+ const escrow = yield* sui.getObject(id, { schema: Escrow })
5156
+ const field = yield* sui.getDynamicFieldOption(id, NOTE_NAME)
5157
+ const note = yield* Option.match(field, {
5158
+ onNone: () => Effect.void,
5159
+ onSome: (entry) => SuiSchema.decode(Note, entry.value.bcs)
5160
+ })
5161
+ yield* Console.log(`${escrow.id} holds ${escrow.content.amount}, note: ${note ?? "none"}`)
5162
+ })
5163
+
5164
+ if (import.meta.main) {
5165
+ // Imported here rather than at the top so that a test can import `program`
5166
+ // without pulling a platform package into the test process.
5167
+ const { BunRuntime } = await import("@effect/platform-bun")
5168
+ BunRuntime.runMain(program.pipe(Effect.provide(Sui.layerConfig)))
5169
+ }
5170
+ ```
5171
+
5172
+ ### `examples/compare-read.sdk.ts`
5173
+
5174
+ ```ts
5175
+ /**
5176
+ * Reads one Escrow object and an optional dynamic field on it, written
5177
+ * against the canonical 2.30 SDK: a gRPC client, the Core API, manual BCS.
5178
+ *
5179
+ * Compare with `examples/compare-read.effect.ts`. Both print identical
5180
+ * output for the same id.
5181
+ *
5182
+ * Run with `SUI_NETWORK=testnet ESCROW_ID=0x… bun examples/compare-read.sdk.ts`.
5183
+ */
5184
+ import { bcs } from "@mysten/sui/bcs"
5185
+ import type { ClientWithCoreApi } from "@mysten/sui/client"
5186
+ import { ObjectError } from "@mysten/sui/client"
5187
+ import { SuiGrpcClient } from "@mysten/sui/grpc"
5188
+
5189
+ const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
5190
+ const ESCROW_TYPE = `${PKG}::escrow::Escrow`
5191
+ const EscrowBcs = bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() })
5192
+ // A note some escrows carry, keyed by the literal index 0. The key is a
5193
+ // primitive `u64`, not a struct tag, so nothing here parses it as one.
5194
+ const NOTE_NAME = { type: "u64", bcs: bcs.u64().serialize(0).toBytes() }
5195
+
5196
+ /** Reads the escrow at `id` plus its optional note. */
5197
+ export const readEscrow = async (
5198
+ client: ClientWithCoreApi,
5199
+ id: string
5200
+ ): Promise<{ id: string; amount: string; note: string | undefined }> => {
5201
+ let object
5202
+ try {
5203
+ ;({ object } = await client.core.getObject({ objectId: id, include: { content: true } }))
5204
+ } catch (error) {
5205
+ // `getObject` throws rather than returning a value that says which of
5206
+ // "missing", "deleted" or "unreachable" happened; that three-way split
5207
+ // is `ObjectError.reason`, read by hand.
5208
+ if (error instanceof ObjectError) {
5209
+ if (error.reason === "notFound") throw new Error(`escrow ${id} does not exist`)
5210
+ if (error.reason === "deleted") throw new Error(`escrow ${id} was deleted`)
5211
+ throw new Error(`escrow ${id}: node could not say what happened to it`, { cause: error })
5212
+ }
5213
+ throw error
5214
+ }
5215
+ // No type parameters on this tag, so a plain string comparison is honest;
5216
+ // a generic type would need to be parsed and compared piece by piece,
5217
+ // which is what sui-effect's bridge does for every caller.
5218
+ if (object.type !== ESCROW_TYPE) {
5219
+ throw new Error(`${id} is a ${object.type}, not ${ESCROW_TYPE}`)
5220
+ }
5221
+ const { id: escrowId, amount } = EscrowBcs.parse(object.content)
5222
+
5223
+ let note: string | undefined
5224
+ try {
5225
+ const { dynamicField } = await client.core.getDynamicField({ parentId: id, name: NOTE_NAME })
5226
+ note = bcs.u64().parse(dynamicField.value.bcs)
5227
+ } catch (error) {
5228
+ // Absence is normal here; only a reason other than "notFound"/"deleted"
5229
+ // is a real problem.
5230
+ if (!(error instanceof ObjectError) || error.reason === "unknown") throw error
5231
+ }
5232
+
5233
+ return { id: escrowId, amount, note }
5234
+ }
5235
+
5236
+ if (import.meta.main) {
5237
+ const id = process.env.ESCROW_ID
5238
+ if (!id) throw new Error("ESCROW_ID is required")
5239
+ const client = new SuiGrpcClient({
5240
+ network: (process.env.SUI_NETWORK ?? "testnet") as "testnet",
5241
+ baseUrl: process.env.SUI_RPC_URL ?? "https://fullnode.testnet.sui.io:443"
5242
+ })
5243
+ const escrow = await readEscrow(client, id)
5244
+ console.log(`${escrow.id} holds ${escrow.amount}, note: ${escrow.note ?? "none"}`)
5245
+ }
5246
+ ```
5247
+
5248
+ ### `examples/compare-write.effect.ts`
5249
+
5250
+ ```ts
5251
+ /**
5252
+ * Claims an escrow through `Tx.run`, sui-effect's write path.
5253
+ *
5254
+ * Compare with `examples/compare-write.sdk.ts`, which does the same thing
5255
+ * against the canonical SDK client. Both sign with `SUI_PRIVATE_KEY` and
5256
+ * print the created receipt's object id.
5257
+ *
5258
+ * What `Tx.run` adds over the SDK file: a default expiration bounded to the
5259
+ * current epoch, a journal entry written before the first execute so a crash
5260
+ * mid-flight leaves a record, resending the identical signed bytes rather
5261
+ * than rebuilding on a transient failure, `reconcile` when a submission's
5262
+ * outcome is unknown, and a sender lock so two concurrent claims from one
5263
+ * address cannot pick the same gas coin. What it costs is the Effect
5264
+ * vocabulary below.
5265
+ *
5266
+ * Run with:
5267
+ * SUI_NETWORK=testnet SUI_PRIVATE_KEY=suiprivkey1… ESCROW_ID=0x… bun examples/compare-write.effect.ts
5268
+ */
5269
+ import { bcs } from "@mysten/sui/bcs"
5270
+ import { Config, Console, Effect } from "effect"
5271
+ import { ObjectId, SuiSchema } from "../src/index.ts"
5272
+ import { Script } from "../src/script.ts"
5273
+ import { Tx } from "../src/tx.ts"
5274
+
5275
+ const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
5276
+
5277
+ const Escrow = SuiSchema.bcs(
5278
+ bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() }),
5279
+ `${PKG}::escrow::Escrow`
5280
+ )
5281
+
5282
+ /**
5283
+ * The program itself, exported so a test can run it against the fake. Every
5284
+ * failure it can produce is in the generator's inferred error type, with no
5285
+ * handling lines anywhere in this file: `ObjectNotFound`, `ObjectDeleted`,
5286
+ * `ObjectUnavailable`, `DecodeError`, `TransportError`, `BuildError`,
5287
+ * `SimulationFailed`, `SigningError`, `PolicyDenied`, `NotApplied`,
5288
+ * `JournalError`, `UnexpectedEffects`, `ExecutionFailed`, `SubmissionUnknown`,
5289
+ * plus `ConfigError` and `NetworkMismatch` from the layer.
5290
+ */
5291
+ export const program = Effect.gen(function*() {
5292
+ const { signer, sui } = yield* Script
5293
+ const id = yield* Config.schema(ObjectId, "ESCROW_ID")
5294
+ const escrow = yield* sui.getObject(id, { schema: Escrow })
5295
+ const executed = yield* Tx.run((tx) => {
5296
+ tx.moveCall({
5297
+ target: `${PKG}::escrow::claim`,
5298
+ arguments: [tx.object(id), tx.pure.u64(escrow.content.amount)]
5299
+ })
5300
+ }, { signer })
5301
+ const receipt = yield* executed.expectCreated(`${PKG}::escrow::Receipt`)
5302
+ yield* Console.log(receipt.id)
5303
+ })
5304
+
5305
+ if (import.meta.main) {
5306
+ await Script.run(program)
5307
+ }
5308
+ ```
5309
+
5310
+ ### `examples/compare-write.sdk.ts`
5311
+
5312
+ ```ts
5313
+ /**
5314
+ * Claims an escrow through a PTB, written against the canonical 2.30 SDK.
5315
+ *
5316
+ * Compare with `examples/compare-write.effect.ts`, which does the same thing
5317
+ * through `Tx.run` and notes what that adds over this file. Both sign with
5318
+ * `SUI_PRIVATE_KEY` and print the created receipt's object id.
5319
+ *
5320
+ * Run with:
5321
+ * SUI_NETWORK=testnet SUI_PRIVATE_KEY=suiprivkey1… ESCROW_ID=0x… bun examples/compare-write.sdk.ts
5322
+ */
5323
+ import { bcs } from "@mysten/sui/bcs"
5324
+ import type { ClientWithCoreApi } from "@mysten/sui/client"
5325
+ import type { Keypair } from "@mysten/sui/cryptography"
5326
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography"
5327
+ import { SuiGrpcClient } from "@mysten/sui/grpc"
5328
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
5329
+ import { Secp256k1Keypair } from "@mysten/sui/keypairs/secp256k1"
5330
+ import { Secp256r1Keypair } from "@mysten/sui/keypairs/secp256r1"
5331
+ import { Transaction } from "@mysten/sui/transactions"
5332
+
5333
+ const PKG = "0x0000000000000000000000000000000000000000000000000000000000000002"
5334
+ const RECEIPT_TYPE = `${PKG}::escrow::Receipt`
5335
+ const EscrowBcs = bcs.struct("Escrow", { id: bcs.Address, amount: bcs.u64() })
5336
+
5337
+ // The scheme flag on a Bech32 key names one of three keypair classes;
5338
+ // nothing else decodes it, and a fourth scheme has no keypair class at all.
5339
+ const KEYPAIR_FOR: Record<string, undefined | ((secretKey: Uint8Array) => Keypair)> = {
5340
+ ED25519: Ed25519Keypair.fromSecretKey,
5341
+ Secp256k1: Secp256k1Keypair.fromSecretKey,
5342
+ Secp256r1: Secp256r1Keypair.fromSecretKey
5343
+ }
5344
+
5345
+ /**
5346
+ * Reads the escrow, claims it, and returns the created receipt's object id.
5347
+ *
5348
+ * Builds, signs and executes as three explicit steps rather than the
5349
+ * one-call `signAndExecuteTransaction`, so this can run against an in-memory
5350
+ * client with no live network or signer behind it.
5351
+ */
5352
+ export const claimEscrow = async (
5353
+ client: ClientWithCoreApi,
5354
+ escrowId: string,
5355
+ keypair: Keypair
5356
+ ): Promise<string> => {
5357
+ const { object } = await client.core.getObject({ objectId: escrowId, include: { content: true } })
5358
+ const { amount } = EscrowBcs.parse(object.content)
5359
+
5360
+ const tx = new Transaction()
5361
+ tx.setSenderIfNotSet(keypair.toSuiAddress())
5362
+ tx.moveCall({ target: `${PKG}::escrow::claim`, arguments: [tx.object(escrowId), tx.pure.u64(amount)] })
5363
+ const bytes = await tx.build({ client })
5364
+ const { signature } = await keypair.signTransaction(bytes)
5365
+
5366
+ const result = await client.core.executeTransaction({
5367
+ transaction: bytes,
5368
+ signatures: [signature],
5369
+ include: { effects: true, objectTypes: true }
5370
+ })
5371
+ const digest = (result.Transaction ?? result.FailedTransaction).digest
5372
+ await client.core.waitForTransaction({ digest })
5373
+
5374
+ if (result.$kind === "FailedTransaction") {
5375
+ const { error } = result.FailedTransaction.status
5376
+ if (error?.$kind === "MoveAbort") {
5377
+ const { abortCode, cleverError } = error.MoveAbort
5378
+ throw new Error(`claim aborted: code ${abortCode}${cleverError?.constantName ? ` (${cleverError.constantName})` : ""}`)
5379
+ }
5380
+ throw new Error(`claim failed: ${error?.message}`)
5381
+ }
5382
+
5383
+ // Effects list every changed object by id; only the `objectTypes` join
5384
+ // says which one is the receipt. sui-effect's `expectCreated` is this join,
5385
+ // plus a check that exactly one match exists.
5386
+ const { changedObjects } = result.Transaction.effects
5387
+ const created = changedObjects.find(
5388
+ (change) => change.idOperation === "Created" && result.Transaction.objectTypes[change.objectId] === RECEIPT_TYPE
5389
+ )
5390
+ if (created === undefined) throw new Error(`claim applied (${digest}) but created no ${RECEIPT_TYPE}`)
5391
+ return created.objectId
5392
+ }
5393
+
5394
+ if (import.meta.main) {
5395
+ const escrowId = process.env.ESCROW_ID
5396
+ const key = process.env.SUI_PRIVATE_KEY
5397
+ if (!escrowId || !key) throw new Error("ESCROW_ID and SUI_PRIVATE_KEY are required")
5398
+ const parsed = decodeSuiPrivateKey(key)
5399
+ const fromSecretKey = KEYPAIR_FOR[parsed.scheme]
5400
+ if (!fromSecretKey) throw new Error(`${parsed.scheme} has no keypair class here (use a remote signer)`)
5401
+ const client = new SuiGrpcClient({
5402
+ network: (process.env.SUI_NETWORK ?? "testnet") as "testnet",
5403
+ baseUrl: process.env.SUI_RPC_URL ?? "https://fullnode.testnet.sui.io:443"
5404
+ })
5405
+ console.log(await claimEscrow(client, escrowId, fromSecretKey(parsed.secretKey)))
5406
+ }
5407
+ ```
5408
+
4987
5409
  ### `examples/extension-consumer.ts`
4988
5410
 
4989
5411
  ```ts
@@ -6120,13 +6542,16 @@ import {
6120
6542
  Stream
6121
6543
  } from "effect"
6122
6544
  import { TestClock } from "effect/testing"
6123
- import type { Sui, SuiCore } from "@unconfirmed/sui-effect"
6124
- import { KNOWN_CHAIN_IDS, ObjectId, SuiAddress, SuiSchema } from "@unconfirmed/sui-effect"
6545
+ import type { ChangedRef, Recipe, Sui, SuiCore } from "@unconfirmed/sui-effect"
6546
+ import { KNOWN_CHAIN_IDS, ObjectId, SuiAddress, SuiError, SuiSchema } from "@unconfirmed/sui-effect"
6547
+ import type { PromiseFace } from "@unconfirmed/sui-effect/extension"
6125
6548
  import { FakeOutcome, layerExtensionTest, layerTest, SuiCoreFake, SuiTest } from "@unconfirmed/sui-effect/testing"
6126
6549
  import { Journal, Signer } from "@unconfirmed/sui-effect/tx"
6550
+ import type { EscrowObject, EscrowService } from "../src/Escrow.ts"
6127
6551
  import { DEPLOYMENTS, Escrow } from "../src/Escrow.ts"
6128
6552
  import { escrow as escrowRegistration } from "../src/extension.ts"
6129
6553
  import { EscrowNotFound, EscrowSettlementUnknown, EscrowUnsupportedNetwork } from "../src/errors.ts"
6554
+ import type { PlatformService } from "../src/Platform.ts"
6130
6555
  import { Platform, platform as platformRegistration } from "../src/Platform.ts"
6131
6556
  import { ESCROW_PACKAGE, receiptType, Settlement, SettlementContent } from "../src/schema.ts"
6132
6557
 
@@ -6655,6 +7080,92 @@ describe("layerConfig validates through the typed deployment path", () => {
6655
7080
  expect(String(exit)).toContain("32-byte Sui object id")
6656
7081
  })
6657
7082
  })
7083
+
7084
+ /**
7085
+ * The face's **type**, asserted rather than described.
7086
+ *
7087
+ * This is the test every extension copies. `PromiseFace<Service>` is what a
7088
+ * Promise consumer actually holds, and it is derived, so nothing in the service
7089
+ * says out loud what it produced: an `Effect` member has to become a
7090
+ * Promise-returning method, a `Stream` member an `AsyncIterable`, a synchronous
7091
+ * member has to stay synchronous, and a **namespace has to be mapped all the
7092
+ * way down** — `PlatformService.escrow` is an interface, and until 0.1.1 an
7093
+ * interface-typed namespace kept its `Effect` members in the type while the
7094
+ * runtime handed back Promises.
7095
+ *
7096
+ * Write one of these per namespace. It costs four lines and it is the only
7097
+ * thing that catches a face type that has quietly stopped matching the runtime.
7098
+ */
7099
+ describe("the Promise face type", () => {
7100
+ /** Compile-time assignability, as a value a test can assert on. */
7101
+ const assignableTo = <_A extends _B, _B>(): true => true
7102
+
7103
+ type EscrowFace = PromiseFace<EscrowService>
7104
+ type PlatformFace = PromiseFace<PlatformService>
7105
+
7106
+ test("an Effect member becomes a Promise-returning method", () => {
7107
+ expect(assignableTo<EscrowFace["get"], (id: ObjectId) => Promise<EscrowObject>>()).toBe(true)
7108
+ expect(assignableTo<EscrowFace["feeCollector"], () => Promise<SuiAddress>>()).toBe(true)
7109
+ })
7110
+
7111
+ test("a Stream member becomes an AsyncIterable", () => {
7112
+ expect(
7113
+ assignableTo<EscrowFace["owned"]["stream"], (owner: SuiAddress) => AsyncIterable<EscrowObject>>()
7114
+ ).toBe(true)
7115
+ })
7116
+
7117
+ test("a synchronous member stays synchronous", () => {
7118
+ expect(assignableTo<EscrowFace["packageId"], string>()).toBe(true)
7119
+ expect(assignableTo<EscrowFace["claim"], (escrow: EscrowObject) => Recipe>()).toBe(true)
7120
+ })
7121
+
7122
+ test("an interface-typed namespace is mapped all the way down", () => {
7123
+ // `PlatformService.escrow` is `EscrowService`, an interface. The members
7124
+ // reached through it must be the mapped ones, not the Effect ones.
7125
+ expect(assignableTo<PlatformFace["escrow"]["get"], (id: ObjectId) => Promise<EscrowObject>>())
7126
+ .toBe(true)
7127
+ expect(
7128
+ assignableTo<
7129
+ PlatformFace["escrow"]["owned"]["count"],
7130
+ (owner: SuiAddress) => Promise<number>
7131
+ >()
7132
+ ).toBe(true)
7133
+ expect(assignableTo<PlatformFace["escrow"]["packageId"], string>()).toBe(true)
7134
+ })
7135
+
7136
+ test("the composition's own member is mapped too", () => {
7137
+ expect(
7138
+ assignableTo<
7139
+ PlatformFace["claimEverything"],
7140
+ (ids: ReadonlyArray<ObjectId>, opts: { readonly signer: Signer }) => Promise<
7141
+ ReadonlyArray<ChangedRef>
7142
+ >
7143
+ >()
7144
+ ).toBe(true)
7145
+ })
7146
+ })
7147
+
7148
+ /**
7149
+ * The errors serialize with their `outcome`, which is what a wrapper script
7150
+ * acts on and what an operator reads out of a log line.
7151
+ */
7152
+ describe("the errors", () => {
7153
+ test("SuiError.toJson keeps the outcome, though it is a class field", () => {
7154
+ const error = new EscrowSettlementUnknown({
7155
+ escrowId: ESCROW_ID,
7156
+ digest: "1".repeat(32) as never,
7157
+ message: "the operator never confirmed"
7158
+ })
7159
+ const json = SuiError.toJson(error)
7160
+ expect(json["_tag"]).toBe("escrow/EscrowSettlementUnknown")
7161
+ expect(json["escrowId"]).toBe(ESCROW_ID)
7162
+ // `outcome` is declared as a class field — not a schema field — because
7163
+ // that is the shape that reads well at the call site. `toJson` reads it off
7164
+ // the instance, so it is in the JSON anyway.
7165
+ expect(json["outcome"]).toBe("unknown")
7166
+ expect(json["outcome"]).toBe(SuiError.outcome(error))
7167
+ })
7168
+ })
6658
7169
  ```
6659
7170
 
6660
7171
  ### `examples/extension-template/package.json`