@midnight-ntwrk/wallet-sdk-facade 1.0.0-beta.9 → 1.0.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,130 @@
1
+ # @midnight-ntwrk/wallet-sdk-facade
2
+
3
+ Unified facade for the Midnight Wallet SDK that combines all wallet types into a single API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @midnight-ntwrk/wallet-sdk-facade
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ The Wallet Facade provides a high-level unified interface that aggregates the functionality of all wallet types
14
+ (shielded, unshielded, and dust). It simplifies wallet operations by providing:
15
+
16
+ - Combined state management across all wallet types
17
+ - Unified transaction balancing for shielded, unshielded, and dust tokens
18
+ - Coordinated transfer and swap operations
19
+ - Simplified transaction finalization flow
20
+ - Dust registration management
21
+
22
+ ## Usage
23
+
24
+ ### Initializing the Facade
25
+
26
+ ```typescript
27
+ import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
28
+
29
+ const facade = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
30
+
31
+ // Start all wallets
32
+ await facade.start(shieldedSecretKeys, dustSecretKey);
33
+ ```
34
+
35
+ ### Observing Combined State
36
+
37
+ ```typescript
38
+ facade.state().subscribe((state) => {
39
+ console.log('Shielded:', state.shielded);
40
+ console.log('Unshielded:', state.unshielded);
41
+ console.log('Dust:', state.dust);
42
+ console.log('All synced:', state.isSynced);
43
+ });
44
+
45
+ // Or wait for full sync
46
+ const syncedState = await facade.waitForSyncedState();
47
+ ```
48
+
49
+ ### Creating Transfer Transactions
50
+
51
+ ```typescript
52
+ const recipe = await facade.transferTransaction(
53
+ [
54
+ {
55
+ type: 'shielded',
56
+ outputs: [{ type: 'TOKEN_B', receiverAddress: shieldedAddr, amount: 1000n }],
57
+ },
58
+ {
59
+ type: 'unshielded',
60
+ outputs: [{ type: 'TOKEN_A', receiverAddress: unshieldedAddr, amount: 500n }],
61
+ },
62
+ ],
63
+ { shieldedSecretKeys, dustSecretKey },
64
+ { ttl: new Date(Date.now() + 3600000) },
65
+ );
66
+ ```
67
+
68
+ ### Balancing Transactions
69
+
70
+ ```typescript
71
+ // Balance a finalized transaction
72
+ const recipe = await facade.balanceFinalizedTransaction(
73
+ finalizedTx,
74
+ { shieldedSecretKeys, dustSecretKey },
75
+ { ttl, tokenKindsToBalance: 'all' }, // or ['shielded', 'dust']
76
+ );
77
+
78
+ // Finalize the balanced recipe
79
+ const finalTx = await facade.finalizeRecipe(recipe);
80
+
81
+ // Submit to the network
82
+ const txId = await facade.submitTransaction(finalTx);
83
+ ```
84
+
85
+ ### Creating Swap Offers
86
+
87
+ ```typescript
88
+ const swapRecipe = await facade.initSwap(
89
+ { shielded: { NIGHT: 1000n } }, // inputs
90
+ [{ type: 'shielded', outputs: [{ type: 'TOKEN_A', receiverAddress, amount: 100n }] }], // outputs
91
+ { shieldedSecretKeys, dustSecretKey },
92
+ { ttl, payFees: false },
93
+ );
94
+ ```
95
+
96
+ ### Dust Registration
97
+
98
+ ```typescript
99
+ // Register Night UTXOs for dust generation
100
+ const registrationRecipe = await facade.registerNightUtxosForDustGeneration(
101
+ nightUtxos,
102
+ nightVerifyingKey,
103
+ signDustRegistration,
104
+ );
105
+
106
+ // Estimate registration costs
107
+ const { fee, dustGenerationEstimations } = await facade.estimateRegistration(nightUtxos);
108
+ ```
109
+
110
+ ## Types
111
+
112
+ ### BalancingRecipe
113
+
114
+ The facade returns different recipe types depending on the input transaction:
115
+
116
+ - `FinalizedTransactionRecipe` - For finalized transactions
117
+ - `UnboundTransactionRecipe` - For unbound transactions
118
+ - `UnprovenTransactionRecipe` - For unproven transactions
119
+
120
+ ### TokenKindsToBalance
121
+
122
+ Control which token types to balance:
123
+
124
+ ```typescript
125
+ type TokenKindsToBalance = 'all' | ('dust' | 'shielded' | 'unshielded')[];
126
+ ```
127
+
128
+ ## License
129
+
130
+ Apache-2.0
package/dist/index.d.ts CHANGED
@@ -1,10 +1,34 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { ShieldedWalletState, type ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
3
3
  import { type UnshieldedWallet, UnshieldedWalletState } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
4
- import { type Utxo } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
5
- import { AnyTransaction, DustWallet, DustWalletState } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
6
- import { ProvingRecipe } from '@midnight-ntwrk/wallet-sdk-shielded/v1';
7
- import * as ledger from '@midnight-ntwrk/ledger-v6';
4
+ import { AnyTransaction, DustWallet, DustWalletState, CoinsAndBalances as DustCoinsAndBalances } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
5
+ import * as ledger from '@midnight-ntwrk/ledger-v7';
6
+ export type UnboundTransaction = ledger.Transaction<ledger.SignatureEnabled, ledger.Proof, ledger.PreBinding>;
7
+ type TokenKind = 'dust' | 'shielded' | 'unshielded';
8
+ type TokenKindsToBalance = 'all' | TokenKind[];
9
+ declare const TokenKindsToBalance: {
10
+ allTokenKinds: string[];
11
+ toFlags: (tokenKinds: TokenKindsToBalance) => {
12
+ shouldBalanceUnshielded: boolean;
13
+ shouldBalanceShielded: boolean;
14
+ shouldBalanceDust: boolean;
15
+ };
16
+ };
17
+ export type FinalizedTransactionRecipe = {
18
+ type: 'FINALIZED_TRANSACTION';
19
+ originalTransaction: ledger.FinalizedTransaction;
20
+ balancingTransaction: ledger.UnprovenTransaction;
21
+ };
22
+ export type UnboundTransactionRecipe = {
23
+ type: 'UNBOUND_TRANSACTION';
24
+ baseTransaction: UnboundTransaction;
25
+ balancingTransaction?: ledger.UnprovenTransaction | undefined;
26
+ };
27
+ export type UnprovenTransactionRecipe = {
28
+ type: 'UNPROVEN_TRANSACTION';
29
+ transaction: ledger.UnprovenTransaction;
30
+ };
31
+ export type BalancingRecipe = FinalizedTransactionRecipe | UnboundTransactionRecipe | UnprovenTransactionRecipe;
8
32
  export interface TokenTransfer {
9
33
  type: ledger.RawTokenType;
10
34
  receiverAddress: string;
@@ -20,25 +44,78 @@ export type CombinedSwapInputs = {
20
44
  };
21
45
  export type CombinedSwapOutputs = CombinedTokenTransfer;
22
46
  export type TransactionIdentifier = string;
47
+ export type UtxoWithMeta = {
48
+ utxo: ledger.Utxo;
49
+ meta: {
50
+ ctime: Date;
51
+ };
52
+ };
53
+ export declare class FacadeState {
54
+ readonly shielded: ShieldedWalletState;
55
+ readonly unshielded: UnshieldedWalletState;
56
+ readonly dust: DustWalletState;
57
+ get isSynced(): boolean;
58
+ constructor(shielded: ShieldedWalletState, unshielded: UnshieldedWalletState, dust: DustWalletState);
59
+ }
23
60
  export declare class WalletFacade {
24
- shielded: ShieldedWallet;
25
- unshielded: UnshieldedWallet;
26
- dust: DustWallet;
61
+ readonly shielded: ShieldedWallet;
62
+ readonly unshielded: UnshieldedWallet;
63
+ readonly dust: DustWallet;
27
64
  constructor(shieldedWallet: ShieldedWallet, unshieldedWallet: UnshieldedWallet, dustWallet: DustWallet);
28
- state(): Observable<{
29
- shielded: ShieldedWalletState;
30
- unshielded: UnshieldedWalletState;
31
- dust: DustWalletState;
32
- }>;
65
+ private defaultTtl;
66
+ private mergeUnprovenTransactions;
67
+ private createDustActionTransaction;
68
+ state(): Observable<FacadeState>;
69
+ waitForSyncedState(): Promise<FacadeState>;
33
70
  submitTransaction(tx: ledger.FinalizedTransaction): Promise<TransactionIdentifier>;
34
- balanceTransaction(zswapSecretKeys: ledger.ZswapSecretKeys, dustSecretKeys: ledger.DustSecretKey, tx: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>, ttl: Date): Promise<ProvingRecipe.ProvingRecipe<ledger.FinalizedTransaction>>;
35
- finalizeTransaction(recipe: ProvingRecipe.ProvingRecipe<ledger.FinalizedTransaction>): Promise<ledger.FinalizedTransaction>;
36
- signTransaction(tx: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<ledger.UnprovenTransaction>;
71
+ balanceFinalizedTransaction(tx: ledger.FinalizedTransaction, secretKeys: {
72
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
73
+ dustSecretKey: ledger.DustSecretKey;
74
+ }, options: {
75
+ ttl: Date;
76
+ tokenKindsToBalance?: TokenKindsToBalance;
77
+ }): Promise<FinalizedTransactionRecipe>;
78
+ balanceUnboundTransaction(tx: UnboundTransaction, secretKeys: {
79
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
80
+ dustSecretKey: ledger.DustSecretKey;
81
+ }, options: {
82
+ ttl: Date;
83
+ tokenKindsToBalance?: TokenKindsToBalance;
84
+ }): Promise<UnboundTransactionRecipe>;
85
+ balanceUnprovenTransaction(tx: ledger.UnprovenTransaction, secretKeys: {
86
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
87
+ dustSecretKey: ledger.DustSecretKey;
88
+ }, options: {
89
+ ttl: Date;
90
+ tokenKindsToBalance?: TokenKindsToBalance;
91
+ }): Promise<UnprovenTransactionRecipe>;
92
+ finalizeRecipe(recipe: BalancingRecipe): Promise<ledger.FinalizedTransaction>;
93
+ signRecipe(recipe: BalancingRecipe, signSegment: (data: Uint8Array) => ledger.Signature): Promise<BalancingRecipe>;
94
+ signUnprovenTransaction(tx: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<ledger.UnprovenTransaction>;
95
+ signUnboundTransaction(tx: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<UnboundTransaction>;
96
+ finalizeTransaction(tx: ledger.UnprovenTransaction): Promise<ledger.FinalizedTransaction>;
37
97
  calculateTransactionFee(tx: AnyTransaction): Promise<bigint>;
38
- transferTransaction(zswapSecretKeys: ledger.ZswapSecretKeys, dustSecretKey: ledger.DustSecretKey, outputs: CombinedTokenTransfer[], ttl: Date): Promise<ProvingRecipe.TransactionToProve>;
39
- registerNightUtxosForDustGeneration(nightUtxos: Utxo[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => Promise<ledger.Signature> | ledger.Signature, dustReceiverAddress?: string): Promise<ProvingRecipe.TransactionToProve>;
40
- initSwap(zswapSecretKeys: ledger.ZswapSecretKeys, desiredInputs: CombinedSwapInputs, desiredOutputs: CombinedSwapOutputs[], ttl: Date): Promise<ledger.UnprovenTransaction>;
41
- deregisterFromDustGeneration(nightUtxos: Utxo[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => Promise<ledger.Signature> | ledger.Signature): Promise<ProvingRecipe.TransactionToProve>;
42
- start(zswapSecretKeys: ledger.ZswapSecretKeys, dustSecretKey: ledger.DustSecretKey): Promise<void>;
98
+ transferTransaction(outputs: CombinedTokenTransfer[], secretKeys: {
99
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
100
+ dustSecretKey: ledger.DustSecretKey;
101
+ }, options: {
102
+ ttl: Date;
103
+ payFees?: boolean;
104
+ }): Promise<UnprovenTransactionRecipe>;
105
+ estimateRegistration(nightUtxos: readonly UtxoWithMeta[]): Promise<{
106
+ fee: bigint;
107
+ dustGenerationEstimations: ReadonlyArray<DustCoinsAndBalances.UtxoWithFullDustDetails>;
108
+ }>;
109
+ initSwap(desiredInputs: CombinedSwapInputs, desiredOutputs: CombinedSwapOutputs[], secretKeys: {
110
+ shieldedSecretKeys: ledger.ZswapSecretKeys;
111
+ dustSecretKey: ledger.DustSecretKey;
112
+ }, options: {
113
+ ttl: Date;
114
+ payFees?: boolean;
115
+ }): Promise<UnprovenTransactionRecipe>;
116
+ registerNightUtxosForDustGeneration(nightUtxos: readonly UtxoWithMeta[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => ledger.Signature, dustReceiverAddress?: string): Promise<UnprovenTransactionRecipe>;
117
+ deregisterFromDustGeneration(nightUtxos: UtxoWithMeta[], nightVerifyingKey: ledger.SignatureVerifyingKey, signDustRegistration: (payload: Uint8Array) => ledger.Signature): Promise<UnprovenTransactionRecipe>;
118
+ start(shieldedSecretKeys: ledger.ZswapSecretKeys, dustSecretKey: ledger.DustSecretKey): Promise<void>;
43
119
  stop(): Promise<void>;
44
120
  }
121
+ export {};
package/dist/index.js CHANGED
@@ -1,5 +1,44 @@
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.
1
13
  import { combineLatest, map } from 'rxjs';
2
- import { ProvingRecipe } from '@midnight-ntwrk/wallet-sdk-shielded/v1';
14
+ import { Array as Arr, pipe } from 'effect';
15
+ import * as ledger from '@midnight-ntwrk/ledger-v7';
16
+ const TokenKindsToBalance = new (class {
17
+ allTokenKinds = ['shielded', 'unshielded', 'dust'];
18
+ toFlags = (tokenKinds) => {
19
+ return pipe(tokenKinds, (kinds) => (kinds === 'all' ? this.allTokenKinds : kinds), (kinds) => ({
20
+ shouldBalanceUnshielded: kinds.includes('unshielded'),
21
+ shouldBalanceShielded: kinds.includes('shielded'),
22
+ shouldBalanceDust: kinds.includes('dust'),
23
+ }));
24
+ };
25
+ })();
26
+ export class FacadeState {
27
+ shielded;
28
+ unshielded;
29
+ dust;
30
+ get isSynced() {
31
+ return (this.shielded.state.progress.isStrictlyComplete() &&
32
+ this.dust.state.progress.isStrictlyComplete() &&
33
+ this.unshielded.progress.isStrictlyComplete());
34
+ }
35
+ constructor(shielded, unshielded, dust) {
36
+ this.shielded = shielded;
37
+ this.unshielded = unshielded;
38
+ this.dust = dust;
39
+ }
40
+ }
41
+ const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
3
42
  export class WalletFacade {
4
43
  shielded;
5
44
  unshielded;
@@ -9,51 +48,188 @@ export class WalletFacade {
9
48
  this.unshielded = unshieldedWallet;
10
49
  this.dust = dustWallet;
11
50
  }
51
+ defaultTtl() {
52
+ return new Date(Date.now() + DEFAULT_TTL_MS);
53
+ }
54
+ mergeUnprovenTransactions(a, b) {
55
+ if (a && b)
56
+ return a.merge(b);
57
+ return a ?? b;
58
+ }
59
+ async createDustActionTransaction(action, nightUtxos, nightVerifyingKey, signDustRegistration) {
60
+ const ttl = this.defaultTtl();
61
+ const transaction = await this.dust.createDustGenerationTransaction(undefined, ttl, nightUtxos.map(({ utxo, meta }) => ({ ...utxo, ctime: meta.ctime })), nightVerifyingKey, action.type === 'registration' ? action.dustReceiverAddress : undefined);
62
+ const intent = transaction.intents?.get(1);
63
+ if (!intent) {
64
+ throw Error('Dust generation transaction is missing intent segment 1.');
65
+ }
66
+ const signatureData = intent.signatureData(1);
67
+ const signature = await Promise.resolve(signDustRegistration(signatureData));
68
+ return await this.dust.addDustGenerationSignature(transaction, signature);
69
+ }
12
70
  state() {
13
- return combineLatest([this.shielded.state, this.unshielded.state(), this.dust.state]).pipe(map(([shieldedState, unshieldedState, dustState]) => ({
14
- shielded: shieldedState,
15
- unshielded: unshieldedState,
16
- dust: dustState,
17
- })));
71
+ return combineLatest([this.shielded.state, this.unshielded.state, this.dust.state]).pipe(map(([shieldedState, unshieldedState, dustState]) => new FacadeState(shieldedState, unshieldedState, dustState)));
72
+ }
73
+ async waitForSyncedState() {
74
+ const [shieldedState, unshieldedState, dustState] = await Promise.all([
75
+ this.shielded.waitForSyncedState(),
76
+ this.unshielded.waitForSyncedState(),
77
+ this.dust.waitForSyncedState(),
78
+ ]);
79
+ return new FacadeState(shieldedState, unshieldedState, dustState);
18
80
  }
19
81
  async submitTransaction(tx) {
20
82
  await this.shielded.submitTransaction(tx, 'Finalized');
21
83
  return tx.identifiers().at(-1);
22
84
  }
23
- async balanceTransaction(zswapSecretKeys, dustSecretKeys, tx, ttl) {
24
- const unshieldedBalancedTx = await this.unshielded.balanceTransaction(tx);
25
- const recipe = await this.shielded.balanceTransaction(zswapSecretKeys, unshieldedBalancedTx, []);
85
+ async balanceFinalizedTransaction(tx, secretKeys, options) {
86
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
87
+ const { ttl, tokenKindsToBalance = 'all' } = options;
88
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
89
+ // Step 1: Run unshielded and shielded balancing
90
+ const unshieldedBalancingTx = shouldBalanceUnshielded
91
+ ? await this.unshielded.balanceFinalizedTransaction(tx)
92
+ : undefined;
93
+ const shieldedBalancingTx = shouldBalanceShielded
94
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
95
+ : undefined;
96
+ // Step 2: Merge unshielded and shielded balancing
97
+ const mergedBalancingTx = this.mergeUnprovenTransactions(shieldedBalancingTx, unshieldedBalancingTx);
98
+ // Step 3: Conditionally add dust/fee balancing
99
+ const feeBalancingTx = shouldBalanceDust
100
+ ? await this.dust.balanceTransactions(dustSecretKey, mergedBalancingTx ? [tx, mergedBalancingTx] : [tx], ttl)
101
+ : undefined;
102
+ // Step 4: Merge fee balancing and create final recipe
103
+ const balancingTx = this.mergeUnprovenTransactions(mergedBalancingTx, feeBalancingTx);
104
+ if (!balancingTx) {
105
+ throw new Error('No balancing transaction was created. Please check your transaction.');
106
+ }
107
+ return {
108
+ type: 'FINALIZED_TRANSACTION',
109
+ originalTransaction: tx,
110
+ balancingTransaction: balancingTx,
111
+ };
112
+ }
113
+ async balanceUnboundTransaction(tx, secretKeys, options) {
114
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
115
+ const { ttl, tokenKindsToBalance = 'all' } = options;
116
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
117
+ // Step 1: Run unshielded and shielded balancing
118
+ const shieldedBalancingTx = shouldBalanceShielded
119
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
120
+ : undefined;
121
+ // For unbound transactions, unshielded balancing happens in place not with a balancing transaction
122
+ const balancedUnshieldedTx = shouldBalanceUnshielded
123
+ ? await this.unshielded.balanceUnboundTransaction(tx)
124
+ : undefined;
125
+ // Step 2: Unbound unshielded tx are balanced in place, use it as base tx if present
126
+ const baseTx = balancedUnshieldedTx ?? tx;
127
+ // Step 3: Conditionally add dust/fee balancing
128
+ const feeBalancingTransaction = shouldBalanceDust
129
+ ? await this.dust.balanceTransactions(dustSecretKey, shieldedBalancingTx ? [baseTx, shieldedBalancingTx] : [baseTx], ttl)
130
+ : undefined;
131
+ // Step 4: Create the final balancing transaction
132
+ const balancingTransaction = this.mergeUnprovenTransactions(shieldedBalancingTx, feeBalancingTransaction);
133
+ // if there is no balancingTransaction and there was no unshielded tx balancing (in place) throw an error.
134
+ if (!balancingTransaction && !balancedUnshieldedTx) {
135
+ throw new Error('No balancing transaction was created. Please check your transaction.');
136
+ }
137
+ return {
138
+ type: 'UNBOUND_TRANSACTION',
139
+ baseTransaction: baseTx,
140
+ balancingTransaction: balancingTransaction ?? undefined,
141
+ };
142
+ }
143
+ async balanceUnprovenTransaction(tx, secretKeys, options) {
144
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
145
+ const { ttl, tokenKindsToBalance = 'all' } = options;
146
+ const { shouldBalanceDust, shouldBalanceShielded, shouldBalanceUnshielded } = TokenKindsToBalance.toFlags(tokenKindsToBalance);
147
+ // Step 1: Run unshielded and shielded balancing
148
+ const shieldedBalancingTx = shouldBalanceShielded
149
+ ? await this.shielded.balanceTransaction(shieldedSecretKeys, tx)
150
+ : undefined;
151
+ // For unproven transactions, unshielded balancing happens in place
152
+ const balancedUnshieldedTx = shouldBalanceUnshielded
153
+ ? await this.unshielded.balanceUnprovenTransaction(tx)
154
+ : undefined;
155
+ // Step 2: Use the balanced unshielded tx if present, otherwise use the original tx
156
+ const baseTx = balancedUnshieldedTx ?? tx;
157
+ // Step 3: Merge shielded balancing into base tx if present
158
+ const mergedTx = this.mergeUnprovenTransactions(baseTx, shieldedBalancingTx);
159
+ // Step 4: Conditionally add dust/fee balancing
160
+ const feeBalancingTx = shouldBalanceDust
161
+ ? await this.dust.balanceTransactions(dustSecretKey, [mergedTx], ttl)
162
+ : undefined;
163
+ // Step 5: Merge fee balancing if present
164
+ const balancedTx = this.mergeUnprovenTransactions(mergedTx, feeBalancingTx);
165
+ return {
166
+ type: 'UNPROVEN_TRANSACTION',
167
+ transaction: balancedTx,
168
+ };
169
+ }
170
+ async finalizeRecipe(recipe) {
26
171
  switch (recipe.type) {
27
- case ProvingRecipe.TRANSACTION_TO_PROVE:
28
- return await this.dust.addFeePayment(dustSecretKeys, recipe.transaction, new Date(), ttl);
29
- case ProvingRecipe.BALANCE_TRANSACTION_TO_PROVE: {
30
- // if the shielded wallet returned a proven transaction, we need to pay fees with the dust wallet
31
- const balancedTx = await this.dust.addFeePayment(dustSecretKeys, recipe.transactionToProve, new Date(), ttl);
32
- if (balancedTx.type !== ProvingRecipe.TRANSACTION_TO_PROVE) {
33
- throw Error('Unexpected transaction type after adding fee payment.');
34
- }
172
+ case 'FINALIZED_TRANSACTION': {
173
+ const finalizedBalancing = await this.finalizeTransaction(recipe.balancingTransaction);
174
+ return recipe.originalTransaction.merge(finalizedBalancing);
175
+ }
176
+ case 'UNBOUND_TRANSACTION': {
177
+ const finalizedBalancingTx = recipe.balancingTransaction
178
+ ? await this.finalizeTransaction(recipe.balancingTransaction)
179
+ : undefined;
180
+ const finalizedTransaction = recipe.baseTransaction.bind();
181
+ return finalizedBalancingTx ? finalizedTransaction.merge(finalizedBalancingTx) : finalizedTransaction;
182
+ }
183
+ case 'UNPROVEN_TRANSACTION': {
184
+ return await this.finalizeTransaction(recipe.transaction);
185
+ }
186
+ }
187
+ }
188
+ async signRecipe(recipe, signSegment) {
189
+ switch (recipe.type) {
190
+ case 'FINALIZED_TRANSACTION': {
191
+ const signedBalancingTx = await this.signUnprovenTransaction(recipe.balancingTransaction, signSegment);
35
192
  return {
36
- ...recipe,
37
- transactionToProve: balancedTx.transaction,
193
+ type: 'FINALIZED_TRANSACTION',
194
+ originalTransaction: recipe.originalTransaction,
195
+ balancingTransaction: signedBalancingTx,
38
196
  };
39
197
  }
40
- case ProvingRecipe.NOTHING_TO_PROVE: {
41
- // @TODO fix casting
42
- const txToBalance = recipe.transaction;
43
- return await this.dust.addFeePayment(dustSecretKeys, txToBalance, new Date(), ttl);
198
+ case 'UNBOUND_TRANSACTION': {
199
+ const signedBalancingTx = recipe.balancingTransaction
200
+ ? await this.signUnprovenTransaction(recipe.balancingTransaction, signSegment)
201
+ : undefined;
202
+ const signedBaseTx = await this.signUnboundTransaction(recipe.baseTransaction, signSegment);
203
+ return {
204
+ type: 'UNBOUND_TRANSACTION',
205
+ baseTransaction: signedBaseTx,
206
+ balancingTransaction: signedBalancingTx,
207
+ };
208
+ }
209
+ case 'UNPROVEN_TRANSACTION': {
210
+ const signedTx = await this.signUnprovenTransaction(recipe.transaction, signSegment);
211
+ return {
212
+ type: 'UNPROVEN_TRANSACTION',
213
+ transaction: signedTx,
214
+ };
44
215
  }
45
216
  }
46
217
  }
47
- async finalizeTransaction(recipe) {
48
- return await this.shielded.finalizeTransaction(recipe);
218
+ async signUnprovenTransaction(tx, signSegment) {
219
+ return await this.unshielded.signUnprovenTransaction(tx, signSegment);
220
+ }
221
+ async signUnboundTransaction(tx, signSegment) {
222
+ return await this.unshielded.signUnboundTransaction(tx, signSegment);
49
223
  }
50
- async signTransaction(tx, signSegment) {
51
- return await this.unshielded.signTransaction(tx, signSegment);
224
+ async finalizeTransaction(tx) {
225
+ return await this.shielded.finalizeTransaction(tx);
52
226
  }
53
227
  async calculateTransactionFee(tx) {
54
- return await this.dust.calculateFee(tx);
228
+ return await this.dust.calculateFee([tx]);
55
229
  }
56
- async transferTransaction(zswapSecretKeys, dustSecretKey, outputs, ttl) {
230
+ async transferTransaction(outputs, secretKeys, options) {
231
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
232
+ const { ttl, payFees = true } = options;
57
233
  const unshieldedOutputs = outputs
58
234
  .filter((output) => output.type === 'unshielded')
59
235
  .flatMap((output) => output.outputs);
@@ -61,69 +237,36 @@ export class WalletFacade {
61
237
  if (unshieldedOutputs.length === 0 && shieldedOutputs.length === 0) {
62
238
  throw Error('At least one shielded or unshielded output is required.');
63
239
  }
64
- let shieldedTxRecipe = undefined;
65
- let unshieldedTx = undefined;
66
- if (unshieldedOutputs.length > 0) {
67
- unshieldedTx = await this.unshielded.transferTransaction(unshieldedOutputs, ttl);
68
- }
69
- if (shieldedOutputs.length > 0) {
70
- shieldedTxRecipe = await this.shielded.transferTransaction(zswapSecretKeys, shieldedOutputs);
71
- }
72
- // if there's a shielded tx only, return it as it's already balanced
73
- if (shieldedTxRecipe !== undefined && unshieldedTx === undefined) {
74
- if (shieldedTxRecipe.type !== 'TransactionToProve') {
75
- throw Error('Unexpected transaction type.');
76
- }
77
- const recipe = await this.dust.addFeePayment(dustSecretKey, shieldedTxRecipe.transaction, new Date(), ttl);
78
- if (recipe.type !== 'TransactionToProve') {
79
- throw Error('Unexpected transaction type after adding fee payment.');
80
- }
81
- return recipe;
82
- }
83
- // if there's an unshielded tx only, pay fees (balance) with shielded wallet
84
- if (shieldedTxRecipe === undefined && unshieldedTx !== undefined) {
85
- const recipe = await this.dust.addFeePayment(dustSecretKey, unshieldedTx, new Date(), ttl);
86
- if (recipe.type !== 'TransactionToProve') {
87
- throw Error('Unexpected transaction type after adding fee payment.');
88
- }
89
- return recipe;
90
- }
91
- // if there's a shielded and unshielded tx, pay fees for unshielded and merge them
92
- if (shieldedTxRecipe !== undefined && unshieldedTx !== undefined) {
93
- if (shieldedTxRecipe.type !== 'TransactionToProve') {
94
- throw Error('Unexpected transaction type.');
95
- }
96
- const txToBalance = shieldedTxRecipe.transaction.merge(unshieldedTx);
97
- const recipe = await this.dust.addFeePayment(dustSecretKey, txToBalance, new Date(), ttl);
98
- if (recipe.type !== 'TransactionToProve') {
99
- throw Error('Unexpected transaction type after adding fee payment.');
100
- }
101
- return recipe;
102
- }
103
- throw Error('Unexpected transaction state.');
240
+ const shieldedTx = shieldedOutputs.length > 0
241
+ ? await this.shielded.transferTransaction(shieldedSecretKeys, shieldedOutputs)
242
+ : undefined;
243
+ const unshieldedTx = unshieldedOutputs.length > 0 ? await this.unshielded.transferTransaction(unshieldedOutputs, ttl) : undefined;
244
+ const mergedTxs = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
245
+ // Add fee payment
246
+ const feeBalancingTx = payFees ? await this.dust.balanceTransactions(dustSecretKey, [mergedTxs], ttl) : undefined;
247
+ const finalTx = this.mergeUnprovenTransactions(mergedTxs, feeBalancingTx);
248
+ return {
249
+ type: 'UNPROVEN_TRANSACTION',
250
+ transaction: finalTx,
251
+ };
104
252
  }
105
- async registerNightUtxosForDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration, dustReceiverAddress) {
106
- if (nightUtxos.length === 0) {
107
- throw Error('At least one Night UTXO is required.');
108
- }
253
+ async estimateRegistration(nightUtxos) {
254
+ const now = new Date();
109
255
  const dustState = await this.dust.waitForSyncedState();
110
- const receiverAddress = dustReceiverAddress ?? dustState.dustAddress;
111
- const nextBlock = new Date();
112
- const ttl = new Date(nextBlock.getTime() + 60 * 60 * 1000);
113
- const transaction = await this.dust.createDustGenerationTransaction(nextBlock, ttl, nightUtxos.map((utxo) => ({ ...utxo, ctime: new Date(utxo.ctime) })), nightVerifyingKey, receiverAddress);
114
- const intent = transaction.intents?.get(1);
115
- if (!intent) {
116
- throw Error('Dust generation transaction is missing intent segment 1.');
117
- }
118
- const signatureData = intent.signatureData(1);
119
- const signature = await Promise.resolve(signDustRegistration(signatureData));
120
- const recipe = await this.dust.addDustGenerationSignature(transaction, signature);
121
- if (recipe.type !== ProvingRecipe.TRANSACTION_TO_PROVE) {
122
- throw Error('Unexpected recipe type returned when registering Night UTXOs.');
123
- }
124
- return recipe;
256
+ const dustGenerationEstimations = pipe(nightUtxos, Arr.map(({ utxo, meta }) => ({ ...utxo, ctime: meta.ctime })), (utxosWithMeta) => dustState.estimateDustGeneration(utxosWithMeta, now), (estimatedUtxos) => dustState.capabilities.coinsAndBalances.splitNightUtxos(estimatedUtxos), (split) => split.guaranteed);
257
+ const fakeSigningKey = ledger.sampleSigningKey();
258
+ const fakeVerifyingKey = ledger.signatureVerifyingKey(fakeSigningKey);
259
+ const fakeRegistrationRecipe = await this.registerNightUtxosForDustGeneration(nightUtxos, fakeVerifyingKey, (payload) => ledger.signData(fakeSigningKey, payload), dustState.dustAddress);
260
+ const finalizedFakeTx = fakeRegistrationRecipe.transaction.mockProve().bind();
261
+ const fee = await this.calculateTransactionFee(finalizedFakeTx);
262
+ return {
263
+ fee,
264
+ dustGenerationEstimations,
265
+ };
125
266
  }
126
- async initSwap(zswapSecretKeys, desiredInputs, desiredOutputs, ttl) {
267
+ async initSwap(desiredInputs, desiredOutputs, secretKeys, options) {
268
+ const { shieldedSecretKeys, dustSecretKey } = secretKeys;
269
+ const { ttl, payFees = false } = options;
127
270
  const { shielded: shieldedInputs, unshielded: unshieldedInputs } = desiredInputs;
128
271
  const shieldedOutputs = desiredOutputs
129
272
  .filter((output) => output.type === 'shielded')
@@ -136,44 +279,48 @@ export class WalletFacade {
136
279
  if (!hasShieldedPart && !hasUnshieldedPart) {
137
280
  throw Error('At least one shielded or unshielded swap is required.');
138
281
  }
139
- const shieldedTxRecipe = hasShieldedPart && shieldedInputs !== undefined
140
- ? await this.shielded.initSwap(zswapSecretKeys, shieldedInputs, shieldedOutputs)
282
+ const shieldedTx = hasShieldedPart && shieldedInputs !== undefined
283
+ ? await this.shielded.initSwap(shieldedSecretKeys, shieldedInputs, shieldedOutputs)
141
284
  : undefined;
142
285
  const unshieldedTx = hasUnshieldedPart && unshieldedInputs !== undefined
143
286
  ? await this.unshielded.initSwap(unshieldedInputs, unshieldedOutputs, ttl)
144
287
  : undefined;
145
- if (shieldedTxRecipe !== undefined && shieldedTxRecipe.type !== ProvingRecipe.TRANSACTION_TO_PROVE) {
146
- throw Error('Unexpected transaction type.');
288
+ const combinedTx = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
289
+ if (!combinedTx) {
290
+ throw Error('Unexpected transaction state.');
147
291
  }
148
- if (shieldedTxRecipe && unshieldedTx) {
149
- return shieldedTxRecipe.transaction.merge(unshieldedTx);
150
- }
151
- if (shieldedTxRecipe) {
152
- return shieldedTxRecipe.transaction;
153
- }
154
- if (unshieldedTx) {
155
- return unshieldedTx;
292
+ const feeBalancingTx = payFees ? await this.dust.balanceTransactions(dustSecretKey, [combinedTx], ttl) : undefined;
293
+ const finalTx = this.mergeUnprovenTransactions(combinedTx, feeBalancingTx);
294
+ return {
295
+ type: 'UNPROVEN_TRANSACTION',
296
+ transaction: finalTx,
297
+ };
298
+ }
299
+ async registerNightUtxosForDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration, dustReceiverAddress) {
300
+ if (nightUtxos.length === 0) {
301
+ throw Error('At least one Night UTXO is required.');
156
302
  }
157
- throw Error('Unexpected transaction state.');
303
+ const dustState = await this.dust.waitForSyncedState();
304
+ const receiverAddress = dustReceiverAddress ?? dustState.dustAddress;
305
+ const dustRegistrationTx = await this.createDustActionTransaction({ type: 'registration', dustReceiverAddress: receiverAddress }, nightUtxos, nightVerifyingKey, signDustRegistration);
306
+ return {
307
+ type: 'UNPROVEN_TRANSACTION',
308
+ transaction: dustRegistrationTx,
309
+ };
158
310
  }
159
311
  async deregisterFromDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration) {
160
- const nextBlock = new Date();
161
- const ttl = new Date(nextBlock.getTime() + 60 * 60 * 1000);
162
- const transaction = await this.dust.createDustGenerationTransaction(nextBlock, ttl, nightUtxos.map((utxo) => ({ ...utxo, ctime: new Date(utxo.ctime) })), nightVerifyingKey, undefined);
163
- const intent = transaction.intents?.get(1);
164
- if (!intent) {
165
- throw Error('Dust generation transaction is missing intent segment 1.');
166
- }
167
- const signatureData = intent.signatureData(1);
168
- const signature = await Promise.resolve(signDustRegistration(signatureData));
169
- const recipe = await this.dust.addDustGenerationSignature(transaction, signature);
170
- if (recipe.type !== ProvingRecipe.TRANSACTION_TO_PROVE) {
171
- throw Error('Unexpected recipe type returned when registering Night UTXOs.');
172
- }
173
- return recipe;
312
+ const dustDeregistrationTx = await this.createDustActionTransaction({ type: 'deregistration' }, nightUtxos, nightVerifyingKey, signDustRegistration);
313
+ return {
314
+ type: 'UNPROVEN_TRANSACTION',
315
+ transaction: dustDeregistrationTx,
316
+ };
174
317
  }
175
- async start(zswapSecretKeys, dustSecretKey) {
176
- await Promise.all([this.shielded.start(zswapSecretKeys), this.unshielded.start(), this.dust.start(dustSecretKey)]);
318
+ async start(shieldedSecretKeys, dustSecretKey) {
319
+ await Promise.all([
320
+ this.shielded.start(shieldedSecretKeys),
321
+ this.unshielded.start(),
322
+ this.dust.start(dustSecretKey),
323
+ ]);
177
324
  }
178
325
  async stop() {
179
326
  await Promise.all([this.shielded.stop(), this.unshielded.stop(), this.dust.stop()]);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@midnight-ntwrk/wallet-sdk-facade",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
- "author": "IOHK",
8
+ "author": "Midnight Foundation",
9
9
  "license": "Apache-2.0",
10
10
  "publishConfig": {
11
11
  "registry": "https://npm.pkg.github.com/"
@@ -24,28 +24,31 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@midnight-ntwrk/ledger-v6": "6.1.0-alpha.5",
28
- "@midnight-ntwrk/wallet-sdk-abstractions": "1.0.0-beta.8",
29
- "@midnight-ntwrk/wallet-sdk-address-format": "3.0.0-beta.7",
30
- "@midnight-ntwrk/wallet-sdk-dust-wallet": "1.0.0-beta.8",
31
- "@midnight-ntwrk/wallet-sdk-hd": "3.0.0-beta.6",
32
- "@midnight-ntwrk/wallet-sdk-shielded": "1.0.0-beta.9",
33
- "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "1.0.0-beta.11",
27
+ "@midnight-ntwrk/ledger-v7": "7.0.0",
28
+ "@midnight-ntwrk/wallet-sdk-abstractions": "1.0.0",
29
+ "@midnight-ntwrk/wallet-sdk-address-format": "3.0.0",
30
+ "@midnight-ntwrk/wallet-sdk-dust-wallet": "1.0.0",
31
+ "@midnight-ntwrk/wallet-sdk-hd": "3.0.0",
32
+ "@midnight-ntwrk/wallet-sdk-shielded": "1.0.0",
33
+ "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "1.0.0",
34
34
  "rxjs": "^7.5"
35
35
  },
36
36
  "devDependencies": {
37
+ "@midnight-ntwrk/wallet-sdk-prover-client": "1.0.0",
37
38
  "eslint": "^9.37.0",
39
+ "prettier": "^3.7.0",
38
40
  "publint": "~0.3.14",
39
41
  "rimraf": "^6.0.1",
40
- "testcontainers": "^11.4.0",
42
+ "testcontainers": "^11.10.0",
41
43
  "typescript": "^5.9.3",
42
- "vitest": "^3.2.4"
44
+ "vitest": "^4.0.16"
43
45
  },
44
46
  "scripts": {
45
47
  "typecheck": "tsc -b ./tsconfig.json --noEmit",
46
48
  "test": "vitest run",
47
49
  "lint": "eslint --max-warnings 0",
48
- "format": "prettier --write \"**/*.{ts,js,json,yaml,yml}\"",
50
+ "format": "prettier --write \"**/*.{ts,js,json,yaml,yml,md}\"",
51
+ "format:check": "prettier --check \"**/*.{ts,js,json,yaml,yml,md}\"",
49
52
  "dist": "tsc -b ./tsconfig.build.json",
50
53
  "dist:publish": "tsc -b ./tsconfig.publish.json",
51
54
  "clean": "rimraf --glob dist 'tsconfig.*.tsbuildinfo' && date +%s > .clean-timestamp",