@midnight-ntwrk/wallet-sdk-abstractions 3.0.0-beta.0 → 3.0.0-beta.2

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.
@@ -11,6 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import { Schema } from 'effect';
14
+ import { coversTransaction, } from './TransactionHistoryStorage.js';
14
15
  /**
15
16
  * In-memory implementation of the TransactionHistoryStorage interface.
16
17
  *
@@ -42,9 +43,11 @@ export class InMemoryTransactionHistoryStorage {
42
43
  const { finalizedBlock, ...rest } = input;
43
44
  const entry = { ...rest, lifecycle: { status: 'finalized', finalizedBlock } };
44
45
  await this.#upsert(entry);
45
- this.#clearPendingByIdentifiers(entry.identifiers, entry.hash);
46
+ this.#clearUnfinalizedCoveredBy(entry);
46
47
  }
47
48
  async gotRejected(input) {
49
+ if (this.#inclusionRecorded(input))
50
+ return;
48
51
  const { rejectedAt, reason, ...rest } = input;
49
52
  const lifecycle = reason !== undefined
50
53
  ? { status: 'rejected', rejectedAt, reason }
@@ -80,16 +83,15 @@ export class InMemoryTransactionHistoryStorage {
80
83
  this.#storage.set(entry.hash, existing ? this.#merge(existing, entry) : entry);
81
84
  return Promise.resolve();
82
85
  }
83
- #clearPendingByIdentifiers(identifiers, newHash) {
84
- if (identifiers.length === 0)
85
- return;
86
- const provided = new Set(identifiers);
87
- const match = [...this.#storage.values()].find((entry) => entry.identifiers.length > 0 &&
88
- entry.identifiers.every((id) => provided.has(id)) &&
89
- entry.lifecycle.status === 'pending' &&
90
- entry.hash !== newHash);
91
- if (match) {
92
- this.#storage.delete(match.hash);
93
- }
86
+ /** Checked synchronously with the write that follows, so a verdict racing sync cannot supersede inclusion. */
87
+ #inclusionRecorded(key) {
88
+ return [...this.#storage.values()].some((entry) => entry.lifecycle.status === 'finalized' && coversTransaction(entry, key));
89
+ }
90
+ #clearUnfinalizedCoveredBy(finalized) {
91
+ [...this.#storage.values()]
92
+ .filter((entry) => entry.lifecycle.status !== 'finalized' &&
93
+ entry.hash !== finalized.hash &&
94
+ coversTransaction(finalized, entry))
95
+ .forEach((entry) => this.#storage.delete(entry.hash));
94
96
  }
95
97
  }
@@ -1,20 +1,28 @@
1
1
  import { Equivalence } from 'effect';
2
2
  import type * as ProtocolVersion from './ProtocolVersion.js';
3
3
  /**
4
- * A type that associates some state with a given version of the Midnight protocol.
4
+ * A type that associates some state with a given version of the Midnight protocol, and with the variant that produced
5
+ * it.
5
6
  *
7
+ * @remarks
8
+ * `variantTag` is the tag of the wallet variant the emission came from. The runtime knows it at the moment it publishes
9
+ * a state, so it hands it over rather than leaving readers to infer the producing implementation from `version` —
10
+ * which would mean re-deriving activation ranges, and casting, on every emission. It travels with the state so that
11
+ * selecting the capabilities that understand a state is a lookup by tag.
6
12
  * @typeParam TState The type of state.
13
+ * @typeParam TVariantTag The tag, or union of tags, of the variants that can produce this state.
7
14
  */
