@misofm/effect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +71 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts +54 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +131 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/reads.d.ts +39 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +107 -0
- package/dist/reads.js.map +1 -0
- package/dist/sui-client.d.ts +17 -0
- package/dist/sui-client.d.ts.map +1 -0
- package/dist/sui-client.js +22 -0
- package/dist/sui-client.js.map +1 -0
- package/package.json +82 -0
- package/src/errors.ts +59 -0
- package/src/execute.ts +177 -0
- package/src/index.ts +10 -0
- package/src/reads.ts +165 -0
- package/src/sui-client.ts +27 -0
package/src/reads.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The shared read primitives every `@misofm/*` package's queries are built
|
|
5
|
+
// from. Every primitive requires the `SuiClient` service instead of taking a
|
|
6
|
+
// client parameter, and every not-found path is classified into a typed
|
|
7
|
+
// `ObjectNotFoundError` rather than a bare thrown error — see `isNotFound`
|
|
8
|
+
// below, ported from the transport-sniffing heuristic each package used to
|
|
9
|
+
// hand-roll.
|
|
10
|
+
|
|
11
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
12
|
+
import type { SuiClientTypes } from "@mysten/sui/client";
|
|
13
|
+
|
|
14
|
+
import { BcsDecodeError, ObjectNotFoundError, ObjectTypeMismatchError, SuiRpcError } from "./errors.ts";
|
|
15
|
+
import { SuiClient } from "./sui-client.ts";
|
|
16
|
+
|
|
17
|
+
/** One object's BCS content, on-chain type, and version. */
|
|
18
|
+
export interface ObjectContent {
|
|
19
|
+
readonly content: Uint8Array;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly version: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A dynamic field entry as returned by `listDynamicFields` (name + value type, not the decoded value). */
|
|
25
|
+
export type DynamicField = SuiClientTypes.DynamicFieldEntry;
|
|
26
|
+
|
|
27
|
+
/** Any generated BCS codec with a `parse` method — the shape every `src/contracts/**` struct exports. */
|
|
28
|
+
export interface BcsParser<T> {
|
|
29
|
+
parse(bytes: Uint8Array): T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Identifies the object/type a `decodeBcs` failure is reported against. */
|
|
33
|
+
export interface DecodeBcsContext {
|
|
34
|
+
/** Fully-qualified Move type (or domain type name) being decoded. */
|
|
35
|
+
readonly type: string;
|
|
36
|
+
/** Object id being decoded, when decoding a specific object's content. */
|
|
37
|
+
readonly objectId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fetches one object's BCS content by id; fails with `ObjectNotFoundError` if it does not exist. */
|
|
41
|
+
export const getObjectContent = Effect.fn("getObjectContent")(function* (
|
|
42
|
+
objectId: string,
|
|
43
|
+
): Effect.fn.Return<ObjectContent, ObjectNotFoundError | SuiRpcError, SuiClient> {
|
|
44
|
+
const client = yield* SuiClient;
|
|
45
|
+
const { object } = yield* Effect.tryPromise({
|
|
46
|
+
try: (signal) => client.core.getObject({ objectId, include: { content: true }, signal }),
|
|
47
|
+
catch: (cause) => classifyObjectError(objectId, "getObject", cause),
|
|
48
|
+
});
|
|
49
|
+
if (!object.content) {
|
|
50
|
+
return yield* new ObjectNotFoundError({ objectId });
|
|
51
|
+
}
|
|
52
|
+
return { content: object.content, type: object.type, version: object.version };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** Like {@link getObjectContent}, but a missing object resolves to `Option.none()` instead of failing. */
|
|
56
|
+
export const getOptionalObjectContent = Effect.fn("getOptionalObjectContent")(function* (
|
|
57
|
+
objectId: string,
|
|
58
|
+
): Effect.fn.Return<Option.Option<ObjectContent>, SuiRpcError, SuiClient> {
|
|
59
|
+
return yield* getObjectContent(objectId).pipe(
|
|
60
|
+
Effect.map(Option.some),
|
|
61
|
+
Effect.catchTag("ObjectNotFoundError", () => Effect.succeed(Option.none())),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** Fetches many objects' BCS content in one Core request; ids that are missing or errored are omitted from the map. */
|
|
66
|
+
export const getObjectsContent = Effect.fn("getObjectsContent")(function* (
|
|
67
|
+
objectIds: readonly string[],
|
|
68
|
+
): Effect.fn.Return<ReadonlyMap<string, { content: Uint8Array; type: string }>, SuiRpcError, SuiClient> {
|
|
69
|
+
const out = new Map<string, { content: Uint8Array; type: string }>();
|
|
70
|
+
if (objectIds.length === 0) return out;
|
|
71
|
+
|
|
72
|
+
const client = yield* SuiClient;
|
|
73
|
+
const { objects } = yield* Effect.tryPromise({
|
|
74
|
+
try: (signal) => client.core.getObjects({ objectIds: [...objectIds], include: { content: true }, signal }),
|
|
75
|
+
catch: (cause) => new SuiRpcError({ operation: "getObjects", cause }),
|
|
76
|
+
});
|
|
77
|
+
for (const obj of objects) {
|
|
78
|
+
if (obj instanceof Error || !obj.content) continue;
|
|
79
|
+
out.set(obj.objectId, { content: obj.content, type: obj.type });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Pages every dynamic field under `parentId` through `client.core.listDynamicFields`, following the cursor. */
|
|
85
|
+
export function listDynamicFields(parentId: string): Stream.Stream<DynamicField, SuiRpcError, SuiClient> {
|
|
86
|
+
return Stream.paginate<string | null, DynamicField, SuiRpcError, SuiClient>(null, (cursor) =>
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
const client = yield* SuiClient;
|
|
89
|
+
const page = yield* Effect.tryPromise({
|
|
90
|
+
try: (signal) => client.core.listDynamicFields({ parentId, cursor, signal }),
|
|
91
|
+
catch: (cause) => new SuiRpcError({ operation: "listDynamicFields", cause }),
|
|
92
|
+
});
|
|
93
|
+
const next = page.hasNextPage ? Option.some(page.cursor) : Option.none();
|
|
94
|
+
return [page.dynamicFields, next] as const;
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Runs a generated BCS codec's `.parse`, then decodes the result into a domain `Schema` type; both failure paths become `BcsDecodeError`. */
|
|
100
|
+
export const decodeBcs = Effect.fn("decodeBcs")(function* <A>(
|
|
101
|
+
codec: BcsParser<unknown>,
|
|
102
|
+
schema: Schema.Codec<A, any, never, never>,
|
|
103
|
+
bytes: Uint8Array,
|
|
104
|
+
context: DecodeBcsContext,
|
|
105
|
+
): Effect.fn.Return<A, BcsDecodeError> {
|
|
106
|
+
const parsed = yield* Effect.try({
|
|
107
|
+
try: () => codec.parse(bytes),
|
|
108
|
+
catch: (cause) => new BcsDecodeError({ type: context.type, objectId: context.objectId, cause }),
|
|
109
|
+
});
|
|
110
|
+
return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
111
|
+
Effect.mapError((cause) => new BcsDecodeError({ type: context.type, objectId: context.objectId, cause })),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** Fails with `ObjectTypeMismatchError` unless `actual` is exactly `expected`. */
|
|
116
|
+
export const assertObjectType = Effect.fn("assertObjectType")(function* (
|
|
117
|
+
objectId: string,
|
|
118
|
+
actual: string,
|
|
119
|
+
expected: string,
|
|
120
|
+
): Effect.fn.Return<void, ObjectTypeMismatchError> {
|
|
121
|
+
if (actual !== expected) {
|
|
122
|
+
return yield* new ObjectTypeMismatchError({ objectId, expected, actual });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── Private ──────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Classifies a Core API rejection: a missing object becomes `ObjectNotFoundError`, anything else `SuiRpcError`. */
|
|
129
|
+
function classifyObjectError(objectId: string, operation: string, cause: unknown): ObjectNotFoundError | SuiRpcError {
|
|
130
|
+
return isNotFound(cause) ? new ObjectNotFoundError({ objectId }) : new SuiRpcError({ operation, cause });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when `e` is a "this object does not exist" error from any of the Sui
|
|
135
|
+
* client transports. Matches, in order of preference:
|
|
136
|
+
*
|
|
137
|
+
* 1. Structured `ObjectError.code` values thrown by the JSON-RPC core client
|
|
138
|
+
* (`notExists`, `deleted`, `dynamicFieldNotFound`) and the GraphQL core
|
|
139
|
+
* client (`notFound`). The class itself is not exported by `@mysten/sui`,
|
|
140
|
+
* so we duck-type on `code`.
|
|
141
|
+
* 2. The message shapes those clients (and the gRPC core client, which wraps
|
|
142
|
+
* the server's per-object status message in a plain `Error`) produce:
|
|
143
|
+
* "Object 0x… does not exist" / "Object 0x… not found" / "Object 0x… has
|
|
144
|
+
* been deleted" / "Dynamic field not found for object 0x…" / "No object
|
|
145
|
+
* found for id 0x…".
|
|
146
|
+
*
|
|
147
|
+
* Transport/protocol errors must NOT match: every message pattern requires
|
|
148
|
+
* object-ish context ("object" / "dynamic field"), so e.g. a JSON-RPC
|
|
149
|
+
* "Method not found" or a gRPC "peer not found" is never treated as a missing
|
|
150
|
+
* object and is reported as a `SuiRpcError` instead.
|
|
151
|
+
*/
|
|
152
|
+
function isNotFound(e: unknown): boolean {
|
|
153
|
+
if (typeof e === "object" && e !== null && "code" in e) {
|
|
154
|
+
const code = (e as { code: unknown }).code;
|
|
155
|
+
if (code === "notExists" || code === "deleted" || code === "dynamicFieldNotFound" || code === "notFound") {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
160
|
+
return (
|
|
161
|
+
/\bobject\b[\s\S]*\b(?:not\s?found|does not exist|has been deleted)\b/i.test(msg) ||
|
|
162
|
+
/\bdynamic field\b[\s\S]*\bnot\s?found\b/i.test(msg) ||
|
|
163
|
+
/\bno object\b/i.test(msg)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The Sui client services every `@misofm/*` SDK reaches through the Requirements
|
|
5
|
+
// channel instead of a `client` parameter. There is exactly one `SuiClient`
|
|
6
|
+
// service tag (and one `SuiGraphQL` tag) across the SDKs, so a program built
|
|
7
|
+
// from primitives in different packages still needs only one `Layer.provide`.
|
|
8
|
+
|
|
9
|
+
import { Context, Layer } from "effect";
|
|
10
|
+
import type { ClientWithCoreApi } from "@mysten/sui/client";
|
|
11
|
+
import type { SuiGraphQLClient } from "@mysten/sui/graphql";
|
|
12
|
+
|
|
13
|
+
/** The unified Core API client (gRPC / JSON-RPC / GraphQL transports all satisfy it). */
|
|
14
|
+
export class SuiClient extends Context.Service<SuiClient, ClientWithCoreApi>()("@misofm/effect/SuiClient") {
|
|
15
|
+
/** Provides a concrete `ClientWithCoreApi` as the `SuiClient` service. */
|
|
16
|
+
static layer(client: ClientWithCoreApi): Layer.Layer<SuiClient> {
|
|
17
|
+
return Layer.succeed(SuiClient, client);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The optional GraphQL client, required only by type-discovery reads. */
|
|
22
|
+
export class SuiGraphQL extends Context.Service<SuiGraphQL, SuiGraphQLClient>()("@misofm/effect/SuiGraphQL") {
|
|
23
|
+
/** Provides a concrete `SuiGraphQLClient` as the `SuiGraphQL` service. */
|
|
24
|
+
static layer(client: SuiGraphQLClient): Layer.Layer<SuiGraphQL> {
|
|
25
|
+
return Layer.succeed(SuiGraphQL, client);
|
|
26
|
+
}
|
|
27
|
+
}
|