@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.
- package/README.md +123 -0
- package/dist/KeyStore.d.ts +19 -0
- package/dist/KeyStore.js +37 -0
- package/dist/UnshieldedWallet.d.ts +72 -0
- package/dist/UnshieldedWallet.js +156 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +15 -0
- package/dist/v1/CoinsAndBalances.d.ts +13 -0
- package/dist/v1/CoinsAndBalances.js +36 -0
- package/dist/v1/CoreWallet.d.ts +24 -0
- package/dist/v1/CoreWallet.js +60 -0
- package/dist/v1/Keys.d.ts +8 -0
- package/dist/v1/Keys.js +23 -0
- package/dist/v1/RunningV1Variant.d.ts +48 -0
- package/dist/v1/RunningV1Variant.js +114 -0
- package/dist/v1/Serialization.d.ts +12 -0
- package/dist/v1/Serialization.js +64 -0
- package/dist/v1/Sync.d.ts +46 -0
- package/dist/v1/Sync.js +137 -0
- package/dist/v1/SyncProgress.d.ts +18 -0
- package/dist/v1/SyncProgress.js +23 -0
- package/dist/v1/SyncSchema.d.ts +447 -0
- package/dist/v1/SyncSchema.js +114 -0
- package/dist/v1/Transacting.d.ts +125 -0
- package/dist/v1/Transacting.js +385 -0
- package/dist/v1/TransactionHistory.d.ts +54 -0
- package/dist/v1/TransactionHistory.js +66 -0
- package/dist/v1/TransactionOps.d.ts +19 -0
- package/dist/v1/TransactionOps.js +98 -0
- package/dist/v1/UnshieldedState.d.ts +37 -0
- package/dist/v1/UnshieldedState.js +67 -0
- package/dist/v1/V1Builder.d.ts +91 -0
- package/dist/v1/V1Builder.js +149 -0
- package/dist/v1/WalletError.d.ts +94 -0
- package/dist/v1/WalletError.js +35 -0
- package/dist/v1/index.d.ts +14 -0
- package/dist/v1/index.js +26 -0
- package/package.json +57 -0
|
@@ -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>;
|
package/dist/v1/Sync.js
ADDED
|
@@ -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
|
+
};
|