8
- export type ProtocolState<TState> = Readonly<{
15
+ export type ProtocolState<TState, TVariantTag extends string | symbol = string | symbol> = Readonly<{
9
16
  version: ProtocolVersion.ProtocolVersion;
17
+ variantTag: TVariantTag;
10
18
  state: TState;
11
19
  }>;
12
20
  export declare const state: <TState>(ps: ProtocolState<TState>) => TState;
13
21
  /**
14
22
  * Derives an {@link Equivalence.Equivalence} for {@link ProtocolState} values from an equivalence of the underlying
15
- * state. Versions are compared strictly.
23
+ * state. Versions and producing variant tags are compared strictly.
16
24
  *
17
25
  * @param stateEquivalence The equivalence used to compare the `state` field.
18
26
  * @returns An equivalence over `ProtocolState<TState>`.
19
27
  */
20
- export declare const getEquivalence: <TState>(stateEquivalence: Equivalence.Equivalence<TState>) => Equivalence.Equivalence<ProtocolState<TState>>;
28
+ export declare const getEquivalence: <TState, TVariantTag extends string | symbol = string | symbol>(stateEquivalence: Equivalence.Equivalence<TState>) => Equivalence.Equivalence<ProtocolState<TState, TVariantTag>>;
@@ -14,12 +14,13 @@ import { Equivalence } from 'effect';
14
14
  export const state = (ps) => ps.state;
15
15
  /**
16
16
  * Derives an {@link Equivalence.Equivalence} for {@link ProtocolState} values from an equivalence of the underlying
17
- * state. Versions are compared strictly.
17
+ * state. Versions and producing variant tags are compared strictly.
18
18
  *
19
19
  * @param stateEquivalence The equivalence used to compare the `state` field.
20
20
  * @returns An equivalence over `ProtocolState<TState>`.
21
21
  */
22
22
  export const getEquivalence = (stateEquivalence) => Equivalence.struct({
23
23
  version: Equivalence.strict(),
24
+ variantTag: Equivalence.strict(),
24
25
  state: stateEquivalence,
25
26
  });
@@ -1,4 +1,6 @@
1
1
  import * as Brand from 'effect/Brand';
2
+ import * as Either from 'effect/Either';
3
+ import * as Option from 'effect/Option';
2
4
  import * as Schema from 'effect/Schema';
3
5
  /** A branded `bigint` that represents a protocol version. */
4
6
  export type ProtocolVersion = Brand.Branded<bigint, 'ProtocolVersion'>;
@@ -38,3 +40,147 @@ export declare const is: (u: unknown, overrideOptions?: import("effect/SchemaAST
38
40
  export declare const MinSupportedVersion: ProtocolVersion;
39
41
  /** Represents the maximum supported protocol version. */
40
42
  export declare const MaxSupportedVersion: ProtocolVersion;
43
+ /**
44
+ * The protocol version a ledger-v9-native chain hands over at, for the current node line.
45
+ *
46
+ * @remarks
47
+ * Measured, not assumed: a `midnight-node` 2.x reports protocol version `2000000` on its ledger events — the runtime
48
+ * version, scaled, rather than a small ordinal or the ledger major. A chain of the previous node line reports a
49
+ * 1.x-encoded value, which is below this, so a wallet configured with it stays on the V1 variant there and reaches
50
+ * the V2 variant on any v9-native chain.
51
+ *
52
+ * Offered as a named value so applications and test suites pointed at a v9-native chain do not each invent a magic
53
+ * number; {@link V9NativeForkSchedule} is the schedule built from it. The final mainnet fork constant is still an open
54
+ * question; when it is fixed it will join this one here.
55
+ */
56
+ export declare const V9NativeForkVersion: ProtocolVersion;
57
+ /**
58
+ * Where each ledger version after the first begins on a chain, by protocol version.
59
+ *
60
+ * @remarks
61
+ * One key per ledger version the SDK can run, minus the first: ledger-v8 begins at {@link MinSupportedVersion} and needs
62
+ * no entry. `v9` is the protocol version from which ledger-v9 reads the chain — {@link V9NativeForkVersion} on a chain
63
+ * born on ledger-v9, and whatever version the hand-over is enacted at on a chain with ledger-v8 history. The next
64
+ * hard fork adds a key (`v10`) rather than changing this shape, which is what lets an application's configuration
65
+ * outlive the fork.
66
+ *
67
+ * Every entry is required in the wallet packages, for the same reason a single fork version was: where a chain forks is
68
+ * a fact about the chain rather than the SDK, and a wrong guess does not degrade — it decides which ledger version
69
+ * reads the chain. {@link V9NativeForkSchedule} names the schedule of a chain born on ledger-v9; only the facade
70
+ * presets one, `DefaultForkSchedule`, and says there why.
71
+ */
72
+ export type ForkSchedule = Readonly<{
73
+ /** The protocol version from which ledger-v9 reads the chain. */
74
+ v9: ProtocolVersion;
75
+ }>;
76
+ /**
77
+ * The fork schedule of a chain born on ledger-v9: ledger-v9 from {@link V9NativeForkVersion}, and nothing else.
78
+ *
79
+ * @remarks
80
+ * Offered as a named value for the same reason {@link V9NativeForkVersion} is: so applications and test suites pointed
81
+ * at a v9-native chain do not each write the schedule out by hand. A named value and not a default — the wallet
82
+ * packages still require `forks`. The facade's `DefaultForkSchedule` is this value, under the name of what the facade
83
+ * presets.
84
+ */
85
+ export declare const V9NativeForkSchedule: ForkSchedule;
86
+ /**
87
+ * The range of protocol versions on the same side of a protocol boundary as a given version.
88
+ *
89
+ * @remarks
90
+ * What the SDK means by "the same ledger version made these bytes". A protocol boundary divides the timeline into two
91
+ * epochs, and everything that routes on a version — which prover proves a transaction, which validator checks it,
92
+ * which variant may unwrap it — is really asking which epoch it belongs to. Stated once here so the two ends of that
93
+ * question, the wallet stamping a transaction and the caller unwrapping it, cannot compute the boundary differently.
94
+ *
95
+ * A chain whose boundary is at or below the minimum supported version has never had two epochs, so the whole range is
96
+ * one.
97
+ * @param version A version in the epoch of interest.
98
+ * @param forkVersion The version at which the chain hands over to the next ledger version.
99
+ * @returns The half-open range of versions in that epoch.
100
+ */
101
+ export declare const epochOf: (version: ProtocolVersion, forkVersion: ProtocolVersion) => ProtocolVersion.Range;
102
+ /**
103
+ * A value together with the protocol version range it serves.
104
+ *
105
+ * @typeParam T The type of the registered value.
106
+ */
107
+ export type RegistryEntry<T> = Readonly<{
108
+ range: ProtocolVersion.Range;
109
+ value: T;
110
+ }>;
111
+ /**
112
+ * An ordered collection of values, each keyed by the protocol version range it serves.
113
+ *
114
+ * @remarks
115
+ * This is the shared shape behind every "which implementation speaks this protocol version?" question the SDK asks —
116
+ * which codec decodes a block, which prover proves a recipe, which transaction trait understands a pending
117
+ * transaction, which variant a snapshot should be restored into. Keeping one primitive means those answers cannot
118
+ * disagree about what a version range is or where its boundaries lie.
119
+ *
120
+ * Entries are ascending and non-overlapping by construction, and ranges are half-open (`withinRange`), so exactly one
121
+ * entry can match a version. Gaps are allowed: a version no entry covers selects nothing, which callers turn into
122
+ * whichever typed error means "unsupported" in their domain.
123
+ * @typeParam T The type of the registered values.
124
+ */
125
+ export type Registry<T> = Readonly<{
126
+ entries: readonly RegistryEntry<T>[];
127
+ }>;
128
+ declare const RegistryError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
129
+ readonly _tag: "@midnight-ntwrk/wallet-sdk-abstractions/ProtocolVersion/RegistryError";
130
+ } & Readonly<A>;
131
+ /**
132
+ * Raised when entries cannot form a {@link Registry} because their ranges are not ascending, overlap, or cannot be
133
+ * closed at all.
134
+ *
135
+ * @remarks
136
+ * Construction is the only place this can happen: once built, a registry is total — selection returns an
137
+ * {@link Option.Option} rather than failing.
138
+ */
139
+ export declare class RegistryError extends RegistryError_base<{
140
+ readonly message: string;
141
+ /** The protocol versions whose ordering or overlap made the registry invalid. */
142
+ readonly versions: readonly ProtocolVersion[];
143
+ }> {
144
+ }
145
+ /** A registry with no entries: {@link select} on it never finds anything. */
146
+ export declare const emptyRegistry: Registry<never>;
147
+ /**
148
+ * Creates a registry from entries that already carry their own ranges.
149
+ *
150
+ * @param entries The entries, in ascending order of range start; ranges must not overlap, though gaps are allowed.
151
+ * @returns The registry, or a {@link RegistryError} naming the two range boundaries that could not be ordered.
152
+ */
153
+ export declare const makeRegistry: <T>(entries: readonly RegistryEntry<T>[]) => Either.Either<Registry<T>, RegistryError>;
154
+ /**
155
+ * Creates a registry from values keyed by the protocol version each becomes active at, deriving the ranges from the
156
+ * registration order: entry _i_ serves `[sinceVersion(i), sinceVersion(i + 1))`, and the last entry serves
157
+ * `[sinceVersion(last), MaxSupportedVersion)`.
158
+ *
159
+ * @remarks
160
+ * This is the shape registration has everywhere in the SDK — a variant, codec or prover says which version it starts
161
+ * answering for, never which version it stops at — so the boundary between two of them is derived in exactly one
162
+ * place and cannot drift.
163
+ * @param activations The values and the versions they activate at, in strictly ascending order.
164
+ * @returns The registry, or a {@link RegistryError} naming the versions that broke the ordering.
165
+ */
166
+ export declare const makeRegistryFromActivations: <T>(activations: readonly Readonly<{
167
+ sinceVersion: ProtocolVersion;
168
+ value: T;
169
+ }>[]) => Either.Either<Registry<T>, RegistryError>;
170
+ /**
171
+ * Finds the entry whose range contains a given protocol version.
172
+ *
173
+ * @param registry The {@link Registry} to search.
174
+ * @param version The version to find an entry for.
175
+ * @returns The matching entry, or `Option.none()` when no entry covers `version`.
176
+ */
177
+ export declare const selectEntry: <T>(registry: Registry<T>, version: ProtocolVersion) => Option.Option<RegistryEntry<T>>;
178
+ /**
179
+ * Finds the value whose range contains a given protocol version.
180
+ *
181
+ * @param registry The {@link Registry} to search.
182
+ * @param version The version to find a value for.
183
+ * @returns The matching value, or `Option.none()` when no entry covers `version`.
184
+ */
185
+ export declare const select: <T>(registry: Registry<T>, version: ProtocolVersion) => Option.Option<T>;
186
+ export {};
@@ -11,6 +11,9 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import * as Brand from 'effect/Brand';
14
+ import * as Data from 'effect/Data';
15
+ import * as Either from 'effect/Either';
16
+ import * as Option from 'effect/Option';
14
17
  import * as Schema from 'effect/Schema';
15
18
  /** Constructs a branded `bigint` represents a protocol version. */
16
19
  export const ProtocolVersion = Brand.nominal();
@@ -52,3 +55,124 @@ export const is = Schema.is(ProtocolVersionSchema);
52
55
  export const MinSupportedVersion = ProtocolVersion(0n);
53
56
  /** Represents the maximum supported protocol version. */
54
57
  export const MaxSupportedVersion = ProtocolVersion(BigInt(Number.MAX_SAFE_INTEGER));
58
+ /**
59
+ * The protocol version a ledger-v9-native chain hands over at, for the current node line.
60
+ *
61
+ * @remarks
62
+ * Measured, not assumed: a `midnight-node` 2.x reports protocol version `2000000` on its ledger events — the runtime
63
+ * version, scaled, rather than a small ordinal or the ledger major. A chain of the previous node line reports a
64
+ * 1.x-encoded value, which is below this, so a wallet configured with it stays on the V1 variant there and reaches
65
+ * the V2 variant on any v9-native chain.
66
+ *
67
+ * Offered as a named value so applications and test suites pointed at a v9-native chain do not each invent a magic
68
+ * number; {@link V9NativeForkSchedule} is the schedule built from it. The final mainnet fork constant is still an open
69
+ * question; when it is fixed it will join this one here.
70
+ */
71
+ export const V9NativeForkVersion = ProtocolVersion(2000000n);
72
+ /**
73
+ * The fork schedule of a chain born on ledger-v9: ledger-v9 from {@link V9NativeForkVersion}, and nothing else.
74
+ *
75
+ * @remarks
76
+ * Offered as a named value for the same reason {@link V9NativeForkVersion} is: so applications and test suites pointed
77
+ * at a v9-native chain do not each write the schedule out by hand. A named value and not a default — the wallet
78
+ * packages still require `forks`. The facade's `DefaultForkSchedule` is this value, under the name of what the facade
79
+ * presets.
80
+ */
81
+ export const V9NativeForkSchedule = { v9: V9NativeForkVersion };
82
+ /**
83
+ * The range of protocol versions on the same side of a protocol boundary as a given version.
84
+ *
85
+ * @remarks
86
+ * What the SDK means by "the same ledger version made these bytes". A protocol boundary divides the timeline into two
87
+ * epochs, and everything that routes on a version — which prover proves a transaction, which validator checks it,
88
+ * which variant may unwrap it — is really asking which epoch it belongs to. Stated once here so the two ends of that
89
+ * question, the wallet stamping a transaction and the caller unwrapping it, cannot compute the boundary differently.
90
+ *
91
+ * A chain whose boundary is at or below the minimum supported version has never had two epochs, so the whole range is
92
+ * one.
93
+ * @param version A version in the epoch of interest.
94
+ * @param forkVersion The version at which the chain hands over to the next ledger version.
95
+ * @returns The half-open range of versions in that epoch.
96
+ */
97
+ export const epochOf = (version, forkVersion) => forkVersion <= MinSupportedVersion
98
+ ? makeRange(MinSupportedVersion, MaxSupportedVersion)
99
+ : version < forkVersion
100
+ ? makeRange(MinSupportedVersion, forkVersion)
101
+ : makeRange(forkVersion, MaxSupportedVersion);
102
+ /**
103
+ * Raised when entries cannot form a {@link Registry} because their ranges are not ascending, overlap, or cannot be
104
+ * closed at all.
105
+ *
106
+ * @remarks
107
+ * Construction is the only place this can happen: once built, a registry is total — selection returns an
108
+ * {@link Option.Option} rather than failing.
109
+ */
110
+ export class RegistryError extends Data.TaggedError('@midnight-ntwrk/wallet-sdk-abstractions/ProtocolVersion/RegistryError') {
111
+ }
112
+ /** A registry with no entries: {@link select} on it never finds anything. */
113
+ export const emptyRegistry = { entries: [] };
114
+ /**
115
+ * Creates a registry from entries that already carry their own ranges.
116
+ *
117
+ * @param entries The entries, in ascending order of range start; ranges must not overlap, though gaps are allowed.
118
+ * @returns The registry, or a {@link RegistryError} naming the two range boundaries that could not be ordered.
119
+ */
120
+ export const makeRegistry = (entries) => {
121
+ const brokenAt = entries.findIndex((entry, index) => index > 0 && entry.range[0] < entries[index - 1].range[1]);
122
+ return brokenAt < 0
123
+ ? Either.right({ entries })
124
+ : Either.left(new RegistryError({
125
+ message: 'Protocol version registry entries must be ascending and must not overlap.',
126
+ versions: [entries[brokenAt - 1].range[1], entries[brokenAt].range[0]],
127
+ }));
128
+ };
129
+ /**
130
+ * Creates a registry from values keyed by the protocol version each becomes active at, deriving the ranges from the
131
+ * registration order: entry _i_ serves `[sinceVersion(i), sinceVersion(i + 1))`, and the last entry serves
132
+ * `[sinceVersion(last), MaxSupportedVersion)`.
133
+ *
134
+ * @remarks
135
+ * This is the shape registration has everywhere in the SDK — a variant, codec or prover says which version it starts
136
+ * answering for, never which version it stops at — so the boundary between two of them is derived in exactly one
137
+ * place and cannot drift.
138
+ * @param activations The values and the versions they activate at, in strictly ascending order.
139
+ * @returns The registry, or a {@link RegistryError} naming the versions that broke the ordering.
140
+ */
141
+ export const makeRegistryFromActivations = (activations) => {
142
+ const brokenAt = activations.findIndex((activation, index) => index > 0 && activation.sinceVersion <= activations[index - 1].sinceVersion);
143
+ if (brokenAt >= 0) {
144
+ return Either.left(new RegistryError({
145
+ message: 'Protocol version activations must be strictly ascending.',
146
+ versions: [activations[brokenAt - 1].sinceVersion, activations[brokenAt].sinceVersion],
147
+ }));
148
+ }
149
+ const last = activations.at(-1);
150
+ if (last !== undefined && last.sinceVersion >= MaxSupportedVersion) {
151
+ return Either.left(new RegistryError({
152
+ message: 'A protocol version activation must leave room for a range above it.',
153
+ versions: [last.sinceVersion],
154
+ }));
155
+ }
156
+ return Either.right({
157
+ entries: activations.map(({ sinceVersion, value }, index) => ({
158
+ range: makeRange(sinceVersion, activations[index + 1]?.sinceVersion ?? MaxSupportedVersion),
159
+ value,
160
+ })),
161
+ });
162
+ };
163
+ /**
164
+ * Finds the entry whose range contains a given protocol version.
165
+ *
166
+ * @param registry The {@link Registry} to search.
167
+ * @param version The version to find an entry for.
168
+ * @returns The matching entry, or `Option.none()` when no entry covers `version`.
169
+ */
170
+ export const selectEntry = (registry, version) => Option.fromNullable(registry.entries.find((entry) => withinRange(version, entry.range)));
171
+ /**
172
+ * Finds the value whose range contains a given protocol version.
173
+ *
174
+ * @param registry The {@link Registry} to search.
175
+ * @param version The version to find a value for.
176
+ * @returns The matching value, or `Option.none()` when no entry covers `version`.
177
+ */
178
+ export const select = (registry, version) => selectEntry(registry, version).pipe(Option.map((entry) => entry.value));
@@ -0,0 +1,57 @@
1
+ import * as Either from 'effect/Either';
2
+ /**
3
+ * A token type, in the raw form every balance and coin is keyed by.
4
+ *
5
+ * @remarks
6
+ * Deliberately a plain `string` and **not** a branded type, which is a considered exception to this codebase's
7
+ * parse-don't-validate default. Balances are `Record<TokenType, bigint>`, and a branded key type does not produce a
8
+ * string index signature: branding here would break every balance read in the SDK and in every application, to buy a
9
+ * guarantee that the values are not confusable with anything else a caller holds anyway. Use {@link parseTokenType} at
10
+ * the boundary where a token type arrives from outside, which is where the guarantee is actually worth having.
11
+ */
12
+ export type TokenType = string;
13
+ declare const InvalidTokenTypeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
14
+ readonly _tag: "@midnight-ntwrk/wallet-sdk-abstractions/TokenType/InvalidTokenTypeError";
15
+ } & Readonly<A>;
16
+ /** Raised when a value offered as a token type is not one. */
17
+ export declare class InvalidTokenTypeError extends InvalidTokenTypeError_base<{
18
+ readonly message: string;
19
+ /** What was offered, so the caller can report it alongside the field it came from. */
20
+ readonly value: string;
21
+ }> {
22
+ }
23
+ /**
24
+ * The token types the SDK names.
25
+ *
26
+ * @remarks
27
+ * String literals rather than calls into a ledger, and that is load-bearing: these constants reach an application
28
+ * through the umbrella package's root barrel, which is the one place no ledger version's WebAssembly may be loaded. A
29
+ * test pins each of them against both ledger versions, so the literal cannot drift away from what the chain means.
30
+ *
31
+ * There is one constant because there is one raw type to name. Both ledger versions report the same thirty-two bytes
32
+ * for the native, shielded and unshielded token; what distinguishes them is a `tag` on the ledger's own wrapper, and
33
+ * a balance is keyed by the raw type alone.
34
+ */
35
+ export declare const Token: {
36
+ /** Night: the token the chain is denominated in, and the resource Dust is generated from. */
37
+ readonly night: "0000000000000000000000000000000000000000000000000000000000000000";
38
+ };
39
+ /**
40
+ * Reads a token type from a value that came from outside the SDK.
41
+ *
42
+ * @remarks
43
+ * The boundary check the unbranded {@link TokenType} does not make for itself: a dApp request, a URL parameter or a
44
+ * configuration file can carry anything, and a token type that is quietly wrong selects no coins rather than failing.
45
+ * Returns rather than throws, because "this is not a token type" is an ordinary thing for input to be.
46
+ * @example
47
+ * ```typescript
48
+ * const tokenType = parseTokenType(request.tokenType).pipe(
49
+ * Either.getOrElse(() => Token.night),
50
+ * );
51
+ * ```;
52
+ *
53
+ * @param value The value offered as a token type.
54
+ * @returns The token type, or an {@link InvalidTokenTypeError} naming what was offered.
55
+ */
56
+ export declare const parseTokenType: (value: string) => Either.Either<TokenType, InvalidTokenTypeError>;
57
+ export {};
@@ -0,0 +1,68 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) Midnight Foundation
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // You may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ /**
14
+ * The names the SDK gives to token types, so an application never has to ask a ledger version for one.
15
+ *
16
+ * @remarks
17
+ * A token type is thirty-two bytes, written as hexadecimal, and it is the same thirty-two bytes on either side of a
18
+ * protocol boundary — which is the whole reason the SDK can name one for itself. Until now an application wanting the
19
+ * Night token type had to call `nativeToken().raw` on a ledger package, which meant importing a ledger version to
20
+ * read a constant that does not depend on one.
21
+ */
22
+ import * as Data from 'effect/Data';
23
+ import * as Either from 'effect/Either';
24
+ /** Raised when a value offered as a token type is not one. */
25
+ export class InvalidTokenTypeError extends Data.TaggedError('@midnight-ntwrk/wallet-sdk-abstractions/TokenType/InvalidTokenTypeError') {
26
+ }
27
+ /** Thirty-two bytes of lower-case hexadecimal: the form both ledger versions write a token type in. */
28
+ const TOKEN_TYPE_PATTERN = /^[0-9a-f]{64}$/;
29
+ /**
30
+ * The token types the SDK names.
31
+ *
32
+ * @remarks
33
+ * String literals rather than calls into a ledger, and that is load-bearing: these constants reach an application
34
+ * through the umbrella package's root barrel, which is the one place no ledger version's WebAssembly may be loaded. A
35
+ * test pins each of them against both ledger versions, so the literal cannot drift away from what the chain means.
36
+ *
37
+ * There is one constant because there is one raw type to name. Both ledger versions report the same thirty-two bytes
38
+ * for the native, shielded and unshielded token; what distinguishes them is a `tag` on the ledger's own wrapper, and
39
+ * a balance is keyed by the raw type alone.
40
+ */
41
+ export const Token = {
42
+ /** Night: the token the chain is denominated in, and the resource Dust is generated from. */
43
+ night: '0000000000000000000000000000000000000000000000000000000000000000',
44
+ };
45
+ /**
46
+ * Reads a token type from a value that came from outside the SDK.
47
+ *
48
+ * @remarks
49
+ * The boundary check the unbranded {@link TokenType} does not make for itself: a dApp request, a URL parameter or a
50
+ * configuration file can carry anything, and a token type that is quietly wrong selects no coins rather than failing.
51
+ * Returns rather than throws, because "this is not a token type" is an ordinary thing for input to be.
52
+ * @example
53
+ * ```typescript
54
+ * const tokenType = parseTokenType(request.tokenType).pipe(
55
+ * Either.getOrElse(() => Token.night),
56
+ * );
57
+ * ```;
58
+ *
59
+ * @param value The value offered as a token type.
60
+ * @returns The token type, or an {@link InvalidTokenTypeError} naming what was offered.
61
+ */
62
+ export const parseTokenType = (value) => TOKEN_TYPE_PATTERN.test(value)
63
+ ? Either.right(value)
64
+ : Either.left(new InvalidTokenTypeError({
65
+ message: `"${value}" is not a token type: a token type is thirty-two bytes written as sixty-four lower-case ` +
66
+ `hexadecimal characters.`,
67
+ value,
68
+ }));
@@ -109,6 +109,16 @@ export type RejectedTransactionHistoryCommon = TransactionHistoryEntryCommon & {
109
109
  readonly lifecycle: RejectedLifecycle;
110
110
  };
