@midnightntwrk/wallet-sdk-unshielded-wallet 3.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 +123 -0
- package/dist/KeyStore.d.ts +19 -0
- package/dist/KeyStore.js +37 -0
- package/dist/UnshieldedWallet.d.ts +72 -0
- package/dist/UnshieldedWallet.js +156 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +15 -0
- package/dist/v1/CoinsAndBalances.d.ts +13 -0
- package/dist/v1/CoinsAndBalances.js +36 -0
- package/dist/v1/CoreWallet.d.ts +24 -0
- package/dist/v1/CoreWallet.js +60 -0
- package/dist/v1/Keys.d.ts +8 -0
- package/dist/v1/Keys.js +23 -0
- package/dist/v1/RunningV1Variant.d.ts +48 -0
- package/dist/v1/RunningV1Variant.js +114 -0
- package/dist/v1/Serialization.d.ts +12 -0
- package/dist/v1/Serialization.js +64 -0
- package/dist/v1/Sync.d.ts +46 -0
- package/dist/v1/Sync.js +137 -0
- package/dist/v1/SyncProgress.d.ts +18 -0
- package/dist/v1/SyncProgress.js +23 -0
- package/dist/v1/SyncSchema.d.ts +447 -0
- package/dist/v1/SyncSchema.js +114 -0
- package/dist/v1/Transacting.d.ts +125 -0
- package/dist/v1/Transacting.js +385 -0
- package/dist/v1/TransactionHistory.d.ts +54 -0
- package/dist/v1/TransactionHistory.js +66 -0
- package/dist/v1/TransactionOps.d.ts +19 -0
- package/dist/v1/TransactionOps.js +98 -0
- package/dist/v1/UnshieldedState.d.ts +37 -0
- package/dist/v1/UnshieldedState.js +67 -0
- package/dist/v1/V1Builder.d.ts +91 -0
- package/dist/v1/V1Builder.js +149 -0
- package/dist/v1/WalletError.d.ts +94 -0
- package/dist/v1/WalletError.js +35 -0
- package/dist/v1/index.d.ts +14 -0
- package/dist/v1/index.js +26 -0
- package/package.json +57 -0
|
@@ -0,0 +1,385 @@
|
|
|
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 { Either, Option, pipe, Array as Arr } from 'effect';
|
|
15
|
+
import { CoreWallet } from './CoreWallet.js';
|
|
16
|
+
import { InsufficientFundsError, OtherWalletError, TransactingError } from './WalletError.js';
|
|
17
|
+
import { getBalanceRecipe, Imbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
18
|
+
import { TransactionOps } from './TransactionOps.js';
|
|
19
|
+
const GUARANTEED_SEGMENT = 0;
|
|
20
|
+
export const makeDefaultTransactingCapability = (config, getContext) => {
|
|
21
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionOps);
|
|
22
|
+
};
|
|
23
|
+
export class TransactingCapabilityImplementation {
|
|
24
|
+
networkId;
|
|
25
|
+
getCoinSelection;
|
|
26
|
+
txOps;
|
|
27
|
+
getCoins;
|
|
28
|
+
getKeys;
|
|
29
|
+
constructor(networkId, getCoinSelection, getCoins, getKeys, txOps) {
|
|
30
|
+
this.getCoins = getCoins;
|
|
31
|
+
this.networkId = networkId;
|
|
32
|
+
this.getCoinSelection = getCoinSelection;
|
|
33
|
+
this.getKeys = getKeys;
|
|
34
|
+
this.txOps = txOps;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Balances an unbound transaction Note: Unbound transactions are balanced in place and returned
|
|
38
|
+
*
|
|
39
|
+
* @param wallet - The wallet to balance the transaction with
|
|
40
|
+
* @param transaction - The transaction to balance
|
|
41
|
+
* @returns The balanced transaction and the new wallet state if successful, otherwise an error
|
|
42
|
+
*/
|
|
43
|
+
balanceUnboundTransaction(wallet, transaction) {
|
|
44
|
+
return this.#balanceUnboundishTransaction(wallet, transaction);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Balances an unproven transaction Note: This method does the same thing as balanceUnboundTransaction but is provided
|
|
48
|
+
* for convenience and type safety
|
|
49
|
+
*
|
|
50
|
+
* @param wallet - The wallet to balance the transaction with
|
|
51
|
+
* @param transaction - The transaction to balance
|
|
52
|
+
* @returns The balanced transaction and the new wallet state if successful, otherwise an error
|
|
53
|
+
*/
|
|
54
|
+
balanceUnprovenTransaction(wallet, transaction) {
|
|
55
|
+
return this.#balanceUnboundishTransaction(wallet, transaction);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Balances a bound transaction Note: In bound transactions we can only balance the guaranteed section in intents
|
|
59
|
+
*
|
|
60
|
+
* @param wallet - The wallet to balance the transaction with
|
|
61
|
+
* @param transaction - The transaction to balance
|
|
62
|
+
* @returns A balancing counterpart transaction (which should be merged with the original transaction ) and the new
|
|
63
|
+
* wallet state if successful, otherwise an error
|
|
64
|
+
*/
|
|
65
|
+
balanceFinalizedTransaction(wallet, transaction) {
|
|
66
|
+
return Either.gen(this, function* () {
|
|
67
|
+
// Ensure all intents are bound
|
|
68
|
+
const segments = this.txOps.getSegments(transaction);
|
|
69
|
+
for (const segment of segments) {
|
|
70
|
+
const intent = transaction.intents?.get(segment);
|
|
71
|
+
const isBound = this.txOps.isIntentBound(intent);
|
|
72
|
+
if (!isBound) {
|
|
73
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} is not bound` }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// get the first intent so we can use its ttl to create the balancing intent
|
|
77
|
+
const intent = transaction.intents?.get(segments[0]);
|
|
78
|
+
const imbalances = this.txOps.getImbalances(transaction, GUARANTEED_SEGMENT);
|
|
79
|
+
// guaranteed section is balanced
|
|
80
|
+
if (imbalances.size === 0) {
|
|
81
|
+
return [undefined, wallet];
|
|
82
|
+
}
|
|
83
|
+
const recipe = yield* this.#balanceSegment(wallet, imbalances, Imbalances.empty(), this.getCoinSelection());
|
|
84
|
+
const { newState, offer } = yield* this.#prepareOffer(wallet, recipe);
|
|
85
|
+
const balancingIntent = ledger.Intent.new(intent.ttl);
|
|
86
|
+
balancingIntent.guaranteedUnshieldedOffer = offer;
|
|
87
|
+
const segmentId = Option.getOrElse(this.txOps.findAvailableSegmentId(transaction), () => 1);
|
|
88
|
+
const balancingTx = ledger.Transaction.fromParts(this.networkId).addIntent({ tag: 'specific', value: segmentId }, balancingIntent);
|
|
89
|
+
return [balancingTx, newState];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Makes a transfer transaction
|
|
94
|
+
*
|
|
95
|
+
* @param wallet - The wallet to make the transfer with
|
|
96
|
+
* @param outputs - The outputs for the transfer
|
|
97
|
+
* @param ttl - The TTL for the transaction
|
|
98
|
+
* @returns The balanced transfer transaction and the new wallet state if successful, otherwise an error
|
|
99
|
+
*/
|
|
100
|
+
makeTransfer(wallet, outputs, ttl) {
|
|
101
|
+
return Either.gen(this, function* () {
|
|
102
|
+
const { networkId } = this;
|
|
103
|
+
const isValid = outputs.every((output) => output.amount > 0n);
|
|
104
|
+
if (!isValid) {
|
|
105
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all inputs needs to be positive' }));
|
|
106
|
+
}
|
|
107
|
+
const ledgerOutputs = outputs.map((output) => {
|
|
108
|
+
return {
|
|
109
|
+
value: output.amount,
|
|
110
|
+
owner: output.receiverAddress.data.toString('hex'),
|
|
111
|
+
type: output.type,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
const recipe = yield* this.#balanceSegment(wallet, Imbalances.empty(), Imbalances.fromEntries(ledgerOutputs.map((output) => [output.type, output.value])), this.getCoinSelection());
|
|
115
|
+
const { newState, offer } = yield* this.#prepareOffer(wallet, {
|
|
116
|
+
inputs: recipe.inputs,
|
|
117
|
+
outputs: [...recipe.outputs, ...ledgerOutputs],
|
|
118
|
+
});
|
|
119
|
+
const intent = ledger.Intent.new(ttl);
|
|
120
|
+
const hasNightOutput = ledgerOutputs.some((output) => output.type === ledger.nativeToken().raw);
|
|
121
|
+
if (hasNightOutput) {
|
|
122
|
+
intent.fallibleUnshieldedOffer = offer;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
intent.guaranteedUnshieldedOffer = offer;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
newState,
|
|
129
|
+
transaction: ledger.Transaction.fromParts(networkId, undefined, undefined, intent),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
rotateUtxos(wallet, guaranteedUtxos, fallibleUtxos, nightVerifyingKey, ttl) {
|
|
134
|
+
return Either.gen(this, function* () {
|
|
135
|
+
const { networkId } = this;
|
|
136
|
+
if (guaranteedUtxos.length === 0 && fallibleUtxos.length === 0) {
|
|
137
|
+
return yield* Either.left(new TransactingError({ message: 'At least one UTxO must be provided to rotateUtxos' }));
|
|
138
|
+
}
|
|
139
|
+
const ownerAddress = ledger.addressFromKey(nightVerifyingKey);
|
|
140
|
+
const makeOffer = (utxos) => {
|
|
141
|
+
if (utxos.length === 0) {
|
|
142
|
+
return Option.none();
|
|
143
|
+
}
|
|
144
|
+
const inputs = utxos.map(({ utxo }) => ({
|
|
145
|
+
value: utxo.value,
|
|
146
|
+
type: utxo.type,
|
|
147
|
+
intentHash: utxo.intentHash,
|
|
148
|
+
outputNo: utxo.outputNo,
|
|
149
|
+
owner: nightVerifyingKey,
|
|
150
|
+
}));
|
|
151
|
+
const totalValue = pipe(utxos, Arr.map(({ utxo }) => utxo.value), Arr.reduce(0n, (a, b) => a + b));
|
|
152
|
+
const output = {
|
|
153
|
+
owner: ownerAddress,
|
|
154
|
+
type: ledger.nativeToken().raw,
|
|
155
|
+
value: totalValue,
|
|
156
|
+
};
|
|
157
|
+
return Option.some(ledger.UnshieldedOffer.new(inputs, [output], []));
|
|
158
|
+
};
|
|
159
|
+
const allUtxos = [...guaranteedUtxos, ...fallibleUtxos].map(({ utxo }) => utxo);
|
|
160
|
+
const [, walletAfterBooking] = yield* CoreWallet.spendUtxos(wallet, allUtxos);
|
|
161
|
+
const guaranteedOffer = makeOffer(guaranteedUtxos);
|
|
162
|
+
const fallibleOffer = makeOffer(fallibleUtxos);
|
|
163
|
+
const intent = pipe(ledger.Intent.new(ttl), (i) => Option.match(guaranteedOffer, {
|
|
164
|
+
onNone: () => i,
|
|
165
|
+
onSome: (offer) => {
|
|
166
|
+
i.guaranteedUnshieldedOffer = offer;
|
|
167
|
+
return i;
|
|
168
|
+
},
|
|
169
|
+
}), (i) => Option.match(fallibleOffer, {
|
|
170
|
+
onNone: () => i,
|
|
171
|
+
onSome: (offer) => {
|
|
172
|
+
i.fallibleUnshieldedOffer = offer;
|
|
173
|
+
return i;
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
const transaction = ledger.Transaction.fromParts(networkId, undefined, undefined, intent);
|
|
177
|
+
return {
|
|
178
|
+
newState: walletAfterBooking,
|
|
179
|
+
transaction,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Initializes a swap transaction
|
|
185
|
+
*
|
|
186
|
+
* @param wallet - The wallet to initialize the swap for
|
|
187
|
+
* @param desiredInputs - The desired inputs for the swap
|
|
188
|
+
* @param desiredOutputs - The desired outputs for the swap
|
|
189
|
+
* @param ttl - The TTL for the swap
|
|
190
|
+
* @returns The initialized swap transaction and the new wallet state if successful, otherwise an error
|
|
191
|
+
*/
|
|
192
|
+
initSwap(wallet, desiredInputs, desiredOutputs, ttl) {
|
|
193
|
+
return Either.gen(this, function* () {
|
|
194
|
+
const { networkId } = this;
|
|
195
|
+
const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
|
|
196
|
+
if (!outputsValid) {
|
|
197
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all outputs needs to be positive' }));
|
|
198
|
+
}
|
|
199
|
+
const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
|
|
200
|
+
if (!inputsValid) {
|
|
201
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all inputs needs to be positive' }));
|
|
202
|
+
}
|
|
203
|
+
const ledgerOutputs = desiredOutputs.map((output) => ({
|
|
204
|
+
value: output.amount,
|
|
205
|
+
owner: output.receiverAddress.data.toString('hex'),
|
|
206
|
+
type: output.type,
|
|
207
|
+
}));
|
|
208
|
+
const targetImbalances = Imbalances.fromEntries(Object.entries(desiredInputs));
|
|
209
|
+
const recipe = yield* this.#balanceSegment(wallet, Imbalances.empty(), targetImbalances, this.getCoinSelection());
|
|
210
|
+
const { newState, offer } = yield* this.#prepareOffer(wallet, {
|
|
211
|
+
inputs: recipe.inputs,
|
|
212
|
+
outputs: [...recipe.outputs, ...ledgerOutputs],
|
|
213
|
+
});
|
|
214
|
+
const intent = ledger.Intent.new(ttl);
|
|
215
|
+
intent.guaranteedUnshieldedOffer = offer;
|
|
216
|
+
const tx = ledger.Transaction.fromParts(networkId, undefined, undefined, intent);
|
|
217
|
+
return {
|
|
218
|
+
newState,
|
|
219
|
+
transaction: tx,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
signUnprovenTransaction(transaction, signSegment) {
|
|
224
|
+
return this.#signTransactionInternal(transaction, signSegment);
|
|
225
|
+
}
|
|
226
|
+
signUnboundTransaction(transaction, signSegment) {
|
|
227
|
+
return this.#signTransactionInternal(transaction, signSegment);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Internal method to sign either an unproven or unbound transaction
|
|
231
|
+
*
|
|
232
|
+
* @param transaction - The transaction to sign
|
|
233
|
+
* @param signSegment - The signing function
|
|
234
|
+
* @returns The signed transaction if successful, otherwise an error
|
|
235
|
+
*/
|
|
236
|
+
#signTransactionInternal(transaction, signSegment) {
|
|
237
|
+
return Either.gen(this, function* () {
|
|
238
|
+
const segments = this.txOps.getSegments(transaction);
|
|
239
|
+
for (const segment of segments) {
|
|
240
|
+
const signedData = yield* this.txOps.getSignatureData(transaction, segment);
|
|
241
|
+
const signature = signSegment(signedData);
|
|
242
|
+
transaction = yield* this.txOps.addSignature(transaction, signature, segment);
|
|
243
|
+
}
|
|
244
|
+
return transaction;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Reverts a transaction by rolling back all inputs owned by this wallet
|
|
249
|
+
*
|
|
250
|
+
* @param wallet - The wallet to revert the transaction for
|
|
251
|
+
* @param transaction - The transaction to revert (can be FinalizedTransaction, UnboundTransaction, or
|
|
252
|
+
* UnprovenTransaction)
|
|
253
|
+
* @returns The updated wallet with rolled back UTXOs if successful, otherwise an error
|
|
254
|
+
*/
|
|
255
|
+
revertTransaction(wallet, transaction) {
|
|
256
|
+
return pipe(this.txOps.extractOwnInputs(transaction, wallet.publicKey.publicKey), Arr.reduce(Either.right(wallet), (walletAcc, utxo) => pipe(walletAcc, Either.flatMap((w) => CoreWallet.rollbackUtxo(w, utxo)))));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Balances a segment of a transaction
|
|
260
|
+
*
|
|
261
|
+
* @param wallet - The wallet to balance the segment for
|
|
262
|
+
* @param imbalances - The imbalances to balance the segment for
|
|
263
|
+
* @param targetImbalances - The target imbalances to balance the segment for
|
|
264
|
+
* @param coinSelection - The coin selection to use for the balance recipe
|
|
265
|
+
* @returns The balance recipe if successful, otherwise an error
|
|
266
|
+
*/
|
|
267
|
+
#balanceSegment(wallet, imbalances, targetImbalances, coinSelection) {
|
|
268
|
+
return Either.try({
|
|
269
|
+
try: () => getBalanceRecipe({
|
|
270
|
+
coins: this.getCoins()
|
|
271
|
+
.getAvailableCoins(wallet)
|
|
272
|
+
.map(({ utxo }) => utxo),
|
|
273
|
+
initialImbalances: imbalances,
|
|
274
|
+
feeTokenType: '',
|
|
275
|
+
transactionCostModel: {
|
|
276
|
+
inputFeeOverhead: 0n,
|
|
277
|
+
outputFeeOverhead: 0n,
|
|
278
|
+
},
|
|
279
|
+
coinSelection,
|
|
280
|
+
createOutput: (coin) => ({
|
|
281
|
+
...coin,
|
|
282
|
+
owner: wallet.publicKey.addressHex,
|
|
283
|
+
}),
|
|
284
|
+
isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
|
|
285
|
+
targetImbalances,
|
|
286
|
+
}),
|
|
287
|
+
catch: (err) => {
|
|
288
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
289
|
+
return new InsufficientFundsError({
|
|
290
|
+
message: 'Insufficient funds',
|
|
291
|
+
tokenType: err.tokenType,
|
|
292
|
+
amount: imbalances.get(err.tokenType) ?? 0n,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
return new OtherWalletError({
|
|
297
|
+
message: 'Balancing unshielded segment failed',
|
|
298
|
+
cause: err,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Prepares an offer for a given balance recipe
|
|
306
|
+
*
|
|
307
|
+
* @param wallet - The wallet to prepare the offer for
|
|
308
|
+
* @param balanceRecipe - The balance recipe to prepare the offer for
|
|
309
|
+
* @returns The prepared offer and the new wallet state if successful, otherwise an error
|
|
310
|
+
*/
|
|
311
|
+
#prepareOffer(wallet, balanceRecipe) {
|
|
312
|
+
return Either.gen(function* () {
|
|
313
|
+
const [spentInputs, updatedWallet] = yield* CoreWallet.spendUtxos(wallet, balanceRecipe.inputs);
|
|
314
|
+
const { publicKey } = wallet.publicKey;
|
|
315
|
+
const ledgerInputs = spentInputs.map((input) => ({
|
|
316
|
+
...input,
|
|
317
|
+
intentHash: input.intentHash,
|
|
318
|
+
owner: publicKey,
|
|
319
|
+
}));
|
|
320
|
+
const counterOffer = yield* Either.try({
|
|
321
|
+
try: () => ledger.UnshieldedOffer.new(ledgerInputs, [...balanceRecipe.outputs], []),
|
|
322
|
+
catch: (error) => new TransactingError({ message: 'Failed to create counter offer', cause: error }),
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
newState: updatedWallet,
|
|
326
|
+
offer: counterOffer,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
#mergeOffers(offerA, offerB) {
|
|
331
|
+
return pipe(Option.fromNullable(offerB), Option.match({
|
|
332
|
+
onNone: () => Either.right(offerA),
|
|
333
|
+
onSome: (offerB) => Either.try({
|
|
334
|
+
try: () => ledger.UnshieldedOffer.new([...offerB.inputs, ...offerA.inputs], [...offerB.outputs, ...offerA.outputs], [...offerB.signatures, ...offerA.signatures]),
|
|
335
|
+
catch: (error) => new TransactingError({ message: 'Failed to merge offers', cause: error }),
|
|
336
|
+
}),
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Balances an unboundish (unproven or unbound) transaction
|
|
341
|
+
*
|
|
342
|
+
* @param wallet - The wallet to balance the transaction with
|
|
343
|
+
* @param transaction - The transaction to balance
|
|
344
|
+
* @returns The balanced transaction and the new wallet state if successful, otherwise an error
|
|
345
|
+
* @todo - https://shielded.atlassian.net/browse/PM-21260
|
|
346
|
+
*/
|
|
347
|
+
#balanceUnboundishTransaction(wallet, transaction) {
|
|
348
|
+
return Either.gen(this, function* () {
|
|
349
|
+
const segments = this.txOps.getSegments(transaction);
|
|
350
|
+
// no segments to balance
|
|
351
|
+
if (segments.length === 0) {
|
|
352
|
+
return [undefined, wallet];
|
|
353
|
+
}
|
|
354
|
+
for (const segment of [...segments, GUARANTEED_SEGMENT]) {
|
|
355
|
+
const imbalances = this.txOps.getImbalances(transaction, segment);
|
|
356
|
+
// intent is balanced
|
|
357
|
+
if (imbalances.size === 0) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// if segment is GUARANTEED_SEGMENT, use the first intent to place the balancing offer in the guaranteed section
|
|
361
|
+
const intentSegment = segment === GUARANTEED_SEGMENT ? segments[0] : segment;
|
|
362
|
+
const intent = transaction.intents?.get(intentSegment);
|
|
363
|
+
if (!intent) {
|
|
364
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} was not found` }));
|
|
365
|
+
}
|
|
366
|
+
const isBound = this.txOps.isIntentBound(intent);
|
|
367
|
+
if (isBound) {
|
|
368
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} is already bound` }));
|
|
369
|
+
}
|
|
370
|
+
const recipe = yield* this.#balanceSegment(wallet, imbalances, Imbalances.empty(), this.getCoinSelection());
|
|
371
|
+
const { offer } = yield* this.#prepareOffer(wallet, recipe);
|
|
372
|
+
const targetOffer = segment !== GUARANTEED_SEGMENT ? intent.fallibleUnshieldedOffer : intent.guaranteedUnshieldedOffer;
|
|
373
|
+
const mergedOffer = yield* this.#mergeOffers(offer, targetOffer);
|
|
374
|
+
if (segment !== GUARANTEED_SEGMENT) {
|
|
375
|
+
intent.fallibleUnshieldedOffer = mergedOffer;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
intent.guaranteedUnshieldedOffer = mergedOffer;
|
|
379
|
+
}
|
|
380
|
+
transaction.intents = transaction.intents.set(intentSegment, intent);
|
|
381
|
+
}
|
|
382
|
+
return [transaction, wallet];
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TransactionHistoryStorage } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
2
|
+
import { Effect, Schema } from 'effect';
|
|
3
|
+
import { type UnshieldedUpdate } from './SyncSchema.js';
|
|
4
|
+
import { TransactionHistoryError } from './WalletError.js';
|
|
5
|
+
export declare const UnshieldedSectionSchema: Schema.Struct<{
|
|
6
|
+
id: typeof Schema.Number;
|
|
7
|
+
createdUtxos: Schema.Array$<Schema.Struct<{
|
|
8
|
+
value: Schema.Schema<bigint, string, never>;
|
|
9
|
+
owner: typeof Schema.String;
|
|
10
|
+
tokenType: typeof Schema.String;
|
|
11
|
+
intentHash: typeof Schema.String;
|
|
12
|
+
outputIndex: typeof Schema.Number;
|
|
13
|
+
}>>;
|
|
14
|
+
spentUtxos: Schema.Array$<Schema.Struct<{
|
|
15
|
+
value: Schema.Schema<bigint, string, never>;
|
|
16
|
+
owner: typeof Schema.String;
|
|
17
|
+
tokenType: typeof Schema.String;
|
|
18
|
+
intentHash: typeof Schema.String;
|
|
19
|
+
outputIndex: typeof Schema.Number;
|
|
20
|
+
}>>;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const UnshieldedTransactionHistoryEntrySchema: Schema.Struct<{
|
|
23
|
+
unshielded: Schema.Struct<{
|
|
24
|
+
id: typeof Schema.Number;
|
|
25
|
+
createdUtxos: Schema.Array$<Schema.Struct<{
|
|
26
|
+
value: Schema.Schema<bigint, string, never>;
|
|
27
|
+
owner: typeof Schema.String;
|
|
28
|
+
tokenType: typeof Schema.String;
|
|
29
|
+
intentHash: typeof Schema.String;
|
|
30
|
+
outputIndex: typeof Schema.Number;
|
|
31
|
+
}>>;
|
|
32
|
+
spentUtxos: Schema.Array$<Schema.Struct<{
|
|
33
|
+
value: Schema.Schema<bigint, string, never>;
|
|
34
|
+
owner: typeof Schema.String;
|
|
35
|
+
tokenType: typeof Schema.String;
|
|
36
|
+
intentHash: typeof Schema.String;
|
|
37
|
+
outputIndex: typeof Schema.Number;
|
|
38
|
+
}>>;
|
|
39
|
+
}>;
|
|
40
|
+
hash: typeof Schema.String;
|
|
41
|
+
protocolVersion: typeof Schema.Number;
|
|
42
|
+
status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
|
|
43
|
+
identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
|
|
44
|
+
timestamp: Schema.optional<typeof Schema.Date>;
|
|
45
|
+
fees: Schema.optional<Schema.NullOr<typeof Schema.BigInt>>;
|
|
46
|
+
}>;
|
|
47
|
+
export type UnshieldedTransactionHistoryEntry = Schema.Schema.Type<typeof UnshieldedTransactionHistoryEntrySchema>;
|
|
48
|
+
export type TransactionHistoryService = {
|
|
49
|
+
put(update: UnshieldedUpdate): Effect.Effect<void, TransactionHistoryError>;
|
|
50
|
+
};
|
|
51
|
+
export type DefaultTransactionHistoryConfiguration = {
|
|
52
|
+
txHistoryStorage: TransactionHistoryStorage.TransactionHistoryStorage<TransactionHistoryStorage.TransactionHistoryEntryWithHash>;
|
|
53
|
+
};
|
|
54
|
+
export declare const makeDefaultTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { Effect, Schema } from 'effect';
|
|
15
|
+
import { SafeBigInt } from '@midnightntwrk/wallet-sdk-utilities';
|
|
16
|
+
import { TransactionHistoryError } from './WalletError.js';
|
|
17
|
+
const UtxoSchema = Schema.Struct({
|
|
18
|
+
value: SafeBigInt.SafeBigInt,
|
|
19
|
+
owner: Schema.String,
|
|
20
|
+
tokenType: Schema.String,
|
|
21
|
+
intentHash: Schema.String,
|
|
22
|
+
outputIndex: Schema.Number,
|
|
23
|
+
});
|
|
24
|
+
export const UnshieldedSectionSchema = Schema.Struct({
|
|
25
|
+
id: Schema.Number,
|
|
26
|
+
createdUtxos: Schema.Array(UtxoSchema),
|
|
27
|
+
spentUtxos: Schema.Array(UtxoSchema),
|
|
28
|
+
});
|
|
29
|
+
export const UnshieldedTransactionHistoryEntrySchema = Schema.Struct({
|
|
30
|
+
...TransactionHistoryStorage.TransactionHistoryCommonSchema.fields,
|
|
31
|
+
unshielded: UnshieldedSectionSchema,
|
|
32
|
+
});
|
|
33
|
+
const convertUpdateToStorageEntry = ({ transaction, createdUtxos, spentUtxos, status, }) => ({
|
|
34
|
+
hash: transaction.hash,
|
|
35
|
+
protocolVersion: transaction.protocolVersion,
|
|
36
|
+
status,
|
|
37
|
+
identifiers: transaction.identifiers ?? [],
|
|
38
|
+
timestamp: transaction.block.timestamp,
|
|
39
|
+
fees: transaction.fees?.paidFees ?? null,
|
|
40
|
+
unshielded: {
|
|
41
|
+
id: transaction.id,
|
|
42
|
+
createdUtxos: createdUtxos.map(({ utxo }) => ({
|
|
43
|
+
value: utxo.value,
|
|
44
|
+
owner: utxo.owner,
|
|
45
|
+
tokenType: utxo.type,
|
|
46
|
+
intentHash: utxo.intentHash,
|
|
47
|
+
outputIndex: utxo.outputNo,
|
|
48
|
+
})),
|
|
49
|
+
spentUtxos: spentUtxos.map(({ utxo }) => ({
|
|
50
|
+
value: utxo.value,
|
|
51
|
+
owner: utxo.owner,
|
|
52
|
+
tokenType: utxo.type,
|
|
53
|
+
intentHash: utxo.intentHash,
|
|
54
|
+
outputIndex: utxo.outputNo,
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export const makeDefaultTransactionHistoryService = (config, _getContext) => {
|
|
59
|
+
const txHistoryStorage = config.txHistoryStorage;
|
|
60
|
+
return {
|
|
61
|
+
put: (update) => Effect.tryPromise({
|
|
62
|
+
try: () => txHistoryStorage.upsert(convertUpdateToStorageEntry(update)),
|
|
63
|
+
catch: (e) => new TransactionHistoryError({ message: 'Failed to put transaction history entry', cause: e }),
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Either, type Option } from 'effect';
|
|
2
|
+
import { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
3
|
+
import type * as ledger from '@midnight-ntwrk/ledger-v8';
|
|
4
|
+
import { type WalletError } from './WalletError.js';
|
|
5
|
+
/** Unbound transaction type. This is a transaction that has no signatures and is not bound yet. */
|
|
6
|
+
export type UnboundTransaction = ledger.Transaction<ledger.SignatureEnabled, ledger.Proof, ledger.PreBinding>;
|
|
7
|
+
/** Utility type to extract the Intent type from a Transaction type. Maps Transaction<S, P, B> to Intent<S, P, B>. */
|
|
8
|
+
export type IntentOf<T> = T extends ledger.Transaction<infer S, infer P, infer B> ? ledger.Intent<S, P, B> : never;
|
|
9
|
+
export type TransactionOps = {
|
|
10
|
+
getSignatureData: (transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>, segment: number) => Either.Either<Uint8Array, WalletError>;
|
|
11
|
+
getSegments(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): number[];
|
|
12
|
+
findAvailableSegmentId(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): Option.Option<number>;
|
|
13
|
+
addSignature<TTransaction extends ledger.UnprovenTransaction | UnboundTransaction>(transaction: TTransaction, signature: ledger.Signature, segment: number): Either.Either<TTransaction, WalletError>;
|
|
14
|
+
getImbalances(transaction: ledger.FinalizedTransaction | UnboundTransaction | ledger.UnprovenTransaction, segment: number): Imbalances;
|
|
15
|
+
addSignaturesToOffer(offer: ledger.UnshieldedOffer<ledger.SignatureEnabled>, signature: ledger.Signature, segment: number, offerType: 'guaranteed' | 'fallible'): Either.Either<ledger.UnshieldedOffer<ledger.SignatureEnabled>, WalletError>;
|
|
16
|
+
isIntentBound(intent: ledger.Intent<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): boolean;
|
|
17
|
+
extractOwnInputs(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>, signatureVerifyingKey: ledger.SignatureVerifyingKey): ledger.Utxo[];
|
|
18
|
+
};
|
|
19
|
+
export declare const TransactionOps: TransactionOps;
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { Either, pipe, Array as Arr, Iterable as IterableOps } from 'effect';
|
|
14
|
+
import { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
|
|
15
|
+
import { TransactingError } from './WalletError.js';
|
|
16
|
+
export const TransactionOps = {
|
|
17
|
+
getSignatureData(tx, segment) {
|
|
18
|
+
if (!tx.intents) {
|
|
19
|
+
return Either.left(new TransactingError({ message: 'Transaction has no intents' }));
|
|
20
|
+
}
|
|
21
|
+
const intent = tx.intents.get(segment);
|
|
22
|
+
if (!intent) {
|
|
23
|
+
return Either.left(new TransactingError({ message: `Intent with segment ${segment} was not found` }));
|
|
24
|
+
}
|
|
25
|
+
return Either.try({
|
|
26
|
+
try: () => intent.signatureData(segment),
|
|
27
|
+
catch: (error) => new TransactingError({ message: 'Failed to get offer signature data', cause: error }),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
getSegments(transaction) {
|
|
31
|
+
return transaction.intents?.keys().toArray() ?? [];
|
|
32
|
+
},
|
|
33
|
+
findAvailableSegmentId(transaction) {
|
|
34
|
+
const used = new Set(transaction.intents?.keys() ?? []);
|
|
35
|
+
return pipe(IterableOps.range(1, 65535), IterableOps.findFirst((segmentId) => !used.has(segmentId)));
|
|
36
|
+
},
|
|
37
|
+
// @TODO - https://shielded.atlassian.net/browse/PM-21260
|
|
38
|
+
addSignature(transaction, signature, segment) {
|
|
39
|
+
return Either.gen(function* () {
|
|
40
|
+
if (!transaction.intents || transaction.intents.size === 0) {
|
|
41
|
+
return yield* Either.left(new TransactingError({ message: 'No intents found in the transaction' }));
|
|
42
|
+
}
|
|
43
|
+
const intent = transaction.intents?.get(segment);
|
|
44
|
+
if (!intent) {
|
|
45
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} was not found` }));
|
|
46
|
+
}
|
|
47
|
+
if (TransactionOps.isIntentBound(intent)) {
|
|
48
|
+
return yield* Either.left(new TransactingError({ message: `Intent at segment ${segment} is already bound` }));
|
|
49
|
+
}
|
|
50
|
+
if (intent.fallibleUnshieldedOffer) {
|
|
51
|
+
intent.fallibleUnshieldedOffer = yield* TransactionOps.addSignaturesToOffer(intent.fallibleUnshieldedOffer, signature, segment, 'fallible');
|
|
52
|
+
}
|
|
53
|
+
if (intent.guaranteedUnshieldedOffer) {
|
|
54
|
+
intent.guaranteedUnshieldedOffer = yield* TransactionOps.addSignaturesToOffer(intent.guaranteedUnshieldedOffer, signature, segment, 'guaranteed');
|
|
55
|
+
}
|
|
56
|
+
transaction.intents = transaction.intents.set(segment, intent);
|
|
57
|
+
return transaction;
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
getImbalances(transaction, segment) {
|
|
61
|
+
const imbalances = transaction
|
|
62
|
+
.imbalances(segment)
|
|
63
|
+
.entries()
|
|
64
|
+
.filter(([token, value]) => token.tag === 'unshielded' && value !== 0n)
|
|
65
|
+
.map(([token, value]) => [token.raw.toString(), value])
|
|
66
|
+
.toArray();
|
|
67
|
+
return Imbalances.fromEntries(imbalances);
|
|
68
|
+
},
|
|
69
|
+
addSignaturesToOffer(offer, signature, segment, offerType) {
|
|
70
|
+
return pipe(offer.inputs, Arr.map((_, i) => offer.signatures.at(i) ?? signature), (signatures) => Either.try({
|
|
71
|
+
try: () => offer.addSignatures(signatures),
|
|
72
|
+
catch: (error) => new TransactingError({
|
|
73
|
+
message: `Failed to add ${offerType} signature at segment ${segment}`,
|
|
74
|
+
cause: error,
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
},
|
|
78
|
+
isIntentBound(intent) {
|
|
79
|
+
return intent.binding.instance === 'binding';
|
|
80
|
+
},
|
|
81
|
+
extractOwnInputs(transaction, signatureVerifyingKey) {
|
|
82
|
+
const segments = TransactionOps.getSegments(transaction);
|
|
83
|
+
return pipe(segments, Arr.flatMap((segment) => {
|
|
84
|
+
const intent = transaction.intents?.get(segment);
|
|
85
|
+
if (!intent) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const { guaranteedUnshieldedOffer, fallibleUnshieldedOffer } = intent;
|
|
89
|
+
const ownedInputsfromGuaranteedSection = guaranteedUnshieldedOffer?.inputs
|
|
90
|
+
? guaranteedUnshieldedOffer.inputs.filter((input) => input.owner === signatureVerifyingKey)
|
|
91
|
+
: [];
|
|
92
|
+
const ownedInputsfromFallibleSection = fallibleUnshieldedOffer
|
|
93
|
+
? fallibleUnshieldedOffer.inputs.filter((input) => input.owner === signatureVerifyingKey)
|
|
94
|
+
: [];
|
|
95
|
+
return [...ownedInputsfromGuaranteedSection, ...ownedInputsfromFallibleSection];
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type * as ledger from '@midnight-ntwrk/ledger-v8';
|
|
2
|
+
import { Data, Either, HashMap } from 'effect';
|
|
3
|
+
import { ApplyTransactionError, UtxoNotFoundError } from './WalletError.js';
|
|
4
|
+
export interface UtxoMeta {
|
|
5
|
+
readonly ctime: Date;
|
|
6
|
+
readonly registeredForDustGeneration: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type UtxoHash = string;
|
|
9
|
+
export declare class UtxoWithMeta extends Data.Class<{
|
|
10
|
+
readonly utxo: ledger.Utxo;
|
|
11
|
+
readonly meta: UtxoMeta;
|
|
12
|
+
}> {
|
|
13
|
+
}
|
|
14
|
+
export type UpdateStatus = 'SUCCESS' | 'FAILURE' | 'PARTIAL_SUCCESS';
|
|
15
|
+
export interface UnshieldedUpdate {
|
|
16
|
+
readonly createdUtxos: readonly UtxoWithMeta[];
|
|
17
|
+
readonly spentUtxos: readonly UtxoWithMeta[];
|
|
18
|
+
readonly status: UpdateStatus;
|
|
19
|
+
}
|
|
20
|
+
export interface UnshieldedState {
|
|
21
|
+
readonly availableUtxos: HashMap.HashMap<UtxoHash, UtxoWithMeta>;
|
|
22
|
+
readonly pendingUtxos: HashMap.HashMap<UtxoHash, UtxoWithMeta>;
|
|
23
|
+
}
|
|
24
|
+
export declare const UnshieldedState: {
|
|
25
|
+
readonly empty: () => UnshieldedState;
|
|
26
|
+
readonly restore: (availableUtxos: readonly UtxoWithMeta[], pendingUtxos: readonly UtxoWithMeta[]) => UnshieldedState;
|
|
27
|
+
readonly spend: (state: UnshieldedState, utxo: UtxoWithMeta) => Either.Either<UnshieldedState, UtxoNotFoundError>;
|
|
28
|
+
readonly rollbackSpend: (state: UnshieldedState, utxo: UtxoWithMeta) => Either.Either<UnshieldedState, never>;
|
|
29
|
+
readonly spendByUtxo: (state: UnshieldedState, utxo: ledger.Utxo) => Either.Either<UnshieldedState, UtxoNotFoundError>;
|
|
30
|
+
readonly rollbackSpendByUtxo: (state: UnshieldedState, utxo: ledger.Utxo) => Either.Either<UnshieldedState, never>;
|
|
31
|
+
readonly applyUpdate: (state: UnshieldedState, update: UnshieldedUpdate) => Either.Either<UnshieldedState, ApplyTransactionError>;
|
|
32
|
+
readonly applyFailedUpdate: (state: UnshieldedState, update: UnshieldedUpdate) => Either.Either<UnshieldedState, ApplyTransactionError>;
|
|
33
|
+
readonly toArrays: (state: UnshieldedState) => {
|
|
34
|
+
readonly availableUtxos: readonly UtxoWithMeta[];
|
|
35
|
+
readonly pendingUtxos: readonly UtxoWithMeta[];
|
|
36
|
+
};
|
|
37
|
+
};
|