@midnightntwrk/wallet-sdk-shielded 4.0.0-beta.2 → 4.0.0-beta.4
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 +18 -9
- package/dist/Restore.d.ts +40 -0
- package/dist/Restore.js +63 -0
- package/dist/ShieldedWallet.d.ts +248 -62
- package/dist/ShieldedWallet.js +366 -76
- package/dist/ShieldedWalletAPI.d.ts +179 -0
- package/dist/ShieldedWalletAPI.js +75 -0
- package/dist/SingleVariantShieldedWallet.d.ts +35 -0
- package/dist/SingleVariantShieldedWallet.js +240 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/v1/CoinsAndBalances.d.ts +1 -1
- package/dist/v1/CoreWallet.d.ts +48 -1
- package/dist/v1/CoreWallet.js +54 -1
- package/dist/v1/Migration.d.ts +95 -0
- package/dist/v1/Migration.js +82 -0
- package/dist/v1/RunningV1Variant.d.ts +1 -1
- package/dist/v1/RunningV1Variant.js +37 -8
- package/dist/v1/Serialization.js +1 -1
- package/dist/v1/Sync.d.ts +132 -9
- package/dist/v1/Sync.js +272 -36
- package/dist/v1/Transacting.d.ts +1 -1
- package/dist/v1/Transacting.js +3 -2
- package/dist/v1/TransactionHistory.d.ts +9 -2
- package/dist/v1/TransactionHistory.js +26 -5
- package/dist/v1/TransactionOps.d.ts +1 -1
- package/dist/v1/V1Builder.d.ts +87 -26
- package/dist/v1/V1Builder.js +109 -8
- package/dist/v1/WalletError.d.ts +27 -2
- package/dist/v1/WalletError.js +16 -0
- package/dist/v1/index.d.ts +2 -1
- package/dist/v1/index.js +5 -1
- package/dist/v2/CoinsAndBalances.d.ts +24 -0
- package/dist/v2/CoinsAndBalances.js +41 -0
- package/dist/v2/CoreWallet.d.ts +162 -0
- package/dist/v2/CoreWallet.js +305 -0
- package/dist/v2/Keys.d.ts +8 -0
- package/dist/v2/Keys.js +16 -0
- package/dist/v2/Migration.d.ts +112 -0
- package/dist/v2/Migration.js +96 -0
- package/dist/v2/RunningV2Variant.d.ts +40 -0
- package/dist/v2/RunningV2Variant.js +127 -0
- package/dist/v2/Serialization.d.ts +12 -0
- package/dist/v2/Serialization.js +93 -0
- package/dist/v2/Sync.d.ts +208 -0
- package/dist/v2/Sync.js +463 -0
- package/dist/v2/Transacting.d.ts +50 -0
- package/dist/v2/Transacting.js +259 -0
- package/dist/v2/TransactionHistory.d.ts +100 -0
- package/dist/v2/TransactionHistory.js +132 -0
- package/dist/v2/TransactionImbalances.d.ts +10 -0
- package/dist/v2/TransactionImbalances.js +30 -0
- package/dist/v2/TransactionOps.d.ts +17 -0
- package/dist/v2/TransactionOps.js +73 -0
- package/dist/v2/V2Builder.d.ts +154 -0
- package/dist/v2/V2Builder.js +273 -0
- package/dist/v2/WalletError.d.ts +99 -0
- package/dist/v2/WalletError.js +69 -0
- package/dist/v2/index.d.ts +13 -0
- package/dist/v2/index.js +26 -0
- package/package.json +16 -8
|
@@ -0,0 +1,259 @@
|
|
|
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 '@midnightntwrk/ledger-v9';
|
|
14
|
+
import { Array as Arr, Either, Option, pipe, Record } from 'effect';
|
|
15
|
+
import { ArrayOps } from '@midnightntwrk/wallet-sdk-utilities';
|
|
16
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
17
|
+
import { InsufficientFundsError, OtherWalletError } from './WalletError.js';
|
|
18
|
+
import { getBalanceRecipe, Imbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
19
|
+
import { ShieldedCostModel, TransactionImbalances } from './TransactionImbalances.js';
|
|
20
|
+
import { TransactionOps } from './TransactionOps.js';
|
|
21
|
+
export const makeDefaultTransactingCapability = (config, getContext) => {
|
|
22
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionOps.default);
|
|
23
|
+
};
|
|
24
|
+
export const makeSimulatorTransactingCapability = (config, getContext) => {
|
|
25
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionOps.proofErased);
|
|
26
|
+
};
|
|
27
|
+
export class TransactingCapabilityImplementation {
|
|
28
|
+
networkId;
|
|
29
|
+
getCoinSelection;
|
|
30
|
+
txOps;
|
|
31
|
+
getCoins;
|
|
32
|
+
getKeys;
|
|
33
|
+
constructor(networkId, getCoinSelection, getCoins, getKeys, txOps) {
|
|
34
|
+
this.getCoins = getCoins;
|
|
35
|
+
this.networkId = networkId;
|
|
36
|
+
this.getCoinSelection = getCoinSelection;
|
|
37
|
+
this.getKeys = getKeys;
|
|
38
|
+
this.txOps = txOps;
|
|
39
|
+
}
|
|
40
|
+
balanceTransaction(secretKeys, state, tx) {
|
|
41
|
+
return Either.gen(this, function* () {
|
|
42
|
+
const coinSelection = this.getCoinSelection();
|
|
43
|
+
const initialImbalances = this.txOps.getImbalances(tx);
|
|
44
|
+
if (TransactionImbalances.areBalanced(initialImbalances)) {
|
|
45
|
+
return [undefined, state];
|
|
46
|
+
}
|
|
47
|
+
const { newState: afterFallible, fallibleOffers } = yield* this.balanceFallibleSection(secretKeys, state, initialImbalances, coinSelection);
|
|
48
|
+
const { newState: afterGuaranteed, offer: guaranteed } = yield* this.#balanceGuaranteedSection(secretKeys, afterFallible, initialImbalances, coinSelection, Imbalances.empty());
|
|
49
|
+
const balancedTx = Array.from(fallibleOffers.entries()).reduce((tx, [segment, offer]) => tx.addZswapOffer({ tag: 'specific', value: segment }, offer), ledger.Transaction.fromParts(this.networkId, guaranteed));
|
|
50
|
+
return [balancedTx, afterGuaranteed];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
makeTransfer(secretKeys, state, transfers) {
|
|
54
|
+
return Either.gen(this, function* () {
|
|
55
|
+
const positiveTransfers = yield* pipe(transfers, Arr.filter((t) => t.amount > 0n), Arr.match({
|
|
56
|
+
onEmpty: () => Either.left(new OtherWalletError({
|
|
57
|
+
message: 'The amount needs to be positive',
|
|
58
|
+
})),
|
|
59
|
+
onNonEmpty: (nonEmpty) => Either.right(nonEmpty),
|
|
60
|
+
}));
|
|
61
|
+
const networkId = this.networkId;
|
|
62
|
+
const { initialOffersAndCoins, selfCoins } = yield* this.#processDesiredOutputs(state, positiveTransfers);
|
|
63
|
+
const offerToBalance = pipe(initialOffersAndCoins, Arr.map((o) => o.outputOffer), ArrayOps.fold((a, b) => a.merge(b)));
|
|
64
|
+
const unprovenTxToBalance = ledger.Transaction.fromParts(networkId, offerToBalance);
|
|
65
|
+
const imbalances = TransactionOps.unproven.getImbalances(unprovenTxToBalance);
|
|
66
|
+
const { offer, newState } = yield* this.#balanceGuaranteedSection(secretKeys, state, imbalances, this.getCoinSelection(), Imbalances.empty());
|
|
67
|
+
const finalState = CoreWallet.watchCoins(newState, secretKeys, selfCoins);
|
|
68
|
+
const finalTx = unprovenTxToBalance.merge(ledger.Transaction.fromParts(networkId, offer));
|
|
69
|
+
return [finalTx, finalState];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
initSwap(secretKeys, state, desiredInputs, desiredOutputs) {
|
|
73
|
+
return Either.gen(this, function* () {
|
|
74
|
+
const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
|
|
75
|
+
if (!outputsValid) {
|
|
76
|
+
return yield* Either.left(new OtherWalletError({
|
|
77
|
+
message: 'The amount needs to be positive',
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
|
|
81
|
+
if (!inputsValid) {
|
|
82
|
+
return yield* Either.left(new OtherWalletError({
|
|
83
|
+
message: 'The input amounts need to be positive',
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
const outputsParseResult = yield* this.#processDesiredOutputsPossiblyEmpty(state, desiredOutputs);
|
|
87
|
+
const inputsParseResult = Imbalances.fromEntries(Record.toEntries(desiredInputs));
|
|
88
|
+
const networkId = this.networkId;
|
|
89
|
+
const { offer, newState } = yield* this.#balanceGuaranteedSection(secretKeys, state, TransactionImbalances.empty(), this.getCoinSelection(), inputsParseResult);
|
|
90
|
+
const finalState = CoreWallet.watchCoins(newState, secretKeys, outputsParseResult.selfCoins);
|
|
91
|
+
const balancingTx = ledger.Transaction.fromParts(networkId, offer);
|
|
92
|
+
const finalTx = outputsParseResult.unprovenTxToBalance
|
|
93
|
+
? outputsParseResult.unprovenTxToBalance.merge(balancingTx)
|
|
94
|
+
: balancingTx;
|
|
95
|
+
return [finalTx, finalState];
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
revertTransaction(state, transaction) {
|
|
99
|
+
return Either.try({
|
|
100
|
+
try: () => {
|
|
101
|
+
return CoreWallet.revertTransaction(state, transaction);
|
|
102
|
+
},
|
|
103
|
+
catch: (err) => {
|
|
104
|
+
return new OtherWalletError({
|
|
105
|
+
message: `Error while reverting transaction ${transaction.identifiers().at(0)}`,
|
|
106
|
+
cause: err,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
#prepareOffer(secretKeys, state, recipe, segment) {
|
|
112
|
+
const [inputOffers, stateAfterSpends] = CoreWallet.spendCoins(state, secretKeys, recipe.inputs, segment);
|
|
113
|
+
const stateAfterWatches = CoreWallet.watchCoins(stateAfterSpends, secretKeys, recipe.outputs);
|
|
114
|
+
const outputOffers = recipe.outputs.map((coin) => {
|
|
115
|
+
const output = ledger.ZswapOutput.new(coin, segment, this.getKeys().getCoinPublicKey(state).toHexString(), this.getKeys().getEncryptionPublicKey(state).toHexString());
|
|
116
|
+
return ledger.ZswapOffer.fromOutput(output, coin.type, coin.value);
|
|
117
|
+
});
|
|
118
|
+
return pipe(Arr.appendAll(inputOffers, outputOffers), Arr.match({
|
|
119
|
+
onEmpty: () => Option.none(),
|
|
120
|
+
onNonEmpty: (nonEmpty) => pipe(nonEmpty, ArrayOps.fold((a, b) => a.merge(b)), Option.some),
|
|
121
|
+
}), Option.map((offer) => ({ offer, newState: stateAfterWatches })));
|
|
122
|
+
}
|
|
123
|
+
balanceFallibleSection(secretKeys, state, imbalances, coinSelection) {
|
|
124
|
+
return Array.from(imbalances.fallible.entries()).reduce((acc, [segment, segmentImbalances]) => Either.flatMap(acc, ({ fallibleOffers, newState }) => pipe(Either.try({
|
|
125
|
+
try: () => {
|
|
126
|
+
const fallibleBalanceRecipe = getBalanceRecipe({
|
|
127
|
+
coins: this.getCoins()
|
|
128
|
+
.getAvailableCoins(newState)
|
|
129
|
+
.map((c) => c.coin),
|
|
130
|
+
initialImbalances: segmentImbalances,
|
|
131
|
+
transactionCostModel: ShieldedCostModel,
|
|
132
|
+
feeTokenType: '',
|
|
133
|
+
coinSelection,
|
|
134
|
+
createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
|
|
135
|
+
// Distinct coins can share a type and a value; only the nonce tells them apart.
|
|
136
|
+
isCoinEqual: (a, b) => a.nonce === b.nonce,
|
|
137
|
+
});
|
|
138
|
+
return this.#prepareOffer(secretKeys, newState, fallibleBalanceRecipe, segment);
|
|
139
|
+
},
|
|
140
|
+
catch: (err) => {
|
|
141
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
142
|
+
return new InsufficientFundsError({
|
|
143
|
+
message: `Insufficient funds for fallible segment ${segment}`,
|
|
144
|
+
tokenType: err.tokenType,
|
|
145
|
+
amount: segmentImbalances.get(err.tokenType) ?? 0n,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return new OtherWalletError({
|
|
149
|
+
message: `Balancing fallible segment ${segment} failed`,
|
|
150
|
+
cause: err,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
}), Either.map(Option.match({
|
|
154
|
+
onNone: () => ({ fallibleOffers, newState }),
|
|
155
|
+
onSome: (res) => ({
|
|
156
|
+
fallibleOffers: new Map([...fallibleOffers, [segment, res.offer]]),
|
|
157
|
+
newState: res.newState,
|
|
158
|
+
}),
|
|
159
|
+
})))), Either.right({
|
|
160
|
+
fallibleOffers: new Map(),
|
|
161
|
+
newState: state,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
#balanceGuaranteedSection(secretKeys, state, imbalances, coinSelection, targetImbalances) {
|
|
165
|
+
return Either.gen(this, function* () {
|
|
166
|
+
const balanceRecipe = yield* Either.try({
|
|
167
|
+
try: () => getBalanceRecipe({
|
|
168
|
+
coins: this.getCoins()
|
|
169
|
+
.getAvailableCoins(state)
|
|
170
|
+
.map((c) => c.coin),
|
|
171
|
+
initialImbalances: imbalances.guaranteed,
|
|
172
|
+
transactionCostModel: ShieldedCostModel,
|
|
173
|
+
feeTokenType: '',
|
|
174
|
+
coinSelection,
|
|
175
|
+
createOutput: (coin) => ledger.createShieldedCoinInfo(coin.type, coin.value),
|
|
176
|
+
isCoinEqual: (a, b) => a.nonce === b.nonce,
|
|
177
|
+
targetImbalances,
|
|
178
|
+
}),
|
|
179
|
+
catch: (err) => {
|
|
180
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
181
|
+
return new InsufficientFundsError({
|
|
182
|
+
message: 'Insufficient funds',
|
|
183
|
+
tokenType: err.tokenType,
|
|
184
|
+
amount: imbalances.guaranteed.get(err.tokenType) ?? 0n,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
return new OtherWalletError({
|
|
189
|
+
message: 'Balancing guaranteed section failed',
|
|
190
|
+
cause: err,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
// An empty recipe (e.g. the guaranteed section is already balanced) yields no offer.
|
|
196
|
+
// Treat that as "nothing to add" rather than a failure, mirroring the fallible section.
|
|
197
|
+
return pipe(this.#prepareOffer(secretKeys, state, balanceRecipe, 0), Option.match({
|
|
198
|
+
onNone: () => ({ offer: undefined, newState: state }),
|
|
199
|
+
onSome: (result) => result,
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
#processDesiredOutputs(state, transfers) {
|
|
204
|
+
return Either.try({
|
|
205
|
+
try: () => {
|
|
206
|
+
const initialOffersAndCoins = pipe(transfers, Arr.map((transfer) => {
|
|
207
|
+
const { receiverAddress, type, amount } = transfer;
|
|
208
|
+
const coin = ledger.createShieldedCoinInfo(type, amount);
|
|
209
|
+
const output = ledger.ZswapOutput.new(coin, 0, receiverAddress.coinPublicKey.toHexString(), receiverAddress.encryptionPublicKey.toHexString());
|
|
210
|
+
const outputOffer = ledger.ZswapOffer.fromOutput(output, type, amount);
|
|
211
|
+
return {
|
|
212
|
+
coin,
|
|
213
|
+
outputOffer,
|
|
214
|
+
isForSelf: receiverAddress.coinPublicKey.equals(this.getKeys().getCoinPublicKey(state)),
|
|
215
|
+
};
|
|
216
|
+
}));
|
|
217
|
+
const selfCoins = Arr.flatMap(initialOffersAndCoins, ({ coin, isForSelf }) => {
|
|
218
|
+
if (isForSelf) {
|
|
219
|
+
return [coin];
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return { initialOffersAndCoins, selfCoins };
|
|
226
|
+
},
|
|
227
|
+
catch: (err) => {
|
|
228
|
+
return new OtherWalletError({
|
|
229
|
+
message: 'Failed to process desired outputs',
|
|
230
|
+
cause: err,
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
#processDesiredOutputsPossiblyEmpty(state, desiredOutputs) {
|
|
236
|
+
return pipe(desiredOutputs, Arr.match({
|
|
237
|
+
onEmpty: () => {
|
|
238
|
+
return Either.right({
|
|
239
|
+
imbalances: TransactionImbalances.empty(),
|
|
240
|
+
selfCoins: [],
|
|
241
|
+
unprovenTxToBalance: null,
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
onNonEmpty: (desiredOutputs) => {
|
|
245
|
+
return pipe(this.#processDesiredOutputs(state, desiredOutputs), Either.map(({ initialOffersAndCoins, selfCoins }) => {
|
|
246
|
+
const networkId = this.networkId;
|
|
247
|
+
const offerToBalance = pipe(initialOffersAndCoins, Arr.map((o) => o.outputOffer), ArrayOps.fold((a, b) => a.merge(b)));
|
|
248
|
+
const unprovenTxToBalance = ledger.Transaction.fromParts(networkId, offerToBalance);
|
|
249
|
+
const imbalances = TransactionOps.unproven.getImbalances(unprovenTxToBalance);
|
|
250
|
+
return {
|
|
251
|
+
imbalances,
|
|
252
|
+
selfCoins,
|
|
253
|
+
unprovenTxToBalance,
|
|
254
|
+
};
|
|
255
|
+
}));
|
|
256
|
+
},
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { TransactionHistoryStorage } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
2
|
+
import type * as ledger from '@midnightntwrk/ledger-v9';
|
|
3
|
+
import { Duration, Effect, Schema } from 'effect';
|
|
4
|
+
import { TransactionHistoryError } from './WalletError.js';
|
|
5
|
+
export declare const QualifiedShieldedCoinInfoSchema: Schema.Struct<{
|
|
6
|
+
type: typeof Schema.String;
|
|
7
|
+
nonce: typeof Schema.String;
|
|
8
|
+
value: typeof Schema.BigInt;
|
|
9
|
+
mtIndex: typeof Schema.BigInt;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const ShieldedSectionSchema: Schema.Struct<{
|
|
12
|
+
receivedCoins: Schema.Array$<Schema.Struct<{
|
|
13
|
+
type: typeof Schema.String;
|
|
14
|
+
nonce: typeof Schema.String;
|
|
15
|
+
value: typeof Schema.BigInt;
|
|
16
|
+
mtIndex: typeof Schema.BigInt;
|
|
17
|
+
}>>;
|
|
18
|
+
spentCoins: Schema.Array$<Schema.Struct<{
|
|
19
|
+
type: typeof Schema.String;
|
|
20
|
+
nonce: typeof Schema.String;
|
|
21
|
+
value: typeof Schema.BigInt;
|
|
22
|
+
mtIndex: typeof Schema.BigInt;
|
|
23
|
+
}>>;
|
|
24
|
+
}>;
|
|
25
|
+
type ShieldedSection = Schema.Schema.Type<typeof ShieldedSectionSchema>;
|
|
26
|
+
/**
|
|
27
|
+
* Shielded entry schema. Extends the common entry shape with an optional `shielded` section. Tightening — required
|
|
28
|
+
* `shielded` plus required `protocolVersion`/`status` — happens at the writer-input type, not on the stored shape.
|
|
29
|
+
*/
|
|
30
|
+
export declare const ShieldedTransactionHistoryEntrySchema: Schema.Struct<Readonly<{
|
|
31
|
+
hash: typeof Schema.String;
|
|
32
|
+
identifiers: Schema.Array$<typeof Schema.String>;
|
|
33
|
+
protocolVersion: Schema.optional<typeof Schema.Number>;
|
|
34
|
+
status: Schema.optional<Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>>;
|
|
35
|
+
timestamp: Schema.optional<typeof Schema.Date>;
|
|
36
|
+
fees: Schema.optional<Schema.NullOr<typeof Schema.BigInt>>;
|
|
37
|
+
lifecycle: Schema.Union<[Schema.Struct<{
|
|
38
|
+
status: Schema.Literal<["pending"]>;
|
|
39
|
+
submittedAt: typeof Schema.Date;
|
|
40
|
+
}>, Schema.Struct<{
|
|
41
|
+
status: Schema.Literal<["finalized"]>;
|
|
42
|
+
finalizedBlock: Schema.Struct<{
|
|
43
|
+
hash: typeof Schema.String;
|
|
44
|
+
height: typeof Schema.Number;
|
|
45
|
+
timestamp: typeof Schema.Date;
|
|
46
|
+
}>;
|
|
47
|
+
}>, Schema.Struct<{
|
|
48
|
+
status: Schema.Literal<["rejected"]>;
|
|
49
|
+
rejectedAt: typeof Schema.Date;
|
|
50
|
+
reason: Schema.optional<typeof Schema.String>;
|
|
51
|
+
}>]>;
|
|
52
|
+
}> & {
|
|
53
|
+
shielded: Schema.optional<Schema.Struct<{
|
|
54
|
+
receivedCoins: Schema.Array$<Schema.Struct<{
|
|
55
|
+
type: typeof Schema.String;
|
|
56
|
+
nonce: typeof Schema.String;
|
|
57
|
+
value: typeof Schema.BigInt;
|
|
58
|
+
mtIndex: typeof Schema.BigInt;
|
|
59
|
+
}>>;
|
|
60
|
+
spentCoins: Schema.Array$<Schema.Struct<{
|
|
61
|
+
type: typeof Schema.String;
|
|
62
|
+
nonce: typeof Schema.String;
|
|
63
|
+
value: typeof Schema.BigInt;
|
|
64
|
+
mtIndex: typeof Schema.BigInt;
|
|
65
|
+
}>>;
|
|
66
|
+
}>>;
|
|
67
|
+
}>;
|
|
68
|
+
export type ShieldedTransactionHistoryEntry = Schema.Schema.Type<typeof ShieldedTransactionHistoryEntrySchema>;
|
|
69
|
+
export type ShieldedHistoryStorage = TransactionHistoryStorage.TransactionHistoryReader<TransactionHistoryStorage.TransactionHistoryEntryWithHash> & TransactionHistoryStorage.TransactionHistoryWriter<ShieldedTransactionHistoryEntry>;
|
|
70
|
+
export type DefaultTransactionHistoryConfiguration = {
|
|
71
|
+
txHistoryStorage: ShieldedHistoryStorage;
|
|
72
|
+
indexerClientConnection: {
|
|
73
|
+
indexerHttpUrl: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* How long to keep re-querying the indexer for a transaction's details before giving up, when the WS event arrives
|
|
77
|
+
* ahead of HTTP ingestion. This is a bounded window, not a guarantee: if the indexer lags beyond it the shielded
|
|
78
|
+
* section is lost (the change is not re-processed, even across restarts) and the failure is logged. Default: 2
|
|
79
|
+
* minutes.
|
|
80
|
+
*/
|
|
81
|
+
transactionDetailsRetryWindow?: Duration.DurationInput;
|
|
82
|
+
};
|
|
83
|
+
export type TransactionDetails = {
|
|
84
|
+
hash: string;
|
|
85
|
+
block: {
|
|
86
|
+
hash: string;
|
|
87
|
+
height: number;
|
|
88
|
+
timestamp: number;
|
|
89
|
+
};
|
|
90
|
+
status: 'SUCCESS' | 'FAILURE' | 'PARTIAL_SUCCESS';
|
|
91
|
+
identifiers: readonly string[];
|
|
92
|
+
};
|
|
93
|
+
export type TransactionHistoryService = {
|
|
94
|
+
put(changes: ledger.ZswapStateChanges, metadata: TransactionDetails, protocolVersion: number): Effect.Effect<void, TransactionHistoryError>;
|
|
95
|
+
getTransactionDetails(hash: TransactionHistoryStorage.TransactionHash): Effect.Effect<TransactionDetails, TransactionHistoryError>;
|
|
96
|
+
};
|
|
97
|
+
export declare const mergeShieldedSections: (existing: ShieldedSection, incoming: ShieldedSection) => ShieldedSection;
|
|
98
|
+
export declare const makeDefaultTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
|
|
99
|
+
export declare const makeSimulatorTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
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 { TransactionHistoryStorage } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
14
|
+
import { Duration, Array as EArray, Effect, Schedule, Schema } from 'effect';
|
|
15
|
+
import { TransactionHistoryDetail } from '@midnightntwrk/wallet-sdk-indexer-client';
|
|
16
|
+
import { HttpQueryClient } from '@midnightntwrk/wallet-sdk-indexer-client/effect';
|
|
17
|
+
import { TransactionHistoryError } from './WalletError.js';
|
|
18
|
+
export const QualifiedShieldedCoinInfoSchema = Schema.Struct({
|
|
19
|
+
type: Schema.String,
|
|
20
|
+
nonce: Schema.String,
|
|
21
|
+
value: Schema.BigInt,
|
|
22
|
+
mtIndex: Schema.BigInt,
|
|
23
|
+
});
|
|
24
|
+
export const ShieldedSectionSchema = Schema.Struct({
|
|
25
|
+
receivedCoins: Schema.Array(QualifiedShieldedCoinInfoSchema),
|
|
26
|
+
spentCoins: Schema.Array(QualifiedShieldedCoinInfoSchema),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Shielded entry schema. Extends the common entry shape with an optional `shielded` section. Tightening — required
|
|
30
|
+
* `shielded` plus required `protocolVersion`/`status` — happens at the writer-input type, not on the stored shape.
|
|
31
|
+
*/
|
|
32
|
+
export const ShieldedTransactionHistoryEntrySchema = TransactionHistoryStorage.extendEntrySchema({
|
|
33
|
+
shielded: Schema.optional(ShieldedSectionSchema),
|
|
34
|
+
});
|
|
35
|
+
const coinEquals = Schema.equivalence(QualifiedShieldedCoinInfoSchema);
|
|
36
|
+
const isFinalized = (entry) => entry !== undefined && entry.lifecycle.status === 'finalized';
|
|
37
|
+
export const mergeShieldedSections = (existing, incoming) => ({
|
|
38
|
+
receivedCoins: EArray.unionWith(existing.receivedCoins, incoming.receivedCoins, coinEquals),
|
|
39
|
+
spentCoins: EArray.unionWith(existing.spentCoins, incoming.spentCoins, coinEquals),
|
|
40
|
+
});
|
|
41
|
+
const convertUpdateToFinalizedInput = (changes, metadata, protocolVersion) => ({
|
|
42
|
+
hash: changes.source,
|
|
43
|
+
protocolVersion,
|
|
44
|
+
status: metadata.status,
|
|
45
|
+
identifiers: metadata.identifiers,
|
|
46
|
+
finalizedBlock: {
|
|
47
|
+
hash: metadata.block.hash,
|
|
48
|
+
height: metadata.block.height,
|
|
49
|
+
timestamp: new Date(metadata.block.timestamp),
|
|
50
|
+
},
|
|
51
|
+
shielded: {
|
|
52
|
+
receivedCoins: changes.receivedCoins.map(({ mt_index, ...rest }) => ({ ...rest, mtIndex: mt_index })),
|
|
53
|
+
spentCoins: changes.spentCoins.map(({ mt_index, ...rest }) => ({ ...rest, mtIndex: mt_index })),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const gotFinalizedShielded = (txHistoryStorage, input) => Effect.tryPromise({
|
|
57
|
+
try: () => txHistoryStorage.gotFinalized(input),
|
|
58
|
+
catch: (e) => new TransactionHistoryError({ message: `Failed to record finalized history entry for ${input.hash}`, cause: e }),
|
|
59
|
+
});
|
|
60
|
+
export const makeDefaultTransactionHistoryService = (config, _getContext) => {
|
|
61
|
+
const txHistoryStorage = config.txHistoryStorage;
|
|
62
|
+
const queryClientLayer = HttpQueryClient.layer({ url: config.indexerClientConnection.indexerHttpUrl });
|
|
63
|
+
const retryWindow = config.transactionDetailsRetryWindow ?? Duration.minutes(2);
|
|
64
|
+
return {
|
|
65
|
+
put: (changes, metadata, protocolVersion) => gotFinalizedShielded(txHistoryStorage, convertUpdateToFinalizedInput(changes, metadata, protocolVersion)),
|
|
66
|
+
getTransactionDetails: (hash) => Effect.gen(function* () {
|
|
67
|
+
const statusQuery = yield* TransactionHistoryDetail;
|
|
68
|
+
const result = yield* statusQuery({ transactionHash: hash });
|
|
69
|
+
// The WS ZswapEvents stream can deliver a relevant event before the indexer's HTTP `transactions(...)`
|
|
70
|
+
// endpoint has ingested the same hash — a normal race on any deployed network. In that window the array
|
|
71
|
+
// is empty; fail with a *typed* error so the retry schedule below engages and re-queries once the indexer
|
|
72
|
+
// catches up (rather than dereferencing `undefined` and dying with an unretriable defect).
|
|
73
|
+
const tx = yield* EArray.head(result.transactions).pipe(Effect.orElseFail(() => new TransactionHistoryError({ message: `Indexer has not yet indexed transaction ${hash}` })));
|
|
74
|
+
const rawStatus = tx.__typename === 'RegularTransaction' ? tx.transactionResult.status : undefined;
|
|
75
|
+
const status = rawStatus === 'FAILURE' || rawStatus === 'PARTIAL_SUCCESS' ? rawStatus : 'SUCCESS';
|
|
76
|
+
const identifiers = tx.__typename === 'RegularTransaction' ? tx.identifiers : [];
|
|
77
|
+
return {
|
|
78
|
+
hash: tx.hash,
|
|
79
|
+
block: {
|
|
80
|
+
hash: tx.block.hash,
|
|
81
|
+
height: tx.block.height,
|
|
82
|
+
timestamp: tx.block.timestamp,
|
|
83
|
+
},
|
|
84
|
+
status,
|
|
85
|
+
identifiers,
|
|
86
|
+
};
|
|
87
|
+
}).pipe(Effect.provide(queryClientLayer), Effect.scoped,
|
|
88
|
+
// Retry for a bounded window (default 2 min) while the indexer catches up. The exponential delay is capped
|
|
89
|
+
// at 10s (`either` takes the earlier of the two schedules) so probes keep landing every ≤10s late in the
|
|
90
|
+
// window — uncapped doubling would leave minute-long gaps and overshoot the window. Jitter the delays so a
|
|
91
|
+
// batch of concurrent lookups that all hit the indexer-lag race don't retry in lockstep against an
|
|
92
|
+
// already-behind indexer. Beyond the window we give up — the change is not re-processed, so the caller logs
|
|
93
|
+
// the loss. Only the typed "not yet indexed" failure is transient; anything else (bad URL, 4xx, schema
|
|
94
|
+
// mismatch) cannot succeed by waiting, so it fails fast instead of holding a fan-out slot for the whole
|
|
95
|
+
// window.
|
|
96
|
+
Effect.retry({
|
|
97
|
+
schedule: Schedule.exponential(Duration.seconds(1)).pipe(Schedule.either(Schedule.spaced(Duration.seconds(10))), Schedule.jittered, Schedule.upTo(retryWindow)),
|
|
98
|
+
while: (error) => error instanceof TransactionHistoryError,
|
|
99
|
+
}),
|
|
100
|
+
// Let our own "not yet indexed" error through untouched; only wrap the indexer query's ClientError/ServerError.
|
|
101
|
+
Effect.mapError((error) => error instanceof TransactionHistoryError
|
|
102
|
+
? error
|
|
103
|
+
: new TransactionHistoryError({
|
|
104
|
+
message: `Failed to fetch transaction metadata for ${hash}`,
|
|
105
|
+
cause: error,
|
|
106
|
+
}))),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export const makeSimulatorTransactionHistoryService = (config, _getContext) => {
|
|
110
|
+
const txHistoryStorage = config.txHistoryStorage;
|
|
111
|
+
return {
|
|
112
|
+
put: (changes, metadata, protocolVersion) => {
|
|
113
|
+
const input = convertUpdateToFinalizedInput(changes, metadata, protocolVersion);
|
|
114
|
+
return gotFinalizedShielded(txHistoryStorage, { ...input, timestamp: input.finalizedBlock.timestamp });
|
|
115
|
+
},
|
|
116
|
+
getTransactionDetails: (hash) => Effect.tryPromise({
|
|
117
|
+
try: () => txHistoryStorage.get(hash),
|
|
118
|
+
catch: (e) => new TransactionHistoryError({ message: `Failed to get transaction details for ${hash}`, cause: e }),
|
|
119
|
+
}).pipe(Effect.flatMap((entry) => isFinalized(entry)
|
|
120
|
+
? Effect.succeed({
|
|
121
|
+
hash: entry.hash,
|
|
122
|
+
block: {
|
|
123
|
+
hash: entry.lifecycle.finalizedBlock.hash,
|
|
124
|
+
height: entry.lifecycle.finalizedBlock.height,
|
|
125
|
+
timestamp: entry.lifecycle.finalizedBlock.timestamp.getTime(),
|
|
126
|
+
},
|
|
127
|
+
status: entry.status ?? 'SUCCESS',
|
|
128
|
+
identifiers: entry.identifiers,
|
|
129
|
+
})
|
|
130
|
+
: Effect.fail(new TransactionHistoryError({ message: `No transaction found in storage for hash: ${hash}` })))),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Imbalances, type TransactionCostModel } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
2
|
+
export declare const ShieldedCostModel: TransactionCostModel;
|
|
3
|
+
export type TransactionImbalances = Readonly<{
|
|
4
|
+
guaranteed: Imbalances;
|
|
5
|
+
fallible: ReadonlyMap<number, Imbalances>;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const TransactionImbalances: {
|
|
8
|
+
empty: () => TransactionImbalances;
|
|
9
|
+
areBalanced: (imbalances: TransactionImbalances) => boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
14
|
+
export const ShieldedCostModel = {
|
|
15
|
+
inputFeeOverhead: 0n,
|
|
16
|
+
outputFeeOverhead: 0n,
|
|
17
|
+
};
|
|
18
|
+
export const TransactionImbalances = new (class {
|
|
19
|
+
empty = () => {
|
|
20
|
+
return {
|
|
21
|
+
guaranteed: Imbalances.empty(),
|
|
22
|
+
fallible: new Map(),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
areBalanced = (imbalances) => {
|
|
26
|
+
const areGuaranteedAllZeroes = imbalances.guaranteed.entries().every(([, value]) => value === 0n);
|
|
27
|
+
const areFallibleAllZeroes = Array.from(imbalances.fallible.values()).every((segmentImbalances) => segmentImbalances.entries().every(([, value]) => value === 0n));
|
|
28
|
+
return areGuaranteedAllZeroes && areFallibleAllZeroes;
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
2
|
+
import { type TransactionImbalances } from './TransactionImbalances.js';
|
|
3
|
+
import type * as ledger from '@midnightntwrk/ledger-v9';
|
|
4
|
+
export type TransactionOps<Tx> = {
|
|
5
|
+
getImbalances(tx: Tx): TransactionImbalances;
|
|
6
|
+
id(tx: Tx): string;
|
|
7
|
+
};
|
|
8
|
+
export declare const TransactionOps: {
|
|
9
|
+
default: TransactionOps<ledger.FinalizedTransaction>;
|
|
10
|
+
proofErased: TransactionOps<ledger.ProofErasedTransaction>;
|
|
11
|
+
unproven: TransactionOps<ledger.UnprovenTransaction>;
|
|
12
|
+
shared: {
|
|
13
|
+
getImbalances(tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction): TransactionImbalances;
|
|
14
|
+
getGuaranteedImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => Imbalances;
|
|
15
|
+
getFallibleImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => ReadonlyMap<number, Imbalances>;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
14
|
+
export const TransactionOps = new (class {
|
|
15
|
+
default = {
|
|
16
|
+
getImbalances(tx) {
|
|
17
|
+
return TransactionOps.shared.getImbalances(tx);
|
|
18
|
+
},
|
|
19
|
+
id(tx) {
|
|
20
|
+
return tx.identifiers().at(0);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
proofErased = {
|
|
24
|
+
getImbalances(tx) {
|
|
25
|
+
return TransactionOps.shared.getImbalances(tx);
|
|
26
|
+
},
|
|
27
|
+
id(tx) {
|
|
28
|
+
return tx.identifiers().at(0);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
unproven = {
|
|
32
|
+
getImbalances(tx) {
|
|
33
|
+
return TransactionOps.shared.getImbalances(tx);
|
|
34
|
+
},
|
|
35
|
+
id(tx) {
|
|
36
|
+
return tx.identifiers().at(0);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
shared = {
|
|
40
|
+
getImbalances(tx) {
|
|
41
|
+
const guaranteedImbalances = TransactionOps.shared.getGuaranteedImbalances(tx);
|
|
42
|
+
const fallibleImbalances = TransactionOps.shared.getFallibleImbalances(tx);
|
|
43
|
+
return {
|
|
44
|
+
guaranteed: guaranteedImbalances,
|
|
45
|
+
fallible: fallibleImbalances,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
getGuaranteedImbalances: (tx) => {
|
|
49
|
+
const rawGuaranteedImbalances = tx
|
|
50
|
+
.imbalances(0)
|
|
51
|
+
.entries()
|
|
52
|
+
.filter(([token]) => token.tag === 'shielded')
|
|
53
|
+
.map(([token, value]) => {
|
|
54
|
+
return [token.raw, value];
|
|
55
|
+
});
|
|
56
|
+
return Imbalances.fromEntries(rawGuaranteedImbalances);
|
|
57
|
+
},
|
|
58
|
+
getFallibleImbalances: (tx) => {
|
|
59
|
+
const segments = Array.from(tx.fallibleOffer?.keys() ?? []);
|
|
60
|
+
const fallibleImbalances = segments.flatMap((segment) => {
|
|
61
|
+
const rawImbalances = tx
|
|
62
|
+
.imbalances(segment)
|
|
63
|
+
.entries()
|
|
64
|
+
.filter(([token]) => token.tag === 'shielded')
|
|
65
|
+
.map(([token, value]) => {
|
|
66
|
+
return [token.raw, value];
|
|
67
|
+
});
|
|
68
|
+
return [[segment, Imbalances.fromEntries(rawImbalances)]];
|
|
69
|
+
});
|
|
70
|
+
return new Map(fallibleImbalances);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
})();
|