111
111
  export type SerializedTransactionHistory = string;
112
+ /**
113
+ * Whether `entry` records the transaction `key` names: the same hash, or every one of `key`'s identifiers. Identifiers
114
+ * are per-build commitments, so an entry carrying all of them is the same transaction included under an aggregated
115
+ * hash. An empty identifier set names no transaction and therefore neither covers nor is covered.
116
+ *
117
+ * @param entry - The recorded entry.
118
+ * @param key - The hash and identifiers of the transaction being asked about.
119
+ * @returns Whether `entry` is a record of that transaction.
120
+ */
121
+ export declare const coversTransaction: (entry: Pick<TransactionHistoryEntryCommon, "hash" | "identifiers">, key: Pick<TransactionHistoryEntryCommon, "hash" | "identifiers">) => boolean;
112
122
  /**
113
123
  * An entry with common fields plus any additional properties (wallet sections). Used by wallet packages for
114
124
  * projection/filtering when the exact type is not known.
@@ -160,6 +170,11 @@ export interface TransactionHistoryReader<T extends {
160
170
  * keys (in particular: clearing a prior `pending` entry keyed by an identifier when its `finalized` or `rejected`
161
171
  * counterpart arrives keyed by the tx hash).
162
172
  *
173
+ * Recorded inclusion takes precedence over a later rejection. Sync records inclusion independently of the
174
+ * pending-status poller, so a verdict may arrive after a `finalized` entry {@link coversTransaction covers} the
175
+ * transaction, and that verdict must neither overwrite the entry nor leave a rejected duplicate under the submission's
176
+ * own hash. Implementations check this atomically with the write.
177
+ *
163
178
  * `T` appears only in input position, so this interface is **contravariant** in T: a `TransactionHistoryWriter<Wider>`
164
179
  * is assignable to `TransactionHistoryWriter<Narrower>`.
165
180
  */
