@midnight-ntwrk/wallet-sdk-shielded 4.0.0-beta.2 → 4.0.0-beta.4
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/README.md +18 -9
- package/dist/Restore.d.ts +40 -0
- package/dist/Restore.js +63 -0
- package/dist/ShieldedWallet.d.ts +248 -62
- package/dist/ShieldedWallet.js +366 -76
- package/dist/ShieldedWalletAPI.d.ts +179 -0
- package/dist/ShieldedWalletAPI.js +75 -0
- package/dist/SingleVariantShieldedWallet.d.ts +35 -0
- package/dist/SingleVariantShieldedWallet.js +240 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/v1/CoinsAndBalances.d.ts +1 -1
- package/dist/v1/CoreWallet.d.ts +48 -1
- package/dist/v1/CoreWallet.js +54 -1
- package/dist/v1/Migration.d.ts +95 -0
- package/dist/v1/Migration.js +82 -0
- package/dist/v1/RunningV1Variant.d.ts +1 -1
- package/dist/v1/RunningV1Variant.js +37 -8
- package/dist/v1/Serialization.js +1 -1
- package/dist/v1/Sync.d.ts +132 -9
- package/dist/v1/Sync.js +272 -36
- package/dist/v1/Transacting.d.ts +1 -1
- package/dist/v1/Transacting.js +3 -2
- package/dist/v1/TransactionHistory.d.ts +9 -2
- package/dist/v1/TransactionHistory.js +26 -5
- package/dist/v1/TransactionOps.d.ts +1 -1
- package/dist/v1/V1Builder.d.ts +87 -26
- package/dist/v1/V1Builder.js +109 -8
- package/dist/v1/WalletError.d.ts +27 -2
- package/dist/v1/WalletError.js +16 -0
- package/dist/v1/index.d.ts +2 -1
- package/dist/v1/index.js +5 -1
- package/dist/v2/CoinsAndBalances.d.ts +24 -0
- package/dist/v2/CoinsAndBalances.js +41 -0
- package/dist/v2/CoreWallet.d.ts +162 -0
- package/dist/v2/CoreWallet.js +305 -0
- package/dist/v2/Keys.d.ts +8 -0
- package/dist/v2/Keys.js +16 -0
- package/dist/v2/Migration.d.ts +112 -0
- package/dist/v2/Migration.js +96 -0
- package/dist/v2/RunningV2Variant.d.ts +40 -0
- package/dist/v2/RunningV2Variant.js +127 -0
- package/dist/v2/Serialization.d.ts +12 -0
- package/dist/v2/Serialization.js +93 -0
- package/dist/v2/Sync.d.ts +208 -0
- package/dist/v2/Sync.js +463 -0
- package/dist/v2/Transacting.d.ts +50 -0
- package/dist/v2/Transacting.js +259 -0
- package/dist/v2/TransactionHistory.d.ts +100 -0
- package/dist/v2/TransactionHistory.js +132 -0
- package/dist/v2/TransactionImbalances.d.ts +10 -0
- package/dist/v2/TransactionImbalances.js +30 -0
- package/dist/v2/TransactionOps.d.ts +17 -0
- package/dist/v2/TransactionOps.js +73 -0
- package/dist/v2/V2Builder.d.ts +154 -0
- package/dist/v2/V2Builder.js +273 -0
- package/dist/v2/WalletError.d.ts +99 -0
- package/dist/v2/WalletError.js +69 -0
- package/dist/v2/index.d.ts +13 -0
- package/dist/v2/index.js +26 -0
- package/package.json +16 -8
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
import * as ledger from '@midnightntwrk/ledger-v9';
|
|
14
|
+
import { WalletSeed } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
15
|
+
import { EitherOps, LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
16
|
+
import { Effect, Either, pipe } from 'effect';
|
|
17
|
+
import { CoreWallet, PublicKeys } from './CoreWallet.js';
|
|
18
|
+
/**
|
|
19
|
+
* The migration used when there is no previous wallet at all.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* It backs `WalletLike.startEmpty`, which builds a wallet before any key material exists. The resulting state has no
|
|
23
|
+
* coins and public keys derived from a fixed placeholder seed; it is a scaffold to be replaced by a real start, not a
|
|
24
|
+
* wallet anybody can transact with. Preserved verbatim from the ledger-v8 implementation so that `startEmpty` behaves
|
|
25
|
+
* exactly as it did.
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const builder = new V2Builder().withDefaults().withMigration(makeEmptyWalletMigration({ networkId }));
|
|
29
|
+
* ```;
|
|
30
|
+
*
|
|
31
|
+
* @param configuration Supplies the network the scaffold state claims to be on.
|
|
32
|
+
* @returns A migration from `null`.
|
|
33
|
+
*/
|
|
34
|
+
export const makeEmptyWalletMigration = (configuration) => ({
|
|
35
|
+
migrate: () => {
|
|
36
|
+
const seed = WalletSeed.fromString('0000000000000000000000000000000000000000000000000000000000000001');
|
|
37
|
+
return Effect.succeed(CoreWallet.empty(PublicKeys.fromSecretKeys(ledger.ZswapSecretKeys.fromSeed(seed)), configuration.networkId));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* The migration between two variants that share this ledger version.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Both sides speak the same state type, so the carry is the identity: local Merkle tree, coins, coin hashes and sync
|
|
45
|
+
* progress all remain valid, and nothing has to be re-read. This is the right strategy for a protocol bump that does
|
|
46
|
+
* not change serialization.
|
|
47
|
+
* @returns A migration from a {@link CoreWallet} of this ledger version.
|
|
48
|
+
*/
|
|
49
|
+
export const makeCarryOverMigration = () => ({
|
|
50
|
+
migrate: (previousState) => Effect.succeed(previousState),
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* The migration across a ledger-version boundary: the local state crosses as bytes, identity and position beside it.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* The chain's state translation carries every commitment across the fork in place — the ledger-v9 tree continues at the
|
|
57
|
+
* index the ledger-v8 tree reached, and the indexer does **not** replay the ledger-v8 timeline as new-version events.
|
|
58
|
+
* So the wallet's own state must cross with it, or the coins are gone.
|
|
59
|
+
*
|
|
60
|
+
* It crosses as bytes. The two ledger majors either side of this boundary serialize `ZswapLocalState` under the same
|
|
61
|
+
* `zswap-local-state` codec — the transaction codec moved at the fork, this one did not — so the previous version's
|
|
62
|
+
* serialization is something this version can simply read. The crossing is therefore a round-trip and not a
|
|
63
|
+
* reconstruction, and everything survives it: the coins at the Merkle indices the chain gave them, the height the
|
|
64
|
+
* tree had reached, and the outputs the wallet was still expecting, which no reconstruction from spendable coins
|
|
65
|
+
* could have recovered. `src/v2/test/byteCrossing.test.ts` pins that codec against both real ledgers, and states what
|
|
66
|
+
* to do if it ever goes red.
|
|
67
|
+
*
|
|
68
|
+
* **The coins booked for transactions still in flight cross too, and are released at the first sync update.** Those
|
|
69
|
+
* transactions belong to the ledger version the chain has left behind, so none of them can ever be included and no
|
|
70
|
+
* inclusion will ever clear the reservations they left in `pendingSpends` — the coins would simply stop being
|
|
71
|
+
* spendable. The release cannot happen here, because un-booking a spend needs the secret keys a migration is by
|
|
72
|
+
* design handed none of; it happens at the first sync update instead, which is where keys and carried state meet
|
|
73
|
+
* (`CoreWallet.completeCrossing`). The change outputs those same transactions would have paid back are left as they
|
|
74
|
+
* are, and nothing ever removes them: these entries carry no TTL and `ZswapLocalState.clearPending` does nothing in
|
|
75
|
+
* either ledger, so they sit in `pendingOutputs` for the life of the wallet. A known limitation, and a mild one —
|
|
76
|
+
* they overstate the pending balance, but they reserve no coin and block no spending.
|
|
77
|
+
*
|
|
78
|
+
* Read through `LedgerOps.ledgerTry`, so a codec that has moved is a `WalletError` at the boundary and never a throw:
|
|
79
|
+
* the failure mode of a future major moving `zswap-local-state` is loud, and lands here, where the
|
|
80
|
+
* {@link StateMigration} seam can be given the ledger team's own translation instead.
|
|
81
|
+
*
|
|
82
|
+
* Sync progress is **parked at the fork** rather than rewound: the indexer numbers ledger-v9 events onwards from
|
|
83
|
+
* whatever id it had reached when the fork happened, never from zero, so the inherited cursor is exactly where this
|
|
84
|
+
* wallet's reading resumes. A wallet that rewound to zero would wait on a stretch of the timeline this ledger
|
|
85
|
+
* version's events do not occupy.
|
|
86
|
+
* @returns A migration from a previous-ledger-version wallet.
|
|
87
|
+
*/
|
|
88
|
+
export const makeCrossLedgerMigration = () => ({
|
|
89
|
+
migrate: (previousState) => pipe(LedgerOps.ledgerTry(() => ledger.ZswapLocalState.deserialize(previousState.state.serialize())), Either.map((state) => CoreWallet.fromPreviousVersion({
|
|
90
|
+
state,
|
|
91
|
+
publicKeys: previousState.publicKeys,
|
|
92
|
+
networkId: previousState.networkId,
|
|
93
|
+
protocolVersion: previousState.protocolVersion,
|
|
94
|
+
progress: previousState.progress,
|
|
95
|
+
})), EitherOps.toEffect),
|
|
96
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type * as ledger from '@midnightntwrk/ledger-v9';
|
|
2
|
+
import { Effect, Scope, Stream } from 'effect';
|
|
3
|
+
import { type WalletRuntimeError, type Variant, StateChange } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
|
|
4
|
+
import { type SerializationCapability } from './Serialization.js';
|
|
5
|
+
import { type ChangesResult, type EventsSyncUpdate, type SyncCapability, type SyncService } from './Sync.js';
|
|
6
|
+
import { type TransactingCapability, type TokenTransfer, type BalancingResult } from './Transacting.js';
|
|
7
|
+
import { type WalletError } from './WalletError.js';
|
|
8
|
+
import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
|
|
9
|
+
import { type KeysCapability } from './Keys.js';
|
|
10
|
+
import { type CoinSelection } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
11
|
+
import { type CoreWallet } from './CoreWallet.js';
|
|
12
|
+
import { type TransactionHistoryService } from './TransactionHistory.js';
|
|
13
|
+
export declare namespace RunningV2Variant {
|
|
14
|
+
type Context<TSerialized, TSyncUpdate, TTransaction, TStartAux> = {
|
|
15
|
+
serializationCapability: SerializationCapability<CoreWallet, null, TSerialized>;
|
|
16
|
+
syncService: SyncService<CoreWallet, TStartAux, TSyncUpdate>;
|
|
17
|
+
syncCapability: SyncCapability<CoreWallet, TSyncUpdate, ChangesResult>;
|
|
18
|
+
transactingCapability: TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, TTransaction>;
|
|
19
|
+
coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
|
|
20
|
+
keysCapability: KeysCapability<CoreWallet>;
|
|
21
|
+
coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>;
|
|
22
|
+
transactionHistoryService: TransactionHistoryService;
|
|
23
|
+
};
|
|
24
|
+
type AnyContext = Context<any, any, any, any>;
|
|
25
|
+
}
|
|
26
|
+
export declare const V2Tag: unique symbol;
|
|
27
|
+
export type DefaultRunningV2 = RunningV2Variant<string, EventsSyncUpdate, ledger.FinalizedTransaction, ledger.ZswapSecretKeys>;
|
|
28
|
+
export declare class RunningV2Variant<TSerialized, TSyncUpdate, TTransaction, TStartAux> implements Variant.RunningVariant<typeof V2Tag, CoreWallet> {
|
|
29
|
+
#private;
|
|
30
|
+
readonly __polyTag__: typeof V2Tag;
|
|
31
|
+
readonly state: Stream.Stream<StateChange.StateChange<CoreWallet>, WalletRuntimeError>;
|
|
32
|
+
constructor(scope: Scope.Scope, context: Variant.VariantContext<CoreWallet>, v2Context: RunningV2Variant.Context<TSerialized, TSyncUpdate, TTransaction, TStartAux>);
|
|
33
|
+
startSyncInBackground(startAux: TStartAux): Effect.Effect<void>;
|
|
34
|
+
startSync(startAux: TStartAux): Stream.Stream<void, WalletError, Scope.Scope>;
|
|
35
|
+
balanceTransaction(secretKeys: ledger.ZswapSecretKeys, tx: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Effect.Effect<BalancingResult, WalletError>;
|
|
36
|
+
transferTransaction(secretKeys: ledger.ZswapSecretKeys, outputs: ReadonlyArray<TokenTransfer>): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
|
|
37
|
+
initSwap(secretKeys: ledger.ZswapSecretKeys, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
|
|
38
|
+
revertTransaction(transaction: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Effect.Effect<void, WalletError>;
|
|
39
|
+
serializeState(state: CoreWallet): TSerialized;
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect, pipe, Scope, Stream, SubscriptionRef, Schedule, Duration, Sink, Console } from 'effect';
|
|
2
|
+
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
3
|
+
import { StateChange, VersionChangeType, } from '@midnight-ntwrk/wallet-sdk-runtime/abstractions';
|
|
4
|
+
import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
5
|
+
import { OtherWalletError } from './WalletError.js';
|
|
6
|
+
const progress = (state) => {
|
|
7
|
+
const appliedIndex = state.progress?.appliedIndex ?? 0n;
|
|
8
|
+
const highestRelevantWalletIndex = state.progress?.highestRelevantWalletIndex ?? 0n;
|
|
9
|
+
const highestIndex = state.progress?.highestIndex ?? 0n;
|
|
10
|
+
const highestRelevantIndex = state.progress?.highestRelevantIndex ?? 0n;
|
|
11
|
+
const sourceGap = highestIndex - highestRelevantIndex;
|
|
12
|
+
const applyGap = highestRelevantWalletIndex - appliedIndex;
|
|
13
|
+
return [StateChange.ProgressUpdate({ sourceGap, applyGap })];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reports the protocol version a state observation implies, if any.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Two situations call for a signal. The ordinary one is a transition: sync annotated the state with a version it did
|
|
20
|
+
* not have before, and the runtime decides whether that is a re-annotation inside this variant's range or a hand-over
|
|
21
|
+
* to the next one.
|
|
22
|
+
*
|
|
23
|
+
* The other is a state that arrives already outside this variant's range — a snapshot serialized in the window between
|
|
24
|
+
* the annotation and the migration it should have caused, or one written by an older protocol version entirely.
|
|
25
|
+
* Nothing further will change that value, so waiting for a transition would strand the wallet on a version it does
|
|
26
|
+
* not own. Emitting on sight heals it.
|
|
27
|
+
*/
|
|
28
|
+
const protocolVersionChange = (previous, current, isInitial, activationRange) => {
|
|
29
|
+
const transitioned = previous.protocolVersion != current.protocolVersion;
|
|
30
|
+
const strandedOutsideRange = isInitial && !ProtocolVersion.withinRange(current.protocolVersion, activationRange);
|
|
31
|
+
return transitioned || strandedOutsideRange
|
|
32
|
+
? [
|
|
33
|
+
StateChange.VersionChange({
|
|
34
|
+
change: VersionChangeType.Version({
|
|
35
|
+
version: ProtocolVersion.ProtocolVersion(current.protocolVersion),
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
]
|
|
39
|
+
: [];
|
|
40
|
+
};
|
|
41
|
+
export const V2Tag = Symbol('V2');
|
|
42
|
+
export class RunningV2Variant {
|
|
43
|
+
__polyTag__ = V2Tag;
|
|
44
|
+
#scope;
|
|
45
|
+
#context;
|
|
46
|
+
#v2Context;
|
|
47
|
+
// Variant-wide cap on concurrent tx-history lookups. Each sync batch forks its own fan-out fiber, so a per-batch
|
|
48
|
+
// `concurrency` limit alone would let in-flight indexer queries grow with the number of live batches while their
|
|
49
|
+
// lookups retry through the indexer-lag window. Every lookup acquires a permit here, making the cap global.
|
|
50
|
+
#txHistoryPermits = Effect.makeSemaphore(8).pipe(Effect.runSync);
|
|
51
|
+
state;
|
|
52
|
+
constructor(scope, context, v2Context) {
|
|
53
|
+
this.#scope = scope;
|
|
54
|
+
this.#context = context;
|
|
55
|
+
this.#v2Context = v2Context;
|
|
56
|
+
this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(
|
|
57
|
+
// The accumulator carries the "have we seen anything yet" flag alongside the previous state: the first
|
|
58
|
+
// observation is the only one that can be a restored state nobody has inspected against this variant's
|
|
59
|
+
// range yet, and `SubscriptionRef.changes` replays the current value, so it is exactly this element.
|
|
60
|
+
Stream.mapAccum({ previous: initialState, isInitial: true }, (seen, current) => [{ previous: current, isInitial: false }, [seen.previous, current, seen.isInitial]]))), Stream.mapConcat(([previous, current, isInitial]) => {
|
|
61
|
+
// TODO: emit progress only upon actual change
|
|
62
|
+
return [
|
|
63
|
+
StateChange.State({ state: current }),
|
|
64
|
+
...progress(current),
|
|
65
|
+
...protocolVersionChange(previous, current, isInitial, context.activationRange),
|
|
66
|
+
];
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
startSyncInBackground(startAux) {
|
|
70
|
+
return this.startSync(startAux).pipe(Stream.runScoped(Sink.drain), Effect.forkScoped, Effect.provideService(Scope.Scope, this.#scope));
|
|
71
|
+
}
|
|
72
|
+
startSync(startAux) {
|
|
73
|
+
return pipe(SubscriptionRef.get(this.#context.stateRef), Stream.fromEffect, Stream.flatMap((state) => this.#v2Context.syncService.updates(state, startAux)), Stream.mapEffect((update) => SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => Effect.try({
|
|
74
|
+
try: () => {
|
|
75
|
+
const [newState, changesResult] = this.#v2Context.syncCapability.applyUpdate(state, update, this.#context.activationRange);
|
|
76
|
+
return [changesResult, newState];
|
|
77
|
+
},
|
|
78
|
+
catch: (err) => new OtherWalletError({
|
|
79
|
+
message: 'Error while applying sync update',
|
|
80
|
+
cause: err,
|
|
81
|
+
}),
|
|
82
|
+
})).pipe(Effect.flatMap(({ changes, protocolVersion }) =>
|
|
83
|
+
// Skip the tx-history fork entirely when there are no changes.
|
|
84
|
+
// Forking unconditionally allocates a fiber per apply call (one
|
|
85
|
+
// for every batch the sync emits, even the all-progress ones),
|
|
86
|
+
// which adds up fast during catch-up.
|
|
87
|
+
changes.length === 0
|
|
88
|
+
? Effect.void
|
|
89
|
+
: pipe(Effect.forEach(changes, (change) => pipe(this.#txHistoryPermits.withPermits(1)(pipe(this.#v2Context.transactionHistoryService.getTransactionDetails(change.source), Effect.flatMap((metadata) => this.#v2Context.transactionHistoryService.put(change, metadata, protocolVersion)))), Effect.catchAllCause((cause) =>
|
|
90
|
+
// A sustained indexer outage (longer than getTransactionDetails' retry window) still lands
|
|
91
|
+
// here. applyUpdate has already advanced appliedIndex, so this change.source won't be
|
|
92
|
+
// re-processed — the shielded section is permanently lost. Surface that as a structured
|
|
93
|
+
// error carrying the tx hash, not a silent Console.error defect.
|
|
94
|
+
Effect.logError(cause, `Failed to record shielded tx-history section for ${change.source}`))),
|
|
95
|
+
// `concurrency` only bounds fiber creation within this one batch; the real cap on simultaneous
|
|
96
|
+
// indexer queries is the variant-wide semaphore acquired around each lookup above.
|
|
97
|
+
{ discard: true, concurrency: 8 }), Effect.forkScoped)), Effect.provideService(Scope.Scope, this.#scope))), Stream.tapError((error) => Console.error(error)), Stream.retry(pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.map((delay) => {
|
|
98
|
+
const maxDelay = Duration.minutes(2);
|
|
99
|
+
const jitter = Duration.millis(Math.floor(Math.random() * 1000));
|
|
100
|
+
const delayWithJitter = Duration.toMillis(delay) + Duration.toMillis(jitter);
|
|
101
|
+
return Duration.millis(Math.min(delayWithJitter, Duration.toMillis(maxDelay)));
|
|
102
|
+
}))));
|
|
103
|
+
}
|
|
104
|
+
balanceTransaction(secretKeys, tx) {
|
|
105
|
+
return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
|
|
106
|
+
return pipe(this.#v2Context.transactingCapability.balanceTransaction(secretKeys, state, tx), EitherOps.toEffect);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
transferTransaction(secretKeys, outputs) {
|
|
110
|
+
return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
|
|
111
|
+
return pipe(this.#v2Context.transactingCapability.makeTransfer(secretKeys, state, outputs), EitherOps.toEffect);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
initSwap(secretKeys, desiredInputs, desiredOutputs) {
|
|
115
|
+
return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
|
|
116
|
+
return pipe(this.#v2Context.transactingCapability.initSwap(secretKeys, state, desiredInputs, desiredOutputs), EitherOps.toEffect);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
revertTransaction(transaction) {
|
|
120
|
+
return SubscriptionRef.updateEffect(this.#context.stateRef, (state) => {
|
|
121
|
+
return pipe(this.#v2Context.transactingCapability.revertTransaction(state, transaction), EitherOps.toEffect);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
serializeState(state) {
|
|
125
|
+
return this.#v2Context.serializationCapability.serialize(state);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { WalletError } from './WalletError.js';
|
|
3
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
4
|
+
import { type NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
5
|
+
export type SerializationCapability<TWallet, TAux, TSerialized> = {
|
|
6
|
+
serialize(wallet: TWallet): TSerialized;
|
|
7
|
+
deserialize(aux: TAux, data: TSerialized): Either.Either<TWallet, WalletError>;
|
|
8
|
+
};
|
|
9
|
+
export type DefaultSerializationConfiguration = {
|
|
10
|
+
networkId: NetworkId.NetworkId;
|
|
11
|
+
};
|
|
12
|
+
export declare const makeDefaultV2SerializationCapability: () => SerializationCapability<CoreWallet, null, string>;
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
import { Effect, ParseResult, Either, pipe, Schema } from 'effect';
|
|
14
|
+
import { WalletError } from './WalletError.js';
|
|
15
|
+
import * as ledger from '@midnightntwrk/ledger-v9';
|
|
16
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
17
|
+
const StateSchema = Schema.declare((input) => input instanceof ledger.ZswapLocalState).annotations({
|
|
18
|
+
identifier: 'ledger.ZswapLocalState',
|
|
19
|
+
});
|
|
20
|
+
const Uint8ArraySchema = Schema.declare((input) => input instanceof Uint8Array).annotations({
|
|
21
|
+
identifier: 'Uint8Array',
|
|
22
|
+
});
|
|
23
|
+
const StateFromUInt8Array = () => Schema.asSchema(Schema.transformOrFail(Uint8ArraySchema, StateSchema, {
|
|
24
|
+
encode: (state) => {
|
|
25
|
+
return Effect.try({
|
|
26
|
+
try: () => {
|
|
27
|
+
return state.serialize();
|
|
28
|
+
},
|
|
29
|
+
catch: (err) => {
|
|
30
|
+
return new ParseResult.Unexpected(err, 'Could not serialize local state');
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
decode: (bytes) => Effect.try({
|
|
35
|
+
try: () => ledger.ZswapLocalState.deserialize(bytes),
|
|
36
|
+
catch: (err) => {
|
|
37
|
+
return new ParseResult.Unexpected(err, 'Could not deserialize local state');
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
const HexedState = () => pipe(Schema.Uint8ArrayFromHex, Schema.compose(StateFromUInt8Array()));
|
|
42
|
+
export const makeDefaultV2SerializationCapability = () => {
|
|
43
|
+
const SnapshotSchema = Schema.Struct({
|
|
44
|
+
publicKeys: Schema.Struct({
|
|
45
|
+
coinPublicKey: Schema.String,
|
|
46
|
+
encryptionPublicKey: Schema.String,
|
|
47
|
+
}),
|
|
48
|
+
state: HexedState(),
|
|
49
|
+
protocolVersion: Schema.BigInt,
|
|
50
|
+
offset: Schema.optional(Schema.BigInt),
|
|
51
|
+
networkId: Schema.String,
|
|
52
|
+
coinHashes: Schema.Record({
|
|
53
|
+
key: Schema.String,
|
|
54
|
+
value: Schema.Struct({ nullifier: Schema.String, commitment: Schema.String }),
|
|
55
|
+
}),
|
|
56
|
+
// Set only on a wallet caught between a cross-ledger migration and its first sync update: its state crossed the
|
|
57
|
+
// boundary as bytes, but the hashes over that state need secret keys nobody here has (see
|
|
58
|
+
// `CoreWallet.coinHashesPending`). Optional twice over — a wallet that is not mid-crossing has nothing to declare,
|
|
59
|
+
// and snapshots written before the field existed must keep decoding unchanged.
|
|
60
|
+
coinHashesPending: Schema.optional(Schema.Literal(true)),
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
serialize: (wallet) => {
|
|
64
|
+
const buildSnapshot = (w) => ({
|
|
65
|
+
publicKeys: w.publicKeys,
|
|
66
|
+
state: w.state,
|
|
67
|
+
protocolVersion: w.protocolVersion,
|
|
68
|
+
networkId: w.networkId,
|
|
69
|
+
offset: w.progress?.appliedIndex,
|
|
70
|
+
coinHashes: w.coinHashes,
|
|
71
|
+
...(w.coinHashesPending !== undefined ? { coinHashesPending: w.coinHashesPending } : {}),
|
|
72
|
+
});
|
|
73
|
+
return pipe(wallet, buildSnapshot, Schema.encodeSync(SnapshotSchema), JSON.stringify);
|
|
74
|
+
},
|
|
75
|
+
deserialize: (aux, serialized) => {
|
|
76
|
+
return pipe(serialized, Schema.decodeUnknownEither(Schema.parseJson(SnapshotSchema)), Either.mapLeft((err) => WalletError.other(err)), Either.flatMap((snapshot) => {
|
|
77
|
+
const progress = {
|
|
78
|
+
appliedIndex: snapshot.offset ?? 0n,
|
|
79
|
+
highestRelevantWalletIndex: 0n,
|
|
80
|
+
highestIndex: 0n,
|
|
81
|
+
highestRelevantIndex: 0n,
|
|
82
|
+
isConnected: false,
|
|
83
|
+
};
|
|
84
|
+
// A snapshot that declares its hashes pending is the one shape the validation below would wrongly refuse:
|
|
85
|
+
// its state is full and its hash map is empty by construction, because the migration that wrote it had no
|
|
86
|
+
// secret keys. Every other snapshot goes on being checked against its own state.
|
|
87
|
+
return snapshot.coinHashesPending === true
|
|
88
|
+
? Either.right(CoreWallet.restoreWithPendingCoinHashes(snapshot.publicKeys, snapshot.state, progress, snapshot.protocolVersion, snapshot.networkId))
|
|
89
|
+
: CoreWallet.restoreWithCoinHashes(snapshot.publicKeys, snapshot.state, snapshot.coinHashes, progress, snapshot.protocolVersion, snapshot.networkId);
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as ledger from '@midnightntwrk/ledger-v9';
|
|
2
|
+
import { Option, Schema, type Scope, Stream } from 'effect';
|
|
3
|
+
import { ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
4
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
5
|
+
import { type Simulator, type SimulatorState } from '@midnight-ntwrk/wallet-sdk-capabilities/simulation';
|
|
6
|
+
import { type WalletError } from './WalletError.js';
|
|
7
|
+
import { type TransactionHistoryService } from './TransactionHistory.js';
|
|
8
|
+
export interface SyncService<TState, TStartAux, TUpdate> {
|
|
9
|
+
updates: (state: TState, auxData: TStartAux) => Stream.Stream<TUpdate, WalletError, Scope.Scope>;
|
|
10
|
+
}
|
|
11
|
+
export type ChangesResult = {
|
|
12
|
+
readonly changes: ledger.ZswapStateChanges[];
|
|
13
|
+
readonly protocolVersion: number;
|
|
14
|
+
};
|
|
15
|
+
export interface SyncCapability<TState, TUpdate, TResult> {
|
|
16
|
+
/**
|
|
17
|
+
* Folds a sync update into the wallet state.
|
|
18
|
+
*
|
|
19
|
+
* @param state The state to fold into.
|
|
20
|
+
* @param update The update to apply.
|
|
21
|
+
* @param activeRange The half-open protocol version range the running variant owns. Anything the source reports at or
|
|
22
|
+
* beyond its end belongs to a later variant and must be left unapplied for that variant to fetch.
|
|
23
|
+
*/
|
|
24
|
+
applyUpdate: (state: TState, update: TUpdate, activeRange: ProtocolVersion.ProtocolVersion.Range) => [TState, TResult];
|
|
25
|
+
}
|
|
26
|
+
/** The result of splitting a batch of version-tagged items at a variant's activation boundary. */
|
|
27
|
+
export type BoundarySplit<T> = {
|
|
28
|
+
/** The leading items this variant owns, in source order. */
|
|
29
|
+
readonly applied: readonly T[];
|
|
30
|
+
/** The trailing items belonging to a later protocol version, left for the next variant to re-fetch. */
|
|
31
|
+
readonly deferred: readonly T[];
|
|
32
|
+
/**
|
|
33
|
+
* The protocol version to record on the state: the first deferred item's version when there is one — that is the
|
|
34
|
+
* signal that triggers migration — otherwise the last applied item's version. `None` for an empty batch.
|
|
35
|
+
*/
|
|
36
|
+
readonly observedVersion: Option.Option<ProtocolVersion.ProtocolVersion>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Splits a batch of version-tagged items at the end of a variant's activation range.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* The split is positional, not a filter: everything from the first out-of-range item onwards is deferred, even if a
|
|
43
|
+
* later item reports an in-range version again. A batch is a contiguous slice of one timeline, so applying past a
|
|
44
|
+
* boundary and then resuming behind it would leave a hole no cursor can describe.
|
|
45
|
+
*
|
|
46
|
+
* This is the one place the boundary rule lives; both the indexer capability (per event) and the simulator capability
|
|
47
|
+
* (per block) go through it, so they cannot drift apart.
|
|
48
|
+
* @param items The batch, in source order.
|
|
49
|
+
* @param versionOf Reads the protocol version an item was reported at.
|
|
50
|
+
* @param activeRange The variant's half-open activation range.
|
|
51
|
+
* @returns The applied/deferred split and the version to record.
|
|
52
|
+
*/
|
|
53
|
+
export declare const splitAtVersionBoundary: <T>(items: readonly T[], versionOf: (item: T) => number, activeRange: ProtocolVersion.ProtocolVersion.Range) => BoundarySplit<T>;
|
|
54
|
+
/** Records a batch's observed protocol version on the state, monotonically. A batch that observed none is a no-op. */
|
|
55
|
+
export declare const annotateVersion: (state: CoreWallet, observedVersion: Option.Option<ProtocolVersion.ProtocolVersion>) => CoreWallet;
|
|
56
|
+
export type IndexerClientConnection = {
|
|
57
|
+
indexerHttpUrl: string;
|
|
58
|
+
indexerWsUrl?: string;
|
|
59
|
+
keepAlive?: number;
|
|
60
|
+
/** Cap on the in-flight event queue between the WebSocket push and the apply loop. Default: 10000. */
|
|
61
|
+
bufferSize?: number;
|
|
62
|
+
/** In-flight count at which the disposed WS subscription is reopened. Default: 100. */
|
|
63
|
+
resumeThreshold?: number;
|
|
64
|
+
};
|
|
65
|
+
export type BatchUpdatesConfig = {
|
|
66
|
+
/**
|
|
67
|
+
* Maximum number of events to collect into a single batch before emitting.
|
|
68
|
+
*
|
|
69
|
+
* @default 10
|
|
70
|
+
*/
|
|
71
|
+
readonly size?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Maximum time in milliseconds to wait for a full batch before emitting a partial one. Controls the `groupedWithin`
|
|
74
|
+
* timeout — lower values mean more responsive (but smaller) batches when events arrive slowly.
|
|
75
|
+
*
|
|
76
|
+
* @default 1
|
|
77
|
+
*/
|
|
78
|
+
readonly timeout?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Minimum delay in milliseconds injected between consecutive batches. Prevents the sync stream from saturating
|
|
81
|
+
* downstream consumers when many events are available at once. Set to 0 to disable spacing entirely.
|
|
82
|
+
*
|
|
83
|
+
* @default 4
|
|
84
|
+
*/
|
|
85
|
+
readonly spacing?: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* How often the sync source re-asks the chain which protocol version it is on.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* The event subscription this source reads has no progress arm: it says nothing at all when there is nothing to say. A
|
|
92
|
+
* wallet therefore observes the chain's protocol version only through events, and on a chain that crosses a protocol
|
|
93
|
+
* boundary and then produces no shielded traffic it observes no version at all — it stays on the variant it was
|
|
94
|
+
* running, and everything built through it stays routed to that variant's ledger. Asking on a timer is what closes
|
|
95
|
+
* that, and the cost of asking is one small query per interval on a chain that has not moved.
|
|
96
|
+
*/
|
|
97
|
+
export type VersionWatchConfig = {
|
|
98
|
+
/**
|
|
99
|
+
* Milliseconds between checks. Zero or less turns the watcher off entirely, which is what a source driving a wallet
|
|
100
|
+
* from something other than a live chain wants.
|
|
101
|
+
*
|
|
102
|
+
* @default 30000
|
|
103
|
+
*/
|
|
104
|
+
readonly intervalMs?: number;
|
|
105
|
+
};
|
|
106
|
+
export type DefaultSyncConfiguration = {
|
|
107
|
+
indexerClientConnection: IndexerClientConnection;
|
|
108
|
+
batchUpdates?: BatchUpdatesConfig;
|
|
109
|
+
versionWatch?: VersionWatchConfig;
|
|
110
|
+
};
|
|
111
|
+
export type DefaultSyncContext = {
|
|
112
|
+
transactionHistoryService: TransactionHistoryService;
|
|
113
|
+
};
|
|
114
|
+
export type SecretKeysResource = <A>(cb: (keys: ledger.ZswapSecretKeys) => A) => A;
|
|
115
|
+
export declare const SecretKeysResource: {
|
|
116
|
+
create: (secretKeys: ledger.ZswapSecretKeys) => SecretKeysResource;
|
|
117
|
+
};
|
|
118
|
+
/** A batch of the indexer's event timeline, still encoded. */
|
|
119
|
+
export type EventsWalletSyncUpdate = {
|
|
120
|
+
_tag: 'Events';
|
|
121
|
+
updates: EventsSyncUpdate[];
|
|
122
|
+
secretKeys: ledger.ZswapSecretKeys;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* What the chain says about itself when its timeline says nothing.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* An observation, not a piece of the chain: it moves no cursor, inserts nothing and produces no changes. All it can do
|
|
129
|
+
* is record a protocol version, which is enough, because recording one outside the running variant's activation range
|
|
130
|
+
* is exactly what makes the runtime hand over.
|
|
131
|
+
*
|
|
132
|
+
* `highestEventId` is what makes the record safe to make. Handing over parks the sync cursor where it stands, and the
|
|
133
|
+
* variant that takes over re-fetches from there — so an event still unread below the source's tip would reach it as
|
|
134
|
+
* bytes of the version that preceded it, which its ledger cannot deserialize, and which may in any case be carrying a
|
|
135
|
+
* coin the crossed local state does not yet contain. The signal therefore travels with the far end of the source's
|
|
136
|
+
* event timeline, so the capability can refuse it while anything remains unread. `null` means the source provably
|
|
137
|
+
* holds no zswap event at all, which is the one case where nothing can be unread.
|
|
138
|
+
*/
|
|
139
|
+
export type VersionSignalSyncUpdate = Readonly<{
|
|
140
|
+
_tag: 'VersionSignal';
|
|
141
|
+
/** The protocol version the source's tip was reported under. */
|
|
142
|
+
version: number;
|
|
143
|
+
/** The highest event id the source holds, or `null` when it provably holds no event. */
|
|
144
|
+
highestEventId: number | null;
|
|
145
|
+
}>;
|
|
146
|
+
export declare const VersionSignalSyncUpdate: {
|
|
147
|
+
create: (version: number, highestEventId: number | null) => VersionSignalSyncUpdate;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* What the indexer-backed sync source emits.
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* Ordinarily a batch of events; and, on a timer, what the chain says about its own version when its timeline says
|
|
154
|
+
* nothing.
|
|
155
|
+
*/
|
|
156
|
+
export type WalletSyncUpdate = EventsWalletSyncUpdate | VersionSignalSyncUpdate;
|
|
157
|
+
export declare const WalletSyncUpdate: {
|
|
158
|
+
create: (updates: EventsSyncUpdate[], secretKeys: ledger.ZswapSecretKeys) => EventsWalletSyncUpdate;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* One event of the source's timeline, still encoded.
|
|
162
|
+
*
|
|
163
|
+
* @remarks
|
|
164
|
+
* The event is carried as the source served it rather than as a `ledger.Event`, because whether this ledger version may
|
|
165
|
+
* read it at all is not the source's question to answer. A batch spanning a protocol boundary carries events of the
|
|
166
|
+
* version that follows this one, which this ledger version cannot deserialize — the serialization header names a
|
|
167
|
+
* different version — and after a hand-over the inclusive cursor re-delivers an event of the version that preceded
|
|
168
|
+
* it, which it equally cannot. Both are ordinary, and neither is an error: they belong to the variant either side.
|
|
169
|
+
*
|
|
170
|
+
* So the bytes travel undecoded and only the capability, which knows the activation range, reads the ones it is about
|
|
171
|
+
* to apply — see {@link readEvent}. Decoding here would fail the whole batch on an event nobody intended to apply.
|
|
172
|
+
*/
|
|
173
|
+
export declare const EventsSyncUpdate: Schema.TaggedStruct<"EventsSyncUpdate", {
|
|
174
|
+
id: typeof Schema.Number;
|
|
175
|
+
protocolVersion: typeof Schema.Number;
|
|
176
|
+
maxId: typeof Schema.Number;
|
|
177
|
+
raw: typeof Schema.String;
|
|
178
|
+
}>;
|
|
179
|
+
export type EventsSyncUpdate = Schema.Schema.Type<typeof EventsSyncUpdate>;
|
|
180
|
+
/**
|
|
181
|
+
* Reads an event this variant is going to apply.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* The counterpart of {@link EventsSyncUpdate} carrying its event encoded: a capability calls this on the batch prefix it
|
|
185
|
+
* owns, and never on what it defers. Failure here is a genuine one — an event this variant claimed and cannot read —
|
|
186
|
+
* and is raised rather than returned, because `SyncCapability.applyUpdate` is total in its own domain and the variant
|
|
187
|
+
* already turns a throw from it into a typed synchronization error.
|
|
188
|
+
* @param update The update to read.
|
|
189
|
+
* @returns The event it carries.
|
|
190
|
+
* @throws ParseError if the bytes are not an event this ledger version can deserialize.
|
|
191
|
+
*/
|
|
192
|
+
export declare const readEvent: (update: EventsSyncUpdate) => ledger.Event;
|
|
193
|
+
export declare const makeEventsSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, WalletSyncUpdate>;
|
|
194
|
+
export declare const makeEventsSyncCapability: () => SyncCapability<CoreWallet, WalletSyncUpdate, ChangesResult>;
|
|
195
|
+
export type SimulatorSyncConfiguration = {
|
|
196
|
+
simulator: Simulator;
|
|
197
|
+
};
|
|
198
|
+
/** What the simulator-backed sync source emits: the simulator's state, from which the pending blocks are read. */
|
|
199
|
+
export type SimulatorSyncUpdate = {
|
|
200
|
+
_tag: 'Events';
|
|
201
|
+
update: SimulatorState;
|
|
202
|
+
secretKeys: ledger.ZswapSecretKeys;
|
|
203
|
+
};
|
|
204
|
+
export declare const SimulatorSyncUpdate: {
|
|
205
|
+
create: (update: SimulatorState, secretKeys: ledger.ZswapSecretKeys) => SimulatorSyncUpdate;
|
|
206
|
+
};
|
|
207
|
+
export declare const makeSimulatorSyncService: (config: SimulatorSyncConfiguration) => SyncService<CoreWallet, ledger.ZswapSecretKeys, SimulatorSyncUpdate>;
|
|
208
|
+
export declare const makeSimulatorSyncCapability: () => SyncCapability<CoreWallet, SimulatorSyncUpdate, ChangesResult>;
|