@midnightntwrk/wallet-sdk-dust-wallet 4.2.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 +106 -0
- package/dist/DustWallet.d.ts +101 -0
- package/dist/DustWallet.js +192 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/v1/CoinsAndBalances.d.ts +47 -0
- package/dist/v1/CoinsAndBalances.js +102 -0
- package/dist/v1/CoreWallet.d.ts +31 -0
- package/dist/v1/CoreWallet.js +105 -0
- package/dist/v1/Keys.d.ts +8 -0
- package/dist/v1/Keys.js +11 -0
- package/dist/v1/RunningV1Variant.d.ts +47 -0
- package/dist/v1/RunningV1Variant.js +143 -0
- package/dist/v1/Serialization.d.ts +9 -0
- package/dist/v1/Serialization.js +75 -0
- package/dist/v1/Sync.d.ts +96 -0
- package/dist/v1/Sync.js +218 -0
- package/dist/v1/Transacting.d.ts +97 -0
- package/dist/v1/Transacting.js +327 -0
- package/dist/v1/TransactionHistory.d.ts +69 -0
- package/dist/v1/TransactionHistory.js +106 -0
- package/dist/v1/Utils.d.ts +14 -0
- package/dist/v1/Utils.js +26 -0
- package/dist/v1/V1Builder.d.ts +91 -0
- package/dist/v1/V1Builder.js +166 -0
- package/dist/v1/WalletError.d.ts +43 -0
- package/dist/v1/WalletError.js +23 -0
- package/dist/v1/index.d.ts +12 -0
- package/dist/v1/index.js +24 -0
- package/dist/v1/types/Dust.d.ts +39 -0
- package/dist/v1/types/Dust.js +1 -0
- package/dist/v1/types/index.d.ts +3 -0
- package/dist/v1/types/index.js +15 -0
- package/dist/v1/types/ledger.d.ts +4 -0
- package/dist/v1/types/ledger.js +1 -0
- package/dist/v1/types/transaction.d.ts +4 -0
- package/dist/v1/types/transaction.js +1 -0
- package/package.json +57 -0
package/dist/v1/Sync.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
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 { Effect, Either, Layer, ParseResult, pipe, Schema, Stream, Duration, Chunk, Schedule, } from 'effect';
|
|
14
|
+
import { Event as LedgerEvent, LedgerParameters, } from '@midnight-ntwrk/ledger-v8';
|
|
15
|
+
import { BlockHash, DustLedgerEvents } from '@midnightntwrk/wallet-sdk-indexer-client';
|
|
16
|
+
import { WsSubscriptionClient, HttpQueryClient, ConnectionHelper, } from '@midnightntwrk/wallet-sdk-indexer-client/effect';
|
|
17
|
+
import { EitherOps, LedgerOps } from '@midnightntwrk/wallet-sdk-utilities';
|
|
18
|
+
import { WsURL } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
19
|
+
import { OtherWalletError, SyncWalletError } from './WalletError.js';
|
|
20
|
+
import { getBlockEventsFrom, getLastBlock, } from '@midnightntwrk/wallet-sdk-capabilities/simulation';
|
|
21
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
22
|
+
import { Uint8ArraySchema } from './Serialization.js';
|
|
23
|
+
export const SecretKeysResource = {
|
|
24
|
+
create: (secretKey) => {
|
|
25
|
+
return (cb) => {
|
|
26
|
+
const result = cb(secretKey);
|
|
27
|
+
secretKey.clear();
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const LedgerEventSchema = Schema.declare((input) => input instanceof LedgerEvent).annotations({
|
|
33
|
+
identifier: 'ledger.Event',
|
|
34
|
+
});
|
|
35
|
+
const LedgerEventFromUInt8Array = Schema.asSchema(Schema.transformOrFail(Uint8ArraySchema, LedgerEventSchema, {
|
|
36
|
+
encode: (e) => Effect.try({
|
|
37
|
+
try: () => e.serialize(),
|
|
38
|
+
catch: (err) => new ParseResult.Unexpected(err, 'Could not serialize Ledger Event'),
|
|
39
|
+
}),
|
|
40
|
+
decode: (bytes) => Effect.try({
|
|
41
|
+
try: () => LedgerEvent.deserialize(bytes),
|
|
42
|
+
catch: (err) => new ParseResult.Unexpected(err, 'Could not deserialize Ledger Event'),
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
const HexedEvent = pipe(Schema.Uint8ArrayFromHex, Schema.compose(LedgerEventFromUInt8Array));
|
|
46
|
+
export const SyncEventsUpdateSchema = Schema.Struct({
|
|
47
|
+
id: Schema.Number,
|
|
48
|
+
raw: HexedEvent,
|
|
49
|
+
maxId: Schema.Number,
|
|
50
|
+
});
|
|
51
|
+
export const WalletSyncUpdate = {
|
|
52
|
+
create: (updates, secretKey, timestamp) => {
|
|
53
|
+
return {
|
|
54
|
+
updates,
|
|
55
|
+
secretKey,
|
|
56
|
+
timestamp,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const makeDefaultSyncService = (config) => {
|
|
61
|
+
const indexerSyncService = makeIndexerSyncService(config);
|
|
62
|
+
return {
|
|
63
|
+
updates: (state, secretKey) => {
|
|
64
|
+
const batchSize = config.batchUpdates?.size ?? 10;
|
|
65
|
+
const batchTimeout = Duration.millis(config.batchUpdates?.timeout ?? 1);
|
|
66
|
+
const batchSpacing = config.batchUpdates?.spacing ?? 4;
|
|
67
|
+
return pipe(indexerSyncService.subscribeWallet(state), Stream.groupedWithin(batchSize, batchTimeout), Stream.map(Chunk.toArray), Stream.map((data) => WalletSyncUpdate.create(data, secretKey, new Date())), batchSpacing > 0
|
|
68
|
+
? Stream.schedule(Schedule.spaced(Duration.millis(batchSpacing)))
|
|
69
|
+
: (eventsStream) => eventsStream, Stream.provideSomeLayer(indexerSyncService.connectionLayer()));
|
|
70
|
+
},
|
|
71
|
+
blockData: () => {
|
|
72
|
+
return Effect.gen(function* () {
|
|
73
|
+
const query = yield* BlockHash;
|
|
74
|
+
const result = yield* query({ offset: null });
|
|
75
|
+
return result.block;
|
|
76
|
+
}).pipe(Effect.provide(indexerSyncService.queryClient()), Effect.scoped, Effect.catchAll((err) => Effect.fail(new OtherWalletError({ message: `Encountered unexpected error: ${err.message}`, cause: err }))), Effect.flatMap((blockData) => {
|
|
77
|
+
if (!blockData) {
|
|
78
|
+
throw new OtherWalletError({ message: 'Unable to fetch block data' });
|
|
79
|
+
}
|
|
80
|
+
// TODO: convert to schema
|
|
81
|
+
return LedgerOps.ledgerTry(() => ({
|
|
82
|
+
hash: blockData.hash,
|
|
83
|
+
height: blockData.height,
|
|
84
|
+
ledgerParameters: LedgerParameters.deserialize(Buffer.from(blockData.ledgerParameters, 'hex')),
|
|
85
|
+
timestamp: new Date(blockData.timestamp),
|
|
86
|
+
}));
|
|
87
|
+
}));
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export const makeIndexerSyncService = (config) => {
|
|
92
|
+
return {
|
|
93
|
+
queryClient() {
|
|
94
|
+
return pipe(HttpQueryClient.layer({
|
|
95
|
+
url: config.indexerClientConnection.indexerHttpUrl,
|
|
96
|
+
}), Layer.mapError((error) => new OtherWalletError(error)));
|
|
97
|
+
},
|
|
98
|
+
connectionLayer() {
|
|
99
|
+
const { indexerClientConnection } = config;
|
|
100
|
+
return ConnectionHelper.createWebSocketUrl(indexerClientConnection.indexerHttpUrl, indexerClientConnection.indexerWsUrl).pipe(Either.flatMap((url) => WsURL.make(url)), Either.match({
|
|
101
|
+
onLeft: (error) => Layer.fail(error),
|
|
102
|
+
onRight: (url) => WsSubscriptionClient.layer({ url, keepAlive: indexerClientConnection.keepAlive }),
|
|
103
|
+
}), Layer.mapError((e) => new SyncWalletError({ message: 'Failed to to obtain correct indexer URLs', cause: e })));
|
|
104
|
+
},
|
|
105
|
+
subscribeWallet(state) {
|
|
106
|
+
const { appliedIndex } = state.progress;
|
|
107
|
+
const bufferSize = config.indexerClientConnection.bufferSize ?? 10000;
|
|
108
|
+
const resumeThreshold = config.indexerClientConnection.resumeThreshold ?? 100;
|
|
109
|
+
// The boundary is load-bearing, not waste: this subscription emits only events (no tip/progress
|
|
110
|
+
// sentinel), and `isConnected`/the tip (`maxId`) are set only when an event is received. So the
|
|
111
|
+
// cursor must stay `<= appliedIndex` — never `appliedIndex + 1`. Requesting one event later would
|
|
112
|
+
// deliver nothing to a wallet already at the tip, so `applyUpdate` would never run and sync would
|
|
113
|
+
// hang.
|
|
114
|
+
//
|
|
115
|
+
// A fresh wallet has `appliedIndex === 0n` (the "nothing applied yet" sentinel), so `resumeFrom`
|
|
116
|
+
// is `-1n` and the `variables` mapping below opens the subscription with no `id` — the indexer
|
|
117
|
+
// streams from the very start. A restored wallet has `appliedIndex >= 1`, so `resumeFrom` is
|
|
118
|
+
// `appliedIndex - 1` and the inclusive cursor re-delivers the boundary event.
|
|
119
|
+
const resumeFrom = appliedIndex - 1n;
|
|
120
|
+
return pipe(
|
|
121
|
+
// Backpressure caps the in-flight queue between the WS push and the
|
|
122
|
+
// apply loop. Without it the JS heap grows linearly with catch-up
|
|
123
|
+
// depth, since `Stream.asyncPush({ bufferSize: 'unbounded' })`
|
|
124
|
+
// buffers every event the indexer pushes regardless of apply rate.
|
|
125
|
+
DustLedgerEvents.runWithBackpressure({
|
|
126
|
+
bufferSize,
|
|
127
|
+
resumeThreshold,
|
|
128
|
+
from: resumeFrom,
|
|
129
|
+
// `resumeFrom < 0n` means a fresh wallet: send no `id` so the indexer streams from the very
|
|
130
|
+
// start, rather than relying on `id: 0` sorting below the first real event id.
|
|
131
|
+
variables: (cursor) => ({ id: cursor < 0n ? null : Number(cursor) }),
|
|
132
|
+
key: (r) => BigInt(r.dustLedgerEvents.id),
|
|
133
|
+
}), Stream.mapEffect((subscription) => pipe(Schema.decodeUnknownEither(SyncEventsUpdateSchema)(subscription.dustLedgerEvents), Either.mapLeft((err) => new SyncWalletError(err)), EitherOps.toEffect)), Stream.mapError((error) => new SyncWalletError(error)));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
export const makeDefaultSyncCapability = () => {
|
|
138
|
+
return {
|
|
139
|
+
applyUpdate(state, wrappedUpdate) {
|
|
140
|
+
const { updates, secretKey } = wrappedUpdate;
|
|
141
|
+
// Nothing to update yet
|
|
142
|
+
if (updates.length === 0) {
|
|
143
|
+
return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
|
|
144
|
+
}
|
|
145
|
+
const appliedIndex = state.progress.appliedIndex;
|
|
146
|
+
const freshUpdates = updates.filter((u) => BigInt(u.id) > appliedIndex);
|
|
147
|
+
const highestRelevantWalletIndex = BigInt(updates.at(-1).maxId);
|
|
148
|
+
const [newState, changes] = freshUpdates.length === 0
|
|
149
|
+
? [state, []]
|
|
150
|
+
: CoreWallet.applyEventsWithChanges(state, secretKey, freshUpdates.map((u) => u.raw), wrappedUpdate.timestamp);
|
|
151
|
+
const updatedState = CoreWallet.updateProgress(newState, {
|
|
152
|
+
appliedIndex: freshUpdates.length === 0 ? appliedIndex : BigInt(freshUpdates.at(-1).id),
|
|
153
|
+
highestRelevantWalletIndex,
|
|
154
|
+
isConnected: true,
|
|
155
|
+
});
|
|
156
|
+
return [updatedState, { changes, protocolVersion: Number(updatedState.protocolVersion) }];
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
export const makeSimulatorSyncService = (config) => {
|
|
161
|
+
return {
|
|
162
|
+
updates: (_state, secretKey) => {
|
|
163
|
+
// Get the initial state immediately to ensure we process the genesis block.
|
|
164
|
+
// Then subscribe to state$ for subsequent changes, but deduplicate by block number
|
|
165
|
+
// to avoid processing the same block twice.
|
|
166
|
+
let lastSeenBlockNumber;
|
|
167
|
+
return pipe(Stream.fromEffect(config.simulator.getLatestState()), Stream.concat(config.simulator.state$), Stream.filter((state) => {
|
|
168
|
+
const lastBlock = getLastBlock(state);
|
|
169
|
+
if (lastBlock === undefined) {
|
|
170
|
+
return false; // Skip blank state
|
|
171
|
+
}
|
|
172
|
+
const blockNumber = lastBlock.number;
|
|
173
|
+
// Skip if we've already seen this block (deduplication)
|
|
174
|
+
if (lastSeenBlockNumber !== undefined && blockNumber <= lastSeenBlockNumber) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
lastSeenBlockNumber = blockNumber;
|
|
178
|
+
return true;
|
|
179
|
+
}), Stream.map((state) => ({ update: state, secretKey })));
|
|
180
|
+
},
|
|
181
|
+
blockData: () => {
|
|
182
|
+
return Effect.gen(function* () {
|
|
183
|
+
const state = yield* config.simulator.getLatestState();
|
|
184
|
+
const lastBlock = getLastBlock(state);
|
|
185
|
+
// Use currentTime instead of lastBlock.timestamp for time-sensitive operations
|
|
186
|
+
// (e.g., Dust generation calculation). The currentTime reflects any fast-forwarding
|
|
187
|
+
// that has been done, while lastBlock.timestamp only reflects when the block was produced.
|
|
188
|
+
return {
|
|
189
|
+
hash: lastBlock.hash,
|
|
190
|
+
height: Number(lastBlock.number),
|
|
191
|
+
ledgerParameters: state.ledger.parameters,
|
|
192
|
+
timestamp: state.currentTime,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
export const makeSimulatorSyncCapability = () => {
|
|
199
|
+
return {
|
|
200
|
+
applyUpdate: (state, update) => {
|
|
201
|
+
const lastBlock = getLastBlock(update.update);
|
|
202
|
+
// If no block exists yet (blank simulator), skip update
|
|
203
|
+
if (lastBlock === undefined) {
|
|
204
|
+
return [state, { changes: [], protocolVersion: Number(state.protocolVersion) }];
|
|
205
|
+
}
|
|
206
|
+
// Get all events from blocks starting at appliedIndex (the next block to process).
|
|
207
|
+
// appliedIndex semantics: the first block number we haven't processed yet.
|
|
208
|
+
// Initial: appliedIndex = 0 (haven't processed any blocks)
|
|
209
|
+
// After processing block N: appliedIndex = N + 1 (next block to process)
|
|
210
|
+
const events = [...getBlockEventsFrom(update.update, state.progress.appliedIndex)];
|
|
211
|
+
const [newState, changes] = CoreWallet.applyEventsWithChanges(state, update.secretKey, events, lastBlock.timestamp);
|
|
212
|
+
const updatedState = CoreWallet.updateProgress(newState, {
|
|
213
|
+
appliedIndex: lastBlock.number + 1n,
|
|
214
|
+
});
|
|
215
|
+
return [updatedState, { changes, protocolVersion: Number(updatedState.protocolVersion) }];
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Either, Option } from 'effect';
|
|
2
|
+
import { type DustSecretKey, type Signature, type SignatureVerifyingKey, type FinalizedTransaction, type ProofErasedTransaction, type UnprovenTransaction, type LedgerParameters } from '@midnight-ntwrk/ledger-v8';
|
|
3
|
+
import { type DustAddress } from '@midnightntwrk/wallet-sdk-address-format';
|
|
4
|
+
import { type WalletError } from './WalletError.js';
|
|
5
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
6
|
+
import { type AnyTransaction, type Dust, type NetworkId, type TotalCostParameters } from './types/index.js';
|
|
7
|
+
import { type CoinsAndBalancesCapability, type CoinSelection, type CoinWithValue, type UtxoWithFullDustDetails } from './CoinsAndBalances.js';
|
|
8
|
+
import { type KeysCapability } from './Keys.js';
|
|
9
|
+
/**
|
|
10
|
+
* Result of splitting Night UTxOs into the guaranteed/fallible sections of a Dust registration or deregistration
|
|
11
|
+
* intent, together with the fee-payment allowance the registration may claim from dust generated by the guaranteed
|
|
12
|
+
* UTxO.
|
|
13
|
+
*
|
|
14
|
+
* For deregistration, `feePayment` is always `0n`.
|
|
15
|
+
*/
|
|
16
|
+
export type NightUtxoSplitForDustRegistration = {
|
|
17
|
+
readonly guaranteedUtxos: ReadonlyArray<UtxoWithFullDustDetails>;
|
|
18
|
+
readonly fallibleUtxos: ReadonlyArray<UtxoWithFullDustDetails>;
|
|
19
|
+
readonly feePayment: bigint;
|
|
20
|
+
};
|
|
21
|
+
export interface TransactingCapability<TSecrets, TState, _TTransaction> {
|
|
22
|
+
readonly networkId: NetworkId;
|
|
23
|
+
readonly costParams: TotalCostParameters;
|
|
24
|
+
createDustGenerationTransaction(currentTime: Date, ttl: Date, nightUtxos: ReadonlyArray<UtxoWithFullDustDetails>, nightVerifyingKey: SignatureVerifyingKey, dustReceiverAddress: DustAddress | undefined): Either.Either<UnprovenTransaction, WalletError>;
|
|
25
|
+
/**
|
|
26
|
+
* Decides which Night UTxOs belong in the guaranteed vs fallible section of a dust registration/deregistration
|
|
27
|
+
* transaction, and computes the fee-payment allowance the registration may claim from dust generated by the
|
|
28
|
+
* guaranteed UTxO.
|
|
29
|
+
*
|
|
30
|
+
* For deregistration (`isRegistration === false`) the fee-payment allowance is always `0n`.
|
|
31
|
+
*/
|
|
32
|
+
splitNightUtxosForDustRegistration(utxosWithDustValue: ReadonlyArray<UtxoWithFullDustDetails>, isRegistration: boolean): NightUtxoSplitForDustRegistration;
|
|
33
|
+
/**
|
|
34
|
+
* Attaches a Dust registration / deregistration action onto the intent (at segment 1) of an unproven transaction
|
|
35
|
+
* whose offers have already been built (typically by the unshielded wallet's `rotateUtxos`). The Night UTxOs are
|
|
36
|
+
* assumed to already appear in the intent's guaranteed/fallible offers and to already be booked in the unshielded
|
|
37
|
+
* wallet state.
|
|
38
|
+
*/
|
|
39
|
+
attachDustRegistration(transaction: UnprovenTransaction, currentTime: Date, nightVerifyingKey: SignatureVerifyingKey, dustReceiverAddress: DustAddress | undefined, feePayment: bigint): Either.Either<UnprovenTransaction, WalletError>;
|
|
40
|
+
addDustGenerationSignature(transaction: UnprovenTransaction, signature: Signature): Either.Either<UnprovenTransaction, WalletError>;
|
|
41
|
+
/**
|
|
42
|
+
* Attaches a signature to the DustRegistration in segment 1's `dustActions` only. Unlike
|
|
43
|
+
* {@link addDustGenerationSignature}, this does NOT touch the unshielded offers — those are expected to be signed via
|
|
44
|
+
* the unshielded-wallet signing path. Use this when the caller orchestrates signing across both packages (e.g. the
|
|
45
|
+
* facade's `signRecipe`).
|
|
46
|
+
*/
|
|
47
|
+
addDustRegistrationSignature(transaction: UnprovenTransaction, signature: Signature): Either.Either<UnprovenTransaction, WalletError>;
|
|
48
|
+
calculateFee(transaction: AnyTransaction, ledgerParams: LedgerParameters): bigint;
|
|
49
|
+
estimateFee(secretKey: TSecrets, state: TState, transactions: ReadonlyArray<AnyTransaction>, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): Either.Either<bigint, WalletError>;
|
|
50
|
+
balanceTransactions(secretKey: TSecrets, state: TState, transactions: ReadonlyArray<AnyTransaction>, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): Either.Either<[UnprovenTransaction, TState], WalletError>;
|
|
51
|
+
revertTransaction(state: TState, transaction: AnyTransaction): Either.Either<TState, WalletError>;
|
|
52
|
+
}
|
|
53
|
+
export type DefaultTransactingConfiguration = {
|
|
54
|
+
networkId: NetworkId;
|
|
55
|
+
costParameters: TotalCostParameters;
|
|
56
|
+
};
|
|
57
|
+
export type DefaultTransactingContext = {
|
|
58
|
+
coinSelection: CoinSelection;
|
|
59
|
+
coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
|
|
60
|
+
keysCapability: KeysCapability<CoreWallet>;
|
|
61
|
+
};
|
|
62
|
+
export declare const makeDefaultTransactingCapability: (config: DefaultTransactingConfiguration, getContext: () => DefaultTransactingContext) => TransactingCapability<DustSecretKey, CoreWallet, FinalizedTransaction>;
|
|
63
|
+
export declare const makeSimulatorTransactingCapability: (config: DefaultTransactingConfiguration, getContext: () => DefaultTransactingContext) => TransactingCapability<DustSecretKey, CoreWallet, ProofErasedTransaction>;
|
|
64
|
+
/**
|
|
65
|
+
* Distributes the fee across multiple inputs, draining smaller inputs first when the fee exceeds any single input's
|
|
66
|
+
* value. Finds the next available intent segment id in a transaction.
|
|
67
|
+
*
|
|
68
|
+
* Fallible intent segments occupy the range `[1, 65535]`; segment `0` is reserved for the guaranteed section and is
|
|
69
|
+
* never returned.
|
|
70
|
+
*
|
|
71
|
+
* @param transaction - Transaction whose intent map is inspected.
|
|
72
|
+
* @returns `Some(segmentId)` with the lowest unused id, or `None` if all 65535 fallible segments are taken.
|
|
73
|
+
*/
|
|
74
|
+
export declare const findAvailableSegmentId: (transaction: AnyTransaction) => Option.Option<number>;
|
|
75
|
+
export declare class TransactingCapabilityImplementation<TTransaction extends AnyTransaction> implements TransactingCapability<DustSecretKey, CoreWallet, TTransaction> {
|
|
76
|
+
readonly networkId: string;
|
|
77
|
+
readonly costParams: TotalCostParameters;
|
|
78
|
+
readonly getCoinSelection: () => CoinSelection;
|
|
79
|
+
readonly getCoins: () => CoinsAndBalancesCapability<CoreWallet>;
|
|
80
|
+
readonly getKeys: () => KeysCapability<CoreWallet>;
|
|
81
|
+
constructor(networkId: NetworkId, costParams: TotalCostParameters, getCoinSelection: () => CoinSelection, getCoins: () => CoinsAndBalancesCapability<CoreWallet>, getKeys: () => KeysCapability<CoreWallet>);
|
|
82
|
+
createDustGenerationTransaction(currentTime: Date, ttl: Date, nightUtxos: ReadonlyArray<UtxoWithFullDustDetails>, nightVerifyingKey: SignatureVerifyingKey, dustReceiverAddress: DustAddress | undefined): Either.Either<UnprovenTransaction, WalletError>;
|
|
83
|
+
splitNightUtxosForDustRegistration(utxosWithDustValue: ReadonlyArray<UtxoWithFullDustDetails>, isRegistration: boolean): NightUtxoSplitForDustRegistration;
|
|
84
|
+
attachDustRegistration(transaction: UnprovenTransaction, currentTime: Date, nightVerifyingKey: SignatureVerifyingKey, dustReceiverAddress: DustAddress | undefined, feePayment: bigint): Either.Either<UnprovenTransaction, WalletError>;
|
|
85
|
+
addDustRegistrationSignature(transaction: UnprovenTransaction, signatureData: Signature): Either.Either<UnprovenTransaction, WalletError>;
|
|
86
|
+
addDustGenerationSignature(transaction: UnprovenTransaction, signatureData: Signature): Either.Either<UnprovenTransaction, WalletError>;
|
|
87
|
+
calculateFee(transaction: AnyTransaction, ledgerParams: LedgerParameters): bigint;
|
|
88
|
+
dryRunFee(recipeInputs: ReadonlyArray<CoinWithValue<Dust>>, transactions: ReadonlyArray<FinalizedTransaction | UnprovenTransaction>, secretKey: DustSecretKey, state: CoreWallet, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): bigint;
|
|
89
|
+
static feeImbalance(transaction: AnyTransaction, totalFee: bigint): bigint;
|
|
90
|
+
computeBalancingRecipe(secretKey: DustSecretKey, state: CoreWallet, transactions: ReadonlyArray<FinalizedTransaction | UnprovenTransaction>, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): Either.Either<{
|
|
91
|
+
fee: bigint;
|
|
92
|
+
recipeInputs: ReadonlyArray<CoinWithValue<Dust>>;
|
|
93
|
+
}, WalletError>;
|
|
94
|
+
estimateFee(secretKey: DustSecretKey, state: CoreWallet, transactions: ReadonlyArray<FinalizedTransaction | UnprovenTransaction>, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): Either.Either<bigint, WalletError>;
|
|
95
|
+
balanceTransactions(secretKey: DustSecretKey, state: CoreWallet, transactions: ReadonlyArray<FinalizedTransaction | UnprovenTransaction>, ttl: Date, currentTime: Date, ledgerParams: LedgerParameters): Either.Either<[UnprovenTransaction, CoreWallet], WalletError>;
|
|
96
|
+
revertTransaction(state: CoreWallet, transaction: UnprovenTransaction | TTransaction): Either.Either<CoreWallet, WalletError>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
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 { Effect, Either, pipe, BigInt as BigIntOps, Iterable as IterableOps, Option } from 'effect';
|
|
14
|
+
import { DustActions, DustRegistration, Intent, SignatureEnabled, Transaction, UnshieldedOffer, addressFromKey, nativeToken, } from '@midnight-ntwrk/ledger-v8';
|
|
15
|
+
import { OtherWalletError, TransactingError, InsufficientFundsError } from './WalletError.js';
|
|
16
|
+
import { LedgerOps } from '@midnightntwrk/wallet-sdk-utilities';
|
|
17
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
18
|
+
import { BindingMarker, ProofMarker, SignatureMarker } from './Utils.js';
|
|
19
|
+
import { getBalanceRecipe, Imbalances as CapImbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
20
|
+
export const makeDefaultTransactingCapability = (config, getContext) => {
|
|
21
|
+
return new TransactingCapabilityImplementation(config.networkId, config.costParameters, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability);
|
|
22
|
+
};
|
|
23
|
+
export const makeSimulatorTransactingCapability = (config, getContext) => {
|
|
24
|
+
return new TransactingCapabilityImplementation(config.networkId, config.costParameters, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Distributes the fee across multiple inputs, draining smaller inputs first when the fee exceeds any single input's
|
|
28
|
+
* value. Finds the next available intent segment id in a transaction.
|
|
29
|
+
*
|
|
30
|
+
* Fallible intent segments occupy the range `[1, 65535]`; segment `0` is reserved for the guaranteed section and is
|
|
31
|
+
* never returned.
|
|
32
|
+
*
|
|
33
|
+
* @param transaction - Transaction whose intent map is inspected.
|
|
34
|
+
* @returns `Some(segmentId)` with the lowest unused id, or `None` if all 65535 fallible segments are taken.
|
|
35
|
+
*/
|
|
36
|
+
export const findAvailableSegmentId = (transaction) => {
|
|
37
|
+
const used = new Set(transaction.intents?.keys() ?? []);
|
|
38
|
+
return pipe(IterableOps.range(1, 65535), IterableOps.findFirst((segmentId) => !used.has(segmentId)));
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Distributes the fee across multiple inputs, draining smaller inputs first when the fee exceeds any single input's
|
|
42
|
+
* value.
|
|
43
|
+
*/
|
|
44
|
+
const distributeFeeAcrossInputs = (inputs, fee) => inputs.reduce(({ result, remaining }, input) => {
|
|
45
|
+
const deduction = remaining >= input.value ? input.value : remaining;
|
|
46
|
+
return { result: [...result, { ...input, value: deduction }], remaining: remaining - deduction };
|
|
47
|
+
}, { result: [], remaining: fee }).result;
|
|
48
|
+
export class TransactingCapabilityImplementation {
|
|
49
|
+
networkId;
|
|
50
|
+
costParams;
|
|
51
|
+
getCoinSelection;
|
|
52
|
+
getCoins;
|
|
53
|
+
getKeys;
|
|
54
|
+
constructor(networkId, costParams, getCoinSelection, getCoins, getKeys) {
|
|
55
|
+
this.getCoins = getCoins;
|
|
56
|
+
this.networkId = networkId;
|
|
57
|
+
this.costParams = costParams;
|
|
58
|
+
this.getCoinSelection = getCoinSelection;
|
|
59
|
+
this.getKeys = getKeys;
|
|
60
|
+
}
|
|
61
|
+
createDustGenerationTransaction(currentTime, ttl, nightUtxos, nightVerifyingKey, dustReceiverAddress) {
|
|
62
|
+
const makeOffer = (utxos) => {
|
|
63
|
+
if (utxos.length === 0) {
|
|
64
|
+
return Option.none();
|
|
65
|
+
}
|
|
66
|
+
const totalValue = pipe(utxos, IterableOps.map((coin) => coin.utxo.value), BigIntOps.sumAll);
|
|
67
|
+
const inputs = utxos.map(({ utxo }) => ({
|
|
68
|
+
...utxo,
|
|
69
|
+
owner: nightVerifyingKey,
|
|
70
|
+
}));
|
|
71
|
+
const output = {
|
|
72
|
+
owner: addressFromKey(nightVerifyingKey),
|
|
73
|
+
type: nativeToken().raw,
|
|
74
|
+
value: totalValue,
|
|
75
|
+
};
|
|
76
|
+
return Option.some(UnshieldedOffer.new(inputs, [output], []));
|
|
77
|
+
};
|
|
78
|
+
return Either.gen(this, function* () {
|
|
79
|
+
const receiver = dustReceiverAddress ? dustReceiverAddress.data : undefined;
|
|
80
|
+
return yield* LedgerOps.ledgerTry(() => {
|
|
81
|
+
const network = this.networkId;
|
|
82
|
+
const splitResult = this.getCoins().splitNightUtxos(nightUtxos);
|
|
83
|
+
// if receiver is `undefined`, it means the coin(s) are being deregistered so the allowFeePayment should not be used
|
|
84
|
+
const feePayment = receiver
|
|
85
|
+
? pipe(splitResult.guaranteed, IterableOps.filter((coin) => !coin.utxo.registeredForDustGeneration), IterableOps.map((coin) => coin.dust.generatedNow), BigIntOps.sumAll)
|
|
86
|
+
: 0n;
|
|
87
|
+
const maybeGuaranteedOffer = makeOffer(splitResult.guaranteed);
|
|
88
|
+
const maybeFallibleOffer = makeOffer(splitResult.fallible);
|
|
89
|
+
const dustRegistration = new DustRegistration(SignatureMarker.signature, nightVerifyingKey, receiver, feePayment);
|
|
90
|
+
const dustActions = new DustActions(SignatureMarker.signature, ProofMarker.preProof, currentTime, [], [dustRegistration]);
|
|
91
|
+
const intent = pipe(Intent.new(ttl), (intent) => Option.match(maybeGuaranteedOffer, {
|
|
92
|
+
onNone: () => intent,
|
|
93
|
+
onSome: (guaranteedOffer) => {
|
|
94
|
+
intent.guaranteedUnshieldedOffer = guaranteedOffer;
|
|
95
|
+
return intent;
|
|
96
|
+
},
|
|
97
|
+
}), (intent) => Option.match(maybeFallibleOffer, {
|
|
98
|
+
onNone: () => intent,
|
|
99
|
+
onSome: (fallibleOffer) => {
|
|
100
|
+
intent.fallibleUnshieldedOffer = fallibleOffer;
|
|
101
|
+
return intent;
|
|
102
|
+
},
|
|
103
|
+
}), (intent) => {
|
|
104
|
+
intent.dustActions = dustActions;
|
|
105
|
+
return intent;
|
|
106
|
+
});
|
|
107
|
+
return Transaction.fromParts(network, undefined, undefined, intent);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
splitNightUtxosForDustRegistration(utxosWithDustValue, isRegistration) {
|
|
112
|
+
const splitResult = this.getCoins().splitNightUtxos(utxosWithDustValue);
|
|
113
|
+
// Deregistration must not claim dust as fee payment (matches the historical
|
|
114
|
+
// `dustReceiverAddress === undefined` branch of createDustGenerationTransaction).
|
|
115
|
+
const feePayment = isRegistration
|
|
116
|
+
? pipe(splitResult.guaranteed, IterableOps.filter((coin) => !coin.utxo.registeredForDustGeneration), IterableOps.map((coin) => coin.dust.generatedNow), BigIntOps.sumAll)
|
|
117
|
+
: 0n;
|
|
118
|
+
return {
|
|
119
|
+
guaranteedUtxos: splitResult.guaranteed,
|
|
120
|
+
fallibleUtxos: splitResult.fallible,
|
|
121
|
+
feePayment,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
attachDustRegistration(transaction, currentTime, nightVerifyingKey, dustReceiverAddress, feePayment) {
|
|
125
|
+
return Either.gen(this, function* () {
|
|
126
|
+
const intent = transaction.intents?.get(1);
|
|
127
|
+
if (!intent) {
|
|
128
|
+
return yield* Either.left(new TransactingError({
|
|
129
|
+
message: 'No intent found at segment 1; expected an intent built by rotateUtxos',
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
if (intent.dustActions !== undefined && intent.dustActions.registrations.length > 0) {
|
|
133
|
+
return yield* Either.left(new TransactingError({ message: 'Intent at segment 1 already has a dust registration attached' }));
|
|
134
|
+
}
|
|
135
|
+
return yield* LedgerOps.ledgerTry(() => {
|
|
136
|
+
const receiver = dustReceiverAddress ? dustReceiverAddress.data : undefined;
|
|
137
|
+
const dustRegistration = new DustRegistration(SignatureMarker.signature, nightVerifyingKey, receiver, feePayment);
|
|
138
|
+
const dustActions = new DustActions(SignatureMarker.signature, ProofMarker.preProof, currentTime, [], [dustRegistration]);
|
|
139
|
+
// Intents fetched from `transaction.intents.get(...)` behave like value copies — assigning
|
|
140
|
+
// to a field doesn't propagate back. Match the addDustGenerationSignature pattern: copy the
|
|
141
|
+
// intent via serialize/deserialize, mutate the copy, then set it back into a fresh
|
|
142
|
+
// transaction's intents map.
|
|
143
|
+
const newIntent = Intent.deserialize(SignatureMarker.signature, ProofMarker.preProof, BindingMarker.preBinding, intent.serialize());
|
|
144
|
+
newIntent.dustActions = dustActions;
|
|
145
|
+
const newTransaction = Transaction.deserialize(SignatureMarker.signature, ProofMarker.preProof, BindingMarker.preBinding, transaction.serialize());
|
|
146
|
+
newTransaction.intents = newTransaction.intents.set(1, newIntent);
|
|
147
|
+
return newTransaction;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
addDustRegistrationSignature(transaction, signatureData) {
|
|
152
|
+
return Either.gen(this, function* () {
|
|
153
|
+
const intent = transaction.intents?.get(1);
|
|
154
|
+
if (!intent) {
|
|
155
|
+
return yield* Either.left(new TransactingError({ message: 'No intent found in the transaction intents with segment = 1' }));
|
|
156
|
+
}
|
|
157
|
+
const { dustActions } = intent;
|
|
158
|
+
if (!dustActions) {
|
|
159
|
+
return yield* Either.left(new TransactingError({ message: 'No dustActions found in intent' }));
|
|
160
|
+
}
|
|
161
|
+
const [registration, ...restRegistrations] = dustActions.registrations;
|
|
162
|
+
if (!registration) {
|
|
163
|
+
return yield* Either.left(new TransactingError({ message: 'No registrations found in dustActions' }));
|
|
164
|
+
}
|
|
165
|
+
return yield* LedgerOps.ledgerTry(() => {
|
|
166
|
+
const signature = new SignatureEnabled(signatureData);
|
|
167
|
+
const registrationWithSignature = new DustRegistration(signature.instance, registration.nightKey, registration.dustAddress, registration.allowFeePayment, signature);
|
|
168
|
+
const newDustActions = new DustActions(signature.instance, ProofMarker.preProof, dustActions.ctime, dustActions.spends, [registrationWithSignature, ...restRegistrations]);
|
|
169
|
+
const newIntent = Intent.deserialize(signature.instance, ProofMarker.preProof, BindingMarker.preBinding, intent.serialize());
|
|
170
|
+
newIntent.dustActions = newDustActions;
|
|
171
|
+
const newTransaction = Transaction.deserialize(signature.instance, ProofMarker.preProof, BindingMarker.preBinding, transaction.serialize());
|
|
172
|
+
newTransaction.intents = newTransaction.intents.set(1, newIntent);
|
|
173
|
+
return newTransaction;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
addDustGenerationSignature(transaction, signatureData) {
|
|
178
|
+
return Either.gen(this, function* () {
|
|
179
|
+
const intent = transaction.intents?.get(1);
|
|
180
|
+
if (!intent) {
|
|
181
|
+
return yield* Either.left(new TransactingError({ message: 'No intent found in the transaction intents with segment = 1' }));
|
|
182
|
+
}
|
|
183
|
+
const { dustActions, guaranteedUnshieldedOffer, fallibleUnshieldedOffer } = intent;
|
|
184
|
+
if (!dustActions) {
|
|
185
|
+
return yield* Either.left(new TransactingError({ message: 'No dustActions found in intent' }));
|
|
186
|
+
}
|
|
187
|
+
if (!guaranteedUnshieldedOffer) {
|
|
188
|
+
return yield* Either.left(new TransactingError({ message: 'No guaranteedUnshieldedOffer found in intent' }));
|
|
189
|
+
}
|
|
190
|
+
const [registration, ...restRegistrations] = dustActions.registrations;
|
|
191
|
+
if (!registration) {
|
|
192
|
+
return yield* Either.left(new TransactingError({ message: 'No registrations found in dustActions' }));
|
|
193
|
+
}
|
|
194
|
+
return yield* LedgerOps.ledgerTry(() => {
|
|
195
|
+
const signature = new SignatureEnabled(signatureData);
|
|
196
|
+
const registrationWithSignature = new DustRegistration(signature.instance, registration.nightKey, registration.dustAddress, registration.allowFeePayment, signature);
|
|
197
|
+
const newDustActions = new DustActions(signature.instance, ProofMarker.preProof, dustActions.ctime, dustActions.spends, [registrationWithSignature, ...restRegistrations]);
|
|
198
|
+
// make a copy of intent to avoid mutation
|
|
199
|
+
const newIntent = Intent.deserialize(signature.instance, ProofMarker.preProof, BindingMarker.preBinding, intent.serialize());
|
|
200
|
+
newIntent.dustActions = newDustActions;
|
|
201
|
+
const inputsLen = guaranteedUnshieldedOffer.inputs.length;
|
|
202
|
+
const signatures = [];
|
|
203
|
+
for (let i = 0; i < inputsLen; ++i) {
|
|
204
|
+
signatures.push(guaranteedUnshieldedOffer.signatures.at(i) ?? signatureData);
|
|
205
|
+
}
|
|
206
|
+
newIntent.guaranteedUnshieldedOffer = guaranteedUnshieldedOffer.addSignatures(signatures);
|
|
207
|
+
if (fallibleUnshieldedOffer) {
|
|
208
|
+
const inputsLen = fallibleUnshieldedOffer.inputs.length;
|
|
209
|
+
const signatures = [];
|
|
210
|
+
for (let i = 0; i < inputsLen; ++i) {
|
|
211
|
+
signatures.push(fallibleUnshieldedOffer.signatures.at(i) ?? signatureData);
|
|
212
|
+
}
|
|
213
|
+
newIntent.fallibleUnshieldedOffer = fallibleUnshieldedOffer.addSignatures(signatures);
|
|
214
|
+
}
|
|
215
|
+
// make a copy of transaction to avoid mutation
|
|
216
|
+
const newTransaction = Transaction.deserialize(signature.instance, ProofMarker.preProof, BindingMarker.preBinding, transaction.serialize());
|
|
217
|
+
newTransaction.intents = newTransaction.intents.set(1, newIntent);
|
|
218
|
+
return newTransaction;
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
calculateFee(transaction, ledgerParams) {
|
|
223
|
+
return (transaction.feesWithMargin(ledgerParams, this.costParams.feeBlocksMargin) +
|
|
224
|
+
(this.costParams.additionalFeeOverhead ?? 0n));
|
|
225
|
+
}
|
|
226
|
+
dryRunFee(recipeInputs, transactions, secretKey, state, ttl, currentTime, ledgerParams) {
|
|
227
|
+
const network = this.networkId;
|
|
228
|
+
// Create a balancing tx from recipe inputs without persisting state changes
|
|
229
|
+
const [spends] = CoreWallet.spendCoins(state, secretKey, recipeInputs, currentTime);
|
|
230
|
+
const intent = Intent.new(ttl);
|
|
231
|
+
intent.dustActions = new DustActions(SignatureMarker.signature, ProofMarker.preProof, currentTime, [...spends], []);
|
|
232
|
+
// Merge existing transactions first so we can pick a segment that doesn't collide
|
|
233
|
+
const [first, ...rest] = transactions.map((tx) => tx.eraseProofs());
|
|
234
|
+
const mergedExisting = first ? rest.reduce((acc, tx) => acc.merge(tx), first) : undefined;
|
|
235
|
+
const segmentId = mergedExisting ? Option.getOrElse(findAvailableSegmentId(mergedExisting), () => 1) : 1;
|
|
236
|
+
const balancingTx = Transaction.fromParts(network).addIntent({ tag: 'specific', value: segmentId }, intent);
|
|
237
|
+
const erasedBalancing = balancingTx.eraseProofs();
|
|
238
|
+
const mergedTx = mergedExisting ? mergedExisting.merge(erasedBalancing) : erasedBalancing;
|
|
239
|
+
return this.calculateFee(mergedTx, ledgerParams);
|
|
240
|
+
}
|
|
241
|
+
static feeImbalance(transaction, totalFee) {
|
|
242
|
+
const [_, imbalance] = transaction
|
|
243
|
+
.imbalances(0, totalFee)
|
|
244
|
+
.entries()
|
|
245
|
+
.find(([tt, _]) => tt.tag === 'dust') ?? [];
|
|
246
|
+
return imbalance ?? 0n;
|
|
247
|
+
}
|
|
248
|
+
computeBalancingRecipe(secretKey, state, transactions, ttl, currentTime, ledgerParams) {
|
|
249
|
+
const initialFees = transactions.reduce((total, transaction) => total +
|
|
250
|
+
TransactingCapabilityImplementation.feeImbalance(transaction, this.calculateFee(transaction, ledgerParams)), 0n);
|
|
251
|
+
const dust = this.getCoins().getAvailableCoinsWithGeneratedDust(state, currentTime);
|
|
252
|
+
return pipe(Effect.iterate({ currentFee: initialFees, recipeInputs: [], converged: false }, {
|
|
253
|
+
while: (s) => !s.converged,
|
|
254
|
+
body: ({ currentFee }) => Effect.try({
|
|
255
|
+
try: () => {
|
|
256
|
+
const recipe = getBalanceRecipe({
|
|
257
|
+
coins: dust.map((coin) => ({
|
|
258
|
+
type: 'dust',
|
|
259
|
+
value: coin.value,
|
|
260
|
+
token: coin.token,
|
|
261
|
+
})),
|
|
262
|
+
initialImbalances: CapImbalances.fromEntry('dust', currentFee),
|
|
263
|
+
feeTokenType: 'dust',
|
|
264
|
+
coinSelection: this.getCoinSelection(),
|
|
265
|
+
transactionCostModel: {
|
|
266
|
+
inputFeeOverhead: 0n,
|
|
267
|
+
outputFeeOverhead: 0n,
|
|
268
|
+
},
|
|
269
|
+
createOutput: (coin) => coin,
|
|
270
|
+
isCoinEqual: (a, b) => a.token.nonce === b.token.nonce,
|
|
271
|
+
});
|
|
272
|
+
const recipeInputs = recipe.inputs.map(({ token, value }) => ({ token, value }));
|
|
273
|
+
const newFee = this.dryRunFee(recipeInputs, transactions, secretKey, state, ttl, currentTime, ledgerParams);
|
|
274
|
+
const recipeAmountCoverage = recipeInputs.reduce((sum, input) => sum + input.value, 0n);
|
|
275
|
+
return { currentFee: newFee, recipeInputs, converged: newFee <= recipeAmountCoverage };
|
|
276
|
+
},
|
|
277
|
+
catch: (err) => {
|
|
278
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
279
|
+
return new InsufficientFundsError({
|
|
280
|
+
message: err.message,
|
|
281
|
+
tokenType: err.tokenType,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
return new OtherWalletError({
|
|
286
|
+
message: err instanceof Error ? err.message : 'Dust balancing failed',
|
|
287
|
+
cause: err,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
}), Effect.either, Effect.runSync, Either.map(({ currentFee, recipeInputs }) => ({
|
|
293
|
+
fee: currentFee,
|
|
294
|
+
recipeInputs: distributeFeeAcrossInputs(recipeInputs, currentFee),
|
|
295
|
+
})));
|
|
296
|
+
}
|
|
297
|
+
estimateFee(secretKey, state, transactions, ttl, currentTime, ledgerParams) {
|
|
298
|
+
return pipe(this.computeBalancingRecipe(secretKey, state, transactions, ttl, currentTime, ledgerParams), Either.map(({ fee }) => fee));
|
|
299
|
+
}
|
|
300
|
+
balanceTransactions(secretKey, state, transactions, ttl, currentTime, ledgerParams) {
|
|
301
|
+
const networkId = this.networkId;
|
|
302
|
+
return pipe(this.computeBalancingRecipe(secretKey, state, transactions, ttl, currentTime, ledgerParams), Either.flatMap(({ recipeInputs }) => {
|
|
303
|
+
return LedgerOps.ledgerTry(() => {
|
|
304
|
+
const intent = Intent.new(ttl);
|
|
305
|
+
const [spends, updatedState] = CoreWallet.spendCoins(state, secretKey, recipeInputs, currentTime);
|
|
306
|
+
intent.dustActions = new DustActions(SignatureMarker.signature, ProofMarker.preProof, currentTime, [...spends], []);
|
|
307
|
+
// Merge existing transactions first so we can pick a segment that doesn't collide
|
|
308
|
+
const [first, ...rest] = transactions.map((tx) => tx.eraseProofs());
|
|
309
|
+
const mergedExisting = first ? rest.reduce((acc, tx) => acc.merge(tx), first) : undefined;
|
|
310
|
+
const segmentId = mergedExisting ? Option.getOrElse(findAvailableSegmentId(mergedExisting), () => 1) : 1;
|
|
311
|
+
const feeTransaction = Transaction.fromParts(networkId).addIntent({ tag: 'specific', value: segmentId }, intent);
|
|
312
|
+
return [feeTransaction, updatedState];
|
|
313
|
+
});
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
revertTransaction(state, transaction) {
|
|
317
|
+
return Either.try({
|
|
318
|
+
try: () => CoreWallet.revertTransaction(state, transaction),
|
|
319
|
+
catch: (err) => {
|
|
320
|
+
return new OtherWalletError({
|
|
321
|
+
message: `Error while reverting transaction ${transaction.identifiers().at(0)}`,
|
|
322
|
+
cause: err,
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|