@@ -167,13 +182,14 @@ export interface TransactionHistoryWriter<T extends TransactionHistoryEntryCommo
167
182
  /** Record that a tx has been submitted and is awaiting confirmation. */
168
183
  gotPending(entry: PendingEntryInput<T>): Promise<void>;
169
184
  /**
170
- * Record that a tx has been confirmed on-chain. Inserts/merges the entry under its tx hash and clears any earlier
171
- * `pending` entry whose identifiers are contained in this entry's identifier set.
185
+ * Record that a tx has been confirmed on-chain. Inserts/merges the entry under its tx hash and clears every earlier
186
+ * `pending` or `rejected` entry this entry {@link coversTransaction covers} under another hash.
172
187
  */
173
188
  gotFinalized(entry: FinalizedEntryInput<T>): Promise<void>;
174
189
  /**
175
190
  * Record that a tx will not land — failed, partial-success, TTL-expired, or otherwise reverted. Keyed the same way as
176
- * `gotPending` so the lifecycle transition is in-place.
191
+ * `gotPending` so the lifecycle transition is in-place. Writes nothing when a `finalized` entry already covers the
192
+ * tx.
177
193
  */
178
194
  gotRejected(entry: RejectedEntryInput<T>): Promise<void>;
179
195
  }
@@ -71,3 +71,18 @@ export const extendEntrySchema = (extensionFields) => Schema.Struct({
71
71
  ...TransactionHistoryEntryCommonSchema.fields,
72
72
  ...extensionFields,
73
73
  });
