@midnightntwrk/wallet-sdk-unshielded-wallet 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
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, pipe, Scope, Stream, SubscriptionRef, Schedule, Duration, Sink, Console } from 'effect';
14
+ import { ProtocolVersion } from '@midnightntwrk/wallet-sdk-abstractions';
15
+ import { StateChange, VersionChangeType, } from '@midnightntwrk/wallet-sdk-runtime/abstractions';
16
+ import { EitherOps } from '@midnightntwrk/wallet-sdk-utilities';
17
+ const progress = (state) => {
18
+ const appliedId = state.progress?.appliedId ?? 0n;
19
+ const highestTransactionId = state.progress?.highestTransactionId ?? 0n;
20
+ const sourceGap = highestTransactionId - appliedId;
21
+ const applyGap = appliedId - appliedId;
22
+ return [StateChange.ProgressUpdate({ sourceGap, applyGap })];
23
+ };
24
+ const protocolVersionChange = (previous, current) => {
25
+ return previous.protocolVersion != current.protocolVersion
26
+ ? [
27
+ StateChange.VersionChange({
28
+ change: VersionChangeType.Version({
29
+ version: ProtocolVersion.ProtocolVersion(current.protocolVersion),
30
+ }),
31
+ }),
32
+ ]
33
+ : [];
34
+ };
35
+ export const V1Tag = Symbol('V1');
36
+ export class RunningV1Variant {
37
+ __polyTag__ = V1Tag;
38
+ #scope;
39
+ #context;
40
+ #v1Context;
41
+ state;
42
+ constructor(scope, context, v1Context) {
43
+ this.#scope = scope;
44
+ this.#context = context;
45
+ this.#v1Context = v1Context;
46
+ this.state = Stream.fromEffect(context.stateRef.get).pipe(Stream.flatMap((initialState) => context.stateRef.changes.pipe(Stream.mapAccum(initialState, (previous, current) => {
47
+ return [current, [previous, current]];
48
+ }))), Stream.mapConcat(([previous, current]) => {
49
+ // TODO: emit progress only upon actual change
50
+ return [
51
+ StateChange.State({ state: current }),
52
+ ...progress(current),
53
+ ...protocolVersionChange(previous, current),
54
+ ];
55
+ }));
56
+ }
57
+ startSyncInBackground() {
58
+ return this.startSync().pipe(Stream.runScoped(Sink.drain), Effect.forkScoped, Effect.provideService(Scope.Scope, this.#scope));
59
+ }
60
+ startSync() {
61
+ return pipe(SubscriptionRef.get(this.#context.stateRef), Stream.fromEffect, Stream.flatMap((state) => this.#v1Context.syncService.updates(state)), Stream.mapEffect((update) => {
62
+ return SubscriptionRef.updateEffect(this.#context.stateRef, (state) => pipe(this.#v1Context.syncCapability.applyUpdate(state, update), EitherOps.toEffect));
63
+ }), Stream.tapError((error) => Console.error(error)), Stream.retry(pipe(Schedule.exponential(Duration.seconds(1), 2), Schedule.map((delay) => {
64
+ const maxDelay = Duration.minutes(2);
65
+ const jitter = Duration.millis(Math.floor(Math.random() * 1000));
66
+ const delayWithJitter = Duration.toMillis(delay) + Duration.toMillis(jitter);
67
+ return Duration.millis(Math.min(delayWithJitter, Duration.toMillis(maxDelay)));
68
+ }))));
69
+ }
70
+ balanceFinalizedTransaction(tx) {
71
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
72
+ return pipe(this.#v1Context.transactingCapability.balanceFinalizedTransaction(state, tx), EitherOps.toEffect);
73
+ });
74
+ }
75
+ balanceUnboundTransaction(tx) {
76
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
77
+ return pipe(this.#v1Context.transactingCapability.balanceUnboundTransaction(state, tx), EitherOps.toEffect);
78
+ });
79
+ }
80
+ balanceUnprovenTransaction(tx) {
81
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
82
+ return pipe(this.#v1Context.transactingCapability.balanceUnprovenTransaction(state, tx), EitherOps.toEffect);
83
+ });
84
+ }
85
+ transferTransaction(outputs, ttl) {
86
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
87
+ return pipe(this.#v1Context.transactingCapability.makeTransfer(state, outputs, ttl), EitherOps.toEffect, Effect.map(({ transaction, newState }) => [transaction, newState]));
88
+ });
89
+ }
90
+ rotateUtxos(guaranteedUtxos, fallibleUtxos, nightVerifyingKey, ttl) {
91
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
92
+ return pipe(this.#v1Context.transactingCapability.rotateUtxos(state, guaranteedUtxos, fallibleUtxos, nightVerifyingKey, ttl), EitherOps.toEffect, Effect.map(({ transaction, newState }) => [transaction, newState]));
93
+ });
94
+ }
95
+ initSwap(desiredInputs, desiredOutputs, ttl) {
96
+ return SubscriptionRef.modifyEffect(this.#context.stateRef, (state) => {
97
+ return pipe(this.#v1Context.transactingCapability.initSwap(state, desiredInputs, desiredOutputs, ttl), Effect.map(({ transaction, newState }) => [transaction, newState]));
98
+ });
99
+ }
100
+ signUnprovenTransaction(transaction, signSegment) {
101
+ return this.#v1Context.transactingCapability.signUnprovenTransaction(transaction, signSegment);
102
+ }
103
+ signUnboundTransaction(transaction, signSegment) {
104
+ return this.#v1Context.transactingCapability.signUnboundTransaction(transaction, signSegment);
105
+ }
106
+ revertTransaction(transaction) {
107
+ return SubscriptionRef.updateEffect(this.#context.stateRef, (state) => {
108
+ return pipe(this.#v1Context.transactingCapability.revertTransaction(state, transaction), EitherOps.toEffect);
109
+ });
110
+ }
111
+ serializeState(state) {
112
+ return this.#v1Context.serializationCapability.serialize(state);
113
+ }
114
+ }
@@ -0,0 +1,12 @@
1
+ import { Either } from 'effect';
2
+ import { type WalletError } from './WalletError.js';
3
+ import { CoreWallet } from './CoreWallet.js';
4
+ import { type NetworkId } from '@midnightntwrk/wallet-sdk-abstractions';
5
+ export type SerializationCapability<TWallet, TSerialized> = {
6
+ serialize(wallet: TWallet): TSerialized;
7
+ deserialize(data: TSerialized): Either.Either<TWallet, WalletError>;
8
+ };
9
+ export type DefaultSerializationConfiguration = {
10
+ networkId: NetworkId.NetworkId;
11
+ };
12
+ export declare const makeDefaultV1SerializationCapability: () => SerializationCapability<CoreWallet, string>;
@@ -0,0 +1,64 @@
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 { Either, pipe, Schema } from 'effect';
14
+ import { OtherWalletError } from './WalletError.js';
15
+ import { CoreWallet } from './CoreWallet.js';
16
+ import { ProtocolVersion } from '@midnightntwrk/wallet-sdk-abstractions';
17
+ import { UnshieldedState } from './UnshieldedState.js';
18
+ export const makeDefaultV1SerializationCapability = () => {
19
+ const UtxoWithMetaSchema = Schema.Struct({
20
+ utxo: Schema.Struct({
21
+ value: Schema.BigInt,
22
+ owner: Schema.String,
23
+ type: Schema.String,
24
+ intentHash: Schema.String,
25
+ outputNo: Schema.Number,
26
+ }),
27
+ meta: Schema.Struct({
28
+ ctime: Schema.Date,
29
+ registeredForDustGeneration: Schema.Boolean,
30
+ }),
31
+ });
32
+ const SnapshotSchema = Schema.Struct({
33
+ publicKey: Schema.Struct({
34
+ publicKey: Schema.String,
35
+ addressHex: Schema.String,
36
+ address: Schema.String,
37
+ }),
38
+ state: Schema.Struct({
39
+ availableUtxos: Schema.Array(UtxoWithMetaSchema),
40
+ pendingUtxos: Schema.Array(UtxoWithMetaSchema),
41
+ }),
42
+ protocolVersion: Schema.BigInt,
43
+ appliedId: Schema.optional(Schema.BigInt),
44
+ networkId: Schema.String,
45
+ });
46
+ return {
47
+ serialize: (wallet) => {
48
+ const buildSnapshot = (w) => ({
49
+ publicKey: w.publicKey,
50
+ state: UnshieldedState.toArrays(w.state),
51
+ protocolVersion: w.protocolVersion,
52
+ networkId: w.networkId,
53
+ appliedId: w.progress?.appliedId,
54
+ });
55
+ return pipe(wallet, buildSnapshot, Schema.encodeSync(SnapshotSchema), JSON.stringify);
56
+ },
57
+ deserialize: (serialized) => pipe(serialized, Schema.decodeUnknownEither(Schema.parseJson(SnapshotSchema)), Either.mapLeft((err) => new OtherWalletError(err)), Either.map((snapshot) => {
58
+ return CoreWallet.restore(UnshieldedState.restore(snapshot.state.availableUtxos, snapshot.state.pendingUtxos), snapshot.publicKey, {
59
+ highestTransactionId: snapshot.appliedId ?? 0n,
60
+ appliedId: snapshot.appliedId ?? 0n,
61
+ }, ProtocolVersion.ProtocolVersion(snapshot.protocolVersion), snapshot.networkId);
62
+ })),
63
+ };
64
+ };
@@ -0,0 +1,46 @@
1
+ import { type Scope, Stream, Either } from 'effect';
2
+ import { CoreWallet } from './CoreWallet.js';
3
+ import { type Simulator, type SimulatorState } from '@midnightntwrk/wallet-sdk-capabilities/simulation';
4
+ import { type WalletError } from './WalletError.js';
5
+ import { type TransactionHistoryService } from './TransactionHistory.js';
6
+ import { type WalletSyncUpdate } from './SyncSchema.js';
7
+ export interface SyncService<TState, TUpdate> {
8
+ updates: (state: TState) => Stream.Stream<TUpdate, WalletError, Scope.Scope>;
9
+ }
10
+ export interface SyncCapability<TState, TUpdate> {
11
+ applyUpdate: (state: TState, update: TUpdate) => Either.Either<TState, WalletError>;
12
+ }
13
+ export type IndexerClientConnection = {
14
+ indexerHttpUrl: string;
15
+ indexerWsUrl?: string;
16
+ keepAlive?: number;
17
+ };
18
+ export type DefaultSyncConfiguration = {
19
+ indexerClientConnection: IndexerClientConnection;
20
+ };
21
+ export type DefaultSyncContext = {
22
+ transactionHistoryService: TransactionHistoryService;
23
+ };
24
+ export declare const makeDefaultSyncService: (config: DefaultSyncConfiguration) => SyncService<CoreWallet, WalletSyncUpdate>;
25
+ export declare const makeDefaultSyncCapability: (_config: DefaultSyncConfiguration, getContext: () => DefaultSyncContext) => SyncCapability<CoreWallet, WalletSyncUpdate>;
26
+ export type SimulatorSyncConfiguration = {
27
+ simulator: Simulator;
28
+ };
29
+ export type SimulatorSyncUpdate = {
30
+ update: SimulatorState;
31
+ };
32
+ export declare const makeSimulatorSyncService: (config: SimulatorSyncConfiguration) => SyncService<CoreWallet, SimulatorSyncUpdate>;
33
+ /**
34
+ * Creates a sync capability that extracts UTXOs from the simulator's ledger state and applies them to the wallet.
35
+ *
36
+ * This capability:
37
+ *
38
+ * 1. Extracts all UTXOs for the wallet's address from the simulator ledger
39
+ * 2. Compares with the wallet's current UTXOs to determine created/spent
40
+ * 3. Applies the update to the wallet state
41
+ *
42
+ * Note: The `registeredForDustGeneration` flag is set based on whether the address appears in the ledger's dust
43
+ * delegation table. This is a heuristic that may not perfectly match the indexer's behavior but provides reasonable
44
+ * accuracy.
45
+ */
46
+ export declare const makeSimulatorSyncCapability: () => SyncCapability<CoreWallet, SimulatorSyncUpdate>;
@@ -0,0 +1,137 @@
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, Stream, Schema, pipe, Either, HashMap } from 'effect';
14
+ import { CoreWallet } from './CoreWallet.js';
15
+ import { UtxoWithMeta } from './UnshieldedState.js';
16
+ import { getCurrentBlockNumber, } from '@midnightntwrk/wallet-sdk-capabilities/simulation';
17
+ import { UnshieldedTransactions } from '@midnightntwrk/wallet-sdk-indexer-client';
18
+ import { WsSubscriptionClient, ConnectionHelper } from '@midnightntwrk/wallet-sdk-indexer-client/effect';
19
+ import { SyncWalletError } from './WalletError.js';
20
+ import { WsURL } from '@midnightntwrk/wallet-sdk-utilities/networking';
21
+ import { EitherOps } from '@midnightntwrk/wallet-sdk-utilities';
22
+ import { WalletSyncUpdateSchema } from './SyncSchema.js';
23
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
24
+ export const makeDefaultSyncService = (config) => {
25
+ return {
26
+ updates: (state) => {
27
+ const { indexerClientConnection } = config;
28
+ const webSocketUrlResult = ConnectionHelper.createWebSocketUrl(indexerClientConnection.indexerHttpUrl, indexerClientConnection.indexerWsUrl);
29
+ if (Either.isLeft(webSocketUrlResult)) {
30
+ return Stream.fail(new SyncWalletError(new Error(`Could not derive WebSocket URL from indexer HTTP URL: ${webSocketUrlResult.left.message}`)));
31
+ }
32
+ const indexerWsUrlResult = WsURL.make(webSocketUrlResult.right);
33
+ if (Either.isLeft(indexerWsUrlResult)) {
34
+ return Stream.fail(new SyncWalletError(new Error(`Invalid indexer WS URL: ${indexerWsUrlResult.left.message}`)));
35
+ }
36
+ const indexerWsUrl = indexerWsUrlResult.right;
37
+ const { appliedId } = state.progress;
38
+ const { address } = state.publicKey;
39
+ return pipe(UnshieldedTransactions.run({ address, transactionId: Number(appliedId) }), Stream.provideLayer(WsSubscriptionClient.layer({ url: indexerWsUrl, keepAlive: indexerClientConnection.keepAlive })), Stream.mapError((error) => new SyncWalletError(error)), Stream.mapEffect((subscription) => {
40
+ const { unshieldedTransactions } = subscription;
41
+ return pipe(Schema.decodeUnknownEither(WalletSyncUpdateSchema)(unshieldedTransactions), Either.mapLeft((err) => new SyncWalletError(err)), EitherOps.toEffect);
42
+ }));
43
+ },
44
+ };
45
+ };
46
+ export const makeDefaultSyncCapability = (_config, getContext) => {
47
+ return {
48
+ applyUpdate: (state, update) => {
49
+ if (update.type === 'UnshieldedTransactionsProgress') {
50
+ return Either.right(CoreWallet.updateProgress(state, {
51
+ highestTransactionId: BigInt(update.highestTransactionId),
52
+ isConnected: true,
53
+ }));
54
+ }
55
+ else {
56
+ const updatePayload = {
57
+ createdUtxos: update.createdUtxos,
58
+ spentUtxos: update.spentUtxos,
59
+ status: update.status,
60
+ };
61
+ const stateAfterApplyingUpdate = update.status === 'FAILURE'
62
+ ? CoreWallet.applyFailedUpdate(state, updatePayload)
63
+ : CoreWallet.applyUpdate(state, updatePayload);
64
+ return stateAfterApplyingUpdate.pipe(Either.map((wallet) => {
65
+ const stateAfterUpdatingProgress = CoreWallet.updateProgress(wallet, {
66
+ appliedId: BigInt(update.transaction.id),
67
+ });
68
+ const { transactionHistoryService } = getContext();
69
+ Effect.runFork(transactionHistoryService.put(update));
70
+ return stateAfterUpdatingProgress;
71
+ }));
72
+ }
73
+ },
74
+ };
75
+ };
76
+ export const makeSimulatorSyncService = (config) => {
77
+ return {
78
+ updates: (_state) => {
79
+ // Get the initial state immediately to ensure we process existing blocks.
80
+ // Then subscribe to state$ for subsequent changes.
81
+ return pipe(Stream.fromEffect(config.simulator.getLatestState()), Stream.concat(config.simulator.state$), Stream.map((state) => ({ update: state })));
82
+ },
83
+ };
84
+ };
85
+ /**
86
+ * Creates a sync capability that extracts UTXOs from the simulator's ledger state and applies them to the wallet.
87
+ *
88
+ * This capability:
89
+ *
90
+ * 1. Extracts all UTXOs for the wallet's address from the simulator ledger
91
+ * 2. Compares with the wallet's current UTXOs to determine created/spent
92
+ * 3. Applies the update to the wallet state
93
+ *
94
+ * Note: The `registeredForDustGeneration` flag is set based on whether the address appears in the ledger's dust
95
+ * delegation table. This is a heuristic that may not perfectly match the indexer's behavior but provides reasonable
96
+ * accuracy.
97
+ */
98
+ export const makeSimulatorSyncCapability = () => {
99
+ const utxoKey = (utxo) => `${utxo.intentHash}#${utxo.outputNo}`;
100
+ return {
101
+ applyUpdate: (state, update) => {
102
+ const { ledger: ledgerState, currentTime } = update.update;
103
+ const walletAddress = state.publicKey.addressHex;
104
+ const nativeTokenType = ledger.nativeToken().raw;
105
+ // Heuristic: check if address appears in the ledger's dust delegation table
106
+ const isAddressRegisteredForDust = ledgerState.dust.toString().includes(walletAddress);
107
+ // Build a Map of simulator UTXOs keyed by intent hash + output number
108
+ const simulatorUtxoMap = new Map(Array.from(ledgerState.utxo.filter(walletAddress)).map((utxo) => [
109
+ utxoKey(utxo),
110
+ new UtxoWithMeta({
111
+ utxo,
112
+ meta: {
113
+ ctime: currentTime,
114
+ registeredForDustGeneration: utxo.type === nativeTokenType && isAddressRegisteredForDust,
115
+ },
116
+ }),
117
+ ]));
118
+ // Created: in simulator but not in wallet (neither available nor pending)
119
+ const createdUtxos = Array.from(simulatorUtxoMap)
120
+ .filter(([hash]) => !HashMap.has(state.state.availableUtxos, hash) && !HashMap.has(state.state.pendingUtxos, hash))
121
+ .map(([, utxo]) => utxo);
122
+ // Spent: in wallet (pending or available) but no longer in simulator
123
+ const spentUtxos = [
124
+ ...Array.from(HashMap.entries(state.state.pendingUtxos)),
125
+ ...Array.from(HashMap.entries(state.state.availableUtxos)),
126
+ ]
127
+ .filter(([hash]) => !simulatorUtxoMap.has(hash))
128
+ .map(([, utxo]) => utxo);
129
+ const blockNumber = getCurrentBlockNumber(update.update);
130
+ const updateProgress = (wallet) => CoreWallet.updateProgress(wallet, { appliedId: blockNumber, isConnected: true });
131
+ if (createdUtxos.length === 0 && spentUtxos.length === 0) {
132
+ return Either.right(updateProgress(state));
133
+ }
134
+ return pipe(CoreWallet.applyUpdate(state, { createdUtxos, spentUtxos, status: 'SUCCESS' }), Either.map(updateProgress));
135
+ },
136
+ };
137
+ };
@@ -0,0 +1,18 @@
1
+ export interface SyncProgressData {
2
+ readonly appliedId: bigint;
3
+ readonly highestTransactionId: bigint;
4
+ readonly isConnected: boolean;
5
+ }
6
+ export interface SyncProgressOps {
7
+ isCompleteWithin(data: SyncProgressData, maxGap?: bigint): boolean;
8
+ }
9
+ export interface SyncProgress extends SyncProgressData {
10
+ isStrictlyComplete(): boolean;
11
+ isCompleteWithin(maxGap?: bigint): boolean;
12
+ }
13
+ export declare const SyncProgress: SyncProgressOps;
14
+ export declare const createSyncProgress: (params?: {
15
+ appliedId?: bigint;
16
+ highestTransactionId?: bigint;
17
+ isConnected?: boolean;
18
+ }) => SyncProgress;
@@ -0,0 +1,23 @@
1
+ export const SyncProgress = {
2
+ isCompleteWithin(data, maxGap = 50n) {
3
+ const applyLag = BigInt(Math.abs(Number(data.highestTransactionId - data.appliedId)));
4
+ return data.isConnected && applyLag <= maxGap;
5
+ },
6
+ };
7
+ export const createSyncProgress = (params = {}) => {
8
+ const { appliedId = 0n, highestTransactionId = 0n, isConnected = false } = params;
9
+ const data = {
10
+ appliedId,
11
+ highestTransactionId,
12
+ isConnected,
13
+ };
14
+ return {
15
+ ...data,
16
+ isStrictlyComplete() {
17
+ return SyncProgress.isCompleteWithin(this, 0n);
18
+ },
19
+ isCompleteWithin(maxGap) {
20
+ return SyncProgress.isCompleteWithin(this, maxGap);
21
+ },
22
+ };
23
+ };