@midnightntwrk/wallet-sdk-facade 4.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 ADDED
@@ -0,0 +1,138 @@
1
+ # @midnightntwrk/wallet-sdk-facade
2
+
3
+ > **Note:** It is recommended to use the [`@midnightntwrk/wallet-sdk`](../wallet-sdk/README.md) barrel package, which
4
+ > re-exports this and all other wallet SDK packages through a single dependency.
5
+
6
+ Unified facade for the Midnight Wallet SDK that combines all wallet types into a single API.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @midnightntwrk/wallet-sdk-facade
12
+ ```
13
+
14
+ ## Overview
15
+
16
+ The Wallet Facade provides a high-level unified interface that aggregates the functionality of all wallet types
17
+ (shielded, unshielded, and dust). It simplifies wallet operations by providing:
18
+
19
+ - Combined state management across all wallet types
20
+ - Unified transaction balancing for shielded, unshielded, and dust
21
+ - Coordinated transfer and swap operations
22
+ - Simplified transaction finalization flow
23
+ - Dust registration management
24
+
25
+ ## Usage
26
+
27
+ More detailed and complete examples can be found at [docs snippets](../docs-snippets/src/snippets) (always up-to-date
28
+ with the recent changes) or at the
29
+ [SDK documentation site](https://docs.midnight.network/sdks/official/wallet-developer-guide) (aligned with the recent
30
+ release)
31
+
32
+ ### Initializing the Facade
33
+
34
+ ```typescript
35
+ import { WalletFacade } from '@midnightntwrk/wallet-sdk-facade';
36
+
37
+ const facade = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
38
+
39
+ // Start all wallets
40
+ await facade.start(shieldedSecretKeys, dustSecretKey);
41
+ ```
42
+
43
+ ### Observing Combined State
44
+
45
+ ```typescript
46
+ facade.state().subscribe((state) => {
47
+ console.log('Shielded:', state.shielded);
48
+ console.log('Unshielded:', state.unshielded);
49
+ console.log('Dust:', state.dust);
50
+ console.log('All synced:', state.isSynced);
51
+ });
52
+
53
+ // Or wait for full sync
54
+ const syncedState = await facade.waitForSyncedState();
55
+ ```
56
+
57
+ ### Creating Transfer Transactions
58
+
59
+ ```typescript
60
+ const recipe = await facade.transferTransaction(
61
+ [
62
+ {
63
+ type: 'shielded',
64
+ outputs: [{ type: 'TOKEN_B', receiverAddress: shieldedAddr, amount: 1000n }],
65
+ },
66
+ {
67
+ type: 'unshielded',
68
+ outputs: [{ type: 'TOKEN_A', receiverAddress: unshieldedAddr, amount: 500n }],
69
+ },
70
+ ],
71
+ { shieldedSecretKeys, dustSecretKey },
72
+ { ttl: new Date(Date.now() + 3600000) },
73
+ );
74
+ ```
75
+
76
+ ### Balancing Transactions
77
+
78
+ ```typescript
79
+ // Balance a finalized transaction
80
+ const recipe = await facade.balanceFinalizedTransaction(
81
+ finalizedTx,
82
+ { shieldedSecretKeys, dustSecretKey },
83
+ { ttl, tokenKindsToBalance: 'all' }, // or ['shielded', 'dust']
84
+ );
85
+
86
+ // Finalize the balanced recipe
87
+ const finalTx = await facade.finalizeRecipe(recipe);
88
+
89
+ // Submit to the network
90
+ const txId = await facade.submitTransaction(finalTx);
91
+ ```
92
+
93
+ ### Creating Swap Offers
94
+
95
+ ```typescript
96
+ const swapRecipe = await facade.initSwap(
97
+ { shielded: { NIGHT: 1000n } }, // inputs
98
+ [{ type: 'shielded', outputs: [{ type: 'TOKEN_A', receiverAddress, amount: 100n }] }], // outputs
99
+ { shieldedSecretKeys, dustSecretKey },
100
+ { ttl, payFees: false },
101
+ );
102
+ ```
103
+
104
+ ### Dust Registration
105
+
106
+ ```typescript
107
+ // Register Night UTXOs for dust generation
108
+ const registrationRecipe = await facade.registerNightUtxosForDustGeneration(
109
+ nightUtxos,
110
+ nightVerifyingKey,
111
+ signDustRegistration,
112
+ );
113
+
114
+ // Estimate registration costs
115
+ const { fee, dustGenerationEstimations } = await facade.estimateRegistration(nightUtxos);
116
+ ```
117
+
118
+ ## Types
119
+
120
+ ### BalancingRecipe
121
+
122
+ The facade returns different recipe types depending on the input transaction:
123
+
124
+ - `FinalizedTransactionRecipe` - For finalized transactions
125
+ - `UnboundTransactionRecipe` - For unbound transactions
126
+ - `UnprovenTransactionRecipe` - For unproven transactions
127
+
128
+ ### TokenKindsToBalance
129
+
130
+ Control which token types to balance:
131
+
132
+ ```typescript
133
+ type TokenKindsToBalance = 'all' | ('dust' | 'shielded' | 'unshielded')[];
134
+ ```
135
+
136
+ ## License
137
+
138
+ Apache-2.0
@@ -0,0 +1,333 @@
1
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
2
+ import { type DefaultSubmissionConfiguration, type SubmissionService } from '@midnightntwrk/wallet-sdk-capabilities';
3
+ import { type DefaultProvingConfiguration, type ProvingService, type UnboundTransaction } from '@midnightntwrk/wallet-sdk-capabilities/proving';
4
+ import { type DefaultDustConfiguration, type DustWalletAPI, type DustWalletState } from '@midnightntwrk/wallet-sdk-dust-wallet';
5
+ import { type AnyTransaction, type CoinsAndBalances as DustCoinsAndBalances } from '@midnightntwrk/wallet-sdk-dust-wallet/v1';
6
+ import { type DefaultShieldedConfiguration, type ShieldedWalletAPI, type ShieldedWalletState } from '@midnightntwrk/wallet-sdk-shielded';
7
+ import type { DefaultUnshieldedConfiguration, UnshieldedWalletAPI } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
8
+ import { type UnshieldedWalletState } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
9
+ import { Clock } from '@midnightntwrk/wallet-sdk-utilities';
10
+ import { Schema } from 'effect';
11
+ import { TransactionHistoryStorage } from '@midnightntwrk/wallet-sdk-abstractions';
12
+ import { type Observable } from 'rxjs';
13
+ import { type DefaultPendingTransactionsServiceConfiguration, PendingTransactions, type PendingTransactionsService } from '@midnightntwrk/wallet-sdk-capabilities';
14
+ import { type DustAddress, type ShieldedAddress, type UnshieldedAddress } from '@midnightntwrk/wallet-sdk-address-format';
15
+ /**
16
+ * Full entry schema for transaction history — common fields + all wallet sections. Pass this to
17
+ * `InMemoryTransactionHistoryStorage` to enable serialize/restore.
18
+ */
19
+ export declare const WalletEntrySchema: Schema.Struct<{
20
+ shielded: Schema.optional<Schema.Struct<{
21
+ receivedCoins: Schema.Array$<Schema.Struct<{
22
+ type: typeof Schema.String;
23
+ nonce: typeof Schema.String;
24
+ value: typeof Schema.BigInt;
25
+ mtIndex: typeof Schema.BigInt;
26
+ }>>;
27
+ spentCoins: Schema.Array$<Schema.Struct<{
28
+ type: typeof Schema.String;
29
+ nonce: typeof Schema.String;
30
+ value: typeof Schema.BigInt;
31
+ mtIndex: typeof Schema.BigInt;
32
+ }>>;
33
+ }>>;
34
+ unshielded: Schema.optional<Schema.Struct<{
35
+ id: typeof Schema.Number;
36
+ createdUtxos: Schema.Array$<Schema.Struct<{
37
+ value: Schema.Schema<bigint, string, never>;
38
+ owner: typeof Schema.String;
39
+ tokenType: typeof Schema.String;
40
+ intentHash: typeof Schema.String;
41
+ outputIndex: typeof Schema.Number;
42
+ }>>;
43
+ spentUtxos: Schema.Array$<Schema.Struct<{
44
+ value: Schema.Schema<bigint, string, never>;
45
+ owner: typeof Schema.String;
46
+ tokenType: typeof Schema.String;
47
+ intentHash: typeof Schema.String;
48
+ outputIndex: typeof Schema.Number;
49
+ }>>;
50
+ }>>;
51
+ dust: Schema.optional<Schema.Struct<{
52
+ receivedUtxos: Schema.Array$<Schema.Struct<{
53
+ initialValue: typeof Schema.BigInt;
54
+ nonce: typeof Schema.BigInt;
55
+ seq: typeof Schema.Number;
56
+ backingNight: typeof Schema.String;
57
+ mtIndex: typeof Schema.BigInt;
58
+ }>>;
59
+ spentUtxos: Schema.Array$<Schema.Struct<{
60
+ initialValue: typeof Schema.BigInt;
61
+ nonce: typeof Schema.BigInt;
62
+ seq: typeof Schema.Number;
63
+ backingNight: typeof Schema.String;
64
+ mtIndex: typeof Schema.BigInt;
65
+ }>>;
66
+ }>>;
67
+ hash: typeof Schema.String;
68
+ protocolVersion: typeof Schema.Number;
69
+ status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
70
+ identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
71
+ timestamp: Schema.optional<typeof Schema.Date>;
72
+ fees: Schema.optional<Schema.NullOr<typeof Schema.BigInt>>;
73
+ }>;
74
+ export type WalletEntry = Schema.Schema.Type<typeof WalletEntrySchema>;
75
+ export declare const mergeWalletEntries: (existing: WalletEntry, incoming: WalletEntry) => WalletEntry;
76
+ type TokenKind = 'dust' | 'shielded' | 'unshielded';
77
+ type TokenKindsToBalance = 'all' | TokenKind[];
78
+ declare const TokenKindsToBalance: {
79
+ allTokenKinds: string[];
80
+ toFlags: (tokenKinds: TokenKindsToBalance) => {
81
+ shouldBalanceUnshielded: boolean;
82
+ shouldBalanceShielded: boolean;
83
+ shouldBalanceDust: boolean;
84
+ };
85
+ };
86
+ export type FinalizedTransactionRecipe = {
87
+ type: 'FINALIZED_TRANSACTION';
88
+ originalTransaction: ledger.FinalizedTransaction;
89
+ balancingTransaction: ledger.UnprovenTransaction;
90
+ };
91
+ export type UnboundTransactionRecipe = {
92
+ type: 'UNBOUND_TRANSACTION';
93
+ baseTransaction: UnboundTransaction;
94
+ balancingTransaction?: ledger.UnprovenTransaction | undefined;
95
+ };
96
+ export type UnprovenTransactionRecipe = {
97
+ type: 'UNPROVEN_TRANSACTION';
98
+ transaction: ledger.UnprovenTransaction;
99
+ };
100
+ export type BalancingRecipe = FinalizedTransactionRecipe | UnboundTransactionRecipe | UnprovenTransactionRecipe;
101
+ export declare const BalancingRecipe: {
102
+ isRecipe: (value: unknown) => value is BalancingRecipe;
103
+ getTransactions: (recipe: BalancingRecipe) => readonly AnyTransaction[];
104
+ };
105
+ export interface TokenTransfer<AddressType extends ShieldedAddress | UnshieldedAddress> {
106
+ type: ledger.RawTokenType;
107
+ receiverAddress: AddressType;
108
+ amount: bigint;
109
+ }
110
+ export type ShieldedTokenTransfer = {
111
+ type: 'shielded';
112
+ outputs: TokenTransfer<ShieldedAddress>[];
113
+ };
114
+ export type UnshieldedTokenTransfer = {
115
+ type: 'unshielded';
116
+ outputs: TokenTransfer<UnshieldedAddress>[];
117
+ };
118
+ export type CombinedTokenTransfer = ShieldedTokenTransfer | UnshieldedTokenTransfer;
119
+ export type CombinedSwapInputs = {
120
+ shielded?: Record<ledger.RawTokenType, bigint>;
121
+ unshielded?: Record<ledger.RawTokenType, bigint>;
122
+ };
123
+ export type CombinedSwapOutputs = CombinedTokenTransfer;
124
+ export type TransactionIdentifier = string;
125
+ export type UtxoWithMeta = {
126
+ utxo: ledger.Utxo;
127
+ meta: {
128
+ ctime: Date;
129
+ registeredForDustGeneration: boolean;
130
+ };
131
+ };
132
+ export declare class FacadeState {
133
+ readonly shielded: ShieldedWalletState;
134
+ readonly unshielded: UnshieldedWalletState;
135
+ readonly dust: DustWalletState;
136
+ readonly pending: PendingTransactions.PendingTransactions<ledger.FinalizedTransaction>;
137
+ get isSynced(): boolean;
138
+ constructor(shielded: ShieldedWalletState, unshielded: UnshieldedWalletState, dust: DustWalletState, pending: PendingTransactions.PendingTransactions<ledger.FinalizedTransaction>);
139
+ }
140
+ /**
141
+ * Clock abstraction for obtaining the current time. By default, the facade uses the system clock
142
+ * ({@link Clock.systemClock}); for testing with a simulator, inject a custom clock (e.g. one backed by the simulator's
143
+ * time).
144
+ *
145
+ * Re-exported from `@midnightntwrk/wallet-sdk-utilities` as a namespace so the type is `Clock.Clock` and the default is
146
+ * `Clock.systemClock`. Forwarding the same symbol — rather than re-declaring its members individually — keeps the
147
+ * umbrella `wallet-sdk` package's star-exports unambiguous and lets lower-level packages (e.g. dust-wallet) share it
148
+ * without a circular dependency.
149
+ */
150
+ export { Clock };
151
+ /**
152
+ * The Terms and Conditions returned by the indexer, containing a URL for display and a SHA-256 hash for content
153
+ * verification.
154
+ */
155
+ export type TermsAndConditions = {
156
+ /** The hex-encoded SHA-256 hash of the Terms and Conditions document. */
157
+ hash: string;
158
+ /** The URL pointing to the Terms and Conditions document. */
159
+ url: string;
160
+ };
161
+ /**
162
+ * Minimal configuration required for {@link WalletFacade.fetchTermsAndConditions}. Accepts the shared
163
+ * `indexerClientConnection` sub-object found on all wallet configurations, so callers can pass the full wallet
164
+ * configuration directly without any adaptation.
165
+ */
166
+ export type FetchTermsAndConditionsConfiguration = {
167
+ indexerClientConnection: {
168
+ indexerHttpUrl: string;
169
+ indexerWsUrl?: string;
170
+ };
171
+ };
172
+ export type DefaultConfiguration = DefaultUnshieldedConfiguration & DefaultShieldedConfiguration & DefaultDustConfiguration & DefaultSubmissionConfiguration & DefaultPendingTransactionsServiceConfiguration & Partial<DefaultProvingConfiguration>;
173
+ type MaybePromise<T> = T | Promise<T>;
174
+ /**
175
+ * Parameters object for {@link WalletFacade.init}. It features configuration and bunch of initializers for the wallets
176
+ * and services, all of them are in a form of a function that takes the configuration and returns proper implementation,
177
+ * either synchronously or wrapped in a Promise. Services are optional to provide ({@link WalletFacade.init} will provide
178
+ * default implementations), but all 3 wallets: shielded, unshielded and Dust one need to be present
179
+ */
180
+ export type InitParams<TConfig extends DefaultConfiguration> = {
181
+ configuration: TConfig;
182
+ /** Optional factory for the clock abstraction. Defaults to system clock (`() => new Date()`). */
183
+ clock?: (config: TConfig) => MaybePromise<Clock.Clock>;
184
+ submissionService?: (config: TConfig) => MaybePromise<SubmissionService<ledger.FinalizedTransaction>>;
185
+ pendingTransactionsService?: (config: TConfig) => MaybePromise<PendingTransactionsService<ledger.FinalizedTransaction>>;
186
+ provingService?: (config: TConfig) => MaybePromise<ProvingService<UnboundTransaction>>;
187
+ shielded: (config: TConfig) => MaybePromise<ShieldedWalletAPI>;
188
+ unshielded: (config: TConfig) => MaybePromise<UnshieldedWalletAPI>;
189
+ dust: (config: TConfig) => MaybePromise<DustWalletAPI>;
190
+ };
191
+ export declare class WalletFacade {
192
+ #private;
193
+ private static makeDefaultSubmissionService;
194
+ private static makeDefaultPendingTransactionsService;
195
+ private static makeDefaultProvingService;
196
+ /**
197
+ * Fetches the current Terms and Conditions from the network indexer.
198
+ *
199
+ * This is a static, pre-initialization utility — no wallet instance is required. Wallet builders should call this
200
+ * before or independently of wallet initialization to display the current T&C to end users and obtain the hash for
201
+ * content verification.
202
+ *
203
+ * The returned `hash` is the hex-encoded SHA-256 hash of the document at `url`. Wallet builders are responsible for
204
+ * fetching and rendering the document content via `url` in whatever manner suits their application.
205
+ *
206
+ * @param configuration - An object with an `indexerClientConnection.indexerHttpUrl`. Any wallet configuration that
207
+ * satisfies {@link FetchTermsAndConditionsConfiguration} can be passed directly.
208
+ * @returns A promise resolving to the current {@link TermsAndConditions}, or rejecting if no Terms and Conditions have
209
+ * been set on the network yet.
210
+ */
211
+ static fetchTermsAndConditions(configuration: FetchTermsAndConditionsConfiguration): Promise<TermsAndConditions>;
212
+ /**
213
+ * Default initialization for {@link WalletFacade}. It is a static method, which takes an object holding configuration
214
+ * and initialization of necessary components. Specifically - it requires following fields:
215
+ *
216
+ * - `configuration` - holding a configuration, which needs to extend {@link DefaultConfiguration} - this way allows to
217
+ * convey use-case-specific settings in the same way, as the SDK works by default
218
+ * - `shielded` - a function taking the configuration and returning shielded wallet (or a promise with such)
219
+ * implementing {@link ShieldedWalletAPI}
220
+ * - `unshielded` - a function taking the configuration and returning unshielded wallet (or a promise with such)
221
+ * implementing {@link UnshieldedWalletAPI}
222
+ * - `dust` - a function taking the configuration and returning Dust wallet (or a promise with such) implementing
223
+ * {@link DustWalletAPI} There are some optional services/abstractions to provide, too. If not provided - default
224
+ * implementations will be used, each of them is initialized by a function taking the configuration and returning
225
+ * proper implementation (wrapped in a {@link Promise} or not).
226
+ * - `submissionService` - needs to implement {@link SubmissionService} for a {@link ledger.FinalizedTransaction} to
227
+ * submit transactions to the network, default uses Node RPC connection
228
+ * - `pendingTransactionsService` - needs to implement {@link PendingTransactionsService} for a
229
+ * {@link ledger.FinalizedTransaction} to keep track of pending transactions, default uses in-memory implementation
230
+ * - `provingService` - needs to implement {@link ProvingService} to prove it, default uses proving server
231
+ * - `clock` - needs to implement {@link Clock.Clock} for getting current time, default uses system clock
232
+ */
233
+ static init<TConfig extends DefaultConfiguration>(initParams: InitParams<TConfig>): Promise<WalletFacade>;
234
+ readonly shielded: ShieldedWalletAPI;
235
+ readonly unshielded: UnshieldedWalletAPI;
236
+ readonly dust: DustWalletAPI;
237
+ readonly submissionService: SubmissionService<ledger.FinalizedTransaction>;
238
+ readonly pendingTransactionsService: PendingTransactionsService<ledger.FinalizedTransaction>;
239
+ readonly provingService: ProvingService<UnboundTransaction>;
240
+ readonly clock: Clock.Clock;
241
+ /**
242
+ * Constructor is private on purpose - much of initialization of the facade is potentially asynchronous, and adding
243
+ * new parameters is a breaking change to the users Use {@link WalletFacade.init} instead
244
+ *
245
+ * @private
246
+ */
247
+ private constructor();
248
+ private defaultTtl;
249
+ private mergeUnprovenTransactions;
250
+ private createDustActionTransaction;
251
+ state(): Observable<FacadeState>;
252
+ waitForSyncedState(): Promise<FacadeState>;
253
+ submitTransaction(tx: ledger.FinalizedTransaction): Promise<TransactionIdentifier>;
254
+ balanceFinalizedTransaction(tx: ledger.FinalizedTransaction, secretKeys: {
255
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
256
+ dustSecretKey: ledger.DustSecretKey;
257
+ }, options: {
258
+ ttl: Date;
259
+ tokenKindsToBalance?: TokenKindsToBalance;
260
+ }): Promise<FinalizedTransactionRecipe>;
261
+ balanceUnboundTransaction(tx: UnboundTransaction, secretKeys: {
262
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
263
+ dustSecretKey: ledger.DustSecretKey;
264
+ }, options: {
265
+ ttl: Date;
266
+ tokenKindsToBalance?: TokenKindsToBalance;
267
+ }): Promise<UnboundTransactionRecipe>;
268
+ balanceUnprovenTransaction(tx: ledger.UnprovenTransaction, secretKeys: {
269
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
270
+ dustSecretKey: ledger.DustSecretKey;
271
+ }, options: {
272
+ ttl: Date;
273
+ tokenKindsToBalance?: TokenKindsToBalance;
274
+ }): Promise<UnprovenTransactionRecipe>;
275
+ finalizeRecipe(recipe: BalancingRecipe): Promise<ledger.FinalizedTransaction>;
276
+ signRecipe(recipe: BalancingRecipe, signSegment: (data: Uint8Array) => ledger.Signature): Promise<BalancingRecipe>;
277
+ signUnprovenTransaction(tx: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<ledger.UnprovenTransaction>;
278
+ signUnboundTransaction(tx: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<UnboundTransaction>;
279
+ finalizeTransaction(tx: ledger.UnprovenTransaction): Promise<ledger.FinalizedTransaction>;
280
+ /** Estimates the fee for the given transaction only. This lacks the fees of the balancing transaction. */
281
+ calculateTransactionFee(tx: AnyTransaction): Promise<bigint>;
282
+ /** Calculates the total fee for the given transaction plus the fee of the balancing transaction. */
283
+ estimateTransactionFee(tx: AnyTransaction, secretKey: ledger.DustSecretKey, options?: {
284
+ ttl?: Date;
285
+ currentTime?: Date;
286
+ }): Promise<bigint>;
287
+ transferTransaction(outputs: CombinedTokenTransfer[], secretKeys: {
288
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
289
+ dustSecretKey: ledger.DustSecretKey;
290
+ }, options: {
291
+ ttl: Date;
292
+ payFees?: boolean;
293
+ }): Promise<UnprovenTransactionRecipe>;
294
+ /**
295
+ * Provides estimate of the fee of issuing registration transaction with provided UTxOs
296
+ *
297
+ * @param nightUtxos - Night UTxOs to use for the registration
298
+ * @returns And object informing about fee at the moment, as well as estimation of dust generation of the UTxO(s),
299
+ * that would be used for paying the fee. These include data that allows to compute when the fee could be paid
300
+ */
301
+ estimateRegistration(nightUtxos: readonly UtxoWithMeta[]): Promise<{
302
+ fee: bigint;
303
+ dustGenerationEstimations: ReadonlyArray<DustCoinsAndBalances.UtxoWithFullDustDetails>;
304
+ }>;
305
+ initSwap(desiredInputs: CombinedSwapInputs, desiredOutputs: CombinedSwapOutputs[], secretKeys: {
306
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
307
+ dustSecretKey: ledger.DustSecretKey;
308
+ }, options: {
309
+ ttl: Date;
310
+ payFees?: boolean;
311
+ }): Promise<UnprovenTransactionRecipe>;
312
+ registerNightUtxosForDustGeneration(nightUtxos: readonly UtxoWithMeta[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => ledger.Signature, dustReceiverAddress?: DustAddress): Promise<UnprovenTransactionRecipe>;
313
+ /**
314
+ * Waits until the dust projected to be generated by the given Night UTxOs reaches `requiredAmount`, re-checking every
315
+ * second. Pair with {@link estimateRegistration} to pick `requiredAmount`, then call before
316
+ * {@link registerNightUtxosForDustGeneration} so the registration covers its own fee.
317
+ *
318
+ * @param nightUtxos - Night UTxOs to project generation for; the same set passed to the registration.
319
+ * @param requiredAmount - Dust threshold to wait for. Resolves immediately if `<= 0n`.
320
+ * @param opts.timeoutMs - Deadline, in ms, for the threshold to be reached. Rejects otherwise. Default `300_000`.
321
+ * @throws If `nightUtxos` is empty, or if `requiredAmount` is not reached within `opts.timeoutMs`.
322
+ */
323
+ waitForGeneratedDust(nightUtxos: readonly UtxoWithMeta[], requiredAmount: bigint, opts?: {
324
+ timeoutMs?: number;
325
+ }): Promise<void>;
326
+ deregisterFromDustGeneration(nightUtxos: UtxoWithMeta[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => ledger.Signature): Promise<UnprovenTransactionRecipe>;
327
+ revert(txOrRecipe: AnyTransaction | BalancingRecipe): Promise<void>;
328
+ revertTransaction(tx: AnyTransaction): Promise<void>;
329
+ start(shieldedSecretKeys: ledger.ZswapSecretKeys, dustSecretKey: ledger.DustSecretKey): Promise<void>;
330
+ stop(): Promise<void>;
331
+ queryTxHistoryByHash(hash: TransactionHistoryStorage.TransactionHash): Promise<WalletEntry | undefined>;
332
+ getAllFromTxHistory(): Promise<WalletEntry[]>;
333
+ }
package/dist/index.js ADDED
@@ -0,0 +1,681 @@
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 '@midnight-ntwrk/ledger-v8';
14
+ import { makeDefaultSubmissionService, } from '@midnightntwrk/wallet-sdk-capabilities';
15
+ import { makeDefaultProvingService, } from '@midnightntwrk/wallet-sdk-capabilities/proving';
16
+ import { ShieldedSectionSchema, mergeShieldedSections, } from '@midnightntwrk/wallet-sdk-shielded';
17
+ import { UnshieldedSectionSchema } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
18
+ import { DustSectionSchema, mergeDustSections } from '@midnightntwrk/wallet-sdk-dust-wallet';
19
+ import { Clock } from '@midnightntwrk/wallet-sdk-utilities';
20
+ import { FetchTermsAndConditions as FetchTermsAndConditionsQuery } from '@midnightntwrk/wallet-sdk-indexer-client';
21
+ import { QueryRunner } from '@midnightntwrk/wallet-sdk-indexer-client/effect';
22
+ import { Array as Arr, pipe, Schema } from 'effect';
23
+ import { TransactionHistoryStorage } from '@midnightntwrk/wallet-sdk-abstractions';
24
+ import { combineLatest, map, firstValueFrom, concatMap } from 'rxjs';
25
+ import { PendingTransactions, PendingTransactionsServiceImpl, } from '@midnightntwrk/wallet-sdk-capabilities';
26
+ import { finalizedTransactionTrait } from './transaction.js';
27
+ /**
28
+ * Full entry schema for transaction history — common fields + all wallet sections. Pass this to
29
+ * `InMemoryTransactionHistoryStorage` to enable serialize/restore.
30
+ */
31
+ export const WalletEntrySchema = Schema.Struct({
32
+ ...TransactionHistoryStorage.TransactionHistoryCommonSchema.fields,
33
+ shielded: Schema.optional(ShieldedSectionSchema),
34
+ unshielded: Schema.optional(UnshieldedSectionSchema),
35
+ dust: Schema.optional(DustSectionSchema),
36
+ });
37
+ export const mergeWalletEntries = (existing, incoming) => ({
38
+ ...existing,
39
+ ...incoming,
40
+ ...(existing.shielded !== undefined && incoming.shielded !== undefined
41
+ ? { shielded: mergeShieldedSections(existing.shielded, incoming.shielded) }
42
+ : {}),
43
+ ...(existing.unshielded !== undefined && incoming.unshielded !== undefined
44
+ ? { unshielded: { ...existing.unshielded, ...incoming.unshielded } }
45
+ : {}),
46
+ ...(existing.dust !== undefined && incoming.dust !== undefined
47
+ ? { dust: mergeDustSections(existing.dust, incoming.dust) }
48
+ : {}),
49
+ });
50
+ const isWalletEntry = Schema.is(WalletEntrySchema);
51
+ const TokenKindsToBalance = new (class {
52
+ allTokenKinds = ['shielded', 'unshielded', 'dust'];
53
+ toFlags = (tokenKinds) => {
54
+ return pipe(tokenKinds, (kinds) => (kinds === 'all' ? this.allTokenKinds : kinds), (kinds) => ({
55
+ shouldBalanceUnshielded: kinds.includes('unshielded'),
56
+ shouldBalanceShielded: kinds.includes('shielded'),
57
+ shouldBalanceDust: kinds.includes('dust'),
58
+ }));
59
+ };
60
+ })();
61
+ export const BalancingRecipe = {
62
+ isRecipe: (value) => {
63
+ return (typeof value === 'object' &&
64
+ value !== null &&
65
+ 'type' in value &&
66
+ typeof value.type === 'string' &&
67
+ ['FINALIZED_TRANSACTION', 'UNBOUND_TRANSACTION', 'UNPROVEN_TRANSACTION'].includes(value.type));
68
+ },
69
+ getTransactions: (recipe) => {
70
+ switch (recipe.type) {
71
+ case 'FINALIZED_TRANSACTION': {
72
+ return [recipe.originalTransaction, recipe.balancingTransaction];
73
+ }
74
+ case 'UNBOUND_TRANSACTION': {
75
+ const balancingPart = recipe.balancingTransaction ? [recipe.balancingTransaction] : [];
76
+ return [recipe.baseTransaction, ...balancingPart];
77
+ }
78
+ case 'UNPROVEN_TRANSACTION': {
79
+ return [recipe.transaction];
80
+ }
81
+ }
82
+ },
83
+ };
84
+ export class FacadeState {
85
+ shielded;
86
+ unshielded;
87
+ dust;
88
+ pending;
89
+ get isSynced() {
90
+ return (this.shielded.state.progress.isStrictlyComplete() &&
91
+ this.dust.state.progress.isStrictlyComplete() &&
92
+ this.unshielded.progress.isStrictlyComplete());
93
+ }
94
+ constructor(shielded, unshielded, dust, pending) {
95
+ this.shielded = shielded;
96
+ this.unshielded = unshielded;
97
+ this.dust = dust;
98
+ this.pending = pending;
99
+ }
100
+ }
101
+ const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
102
+ /**
103
+ * Clock abstraction for obtaining the current time. By default, the facade uses the system clock
104
+ * ({@link Clock.systemClock}); for testing with a simulator, inject a custom clock (e.g. one backed by the simulator's
105
+ * time).
106
+ *
107
+ * Re-exported from `@midnightntwrk/wallet-sdk-utilities` as a namespace so the type is `Clock.Clock` and the default is
108
+ * `Clock.systemClock`. Forwarding the same symbol — rather than re-declaring its members individually — keeps the
109
+ * umbrella `wallet-sdk` package's star-exports unambiguous and lets lower-level packages (e.g. dust-wallet) share it
110
+ * without a circular dependency.
111
+ */
112
+ export { Clock };
113
+ export class WalletFacade {
114
+ static makeDefaultSubmissionService(config) {
115
+ return makeDefaultSubmissionService(config);
116
+ }
117
+ static makeDefaultPendingTransactionsService(config) {
118
+ return PendingTransactionsServiceImpl.init({
119
+ configuration: config,
120
+ txTrait: finalizedTransactionTrait,
121
+ });
122
+ }
123
+ static makeDefaultProvingService(config) {
124
+ if (config.provingServerUrl) {
125
+ return makeDefaultProvingService({
126
+ provingServerUrl: config.provingServerUrl,
127
+ });
128
+ }
129
+ else {
130
+ throw new Error("Missing required configuration: 'provingServerUrl' must be set in config, or provide a custom provingService in init parameters.");
131
+ }
132
+ }
133
+ /**
134
+ * Fetches the current Terms and Conditions from the network indexer.
135
+ *
136
+ * This is a static, pre-initialization utility — no wallet instance is required. Wallet builders should call this
137
+ * before or independently of wallet initialization to display the current T&C to end users and obtain the hash for
138
+ * content verification.
139
+ *
140
+ * The returned `hash` is the hex-encoded SHA-256 hash of the document at `url`. Wallet builders are responsible for
141
+ * fetching and rendering the document content via `url` in whatever manner suits their application.
142
+ *
143
+ * @param configuration - An object with an `indexerClientConnection.indexerHttpUrl`. Any wallet configuration that
144
+ * satisfies {@link FetchTermsAndConditionsConfiguration} can be passed directly.
145
+ * @returns A promise resolving to the current {@link TermsAndConditions}, or rejecting if no Terms and Conditions have
146
+ * been set on the network yet.
147
+ */
148
+ static async fetchTermsAndConditions(configuration) {
149
+ const result = await QueryRunner.runPromise(FetchTermsAndConditionsQuery, {}, {
150
+ url: configuration.indexerClientConnection.indexerHttpUrl,
151
+ });
152
+ const tc = result.block?.systemParameters?.termsAndConditions;
153
+ if (!tc) {
154
+ throw new Error('Terms and Conditions are not currently set on the network.');
155
+ }
156
+ return tc;
157
+ }
158
+ /**
159
+ * Default initialization for {@link WalletFacade}. It is a static method, which takes an object holding configuration
160
+ * and initialization of necessary components. Specifically - it requires following fields:
161
+ *
162
+ * - `configuration` - holding a configuration, which needs to extend {@link DefaultConfiguration} - this way allows to
163
+ * convey use-case-specific settings in the same way, as the SDK works by default
164
+ * - `shielded` - a function taking the configuration and returning shielded wallet (or a promise with such)
165
+ * implementing {@link ShieldedWalletAPI}
166
+ * - `unshielded` - a function taking the configuration and returning unshielded wallet (or a promise with such)
167
+ * implementing {@link UnshieldedWalletAPI}
168
+ * - `dust` - a function taking the configuration and returning Dust wallet (or a promise with such) implementing
169
+ * {@link DustWalletAPI} There are some optional services/abstractions to provide, too. If not provided - default
170
+ * implementations will be used, each of them is initialized by a function taking the configuration and returning
171
+ * proper implementation (wrapped in a {@link Promise} or not).
172
+ * - `submissionService` - needs to implement {@link SubmissionService} for a {@link ledger.FinalizedTransaction} to
173
+ * submit transactions to the network, default uses Node RPC connection
174
+ * - `pendingTransactionsService` - needs to implement {@link PendingTransactionsService} for a
175
+ * {@link ledger.FinalizedTransaction} to keep track of pending transactions, default uses in-memory implementation
176
+ * - `provingService` - needs to implement {@link ProvingService} to prove it, default uses proving server
177
+ * - `clock` - needs to implement {@link Clock.Clock} for getting current time, default uses system clock
178
+ */
179
+ static async init(initParams) {
180
+ const submissionService = await Promise.resolve(initParams.submissionService
181
+ ? initParams.submissionService(initParams.configuration)
182
+ : WalletFacade.makeDefaultSubmissionService(initParams.configuration));
183
+ const pendingTransactionsService = await Promise.resolve(initParams.pendingTransactionsService
184
+ ? initParams.pendingTransactionsService(initParams.configuration)
185
+ : WalletFacade.makeDefaultPendingTransactionsService(initParams.configuration));
186
+ const provingService = await Promise.resolve(initParams.provingService
187
+ ? initParams.provingService(initParams.configuration)
188
+ : WalletFacade.makeDefaultProvingService(initParams.configuration));
189
+ const shielded = await Promise.resolve(initParams.shielded(initParams.configuration));
190
+ const unshielded = await Promise.resolve(initParams.unshielded(initParams.configuration));
191
+ const dust = await Promise.resolve(initParams.dust(initParams.configuration));
192
+ const clock = await Promise.resolve(initParams.clock ? initParams.clock(initParams.configuration) : Clock.systemClock);
193
+ return new WalletFacade(shielded, unshielded, dust, submissionService, pendingTransactionsService, provingService, initParams.configuration.txHistoryStorage, clock);
194
+ }
195
+ shielded;
196
+ unshielded;
197
+ dust;
198
+ submissionService;
199
+ pendingTransactionsService;
200
+ provingService;
201
+ #txHistoryStorage;
202
+ clock;
203
+ #pendingSubscription;
204
+ /**
205
+ * Constructor is private on purpose - much of initialization of the facade is potentially asynchronous, and adding
206
+ * new parameters is a breaking change to the users Use {@link WalletFacade.init} instead
207
+ *
208
+ * @private
209
+ */
210
+ constructor(shieldedWallet, unshieldedWallet, dustWallet, submissionService, pendingTransactionsService, provingService, txHistoryStorage, clock = Clock.systemClock) {
211
+ this.shielded = shieldedWallet;
212
+ this.unshielded = unshieldedWallet;
213
+ this.dust = dustWallet;
214
+ this.submissionService = submissionService;
215
+ this.pendingTransactionsService = pendingTransactionsService;
216
+ this.provingService = provingService;
217
+ this.#txHistoryStorage = txHistoryStorage;
218
+ this.clock = clock;
219
+ this.#pendingSubscription = this.pendingTransactionsService
220
+ .state()
221
+ .pipe(concatMap((pending) => PendingTransactions.allFailed(pending)), concatMap((item) => this.revert(item.tx)))
222
+ .subscribe();
223
+ }
224
+ defaultTtl() {
225
+ return new Date(this.clock.now().getTime() + DEFAULT_TTL_MS);
226
+ }
227
+ mergeUnprovenTransactions(a, b) {
228
+ if (a && b)
229
+ return a.merge(b);
230
+ return a ?? b;
231
+ }
232
+ async createDustActionTransaction(action, nightUtxos, nightVerifyingKey, signDustRegistration) {
233
+ const ttl = this.defaultTtl();
234
+ const now = this.clock.now();
235
+ const isRegistration = action.type === 'registration';
236
+ const dustReceiverAddress = isRegistration ? action.dustReceiverAddress : undefined;
237
+ // Step 1 — Dust decides which Night UTxO belongs in the guaranteed slot (the one whose dust
238
+ // generation can pay the fee) and computes the fee-payment allowance.
239
+ const split = await this.dust.splitNightUtxosForDustRegistration(now, nightUtxos.map(({ utxo, meta }) => ({
240
+ ...utxo,
241
+ ctime: meta.ctime,
242
+ registeredForDustGeneration: meta.registeredForDustGeneration,
243
+ })), isRegistration);
244
+ const toUnshieldedUtxoWithMeta = (u) => ({
245
+ utxo: {
246
+ value: u.utxo.value,
247
+ type: u.utxo.type,
248
+ owner: u.utxo.owner,
249
+ intentHash: u.utxo.intentHash,
250
+ outputNo: u.utxo.outputNo,
251
+ },
252
+ meta: {
253
+ ctime: u.utxo.ctime,
254
+ registeredForDustGeneration: u.utxo.registeredForDustGeneration,
255
+ },
256
+ });
257
+ const guaranteedForUnshielded = split.guaranteedUtxos.map(toUnshieldedUtxoWithMeta);
258
+ const fallibleForUnshielded = split.fallibleUtxos.map(toUnshieldedUtxoWithMeta);
259
+ // Step 2 — Unshielded books the Night UTxOs (move available -> pending) and builds the intent
260
+ // with the two offers. After this point, a concurrent build call that wants any of these UTxOs
261
+ // will fail fast.
262
+ const txWithOffers = await this.unshielded.rotateUtxos(guaranteedForUnshielded, fallibleForUnshielded, nightVerifyingKey, ttl);
263
+ // Step 3 — Dust attaches its DustActions onto the intent the unshielded wallet just built.
264
+ // If this fails we must unbook the UTxOs so the caller can retry.
265
+ let txWithDustActions;
266
+ try {
267
+ txWithDustActions = await this.dust.attachDustRegistration(txWithOffers, now, nightVerifyingKey, dustReceiverAddress, split.feePayment);
268
+ }
269
+ catch (error) {
270
+ await this.unshielded.revertTransaction(txWithOffers);
271
+ throw error;
272
+ }
273
+ // Step 4 (first-time registration only) — Fail fast if the dust generated so far by the
274
+ // unregistered guaranteed UTxOs is below the registration's own fee. Submitting would fail
275
+ // on-chain with BalanceCheckOverspend. Skip for re-registration (all guaranteed UTxOs already
276
+ // registered) since `feePayment` is 0 by design and the caller is expected to balance the fee
277
+ // externally via `balanceUnprovenTransaction({ tokenKindsToBalance: ['dust'] })`.
278
+ const hasUnregisteredGuaranteed = split.guaranteedUtxos.some((u) => !u.utxo.registeredForDustGeneration);
279
+ if (isRegistration && hasUnregisteredGuaranteed) {
280
+ const fee = await this.dust.calculateFee([txWithDustActions]);
281
+ if (split.feePayment < fee) {
282
+ await this.unshielded.revertTransaction(txWithOffers);
283
+ throw Error(`Insufficient generated dust to cover registration fee (have ${split.feePayment}, need ${fee}). ` +
284
+ `Use WalletFacade.waitForGeneratedDust(utxos, ${fee}) before retrying.`);
285
+ }
286
+ }
287
+ // Step 5 — Sign via the standard signRecipe pathway, which now stamps both the unshielded
288
+ // offers and the dust registration. Signing failures also need to release the booking.
289
+ try {
290
+ const signedRecipe = await this.signRecipe({ type: 'UNPROVEN_TRANSACTION', transaction: txWithDustActions }, signDustRegistration);
291
+ if (signedRecipe.type !== 'UNPROVEN_TRANSACTION') {
292
+ throw Error('signRecipe returned unexpected recipe type for dust action transaction.');
293
+ }
294
+ return signedRecipe.transaction;
295
+ }
296
+ catch (error) {
297
+ await this.unshielded.revertTransaction(txWithOffers);
298
+ throw error;
299
+ }
300
+ }
301
+ state() {
302
+ return combineLatest([
303
+ this.shielded.state,
304
+ this.unshielded.state,
305
+ this.dust.state,
306
+ this.pendingTransactionsService.state(),
307
+ ]).pipe(map(([shieldedState, unshieldedState, dustState, pending]) => new FacadeState(shieldedState, unshieldedState, dustState, pending)));
308
+ }
309
+ async waitForSyncedState() {
310
+ const [shieldedState, unshieldedState, dustState, pending] = await Promise.all([
311
+ this.shielded.waitForSyncedState(),
312
+ this.unshielded.waitForSyncedState(),
313
+ this.dust.waitForSyncedState(),
314
+ firstValueFrom(this.pendingTransactionsService.state()),
315
+ ]);
316
+ return new FacadeState(shieldedState, unshieldedState, dustState, pending);
317
+ }
318
+ async submitTransaction(tx) {
319
+ try {
320
+ await this.pendingTransactionsService.addPendingTransaction(tx);
321
+ await this.submissionService.submitTransaction(tx, 'Finalized');
322
+ return tx.identifiers().at(-1);
323
+ }
324
+ catch (error) {
325
+ await this.revert(tx);
326
+ throw error;
327
+ }
328
+ }
329
+ async balanceFinalizedTransaction(tx, secretKeys, options) {
330
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
331
+ const { ttl, tokenKindsToBalance = 'all' } = options;
332
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
333
+ // Step 1: Run unshielded and shielded balancing
334
+ const unshieldedBalancingTx = shouldBalanceUnshielded
335
+ ? await this.unshielded.balanceFinalizedTransaction(tx)
336
+ : undefined;
337
+ const shieldedBalancingTx = shouldBalanceShielded
338
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
339
+ : undefined;
340
+ // Step 2: Merge unshielded and shielded balancing
341
+ const mergedBalancingTx = this.mergeUnprovenTransactions(shieldedBalancingTx, unshieldedBalancingTx);
342
+ // Step 3: Conditionally add dust/fee balancing
343
+ const feeBalancingTx = shouldBalanceDust
344
+ ? await this.dust.balanceTransactions(dustSecretKey, mergedBalancingTx ? [tx, mergedBalancingTx] : [tx], ttl)
345
+ : undefined;
346
+ // Step 4: Merge fee balancing and create final recipe
347
+ const balancingTx = this.mergeUnprovenTransactions(mergedBalancingTx, feeBalancingTx);
348
+ if (!balancingTx) {
349
+ throw new Error('No balancing transaction was created. Please check your transaction.');
350
+ }
351
+ return {
352
+ type: 'FINALIZED_TRANSACTION',
353
+ originalTransaction: tx,
354
+ balancingTransaction: balancingTx,
355
+ };
356
+ }
357
+ async balanceUnboundTransaction(tx, secretKeys, options) {
358
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
359
+ const { ttl, tokenKindsToBalance = 'all' } = options;
360
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
361
+ // Step 1: Run unshielded and shielded balancing
362
+ const shieldedBalancingTx = shouldBalanceShielded
363
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
364
+ : undefined;
365
+ // For unbound transactions, unshielded balancing happens in place not with a balancing transaction
366
+ const balancedUnshieldedTx = shouldBalanceUnshielded
367
+ ? await this.unshielded.balanceUnboundTransaction(tx)
368
+ : undefined;
369
+ // Step 2: Unbound unshielded tx are balanced in place, use it as base tx if present
370
+ const baseTx = balancedUnshieldedTx ?? tx;
371
+ // Step 3: Conditionally add dust/fee balancing
372
+ const feeBalancingTransaction = shouldBalanceDust
373
+ ? await this.dust.balanceTransactions(dustSecretKey, shieldedBalancingTx ? [baseTx, shieldedBalancingTx] : [baseTx], ttl)
374
+ : undefined;
375
+ // Step 4: Create the final balancing transaction
376
+ const balancingTransaction = this.mergeUnprovenTransactions(shieldedBalancingTx, feeBalancingTransaction);
377
+ // if there is no balancingTransaction and there was no unshielded tx balancing (in place) throw an error.
378
+ if (!balancingTransaction && !balancedUnshieldedTx) {
379
+ throw new Error('No balancing transaction was created. Please check your transaction.');
380
+ }
381
+ return {
382
+ type: 'UNBOUND_TRANSACTION',
383
+ baseTransaction: baseTx,
384
+ balancingTransaction: balancingTransaction ?? undefined,
385
+ };
386
+ }
387
+ async balanceUnprovenTransaction(tx, secretKeys, options) {
388
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
389
+ const { ttl, tokenKindsToBalance = 'all' } = options;
390
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
391
+ // Step 1: Run unshielded and shielded balancing
392
+ const shieldedBalancingTx = shouldBalanceShielded
393
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
394
+ : undefined;
395
+ // For unproven transactions, unshielded balancing happens in place
396
+ const balancedUnshieldedTx = shouldBalanceUnshielded
397
+ ? await this.unshielded.balanceUnprovenTransaction(tx)
398
+ : undefined;
399
+ // Step 2: Use the balanced unshielded tx if present, otherwise use the original tx
400
+ const baseTx = balancedUnshieldedTx ?? tx;
401
+ // Step 3: Merge shielded balancing into base tx if present
402
+ const mergedTx = this.mergeUnprovenTransactions(baseTx, shieldedBalancingTx);
403
+ // Step 4: Conditionally add dust/fee balancing
404
+ const feeBalancingTx = shouldBalanceDust
405
+ ? await this.dust.balanceTransactions(dustSecretKey, [mergedTx], ttl)
406
+ : undefined;
407
+ // Step 5: Merge fee balancing if present
408
+ const balancedTx = this.mergeUnprovenTransactions(mergedTx, feeBalancingTx);
409
+ return {
410
+ type: 'UNPROVEN_TRANSACTION',
411
+ transaction: balancedTx,
412
+ };
413
+ }
414
+ async finalizeRecipe(recipe) {
415
+ return Promise.resolve(recipe)
416
+ .then(async (recipe) => {
417
+ switch (recipe.type) {
418
+ case 'FINALIZED_TRANSACTION': {
419
+ const finalizedBalancing = await this.finalizeTransaction(recipe.balancingTransaction);
420
+ return recipe.originalTransaction.merge(finalizedBalancing);
421
+ }
422
+ case 'UNBOUND_TRANSACTION': {
423
+ const finalizedBalancingTx = recipe.balancingTransaction
424
+ ? await this.finalizeTransaction(recipe.balancingTransaction)
425
+ : undefined;
426
+ const finalizedTransaction = recipe.baseTransaction.bind();
427
+ return finalizedBalancingTx ? finalizedTransaction.merge(finalizedBalancingTx) : finalizedTransaction;
428
+ }
429
+ case 'UNPROVEN_TRANSACTION': {
430
+ return await this.finalizeTransaction(recipe.transaction);
431
+ }
432
+ }
433
+ })
434
+ .then(async (finalizedTx) => {
435
+ await this.pendingTransactionsService.addPendingTransaction(finalizedTx);
436
+ return finalizedTx;
437
+ });
438
+ }
439
+ async signRecipe(recipe, signSegment) {
440
+ switch (recipe.type) {
441
+ case 'FINALIZED_TRANSACTION': {
442
+ const signedBalancingTx = await this.signUnprovenTransaction(recipe.balancingTransaction, signSegment);
443
+ const withDustSig = await this.#signDustRegistrationIfPresent(signedBalancingTx, signSegment);
444
+ return {
445
+ type: 'FINALIZED_TRANSACTION',
446
+ originalTransaction: recipe.originalTransaction,
447
+ balancingTransaction: withDustSig,
448
+ };
449
+ }
450
+ case 'UNBOUND_TRANSACTION': {
451
+ const signedBalancingTx = recipe.balancingTransaction
452
+ ? await this.signUnprovenTransaction(recipe.balancingTransaction, signSegment).then((tx) => this.#signDustRegistrationIfPresent(tx, signSegment))
453
+ : undefined;
454
+ const signedBaseTx = await this.signUnboundTransaction(recipe.baseTransaction, signSegment);
455
+ return {
456
+ type: 'UNBOUND_TRANSACTION',
457
+ baseTransaction: signedBaseTx,
458
+ balancingTransaction: signedBalancingTx,
459
+ };
460
+ }
461
+ case 'UNPROVEN_TRANSACTION': {
462
+ const signedTx = await this.signUnprovenTransaction(recipe.transaction, signSegment);
463
+ const withDustSig = await this.#signDustRegistrationIfPresent(signedTx, signSegment);
464
+ return {
465
+ type: 'UNPROVEN_TRANSACTION',
466
+ transaction: withDustSig,
467
+ };
468
+ }
469
+ }
470
+ }
471
+ async #signDustRegistrationIfPresent(tx, signSegment) {
472
+ const intent = tx.intents?.get(1);
473
+ const registrations = intent?.dustActions?.registrations ?? [];
474
+ if (!intent || registrations.length === 0) {
475
+ return tx;
476
+ }
477
+ const signature = signSegment(intent.signatureData(1));
478
+ return await this.dust.addDustRegistrationSignature(tx, signature);
479
+ }
480
+ async signUnprovenTransaction(tx, signSegment) {
481
+ return await this.unshielded.signUnprovenTransaction(tx, signSegment);
482
+ }
483
+ async signUnboundTransaction(tx, signSegment) {
484
+ return await this.unshielded.signUnboundTransaction(tx, signSegment);
485
+ }
486
+ async finalizeTransaction(tx) {
487
+ try {
488
+ const unboundTx = await this.provingService.prove(tx);
489
+ const finalizedTx = unboundTx.bind();
490
+ await this.pendingTransactionsService.addPendingTransaction(finalizedTx);
491
+ return finalizedTx;
492
+ }
493
+ catch (error) {
494
+ await Promise.allSettled([
495
+ this.shielded.revertTransaction(tx),
496
+ this.unshielded.revertTransaction(tx),
497
+ this.dust.revertTransaction(tx),
498
+ ]);
499
+ throw error;
500
+ }
501
+ }
502
+ /** Estimates the fee for the given transaction only. This lacks the fees of the balancing transaction. */
503
+ async calculateTransactionFee(tx) {
504
+ return await this.dust.calculateFee([tx]);
505
+ }
506
+ /** Calculates the total fee for the given transaction plus the fee of the balancing transaction. */
507
+ async estimateTransactionFee(tx, secretKey, options) {
508
+ const ttl = options?.ttl ?? this.defaultTtl();
509
+ return await this.dust.estimateFee(secretKey, [tx], ttl, options?.currentTime);
510
+ }
511
+ async transferTransaction(outputs, secretKeys, options) {
512
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
513
+ const { ttl, payFees = true } = options;
514
+ const unshieldedOutputs = outputs
515
+ .filter((output) => output.type === 'unshielded')
516
+ .flatMap((output) => output.outputs);
517
+ const shieldedOutputs = outputs.filter((output) => output.type === 'shielded').flatMap((output) => output.outputs);
518
+ if (unshieldedOutputs.length === 0 && shieldedOutputs.length === 0) {
519
+ throw Error('At least one shielded or unshielded output is required.');
520
+ }
521
+ const shieldedTx = shieldedOutputs.length > 0
522
+ ? await this.shielded.transferTransaction(shieldedSecretKeys, shieldedOutputs)
523
+ : undefined;
524
+ const unshieldedTx = unshieldedOutputs.length > 0 ? await this.unshielded.transferTransaction(unshieldedOutputs, ttl) : undefined;
525
+ const mergedTxs = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
526
+ // Add fee payment
527
+ const feeBalancingTx = payFees ? await this.dust.balanceTransactions(dustSecretKey, [mergedTxs], ttl) : undefined;
528
+ const finalTx = this.mergeUnprovenTransactions(mergedTxs, feeBalancingTx);
529
+ return {
530
+ type: 'UNPROVEN_TRANSACTION',
531
+ transaction: finalTx,
532
+ };
533
+ }
534
+ /**
535
+ * Provides estimate of the fee of issuing registration transaction with provided UTxOs
536
+ *
537
+ * @param nightUtxos - Night UTxOs to use for the registration
538
+ * @returns And object informing about fee at the moment, as well as estimation of dust generation of the UTxO(s),
539
+ * that would be used for paying the fee. These include data that allows to compute when the fee could be paid
540
+ */
541
+ async estimateRegistration(nightUtxos) {
542
+ const now = this.clock.now();
543
+ const dustState = await this.dust.waitForSyncedState();
544
+ const dustGenerationEstimations = pipe(nightUtxos, Arr.map(({ utxo, meta }) => ({
545
+ ...utxo,
546
+ ctime: meta.ctime,
547
+ registeredForDustGeneration: meta.registeredForDustGeneration,
548
+ })), (utxosWithMeta) => dustState.estimateDustGeneration(utxosWithMeta, now), (estimatedUtxos) => dustState.capabilities.coinsAndBalances.splitNightUtxos(estimatedUtxos), (split) => split.guaranteed);
549
+ const fakeSigningKey = ledger.sampleSigningKey();
550
+ const fakeVerifyingKey = ledger.signatureVerifyingKey(fakeSigningKey);
551
+ // Use the legacy dust-only construction path here so estimation does NOT book real UTxOs in the
552
+ // unshielded wallet state. (The race-fix path in createDustActionTransaction books on purpose;
553
+ // estimation is meant to be observation-only.)
554
+ const ttl = this.defaultTtl();
555
+ const fakeUnsignedTx = await this.dust.createDustGenerationTransaction(undefined, ttl, nightUtxos.map(({ utxo, meta }) => ({
556
+ ...utxo,
557
+ ctime: meta.ctime,
558
+ registeredForDustGeneration: meta.registeredForDustGeneration,
559
+ })), fakeVerifyingKey, dustState.address);
560
+ const intent = fakeUnsignedTx.intents?.get(1);
561
+ if (!intent) {
562
+ throw Error('Dust generation transaction is missing intent segment 1.');
563
+ }
564
+ const signatureData = intent.signatureData(1);
565
+ const signature = ledger.signData(fakeSigningKey, signatureData);
566
+ const fakeSignedTx = await this.dust.addDustGenerationSignature(fakeUnsignedTx, signature);
567
+ const finalizedFakeTx = fakeSignedTx.mockProve().bind();
568
+ const fee = await this.calculateTransactionFee(finalizedFakeTx);
569
+ return {
570
+ fee,
571
+ dustGenerationEstimations,
572
+ };
573
+ }
574
+ async initSwap(desiredInputs, desiredOutputs, secretKeys, options) {
575
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
576
+ const { ttl, payFees = false } = options;
577
+ const { shielded: shieldedInputs, unshielded: unshieldedInputs } = desiredInputs;
578
+ const shieldedOutputs = desiredOutputs
579
+ .filter((output) => output.type === 'shielded')
580
+ .flatMap((output) => output.outputs);
581
+ const unshieldedOutputs = desiredOutputs
582
+ .filter((output) => output.type === 'unshielded')
583
+ .flatMap((output) => output.outputs);
584
+ const hasShieldedPart = (shieldedInputs && Object.keys(shieldedInputs).length > 0) || shieldedOutputs.length > 0;
585
+ const hasUnshieldedPart = (unshieldedInputs && Object.keys(unshieldedInputs).length > 0) || unshieldedOutputs.length > 0;
586
+ if (!hasShieldedPart && !hasUnshieldedPart) {
587
+ throw Error('At least one shielded or unshielded swap is required.');
588
+ }
589
+ const shieldedTx = hasShieldedPart && shieldedInputs !== undefined
590
+ ? await this.shielded.initSwap(shieldedSecretKeys, shieldedInputs, shieldedOutputs)
591
+ : undefined;
592
+ const unshieldedTx = hasUnshieldedPart && unshieldedInputs !== undefined
593
+ ? await this.unshielded.initSwap(unshieldedInputs, unshieldedOutputs, ttl)
594
+ : undefined;
595
+ const combinedTx = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
596
+ if (!combinedTx) {
597
+ throw Error('Unexpected transaction state.');
598
+ }
599
+ const feeBalancingTx = payFees ? await this.dust.balanceTransactions(dustSecretKey, [combinedTx], ttl) : undefined;
600
+ const finalTx = this.mergeUnprovenTransactions(combinedTx, feeBalancingTx);
601
+ return {
602
+ type: 'UNPROVEN_TRANSACTION',
603
+ transaction: finalTx,
604
+ };
605
+ }
606
+ async registerNightUtxosForDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration, dustReceiverAddress) {
607
+ if (nightUtxos.length === 0) {
608
+ throw Error('At least one Night UTXO is required.');
609
+ }
610
+ const receiverAddress = dustReceiverAddress ?? (await this.dust.getAddress());
611
+ const dustRegistrationTx = await this.createDustActionTransaction({ type: 'registration', dustReceiverAddress: receiverAddress }, nightUtxos, nightVerifyingKey, signDustRegistration);
612
+ return {
613
+ type: 'UNPROVEN_TRANSACTION',
614
+ transaction: dustRegistrationTx,
615
+ };
616
+ }
617
+ /**
618
+ * Waits until the dust projected to be generated by the given Night UTxOs reaches `requiredAmount`, re-checking every
619
+ * second. Pair with {@link estimateRegistration} to pick `requiredAmount`, then call before
620
+ * {@link registerNightUtxosForDustGeneration} so the registration covers its own fee.
621
+ *
622
+ * @param nightUtxos - Night UTxOs to project generation for; the same set passed to the registration.
623
+ * @param requiredAmount - Dust threshold to wait for. Resolves immediately if `<= 0n`.
624
+ * @param opts.timeoutMs - Deadline, in ms, for the threshold to be reached. Rejects otherwise. Default `300_000`.
625
+ * @throws If `nightUtxos` is empty, or if `requiredAmount` is not reached within `opts.timeoutMs`.
626
+ */
627
+ async waitForGeneratedDust(nightUtxos, requiredAmount, opts) {
628
+ await this.dust.waitForGeneratedDust(nightUtxos.map(({ utxo, meta }) => ({
629
+ ...utxo,
630
+ ctime: meta.ctime,
631
+ registeredForDustGeneration: meta.registeredForDustGeneration,
632
+ })), requiredAmount, this.clock, opts);
633
+ }
634
+ async deregisterFromDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration) {
635
+ const dustDeregistrationTx = await this.createDustActionTransaction({ type: 'deregistration' }, nightUtxos, nightVerifyingKey, signDustRegistration);
636
+ return {
637
+ type: 'UNPROVEN_TRANSACTION',
638
+ transaction: dustDeregistrationTx,
639
+ };
640
+ }
641
+ async revert(txOrRecipe) {
642
+ // avoid instanceof check
643
+ const transactionsToRevert = BalancingRecipe.isRecipe(txOrRecipe)
644
+ ? BalancingRecipe.getTransactions(txOrRecipe)
645
+ : [txOrRecipe];
646
+ await Promise.all(transactionsToRevert.map((tx) => this.revertTransaction(tx)));
647
+ }
648
+ async revertTransaction(tx) {
649
+ await Promise.all([
650
+ this.shielded.revertTransaction(tx),
651
+ this.unshielded.revertTransaction(tx),
652
+ this.dust.revertTransaction(tx),
653
+ ]).then(() => this.pendingTransactionsService.clear(tx));
654
+ }
655
+ async start(shieldedSecretKeys, dustSecretKey) {
656
+ await Promise.all([
657
+ this.shielded.start(shieldedSecretKeys),
658
+ this.unshielded.start(),
659
+ this.dust.start(dustSecretKey),
660
+ this.pendingTransactionsService.start(),
661
+ ]);
662
+ }
663
+ async stop() {
664
+ await Promise.all([
665
+ this.shielded.stop(),
666
+ this.unshielded.stop(),
667
+ this.dust.stop(),
668
+ this.submissionService.close(),
669
+ this.pendingTransactionsService.stop(),
670
+ Promise.resolve(this.#pendingSubscription?.unsubscribe()),
671
+ ]);
672
+ }
673
+ async queryTxHistoryByHash(hash) {
674
+ const raw = await this.#txHistoryStorage.get(hash);
675
+ return raw && isWalletEntry(raw) ? raw : undefined;
676
+ }
677
+ async getAllFromTxHistory() {
678
+ const allEntries = await this.#txHistoryStorage.getAll();
679
+ return [...allEntries];
680
+ }
681
+ }
@@ -0,0 +1,3 @@
1
+ import { type PendingTransactions } from '@midnightntwrk/wallet-sdk-capabilities';
2
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
3
+ export declare const finalizedTransactionTrait: PendingTransactions.TransactionTrait<ledger.FinalizedTransaction>;
@@ -0,0 +1,60 @@
1
+ /*
2
+ * This file is part of MIDNIGHT-WALLET-SDK.
3
+ * Copyright (C) Midnight Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * You may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+ import { Array as Arr, DateTime, Duration, HashSet, Option, Order, pipe } from 'effect';
16
+ import * as ledger from '@midnight-ntwrk/ledger-v8';
17
+ export const finalizedTransactionTrait = {
18
+ areAllTxIdsIncluded(tx, txIds) {
19
+ const txIdsSet = HashSet.fromIterable(tx.identifiers());
20
+ const expectedIdSet = HashSet.fromIterable(txIds);
21
+ return HashSet.isSubset(txIdsSet, expectedIdSet);
22
+ },
23
+ deserialize(serialized) {
24
+ return ledger.Transaction.deserialize('signature', 'proof', 'binding', serialized);
25
+ },
26
+ firstId(tx) {
27
+ return tx.identifiers()[0];
28
+ },
29
+ hasTTLExpired(tx, creationTime, now) {
30
+ const defaultShieldedGracePeriod = ledger.LedgerParameters.initialParameters().dust.dustGracePeriodSeconds;
31
+ const intentTTLs = pipe(tx.intents?.values().toArray() ?? [], Arr.map((i) => i.ttl), Arr.filterMap(DateTime.make));
32
+ const hasDustPayments = pipe(tx.intents?.values().toArray() ?? [], Arr.flatMap((i) => i.dustActions?.spends ?? []), Arr.isNonEmptyArray);
33
+ const hasShieldedOffers = tx.guaranteedOffer != null || (tx.fallibleOffer?.size ?? 0) == 0;
34
+ const maybeShieldedTTL = hasDustPayments || hasShieldedOffers
35
+ ? pipe(creationTime, DateTime.addDuration(Duration.seconds(Number(defaultShieldedGracePeriod))), Arr.of)
36
+ : Arr.empty();
37
+ return pipe(intentTTLs, Arr.appendAll(maybeShieldedTTL), (arr) => Arr.isNonEmptyReadonlyArray(arr)
38
+ ? Option.some(Arr.min(arr, Order.mapInput(Order.Date, DateTime.toDate)))
39
+ : Option.none(), Option.match({
40
+ onNone: () => false,
41
+ onSome: (finalTTL) => DateTime.distance(finalTTL, now) > 0,
42
+ }));
43
+ },
44
+ ids(tx) {
45
+ return tx.identifiers();
46
+ },
47
+ isOneIncludedInOther(tx, otherTx) {
48
+ const txIds = HashSet.fromIterable(tx.identifiers());
49
+ const otherTxIds = HashSet.fromIterable(otherTx.identifiers());
50
+ const smallerSize = Order.min(Order.number)(HashSet.size(txIds), HashSet.size(otherTxIds));
51
+ const intersection = HashSet.intersection(txIds, otherTxIds);
52
+ return HashSet.size(intersection) == smallerSize;
53
+ },
54
+ isTx(tx) {
55
+ return tx instanceof ledger.Transaction;
56
+ },
57
+ serialize(tx) {
58
+ return tx.serialize();
59
+ },
60
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@midnightntwrk/wallet-sdk-facade",
3
+ "version": "4.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "author": "Midnight Foundation",
9
+ "license": "Apache-2.0",
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/",
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist/"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/midnightntwrk/midnight-wallet.git",
20
+ "directory": "packages/facade"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "@midnight-ntwrk/ledger-v8": "^8.1.0",
30
+ "@midnightntwrk/wallet-sdk-abstractions": "^2.1.0",
31
+ "@midnightntwrk/wallet-sdk-address-format": "^3.1.2",
32
+ "@midnightntwrk/wallet-sdk-capabilities": "^3.3.1",
33
+ "@midnightntwrk/wallet-sdk-dust-wallet": "^4.2.0",
34
+ "@midnightntwrk/wallet-sdk-indexer-client": "^1.2.3",
35
+ "@midnightntwrk/wallet-sdk-shielded": "^3.0.2",
36
+ "@midnightntwrk/wallet-sdk-unshielded-wallet": "^3.1.0",
37
+ "rxjs": "^7.8.2"
38
+ },
39
+ "devDependencies": {
40
+ "@midnightntwrk/wallet-sdk-hd": "^3.0.3",
41
+ "@midnightntwrk/wallet-sdk-utilities": "^1.2.0"
42
+ },
43
+ "scripts": {
44
+ "typecheck": "tsc -b ./tsconfig.json --noEmit --force",
45
+ "test": "vitest run",
46
+ "lint": "eslint --max-warnings 0",
47
+ "format": "prettier --write \"**/*.{ts,js,json,yaml,yml,md}\"",
48
+ "format:check": "prettier --check \"**/*.{ts,js,json,yaml,yml,md}\"",
49
+ "dist": "tsc -b ./tsconfig.build.json",
50
+ "dist:publish": "tsc -b ./tsconfig.publish.json",
51
+ "clean": "rimraf --glob dist 'tsconfig.*.tsbuildinfo' && date +%s > .clean-timestamp",
52
+ "publint": "publint --strict"
53
+ }
54
+ }