@midnight-ntwrk/wallet-sdk-shielded 1.0.0-beta.10
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/dist/ShieldedWallet.d.ts +70 -0
- package/dist/ShieldedWallet.js +136 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +13 -0
- package/dist/v1/CoinsAndBalances.d.ts +24 -0
- package/dist/v1/CoinsAndBalances.js +46 -0
- package/dist/v1/CoreWallet.d.ts +49 -0
- package/dist/v1/CoreWallet.js +152 -0
- package/dist/v1/Keys.d.ts +8 -0
- package/dist/v1/Keys.js +16 -0
- package/dist/v1/Proving.d.ts +12 -0
- package/dist/v1/Proving.js +53 -0
- package/dist/v1/ProvingRecipe.d.ts +18 -0
- package/dist/v1/ProvingRecipe.js +3 -0
- package/dist/v1/RunningV1Variant.d.ts +46 -0
- package/dist/v1/RunningV1Variant.js +95 -0
- package/dist/v1/Serialization.d.ts +12 -0
- package/dist/v1/Serialization.js +103 -0
- package/dist/v1/Simulator.d.ts +22 -0
- package/dist/v1/Simulator.js +102 -0
- package/dist/v1/Submission.d.ts +31 -0
- package/dist/v1/Submission.js +69 -0
- package/dist/v1/Sync.d.ts +51 -0
- package/dist/v1/Sync.js +139 -0
- package/dist/v1/SyncProgress.d.ts +22 -0
- package/dist/v1/SyncProgress.js +25 -0
- package/dist/v1/Transacting.d.ts +69 -0
- package/dist/v1/Transacting.js +374 -0
- package/dist/v1/Transaction.d.ts +17 -0
- package/dist/v1/Transaction.js +76 -0
- package/dist/v1/TransactionHistory.d.ts +16 -0
- package/dist/v1/TransactionHistory.js +55 -0
- package/dist/v1/TransactionImbalances.d.ts +10 -0
- package/dist/v1/TransactionImbalances.js +30 -0
- package/dist/v1/V1Builder.d.ts +109 -0
- package/dist/v1/V1Builder.js +201 -0
- package/dist/v1/WalletError.d.ts +75 -0
- package/dist/v1/WalletError.js +59 -0
- package/dist/v1/index.d.ts +16 -0
- package/dist/v1/index.js +28 -0
- package/package.json +77 -0
package/dist/v1/Sync.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) 2025 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 '@midnight-ntwrk/ledger-v6';
|
|
14
|
+
import { Effect, ParseResult, Stream, Schema, pipe, Either } from 'effect';
|
|
15
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
16
|
+
import { ZswapEvents } from '@midnight-ntwrk/wallet-sdk-indexer-client';
|
|
17
|
+
import { WsSubscriptionClient, ConnectionHelper } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
|
|
18
|
+
import { SyncWalletError } from './WalletError.js';
|
|
19
|
+
import { WsURL } from '@midnight-ntwrk/wallet-sdk-utilities/networking';
|
|
20
|
+
import { EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
21
|
+
const Uint8ArraySchema = Schema.declare((input) => input instanceof Uint8Array).annotations({
|
|
22
|
+
identifier: 'Uint8Array',
|
|
23
|
+
});
|
|
24
|
+
export const SecretKeysResource = {
|
|
25
|
+
create: (secretKeys) => {
|
|
26
|
+
/**
|
|
27
|
+
* TODO: future Ledger version will include `clear` function to clear the secret keys,
|
|
28
|
+
* it is intentend to be used here instead of `null`
|
|
29
|
+
*/
|
|
30
|
+
let sk = secretKeys;
|
|
31
|
+
return (cb) => {
|
|
32
|
+
if (sk === null) {
|
|
33
|
+
throw new Error('Secret keys have been consumed');
|
|
34
|
+
}
|
|
35
|
+
const result = cb(sk);
|
|
36
|
+
sk = null;
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
export const WalletSyncUpdate = {
|
|
42
|
+
create: (update, secretKeys) => {
|
|
43
|
+
return {
|
|
44
|
+
update,
|
|
45
|
+
secretKeys: SecretKeysResource.create(secretKeys),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const LedgerEventSchema = Schema.declare((input) => input instanceof ledger.Event).annotations({
|
|
50
|
+
identifier: 'ledger.Event',
|
|
51
|
+
});
|
|
52
|
+
const LedgerEventFromUint8Array = Schema.transformOrFail(Uint8ArraySchema, LedgerEventSchema, {
|
|
53
|
+
encode: (event) => Effect.try({
|
|
54
|
+
try: () => event.serialize(),
|
|
55
|
+
catch: (error) => new ParseResult.Unexpected(error, 'Could not serialize ledger event'),
|
|
56
|
+
}),
|
|
57
|
+
decode: (bytes) => Effect.try({
|
|
58
|
+
try: () => ledger.Event.deserialize(bytes),
|
|
59
|
+
catch: (error) => new ParseResult.Unexpected(error, 'Could not deserialize ledger event'),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
const HexedLedgerEvent = pipe(Schema.Uint8ArrayFromHex, Schema.compose(LedgerEventFromUint8Array));
|
|
63
|
+
const EventsSyncUpdatePayload = Schema.Struct({
|
|
64
|
+
id: Schema.Number,
|
|
65
|
+
raw: Schema.String,
|
|
66
|
+
maxId: Schema.Number,
|
|
67
|
+
});
|
|
68
|
+
export const EventsSyncUpdate = Schema.TaggedStruct('EventsSyncUpdate', {
|
|
69
|
+
id: Schema.Number,
|
|
70
|
+
maxId: Schema.Number,
|
|
71
|
+
event: LedgerEventSchema,
|
|
72
|
+
});
|
|
73
|
+
const EventsSyncUpdateFromPayload = Schema.transformOrFail(EventsSyncUpdatePayload, EventsSyncUpdate, {
|
|
74
|
+
decode: (input) => pipe(Schema.decodeUnknownEither(HexedLedgerEvent)(input.raw), Either.map((event) => ({
|
|
75
|
+
_tag: 'EventsSyncUpdate',
|
|
76
|
+
id: input.id,
|
|
77
|
+
maxId: input.maxId,
|
|
78
|
+
event,
|
|
79
|
+
})), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to decode ledger event payload')), EitherOps.toEffect),
|
|
80
|
+
encode: (output) => pipe(Schema.encodeEither(HexedLedgerEvent)(output.event), Either.map((raw) => ({
|
|
81
|
+
id: output.id,
|
|
82
|
+
raw,
|
|
83
|
+
maxId: output.maxId,
|
|
84
|
+
})), Either.mapLeft((error) => new ParseResult.Unexpected(error, 'Failed to encode ledger event payload')), EitherOps.toEffect),
|
|
85
|
+
});
|
|
86
|
+
export const makeEventsSyncService = (config) => {
|
|
87
|
+
return {
|
|
88
|
+
updates: (state, secretKeys) => {
|
|
89
|
+
const { indexerClientConnection } = config;
|
|
90
|
+
const webSocketUrlResult = ConnectionHelper.createWebSocketUrl(indexerClientConnection.indexerHttpUrl, indexerClientConnection.indexerWsUrl);
|
|
91
|
+
if (Either.isLeft(webSocketUrlResult)) {
|
|
92
|
+
return Stream.fail(new SyncWalletError(new Error(`Could not derive WebSocket URL from indexer HTTP URL: ${webSocketUrlResult.left.message}`)));
|
|
93
|
+
}
|
|
94
|
+
const indexerWsUrlResult = WsURL.make(webSocketUrlResult.right);
|
|
95
|
+
if (Either.isLeft(indexerWsUrlResult)) {
|
|
96
|
+
return Stream.fail(new SyncWalletError(new Error(`Invalid indexer WS URL: ${indexerWsUrlResult.left.message}`)));
|
|
97
|
+
}
|
|
98
|
+
const indexerWsUrl = indexerWsUrlResult.right;
|
|
99
|
+
const appliedIndex = state.progress?.appliedIndex ?? 0n;
|
|
100
|
+
return pipe(ZswapEvents.run({ id: Number(appliedIndex) }), Stream.provideLayer(WsSubscriptionClient.layer({ url: indexerWsUrl })), Stream.mapError((error) => new SyncWalletError(error)), Stream.mapEffect((subscription) => pipe(Schema.decodeUnknownEither(EventsSyncUpdateFromPayload)(subscription.zswapLedgerEvents), Either.mapLeft((err) => new SyncWalletError(err)), EitherOps.toEffect)), Stream.map((data) => WalletSyncUpdate.create(data, secretKeys)));
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
export const makeEventsSyncCapability = () => {
|
|
105
|
+
return {
|
|
106
|
+
applyUpdate: (state, wrappedUpdate) => {
|
|
107
|
+
const nextIndex = BigInt(wrappedUpdate.update.id);
|
|
108
|
+
const highestRelevantWalletIndex = BigInt(wrappedUpdate.update.maxId);
|
|
109
|
+
// in case the nextIndex is less than or equal to the appliedIndex
|
|
110
|
+
// just update highestRelevantWalletIndex
|
|
111
|
+
if (nextIndex <= state.progress.appliedIndex) {
|
|
112
|
+
return CoreWallet.updateProgress(state, {
|
|
113
|
+
highestRelevantWalletIndex,
|
|
114
|
+
isConnected: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return wrappedUpdate.secretKeys((keys) => {
|
|
118
|
+
return CoreWallet.updateProgress(CoreWallet.replayEvents(state, keys, [wrappedUpdate.update.event]), {
|
|
119
|
+
highestRelevantWalletIndex,
|
|
120
|
+
appliedIndex: nextIndex,
|
|
121
|
+
isConnected: true,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export const makeSimulatorSyncService = (config) => {
|
|
128
|
+
return {
|
|
129
|
+
updates: (_state, secretKeys) => config.simulator.state$.pipe(Stream.map((state) => ({ update: state, secretKeys: secretKeys }))),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
export const makeSimulatorSyncCapability = () => {
|
|
133
|
+
return {
|
|
134
|
+
applyUpdate: (state, update) => {
|
|
135
|
+
const { update: { lastTxResult: { events }, }, secretKeys, } = update;
|
|
136
|
+
return CoreWallet.replayEvents(state, secretKeys, events);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SyncProgressData {
|
|
2
|
+
readonly appliedIndex: bigint;
|
|
3
|
+
readonly highestRelevantWalletIndex: bigint;
|
|
4
|
+
readonly highestIndex: bigint;
|
|
5
|
+
readonly highestRelevantIndex: bigint;
|
|
6
|
+
readonly isConnected: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncProgressOps {
|
|
9
|
+
isCompleteWithin(data: SyncProgressData, maxGap?: bigint): boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SyncProgress extends SyncProgressData {
|
|
12
|
+
isStrictlyComplete(): boolean;
|
|
13
|
+
isCompleteWithin(maxGap?: bigint): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const SyncProgress: SyncProgressOps;
|
|
16
|
+
export declare const createSyncProgress: (params?: {
|
|
17
|
+
appliedIndex?: bigint;
|
|
18
|
+
highestRelevantWalletIndex?: bigint;
|
|
19
|
+
highestIndex?: bigint;
|
|
20
|
+
highestRelevantIndex?: bigint;
|
|
21
|
+
isConnected?: boolean;
|
|
22
|
+
}) => SyncProgress;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const SyncProgress = {
|
|
2
|
+
isCompleteWithin(data, maxGap = 50n) {
|
|
3
|
+
const applyLag = BigInt(Math.abs(Number(data.highestRelevantWalletIndex - data.appliedIndex)));
|
|
4
|
+
return data.isConnected && applyLag <= maxGap;
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
export const createSyncProgress = (params = {}) => {
|
|
8
|
+
const { appliedIndex = 0n, highestRelevantWalletIndex = 0n, highestIndex = 0n, highestRelevantIndex = 0n, isConnected = false, } = params;
|
|
9
|
+
const data = {
|
|
10
|
+
appliedIndex,
|
|
11
|
+
highestRelevantWalletIndex,
|
|
12
|
+
highestIndex,
|
|
13
|
+
highestRelevantIndex,
|
|
14
|
+
isConnected,
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
...data,
|
|
18
|
+
isStrictlyComplete() {
|
|
19
|
+
return SyncProgress.isCompleteWithin(this, 0n);
|
|
20
|
+
},
|
|
21
|
+
isCompleteWithin(maxGap) {
|
|
22
|
+
return SyncProgress.isCompleteWithin(this, maxGap);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as ledger from '@midnight-ntwrk/ledger-v6';
|
|
2
|
+
import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
3
|
+
import { Array as Arr, Either } from 'effect';
|
|
4
|
+
import { ProvingRecipe } from './ProvingRecipe.js';
|
|
5
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
6
|
+
import { WalletError } from './WalletError.js';
|
|
7
|
+
import { CoinSelection } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
8
|
+
import { TransactionImbalances } from './TransactionImbalances.js';
|
|
9
|
+
import { TransactionTrait } from './Transaction.js';
|
|
10
|
+
import { CoinsAndBalancesCapability } from './CoinsAndBalances.js';
|
|
11
|
+
import { KeysCapability } from './Keys.js';
|
|
12
|
+
export interface TokenTransfer {
|
|
13
|
+
readonly amount: bigint;
|
|
14
|
+
readonly type: ledger.RawTokenType;
|
|
15
|
+
readonly receiverAddress: string;
|
|
16
|
+
}
|
|
17
|
+
export interface TransactingCapability<TSecrets, TState, TTransaction> {
|
|
18
|
+
balanceTransaction(secrets: TSecrets, state: TState, tx: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Either.Either<{
|
|
19
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
20
|
+
newState: TState;
|
|
21
|
+
}, WalletError>;
|
|
22
|
+
makeTransfer(secrets: TSecrets, state: TState, outputs: ReadonlyArray<TokenTransfer>): Either.Either<{
|
|
23
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
24
|
+
newState: TState;
|
|
25
|
+
}, WalletError>;
|
|
26
|
+
initSwap(secrets: TSecrets, state: TState, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>): Either.Either<{
|
|
27
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
28
|
+
newState: TState;
|
|
29
|
+
}, WalletError>;
|
|
30
|
+
revert(state: TState, tx: TTransaction): Either.Either<TState, WalletError>;
|
|
31
|
+
revertRecipe(state: TState, recipe: ProvingRecipe<TTransaction>): Either.Either<TState, WalletError>;
|
|
32
|
+
}
|
|
33
|
+
export type DefaultTransactingConfiguration = {
|
|
34
|
+
networkId: NetworkId.NetworkId;
|
|
35
|
+
};
|
|
36
|
+
export type DefaultTransactingContext = {
|
|
37
|
+
coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>;
|
|
38
|
+
coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
|
|
39
|
+
keysCapability: KeysCapability<CoreWallet>;
|
|
40
|
+
};
|
|
41
|
+
export declare const makeDefaultTransactingCapability: (config: DefaultTransactingConfiguration, getContext: () => DefaultTransactingContext) => TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, ledger.FinalizedTransaction>;
|
|
42
|
+
export declare const makeSimulatorTransactingCapability: (config: DefaultTransactingConfiguration, getContext: () => DefaultTransactingContext) => TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, ledger.ProofErasedTransaction>;
|
|
43
|
+
export declare class TransactingCapabilityImplementation<TTransaction extends ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>> implements TransactingCapability<ledger.ZswapSecretKeys, CoreWallet, TTransaction> {
|
|
44
|
+
#private;
|
|
45
|
+
readonly networkId: NetworkId.NetworkId;
|
|
46
|
+
readonly getCoinSelection: () => CoinSelection<ledger.QualifiedShieldedCoinInfo>;
|
|
47
|
+
readonly txTrait: TransactionTrait<TTransaction>;
|
|
48
|
+
readonly getCoins: () => CoinsAndBalancesCapability<CoreWallet>;
|
|
49
|
+
readonly getKeys: () => KeysCapability<CoreWallet>;
|
|
50
|
+
constructor(networkId: NetworkId.NetworkId, getCoinSelection: () => CoinSelection<ledger.QualifiedShieldedCoinInfo>, getCoins: () => CoinsAndBalancesCapability<CoreWallet>, getKeys: () => KeysCapability<CoreWallet>, txTrait: TransactionTrait<TTransaction>);
|
|
51
|
+
balanceTransaction(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, tx: TTransaction): Either.Either<{
|
|
52
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
53
|
+
newState: CoreWallet;
|
|
54
|
+
}, WalletError>;
|
|
55
|
+
makeTransfer(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, transfers: Arr.NonEmptyReadonlyArray<TokenTransfer>): Either.Either<{
|
|
56
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
57
|
+
newState: CoreWallet;
|
|
58
|
+
}, WalletError>;
|
|
59
|
+
initSwap(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>): Either.Either<{
|
|
60
|
+
recipe: ProvingRecipe<TTransaction>;
|
|
61
|
+
newState: CoreWallet;
|
|
62
|
+
}, WalletError>;
|
|
63
|
+
revert(state: CoreWallet, tx: TTransaction): Either.Either<CoreWallet, WalletError>;
|
|
64
|
+
revertRecipe(state: CoreWallet, recipe: ProvingRecipe<TTransaction>): Either.Either<CoreWallet, WalletError>;
|
|
65
|
+
balanceFallibleSection(secretKeys: ledger.ZswapSecretKeys, state: CoreWallet, imbalances: TransactionImbalances, coinSelection: CoinSelection<ledger.QualifiedShieldedCoinInfo>): Either.Either<{
|
|
66
|
+
offer: ledger.ZswapOffer<ledger.PreProof> | undefined;
|
|
67
|
+
newState: CoreWallet;
|
|
68
|
+
}, WalletError>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) 2025 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 '@midnight-ntwrk/ledger-v6';
|
|
14
|
+
import { Array as Arr, Data, Either, Option, pipe, Record } from 'effect';
|
|
15
|
+
import { ArrayOps, EitherOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
16
|
+
import { BALANCE_TRANSACTION_TO_PROVE, NOTHING_TO_PROVE, TRANSACTION_TO_PROVE, } from './ProvingRecipe.js';
|
|
17
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
18
|
+
import { AddressError, InsufficientFundsError, OtherWalletError } from './WalletError.js';
|
|
19
|
+
import { getBalanceRecipe, Imbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
20
|
+
import { MidnightBech32m, ShieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
|
|
21
|
+
import { ShieldedCostModel, TransactionImbalances } from './TransactionImbalances.js';
|
|
22
|
+
import { TransactionTrait } from './Transaction.js';
|
|
23
|
+
export const makeDefaultTransactingCapability = (config, getContext) => {
|
|
24
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionTrait.default);
|
|
25
|
+
};
|
|
26
|
+
export const makeSimulatorTransactingCapability = (config, getContext) => {
|
|
27
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionTrait.proofErased);
|
|
28
|
+
};
|
|
29
|
+
class NoSelfOutputsError extends Data.TaggedError('NoSelfOutputs') {
|
|
30
|
+
}
|
|
31
|
+
export class TransactingCapabilityImplementation {
|
|
32
|
+
networkId;
|
|
33
|
+
getCoinSelection;
|
|
34
|
+
txTrait;
|
|
35
|
+
getCoins;
|
|
36
|
+
getKeys;
|
|
37
|
+
constructor(networkId, getCoinSelection, getCoins, getKeys, txTrait) {
|
|
38
|
+
this.getCoins = getCoins;
|
|
39
|
+
this.networkId = networkId;
|
|
40
|
+
this.getCoinSelection = getCoinSelection;
|
|
41
|
+
this.getKeys = getKeys;
|
|
42
|
+
this.txTrait = txTrait;
|
|
43
|
+
}
|
|
44
|
+
balanceTransaction(secretKeys, state, tx) {
|
|
45
|
+
return Either.gen(this, function* () {
|
|
46
|
+
const coinSelection = this.getCoinSelection();
|
|
47
|
+
const networkId = this.networkId;
|
|
48
|
+
const initialImbalances = this.txTrait.getImbalances(tx);
|
|
49
|
+
if (TransactionImbalances.areBalanced(initialImbalances)) {
|
|
50
|
+
return {
|
|
51
|
+
recipe: {
|
|
52
|
+
type: NOTHING_TO_PROVE,
|
|
53
|
+
transaction: tx,
|
|
54
|
+
},
|
|
55
|
+
newState: state,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const { newState: afterFallible, offer: maybeFallible } = yield* this.balanceFallibleSection(secretKeys, state, initialImbalances, coinSelection);
|
|
59
|
+
const { newState: afterGuaranteed, offer: guaranteed } = yield* this.#balanceGuaranteedSection(secretKeys, afterFallible, initialImbalances, coinSelection, 0, Imbalances.empty());
|
|
60
|
+
return {
|
|
61
|
+
newState: afterGuaranteed,
|
|
62
|
+
recipe: {
|
|
63
|
+
type: BALANCE_TRANSACTION_TO_PROVE,
|
|
64
|
+
transactionToBalance: tx,
|
|
65
|
+
transactionToProve: ledger.Transaction.fromParts(networkId, guaranteed, maybeFallible),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
makeTransfer(secretKeys, state, transfers) {
|
|
71
|
+
return Either.gen(this, function* () {
|
|
72
|
+
const positiveTransfers = yield* pipe(transfers, Arr.filter((t) => t.amount > 0n), Arr.match({
|
|
73
|
+
onEmpty: () => Either.left(new OtherWalletError({
|
|
74
|
+
message: 'The amount needs to be positive',
|
|
75
|
+
})),
|
|
76
|
+
onNonEmpty: (nonEmpty) => Either.right(nonEmpty),
|
|
77
|
+
}));
|
|
78
|
+
const networkId = this.networkId;
|
|
79
|
+
const { initialOffersAndCoins, selfCoins } = yield* this.#processDesiredOutputs(state, positiveTransfers);
|
|
80
|
+
const offerToBalance = pipe(initialOffersAndCoins, Arr.map((o) => o.outputOffer), ArrayOps.fold((a, b) => a.merge(b)));
|
|
81
|
+
const unprovenTxToBalance = ledger.Transaction.fromParts(networkId, offerToBalance);
|
|
82
|
+
const imbalances = TransactionTrait.unproven.getImbalances(unprovenTxToBalance);
|
|
83
|
+
const { offer, newState } = yield* this.#balanceGuaranteedSection(secretKeys, state, imbalances, this.getCoinSelection(), selfCoins.length, Imbalances.empty());
|
|
84
|
+
const finalState = CoreWallet.watchCoins(newState, secretKeys, selfCoins);
|
|
85
|
+
const finalTx = unprovenTxToBalance.merge(ledger.Transaction.fromParts(networkId, offer));
|
|
86
|
+
return {
|
|
87
|
+
newState: finalState,
|
|
88
|
+
recipe: {
|
|
89
|
+
type: TRANSACTION_TO_PROVE,
|
|
90
|
+
transaction: finalTx,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
initSwap(secretKeys, state, desiredInputs, desiredOutputs) {
|
|
96
|
+
return Either.gen(this, function* () {
|
|
97
|
+
const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
|
|
98
|
+
if (!outputsValid) {
|
|
99
|
+
return yield* Either.left(new OtherWalletError({
|
|
100
|
+
message: 'The amount needs to be positive',
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
|
|
104
|
+
if (!inputsValid) {
|
|
105
|
+
return yield* Either.left(new OtherWalletError({
|
|
106
|
+
message: 'The input amounts need to be positive',
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
const outputsParseResult = yield* this.#processDesiredOutputsPossiblyEmpty(state, desiredOutputs);
|
|
110
|
+
const inputsParseResult = Imbalances.fromEntries(Record.toEntries(desiredInputs));
|
|
111
|
+
const networkId = this.networkId;
|
|
112
|
+
const { offer, newState } = yield* this.#balanceGuaranteedSection(secretKeys, state, TransactionImbalances.empty(), this.getCoinSelection(), outputsParseResult.selfCoins.length, inputsParseResult);
|
|
113
|
+
const finalState = CoreWallet.watchCoins(newState, secretKeys, outputsParseResult.selfCoins);
|
|
114
|
+
const balancingTx = ledger.Transaction.fromParts(networkId, offer);
|
|
115
|
+
const finalTx = outputsParseResult.unprovenTxToBalance
|
|
116
|
+
? outputsParseResult.unprovenTxToBalance.merge(balancingTx)
|
|
117
|
+
: balancingTx;
|
|
118
|
+
return {
|
|
119
|
+
newState: finalState,
|
|
120
|
+
recipe: {
|
|
121
|
+
type: TRANSACTION_TO_PROVE,
|
|
122
|
+
transaction: finalTx,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
revert(state, tx) {
|
|
128
|
+
return Either.try({
|
|
129
|
+
try: () => {
|
|
130
|
+
return CoreWallet.revertTransaction(state, tx);
|
|
131
|
+
},
|
|
132
|
+
catch: (err) => {
|
|
133
|
+
return new OtherWalletError({
|
|
134
|
+
message: `Error while reverting transaction ${this.txTrait.id(tx)}`,
|
|
135
|
+
cause: err,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
revertRecipe(state, recipe) {
|
|
141
|
+
const doRevert = (tx) => {
|
|
142
|
+
return Either.try({
|
|
143
|
+
try: () => {
|
|
144
|
+
return CoreWallet.revertTransaction(state, tx);
|
|
145
|
+
},
|
|
146
|
+
catch: (err) => {
|
|
147
|
+
return new OtherWalletError({
|
|
148
|
+
message: `Error while reverting transaction ${TransactionTrait.unproven.id(tx)}`,
|
|
149
|
+
cause: err,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
switch (recipe.type) {
|
|
155
|
+
case TRANSACTION_TO_PROVE:
|
|
156
|
+
return doRevert(recipe.transaction);
|
|
157
|
+
case BALANCE_TRANSACTION_TO_PROVE:
|
|
158
|
+
return doRevert(recipe.transactionToProve);
|
|
159
|
+
case NOTHING_TO_PROVE:
|
|
160
|
+
return Either.right(state);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
#prepareOffer(secretKeys, state, recipe, segment) {
|
|
164
|
+
const [inputOffers, stateAfterSpends] = CoreWallet.spendCoins(state, secretKeys, recipe.inputs, segment);
|
|
165
|
+
const stateAfterWatches = CoreWallet.watchCoins(stateAfterSpends, secretKeys, recipe.outputs);
|
|
166
|
+
const outputOffers = recipe.outputs.map((coin) => {
|
|
167
|
+
const output = ledger.ZswapOutput.new(coin, segment, this.getKeys().getCoinPublicKey(state).toHexString(), this.getKeys().getEncryptionPublicKey(state).toHexString());
|
|
168
|
+
return ledger.ZswapOffer.fromOutput(output, coin.type, coin.value);
|
|
169
|
+
});
|
|
170
|
+
return pipe(Arr.appendAll(inputOffers, outputOffers), Arr.match({
|
|
171
|
+
onEmpty: () => Option.none(),
|
|
172
|
+
onNonEmpty: (nonEmpty) => pipe(nonEmpty, ArrayOps.fold((a, b) => a.merge(b)), Option.some),
|
|
173
|
+
}), Option.map((offer) => ({ offer, newState: stateAfterWatches })));
|
|
174
|
+
}
|
|
175
|
+
balanceFallibleSection(secretKeys, state, imbalances, coinSelection) {
|
|
176
|
+
return Either.try({
|
|
177
|
+
try: () => {
|
|
178
|
+
const fallibleBalanceRecipe = getBalanceRecipe({
|
|
179
|
+
coins: this.getCoins()
|
|
180
|
+
.getAvailableCoins(state)
|
|
181
|
+
.map((c) => c.coin),
|
|
182
|
+
initialImbalances: imbalances.fallible,
|
|
183
|
+
transactionCostModel: ShieldedCostModel,
|
|
184
|
+
feeTokenType: '',
|
|
185
|
+
coinSelection,
|
|
186
|
+
createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
|
|
187
|
+
isCoinEqual: (a, b) => a.type === b.type && a.value === b.value,
|
|
188
|
+
});
|
|
189
|
+
return pipe(this.#prepareOffer(secretKeys, state, fallibleBalanceRecipe, 1), Option.match({
|
|
190
|
+
onNone: () => ({
|
|
191
|
+
newState: state,
|
|
192
|
+
offer: undefined,
|
|
193
|
+
}),
|
|
194
|
+
onSome: (res) => res,
|
|
195
|
+
}));
|
|
196
|
+
},
|
|
197
|
+
catch: (err) => {
|
|
198
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
199
|
+
return new InsufficientFundsError({
|
|
200
|
+
message: 'Insufficient funds',
|
|
201
|
+
tokenType: err.tokenType,
|
|
202
|
+
amount: imbalances.fallible.get(err.tokenType) ?? 0n,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
return new OtherWalletError({
|
|
207
|
+
message: 'Balancing fallible section failed',
|
|
208
|
+
cause: err,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
#balanceGuaranteedSection(secretKeys, state, imbalances, coinSelection, knownSelfOutputs, targetImbalances) {
|
|
215
|
+
return Either.gen(this, function* () {
|
|
216
|
+
const balanceRecipe = yield* Either.try({
|
|
217
|
+
try: () => getBalanceRecipe({
|
|
218
|
+
coins: this.getCoins()
|
|
219
|
+
.getAvailableCoins(state)
|
|
220
|
+
.map((c) => c.coin),
|
|
221
|
+
initialImbalances: imbalances.guaranteed,
|
|
222
|
+
transactionCostModel: ShieldedCostModel,
|
|
223
|
+
feeTokenType: '',
|
|
224
|
+
coinSelection,
|
|
225
|
+
createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
|
|
226
|
+
isCoinEqual: (a, b) => a.nonce === b.nonce,
|
|
227
|
+
targetImbalances,
|
|
228
|
+
}),
|
|
229
|
+
catch: (err) => {
|
|
230
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
231
|
+
return new InsufficientFundsError({
|
|
232
|
+
message: 'Insufficient funds',
|
|
233
|
+
tokenType: err.tokenType,
|
|
234
|
+
amount: imbalances.guaranteed.get(err.tokenType) ?? 0n,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return new OtherWalletError({
|
|
239
|
+
message: 'Balancing guaranteed section failed',
|
|
240
|
+
cause: err,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
if (balanceRecipe.outputs.length + knownSelfOutputs == 0) {
|
|
246
|
+
return yield* Either.left(new NoSelfOutputsError({}));
|
|
247
|
+
}
|
|
248
|
+
return yield* pipe(this.#prepareOffer(secretKeys, state, balanceRecipe, 0), Either.fromOption(() => {
|
|
249
|
+
return new OtherWalletError({
|
|
250
|
+
message: 'Could not create a valid guaranteed offer',
|
|
251
|
+
});
|
|
252
|
+
}));
|
|
253
|
+
}).pipe(EitherOps.flatMapLeft((err) => {
|
|
254
|
+
if (err instanceof NoSelfOutputsError) {
|
|
255
|
+
return this.#balanceGuaranteedWithSelfOutput(secretKeys, state, imbalances, coinSelection, targetImbalances);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
return Either.left(err);
|
|
259
|
+
}
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
#balanceGuaranteedWithSelfOutput(secretKeys, state, imbalances, coinSelection, targetImbalances) {
|
|
263
|
+
return Either.gen(this, function* () {
|
|
264
|
+
const additionalOutputValue = 100000n;
|
|
265
|
+
const balanceRecipe = yield* Either.try({
|
|
266
|
+
try: () => {
|
|
267
|
+
return getBalanceRecipe({
|
|
268
|
+
coins: this.getCoins()
|
|
269
|
+
.getAvailableCoins(state)
|
|
270
|
+
.map((c) => c.coin),
|
|
271
|
+
initialImbalances: Imbalances.merge(imbalances.guaranteed, Imbalances.fromEntry(ledger.shieldedToken().raw, -1n * additionalOutputValue)),
|
|
272
|
+
transactionCostModel: ShieldedCostModel,
|
|
273
|
+
feeTokenType: '',
|
|
274
|
+
coinSelection,
|
|
275
|
+
targetImbalances,
|
|
276
|
+
createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
|
|
277
|
+
isCoinEqual: (a, b) => a.nonce === b.nonce,
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
catch: (err) => {
|
|
281
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
282
|
+
return new InsufficientFundsError({
|
|
283
|
+
message: 'Insufficient funds',
|
|
284
|
+
tokenType: err.tokenType,
|
|
285
|
+
amount: imbalances.guaranteed.get(err.tokenType) ?? 0n,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
return new OtherWalletError({
|
|
290
|
+
message: 'Balancing guaranteed section failed',
|
|
291
|
+
cause: err,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const additionalCoin = ledger.createShieldedCoinInfo(ledger.shieldedToken().raw, additionalOutputValue);
|
|
297
|
+
const additionalOffer = pipe(additionalCoin, (coin) => ledger.ZswapOutput.new(coin, 0, this.getKeys().getCoinPublicKey(state).toHexString(), this.getKeys().getEncryptionPublicKey(state).toHexString()), (output) => ledger.ZswapOffer.fromOutput(output, ledger.shieldedToken().raw, additionalOutputValue));
|
|
298
|
+
return yield* pipe(this.#prepareOffer(secretKeys, state, balanceRecipe, 0), Option.map(({ newState, offer }) => {
|
|
299
|
+
return {
|
|
300
|
+
newState: CoreWallet.watchCoins(newState, secretKeys, [additionalCoin]),
|
|
301
|
+
offer: offer.merge(additionalOffer),
|
|
302
|
+
};
|
|
303
|
+
}), Either.fromOption(() => {
|
|
304
|
+
return new OtherWalletError({
|
|
305
|
+
message: 'Balancing guaranteed section failed',
|
|
306
|
+
});
|
|
307
|
+
}));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
#parseAddress(addr) {
|
|
311
|
+
return Either.try({
|
|
312
|
+
try: () => {
|
|
313
|
+
const repr = MidnightBech32m.parse(addr);
|
|
314
|
+
return ShieldedAddress.codec.decode(this.networkId, repr);
|
|
315
|
+
},
|
|
316
|
+
catch: (err) => {
|
|
317
|
+
return new AddressError({
|
|
318
|
+
message: `Address parsing error: ${addr}`,
|
|
319
|
+
originalAddress: addr,
|
|
320
|
+
cause: err,
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
#processDesiredOutputs(state, transfers) {
|
|
326
|
+
return Either.gen(this, function* () {
|
|
327
|
+
const initialOffersAndCoins = yield* pipe(transfers, Arr.map((transfer) => {
|
|
328
|
+
return pipe(this.#parseAddress(transfer.receiverAddress), Either.map((address) => {
|
|
329
|
+
const coin = ledger.createShieldedCoinInfo(transfer.type, transfer.amount);
|
|
330
|
+
const output = ledger.ZswapOutput.new(coin, 0, address.coinPublicKey.toHexString(), address.encryptionPublicKey.toHexString());
|
|
331
|
+
const outputOffer = ledger.ZswapOffer.fromOutput(output, transfer.type, transfer.amount);
|
|
332
|
+
return {
|
|
333
|
+
coin,
|
|
334
|
+
outputOffer,
|
|
335
|
+
isForSelf: address.coinPublicKey.equals(this.getKeys().getCoinPublicKey(state)),
|
|
336
|
+
};
|
|
337
|
+
}));
|
|
338
|
+
}), Either.all);
|
|
339
|
+
const selfCoins = Arr.flatMap(initialOffersAndCoins, ({ coin, isForSelf }) => {
|
|
340
|
+
if (isForSelf) {
|
|
341
|
+
return [coin];
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return { initialOffersAndCoins, selfCoins };
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
#processDesiredOutputsPossiblyEmpty(state, desiredOutputs) {
|
|
351
|
+
return pipe(desiredOutputs, Arr.match({
|
|
352
|
+
onEmpty: () => {
|
|
353
|
+
return Either.right({
|
|
354
|
+
imbalances: TransactionImbalances.empty(),
|
|
355
|
+
selfCoins: [],
|
|
356
|
+
unprovenTxToBalance: null,
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
onNonEmpty: (desiredOutputs) => {
|
|
360
|
+
return pipe(this.#processDesiredOutputs(state, desiredOutputs), Either.map(({ initialOffersAndCoins, selfCoins }) => {
|
|
361
|
+
const networkId = this.networkId;
|
|
362
|
+
const offerToBalance = pipe(initialOffersAndCoins, Arr.map((o) => o.outputOffer), ArrayOps.fold((a, b) => a.merge(b)));
|
|
363
|
+
const unprovenTxToBalance = ledger.Transaction.fromParts(networkId, offerToBalance);
|
|
364
|
+
const imbalances = TransactionTrait.unproven.getImbalances(unprovenTxToBalance);
|
|
365
|
+
return {
|
|
366
|
+
imbalances,
|
|
367
|
+
selfCoins,
|
|
368
|
+
unprovenTxToBalance,
|
|
369
|
+
};
|
|
370
|
+
}));
|
|
371
|
+
},
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
2
|
+
import { TransactionImbalances } from './TransactionImbalances.js';
|
|
3
|
+
import * as ledger from '@midnight-ntwrk/ledger-v6';
|
|
4
|
+
export type TransactionTrait<Tx> = {
|
|
5
|
+
getImbalances(tx: Tx): TransactionImbalances;
|
|
6
|
+
id(tx: Tx): string;
|
|
7
|
+
};
|
|
8
|
+
export declare const TransactionTrait: {
|
|
9
|
+
default: TransactionTrait<ledger.FinalizedTransaction>;
|
|
10
|
+
proofErased: TransactionTrait<ledger.ProofErasedTransaction>;
|
|
11
|
+
unproven: TransactionTrait<ledger.UnprovenTransaction>;
|
|
12
|
+
shared: {
|
|
13
|
+
getImbalances(tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction): TransactionImbalances;
|
|
14
|
+
getGuaranteedImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => Imbalances;
|
|
15
|
+
getFallibleImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => Imbalances;
|
|
16
|
+
};
|
|
17
|
+
};
|