@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 +138 -0
- package/dist/index.d.ts +333 -0
- package/dist/index.js +681 -0
- package/dist/transaction.d.ts +3 -0
- package/dist/transaction.js +60 -0
- package/package.json +54 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|