@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 +130 -0
- package/dist/index.d.ts +97 -20
- package/dist/index.js +265 -118
- package/package.json +15 -12
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 {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
193
|
+
type: 'FINALIZED_TRANSACTION',
|
|
194
|
+
originalTransaction: recipe.originalTransaction,
|
|
195
|
+
balancingTransaction: signedBalancingTx,
|
|
38
196
|
};
|
|
39
197
|
}
|
|
40
|
-
case
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
48
|
-
return await this.
|
|
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
|
|
51
|
-
return await this.
|
|
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(
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
106
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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(
|
|
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
|
|
140
|
-
? await this.shielded.initSwap(
|
|
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
|
-
|
|
146
|
-
|
|
288
|
+
const combinedTx = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
|
|
289
|
+
if (!combinedTx) {
|
|
290
|
+
throw Error('Unexpected transaction state.');
|
|
147
291
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
176
|
-
await Promise.all([
|
|
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
|
|
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": "
|
|
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-
|
|
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
|
|
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.
|
|
42
|
+
"testcontainers": "^11.10.0",
|
|
41
43
|
"typescript": "^5.9.3",
|
|
42
|
-
"vitest": "^
|
|
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",
|