@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.
@@ -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
+ };