74
+ /**
75
+ * Whether `entry` records the transaction `key` names: the same hash, or every one of `key`'s identifiers. Identifiers
76
+ * are per-build commitments, so an entry carrying all of them is the same transaction included under an aggregated
77
+ * hash. An empty identifier set names no transaction and therefore neither covers nor is covered.
78
+ *
79
+ * @param entry - The recorded entry.
80
+ * @param key - The hash and identifiers of the transaction being asked about.
81
+ * @returns Whether `entry` is a record of that transaction.
82
+ */
83
+ export const coversTransaction = (entry, key) => {
84
+ if (entry.hash === key.hash)
85
+ return true;
86
+ const recorded = new Set(entry.identifiers);
87
+ return key.identifiers.length > 0 && key.identifiers.every((id) => recorded.has(id));
88
+ };
@@ -0,0 +1,207 @@
1
+ import * as Either from 'effect/Either';
2
+ import * as Option from 'effect/Option';
3
+ import * as Schema from 'effect/Schema';
4
+ import * as ProtocolVersion from './ProtocolVersion.js';
5
+ import * as SerializedTransaction from './SerializedTransaction.js';
6
+ /**
7
+ * The three stages, as the one thing that decides what they are.
8
+ *
9
+ * @remarks
10
+ * Declared here rather than beside the wire envelope that reads it, because {@link TransactionStage} — the type an
11
+ * application names — is read back off it. Stating the three words once means the type a caller is held to and the
12
+ * values an envelope is parsed against cannot come to disagree: adding a stage to one adds it to the other.
13
+ */
14
+ declare const TransactionStageSchema: Schema.Literal<["Unproven", "Unbound", "Finalized"]>;
15
+ /**
16
+ * How far along the building of a transaction a handle is.
17
+ *
18
+ * @remarks
19
+ * The three points at which a transaction changes hands between an application and the SDK: built but not proved,
20
+ * proved but not yet bound to its own contents, and finalized — proved, signed and bound, which is the only shape the
21
+ * network takes. Both ledger versions have all three, and the words mean the same thing on either side of a protocol
22
+ * boundary, which is why the stage can be plain data on a version-agnostic handle.
23
+ */
24
+ export type TransactionStage = Schema.Schema.Type<typeof TransactionStageSchema>;
25
+ /** What a handle can carry: anything that can write itself out as bytes, which is every ledger's transaction. */
26
+ type Serializable = Readonly<{
27
+ serialize: () => Uint8Array;
28
+ }>;
29
+ declare const ProtocolVersionMismatchError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
30
+ readonly _tag: "@midnight-ntwrk/wallet-sdk-abstractions/WalletTransaction/ProtocolVersionMismatchError";
31
+ } & Readonly<A>;
32
+ /**
33
+ * Raised when a transaction is used at a protocol version other than the one it was built for.
34
+ *
35
+ * @remarks
36
+ * The bytes of a transaction are fixed when it is built, by the ledger version that built them. A protocol boundary
37
+ * does not rewrite them, so a transaction authored on one side of the boundary cannot be understood on the other —
38
+ * not merged, not proved, not submitted. This is what the SDK answers with rather than handing a transaction to a
39
+ * ledger version that would misread it.
40
+ */
41
+ export declare class ProtocolVersionMismatchError extends ProtocolVersionMismatchError_base<{
42
+ readonly message: string;
43
+ /** The protocol version the transaction was built at. */
44
+ readonly authoredFor: ProtocolVersion.ProtocolVersion;
45
+ /** The range of protocol versions the caller can act at. */
46
+ readonly accepted: ProtocolVersion.ProtocolVersion.Range;
47
+ /** How far along the building of the transaction the handle is, for the diagnostic. */
48
+ readonly stage: TransactionStage;
49
+ }> {
50
+ }
51
+ declare const WireFormatError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
52
+ readonly _tag: "@midnight-ntwrk/wallet-sdk-abstractions/WalletTransaction/WireFormatError";
53
+ } & Readonly<A>;
54
+ /**
55
+ * Raised when bytes cannot be read as a transaction of a version this SDK knows — whether they arrived in a wire
56
+ * envelope that names their version, or bare, to be read at the version the reader is acting at.
57
+ */
58
+ export declare class WireFormatError extends WireFormatError_base<{
59
+ readonly message: string;
60
+ /** What went wrong underneath: a parse failure, or whatever the caller's decoder raised. */
61
+ readonly cause?: unknown;
62
+ }> {
63
+ }
64
+ /**
65
+ * The envelope a handle crosses a process boundary in.
66
+ *
67
+ * @remarks
68
+ * Everything in it is JSON-safe, because the boundary it was designed for — a dApp connector — is a JSON one: the
69
+ * protocol version is a decimal string rather than a `bigint`, and the transaction is hex rather than bytes. What
70
+ * travels is exactly what a reader needs to decide whether it can read the rest at all: the envelope format, the
71
+ * protocol version the transaction was built at, the stage it reached, and the transaction's own serialization.
72
+ *
73
+ * Kept deliberately small. The connector contract is an open question at the time of writing, and an envelope that
74
+ * guesses at it would be harder to reconcile than one that carries the minimum.
75
+ */
76
+ declare const WireTransactionSchema: Schema.Struct<{
77
+ wireFormat: Schema.Literal<[1]>;
78
+ protocolVersion: Schema.BrandSchema<bigint & import("effect/Brand").Brand<"ProtocolVersion">, string, never>;
79
+ stage: Schema.Literal<["Unproven", "Unbound", "Finalized"]>;
80
+ transaction: Schema.Schema<Uint8Array<ArrayBufferLike>, string, never>;
81
+ }>;
82
+ /** The wire envelope as it travels: a plain JSON-safe record. See {@link WalletTransaction.toWire}. */
83
+ export type WireTransaction = Schema.Schema.Encoded<typeof WireTransactionSchema>;
84
+ /**
85
+ * A transaction the SDK produced or adopted, sealed together with the protocol version it was built at.
86
+ *
87
+ * @remarks
88
+ * A class rather than a record, and the one place in this package where that earns its keep: a `#private` field is the
89
+ * only way to carry something an application provably cannot reach, and reaching the wrapped transaction is exactly
90
+ * what would re-introduce the ledger-version dependency the handle exists to remove. Nothing about it mutates — the
91
+ * fields are read-only and every operation returns a new value.
92
+ *
93
+ * The stage is a type parameter as well as a field, so a signature can ask for the shape it needs
94
+ * (`WalletTransaction<'Finalized'>`, spelled {@link FinalizedTx}) and a caller who has an unproven transaction finds
95
+ * out at compile time. It is covariant: a `FinalizedTx` is an {@link AnyTx}, which is what the operations that accept
96
+ * a transaction at any stage — reverting one, say — are stated in terms of.
97
+ * @example
98
+ * Sealing a transaction an application built for itself, and handing it back to the wallet:
99
+ *
100
+ * ```typescript
101
+ * import * as ledger from '@midnight-ntwrk/wallet-sdk/ledger/v9';
102
+ *
103
+ * const intent = ledger.Intent.new(ttl);
104
+ * const handle = WalletTransaction.adopt('Unproven', ledger.Transaction.fromParts(networkId, undefined, undefined, intent), version);
105
+ * ```;
106
+ *
107
+ * @typeParam TStage How far along the building of this transaction the handle is.
108
+ */
109
+ export declare class WalletTransaction<out TStage extends TransactionStage = TransactionStage> {
110
+ #private;
111
+ /** The protocol version this transaction was built at, and so the only one it can be used at. */
112
+ readonly protocolVersion: ProtocolVersion.ProtocolVersion;
113
+ /** How far along the building of this transaction the handle is. */
114
+ readonly stage: TStage;
115
+ private constructor();
116
+ /**
117
+ * Seals a transaction built elsewhere into a handle.
118
+ *
119
+ * @remarks
120
+ * How an application that builds its own transactions — with the ledger version it imported from
121
+ * `@midnight-ntwrk/wallet-sdk/ledger/v8` or `/v9` — hands one to the wallet. The version passed here is a claim
122
+ * about which ledger version produced the transaction, and the SDK holds the caller to it: a handle stamped for one
123
+ * side of a protocol boundary is refused by a wallet acting on the other side, with a
124
+ * {@link ProtocolVersionMismatchError}. Until the fork, that means a transaction built with ledger-v9 is refused at
125
+ * run time, however well it type-checks.
126
+ * @param stage How far along the building of the transaction is.
127
+ * @param transaction The transaction, of whichever ledger version built it.
128
+ * @param protocolVersion The protocol version that ledger version serves.
129
+ * @returns The sealed handle.
130
+ */
131
+ static adopt<TStage extends TransactionStage>(stage: TStage, transaction: Serializable, protocolVersion: ProtocolVersion.ProtocolVersion): WalletTransaction<TStage>;
132
+ /**
133
+ * Reads the transaction a handle carries, if it was built for a protocol version the caller can act at.
134
+ *
135
+ * @remarks
136
+ * The SDK's own way in, and the enforcement point for everything an application hands back: a caller states the range
137
+ * of protocol versions it speaks — a variant's activation range, or the era the chain is currently in — and gets
138
+ * the transaction only when the stamp falls inside it. The result type is the caller's to name, because the handle
139
+ * deliberately does not remember it; the range is the whole of the guarantee, which is why it is checked here
140
+ * rather than trusted anywhere else.
141
+ * @param handle The handle to read.
142
+ * @param accepted The range of protocol versions the caller can act at.
143
+ * @returns The carried transaction, or a {@link ProtocolVersionMismatchError} naming what was carried and what was
144
+ * asked for.
145
+ */
146
+ static unwrapWithin<T>(handle: WalletTransaction, accepted: ProtocolVersion.ProtocolVersion.Range): Either.Either<T, ProtocolVersionMismatchError>;
147
+ /**
148
+ * Narrows a handle to a stage, for a caller that has one of unknown stage.
149
+ *
150
+ * @param handle The handle to narrow.
151
+ * @param stage The stage to narrow it to.
152
+ * @returns The same handle, typed at that stage, or nothing when it is at another.
153
+ */
154
+ static atStage<TStage extends TransactionStage>(handle: WalletTransaction, stage: TStage): Option.Option<WalletTransaction<TStage>>;
155
+ /**
156
+ * Determines whether a value is a handle this SDK produced.
157
+ *
158
+ * @remarks
159
+ * Answers on the sealed transaction itself, so a record that merely has the same readable fields is not mistaken for
160
+ * one.
161
+ * @param value The value to test.
162
+ * @returns `true` when `value` is a {@link WalletTransaction}.
163
+ */
164
+ static is(value: unknown): value is WalletTransaction;
165
+ /**
166
+ * Writes a handle out as an envelope that can cross a process boundary.
167
+ *
168
+ * @param handle The handle to write out.
169
+ * @returns The envelope. See {@link WireTransaction}.
170
+ */
171
+ static toWire(handle: WalletTransaction): WireTransaction;
172
+ /**
173
+ * Reads an envelope back into a handle, decoding its bytes with whichever ledger version the envelope names.
174
+ *
175
+ * @remarks
176
+ * The decoder is the caller's because the choice of ledger version is: this package has none, and picking one here
177
+ * would be the hardcoded version the handle exists to avoid. It is handed the version and stage the envelope
178
+ * declares so it can route on them, and it may raise — a deserializer meeting bytes of another ledger version does
179
+ * — which is reported as a {@link WireFormatError} rather than escaping.
180
+ * @param value The envelope, as received.
181
+ * @param decode Produces the transaction from the envelope's bytes, at the version and stage it names.
182
+ * @returns The handle, or a {@link WireFormatError} describing what could not be read.
183
+ */
184
+ static fromWire(value: unknown, decode: (bytes: Uint8Array, protocolVersion: ProtocolVersion.ProtocolVersion, stage: TransactionStage) => Serializable): Either.Either<WalletTransaction, WireFormatError>;
185
+ /**
186
+ * Writes the carried transaction out as bytes.
187
+ *
188
+ * @remarks
189
+ * The one thing every ledger version's transaction can do identically, and so the one thing a handle can offer
190
+ * without knowing which produced it. What the bytes mean still depends on {@link protocolVersion}.
191
+ * @returns The transaction's own serialization.
192
+ */
193
+ serialize(): SerializedTransaction.SerializedTransaction;
194
+ }
195
+ export declare namespace WalletTransaction {
196
+ /** How far along the building of a transaction a handle is. See {@link TransactionStage}. */
197
+ type Stage = TransactionStage;
198
+ }
199
+ /** A transaction that has been built but not proved. */
200
+ export type UnprovenTx = WalletTransaction<'Unproven'>;
201
+ /** A transaction that has been proved but not yet bound to its own contents. */
202
+ export type UnboundTx = WalletTransaction<'Unbound'>;
203
+ /** A transaction that is proved, signed and bound: the only shape the network takes. */
204
+ export type FinalizedTx = WalletTransaction<'Finalized'>;
205
+ /** A transaction at whichever stage it happens to have reached. */
206
+ export type AnyTx = WalletTransaction<TransactionStage>;
207
+ export {};
@@ -0,0 +1,250 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) Midnight Foundation
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // You may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ /**
14
+ * The one transaction type an application carries, whichever ledger version produced it.
15
+ *
16
+ * @remarks
17
+ * An SDK that runs one ledger version before a protocol boundary and another from it has a problem its callers should
18
+ * never have to solve: a transaction built before the boundary and a transaction built after it are objects of two
19
+ * different runtimes, with two different types, and no conversion between them. Naming either type in a public
20
+ * signature is naming a ledger version, which is the thing that breaks when the chain moves on.
21
+ *
22
+ * So the wallet layer hands out a handle instead. It seals the transaction away — an application cannot reach it, and
23
+ * so cannot come to depend on which version made it — and carries the protocol version it was built at as ordinary
24
+ * readable data. That stamp is what the SDK routes on: which prover proves it, which trait recognises it, which
25
+ * variant may unwrap it at all.
26
+ */
27
+ import * as Data from 'effect/Data';
28
+ import * as Either from 'effect/Either';
29
+ import * as Option from 'effect/Option';
30
+ import * as ParseResult from 'effect/ParseResult';
31
+ import * as Schema from 'effect/Schema';
32
+ import * as ProtocolVersion from './ProtocolVersion.js';
33
+ import * as SerializedTransaction from './SerializedTransaction.js';
34
+ /**
35
+ * The three stages, as the one thing that decides what they are.
36
+ *
37
+ * @remarks
38
+ * Declared here rather than beside the wire envelope that reads it, because {@link TransactionStage} — the type an
39
+ * application names — is read back off it. Stating the three words once means the type a caller is held to and the
40
+ * values an envelope is parsed against cannot come to disagree: adding a stage to one adds it to the other.
41
+ */
42
+ const TransactionStageSchema = Schema.Literal('Unproven', 'Unbound', 'Finalized');
43
+ /**
44
+ * Raised when a transaction is used at a protocol version other than the one it was built for.
45
+ *
46
+ * @remarks
47
+ * The bytes of a transaction are fixed when it is built, by the ledger version that built them. A protocol boundary
48
+ * does not rewrite them, so a transaction authored on one side of the boundary cannot be understood on the other —
49
+ * not merged, not proved, not submitted. This is what the SDK answers with rather than handing a transaction to a
50
+ * ledger version that would misread it.
51
+ */
52
+ export class ProtocolVersionMismatchError extends Data.TaggedError('@midnight-ntwrk/wallet-sdk-abstractions/WalletTransaction/ProtocolVersionMismatchError') {
53
+ }
54
+ /**
55
+ * Raised when bytes cannot be read as a transaction of a version this SDK knows — whether they arrived in a wire
56
+ * envelope that names their version, or bare, to be read at the version the reader is acting at.
57
+ */
58
+ export class WireFormatError extends Data.TaggedError('@midnight-ntwrk/wallet-sdk-abstractions/WalletTransaction/WireFormatError') {
59
+ }
60
+ /**
61
+ * The envelope's own format version.
62
+ *
63
+ * @remarks
64
+ * Deliberately separate from the protocol version it carries. A reader has to be able to tell "I do not know this
65
+ * envelope" from "I do not know this ledger version", because only the second is a chain fact.
66
+ */
67
+ const WIRE_FORMAT = 1;
68
+ /**
69
+ * The envelope a handle crosses a process boundary in.
70
+ *
71
+ * @remarks
72
+ * Everything in it is JSON-safe, because the boundary it was designed for — a dApp connector — is a JSON one: the
73
+ * protocol version is a decimal string rather than a `bigint`, and the transaction is hex rather than bytes. What
74
+ * travels is exactly what a reader needs to decide whether it can read the rest at all: the envelope format, the
75
+ * protocol version the transaction was built at, the stage it reached, and the transaction's own serialization.
76
+ *
77
+ * Kept deliberately small. The connector contract is an open question at the time of writing, and an envelope that
78
+ * guesses at it would be harder to reconcile than one that carries the minimum.
79
+ */
80
+ const WireTransactionSchema = Schema.Struct({
81
+ wireFormat: Schema.Literal(WIRE_FORMAT),
82
+ protocolVersion: ProtocolVersion.ProtocolVersionSchema,
83
+ stage: TransactionStageSchema,
84
+ transaction: Schema.Uint8ArrayFromHex,
85
+ });
86
+ const encodeWire = Schema.encodeSync(WireTransactionSchema);
87
+ const decodeWire = Schema.decodeUnknownEither(WireTransactionSchema);
88
+ /**
89
+ * A transaction the SDK produced or adopted, sealed together with the protocol version it was built at.
90
+ *
91
+ * @remarks
92
+ * A class rather than a record, and the one place in this package where that earns its keep: a `#private` field is the
93
+ * only way to carry something an application provably cannot reach, and reaching the wrapped transaction is exactly
94
+ * what would re-introduce the ledger-version dependency the handle exists to remove. Nothing about it mutates — the
95
+ * fields are read-only and every operation returns a new value.
96
+ *
97
+ * The stage is a type parameter as well as a field, so a signature can ask for the shape it needs
98
+ * (`WalletTransaction<'Finalized'>`, spelled {@link FinalizedTx}) and a caller who has an unproven transaction finds
99
+ * out at compile time. It is covariant: a `FinalizedTx` is an {@link AnyTx}, which is what the operations that accept
100
+ * a transaction at any stage — reverting one, say — are stated in terms of.
101
+ * @example
102
+ * Sealing a transaction an application built for itself, and handing it back to the wallet:
103
+ *
104
+ * ```typescript
105
+ * import * as ledger from '@midnight-ntwrk/wallet-sdk/ledger/v9';
106
+ *
107
+ * const intent = ledger.Intent.new(ttl);
108
+ * const handle = WalletTransaction.adopt('Unproven', ledger.Transaction.fromParts(networkId, undefined, undefined, intent), version);
109
+ * ```;
110
+ *
111
+ * @typeParam TStage How far along the building of this transaction the handle is.
112
+ */
113
+ export class WalletTransaction {
114
+ /** The protocol version this transaction was built at, and so the only one it can be used at. */
115
+ protocolVersion;
116
+ /** How far along the building of this transaction the handle is. */
117
+ stage;
118
+ /** The transaction itself, reachable only from inside this class. */
119
+ #carried;
120
+ constructor(protocolVersion, stage, carried) {
121
+ this.protocolVersion = protocolVersion;
122
+ this.stage = stage;
123
+ this.#carried = carried;
124
+ }
125
+ /**
126
+ * Seals a transaction built elsewhere into a handle.
127
+ *
128
+ * @remarks
129
+ * How an application that builds its own transactions — with the ledger version it imported from
130
+ * `@midnight-ntwrk/wallet-sdk/ledger/v8` or `/v9` — hands one to the wallet. The version passed here is a claim
131
+ * about which ledger version produced the transaction, and the SDK holds the caller to it: a handle stamped for one
132
+ * side of a protocol boundary is refused by a wallet acting on the other side, with a
133
+ * {@link ProtocolVersionMismatchError}. Until the fork, that means a transaction built with ledger-v9 is refused at
134
+ * run time, however well it type-checks.
135
+ * @param stage How far along the building of the transaction is.
136
+ * @param transaction The transaction, of whichever ledger version built it.
137
+ * @param protocolVersion The protocol version that ledger version serves.
138
+ * @returns The sealed handle.
139
+ */
140
+ static adopt(stage, transaction, protocolVersion) {
141
+ return new WalletTransaction(protocolVersion, stage, transaction);
142
+ }
143
+ /**
144
+ * Reads the transaction a handle carries, if it was built for a protocol version the caller can act at.
145
+ *
146
+ * @remarks
147
+ * The SDK's own way in, and the enforcement point for everything an application hands back: a caller states the range
148
+ * of protocol versions it speaks — a variant's activation range, or the era the chain is currently in — and gets
149
+ * the transaction only when the stamp falls inside it. The result type is the caller's to name, because the handle
150
+ * deliberately does not remember it; the range is the whole of the guarantee, which is why it is checked here
151
+ * rather than trusted anywhere else.
152
+ * @param handle The handle to read.
153
+ * @param accepted The range of protocol versions the caller can act at.
154
+ * @returns The carried transaction, or a {@link ProtocolVersionMismatchError} naming what was carried and what was
155
+ * asked for.
156
+ */
157
+ static unwrapWithin(handle, accepted) {
158
+ if (!ProtocolVersion.withinRange(handle.protocolVersion, accepted)) {
159
+ const [from, to] = accepted;
160
+ return Either.left(new ProtocolVersionMismatchError({
161
+ message: `This transaction was built for protocol version ${handle.protocolVersion}, and can only be used at ` +
162
+ `that version: its bytes were fixed by the ledger version that produced them. The caller acts at ` +
163
+ `protocol versions ${from} up to (but not including) ${to}. Build the transaction again on the side of ` +
164
+ `the protocol boundary it is to be used on.`,
165
+ authoredFor: handle.protocolVersion,
166
+ accepted,
167
+ stage: handle.stage,
168
+ }));
169
+ }
170
+ // The carried type is erased when a transaction is sealed, and deliberately: remembering it would put a ledger
171
+ // version back into the handle's own type. The version check above is the whole of what can be guaranteed here,
172
+ // and it is the same guarantee the caller would have had to make for itself.
173
+ return Either.right(handle.#carried);
174
+ }
175
+ /**
176
+ * Narrows a handle to a stage, for a caller that has one of unknown stage.
177
+ *
178
+ * @param handle The handle to narrow.
179
+ * @param stage The stage to narrow it to.
180
+ * @returns The same handle, typed at that stage, or nothing when it is at another.
181
+ */
182
+ static atStage(handle, stage) {
183
+ return handle.stage === stage
184
+ ? // Narrowed by the value that the type parameter names; `stage` is the only thing distinguishing the two types.
185
+ Option.some(handle)
186
+ : Option.none();
187
+ }
188
+ /**
189
+ * Determines whether a value is a handle this SDK produced.
190
+ *
191
+ * @remarks
192
+ * Answers on the sealed transaction itself, so a record that merely has the same readable fields is not mistaken for
193
+ * one.
194
+ * @param value The value to test.
195
+ * @returns `true` when `value` is a {@link WalletTransaction}.
196
+ */
197
+ static is(value) {
198
+ return typeof value === 'object' && value !== null && #carried in value;
199
+ }
200
+ /**
201
+ * Writes a handle out as an envelope that can cross a process boundary.
202
+ *
203
+ * @param handle The handle to write out.
204
+ * @returns The envelope. See {@link WireTransaction}.
205
+ */
206
+ static toWire(handle) {
207
+ return encodeWire({
208
+ wireFormat: WIRE_FORMAT,
209
+ protocolVersion: handle.protocolVersion,
210
+ stage: handle.stage,
211
+ transaction: handle.serialize(),
212
+ });
213
+ }
214
+ /**
215
+ * Reads an envelope back into a handle, decoding its bytes with whichever ledger version the envelope names.
216
+ *
217
+ * @remarks
218
+ * The decoder is the caller's because the choice of ledger version is: this package has none, and picking one here
219
+ * would be the hardcoded version the handle exists to avoid. It is handed the version and stage the envelope
220
+ * declares so it can route on them, and it may raise — a deserializer meeting bytes of another ledger version does
221
+ * — which is reported as a {@link WireFormatError} rather than escaping.
222
+ * @param value The envelope, as received.
223
+ * @param decode Produces the transaction from the envelope's bytes, at the version and stage it names.
224
+ * @returns The handle, or a {@link WireFormatError} describing what could not be read.
225
+ */
226
+ static fromWire(value, decode) {
227
+ return decodeWire(value).pipe(Either.mapLeft((error) => new WireFormatError({
228
+ message: `This is not a transaction envelope this SDK can read: ${ParseResult.TreeFormatter.formatErrorSync(error)}`,
229
+ cause: error,
230
+ })), Either.flatMap((envelope) => Either.try({
231
+ try: () => decode(envelope.transaction, envelope.protocolVersion, envelope.stage),
232
+ catch: (cause) => new WireFormatError({
233
+ message: `The envelope declares protocol version ${envelope.protocolVersion}, but its bytes could not be ` +
234
+ `read as a ${envelope.stage.toLowerCase()} transaction of that version.`,
235
+ cause,
236
+ }),
237
+ }).pipe(Either.map((transaction) => WalletTransaction.adopt(envelope.stage, transaction, envelope.protocolVersion)))));
238
+ }
239
+ /**
240
+ * Writes the carried transaction out as bytes.
241
+ *
242
+ * @remarks
243
+ * The one thing every ledger version's transaction can do identically, and so the one thing a handle can offer
244
+ * without knowing which produced it. What the bytes mean still depends on {@link protocolVersion}.
245
+ * @returns The transaction's own serialization.
246
+ */
247
+ serialize() {
248
+ return SerializedTransaction.from(this.#carried);
249
+ }
250
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export * as ProtocolState from './ProtocolState.js';
5
5
  export * as ProtocolVersion from './ProtocolVersion.js';
6
6
  export * as NetworkId from './NetworkId.js';
7
7
  export * as SyncProgress from './SyncProgress.js';
8
+ export * from './TokenType.js';
9
+ export * from './WalletTransaction.js';
8
10
  export * from './InMemoryTransactionHistoryStorage.js';
9
11
  export * from './NoOpTransactionHistoryStorage.js';
10
12
  export * as TransactionHistoryStorage from './TransactionHistoryStorage.js';
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ export * as ProtocolState from './ProtocolState.js';
17
17
  export * as ProtocolVersion from './ProtocolVersion.js';
18
18
  export * as NetworkId from './NetworkId.js';
19
19
  export * as SyncProgress from './SyncProgress.js';
20
+ export * from './TokenType.js';
21
+ export * from './WalletTransaction.js';
20
22
  export * from './InMemoryTransactionHistoryStorage.js';
21
23
  export * from './NoOpTransactionHistoryStorage.js';
22
24
  export * as TransactionHistoryStorage from './TransactionHistoryStorage.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@midnight-ntwrk/wallet-sdk-abstractions",
3
3
  "description": "Domain-specific abstractions for the wallet SDK",
4
- "version": "3.0.0-beta.0",
4
+ "version": "3.0.0-beta.2",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",