@midnight-ntwrk/wallet-sdk-unshielded-wallet 1.0.0-beta.13 → 1.0.0-beta.15

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.
Files changed (59) hide show
  1. package/dist/KeyStore.d.ts +3 -7
  2. package/dist/KeyStore.js +2 -2
  3. package/dist/UnshieldedWallet.d.ts +57 -0
  4. package/dist/UnshieldedWallet.js +120 -0
  5. package/dist/index.d.ts +3 -3
  6. package/dist/index.js +3 -2
  7. package/dist/{tx-history-storage → storage}/InMemoryTransactionHistoryStorage.d.ts +3 -7
  8. package/dist/{tx-history-storage → storage}/TransactionHistoryStorage.d.ts +3 -7
  9. package/dist/{tx-history-storage → storage}/TransactionHistoryStorage.js +3 -7
  10. package/dist/{tx-history-storage → storage}/index.d.ts +1 -0
  11. package/dist/{tx-history-storage → storage}/index.js +1 -0
  12. package/dist/v1/CoinsAndBalances.d.ts +13 -0
  13. package/dist/v1/CoinsAndBalances.js +36 -0
  14. package/dist/v1/CoreWallet.d.ts +24 -0
  15. package/dist/v1/CoreWallet.js +60 -0
  16. package/dist/v1/Keys.d.ts +8 -0
  17. package/dist/v1/Keys.js +23 -0
  18. package/dist/v1/RunningV1Variant.d.ts +41 -0
  19. package/dist/v1/RunningV1Variant.js +91 -0
  20. package/dist/v1/Serialization.d.ts +12 -0
  21. package/dist/v1/Serialization.js +64 -0
  22. package/dist/v1/Simulator.d.ts +22 -0
  23. package/dist/v1/Simulator.js +102 -0
  24. package/dist/v1/Sync.d.ts +32 -0
  25. package/dist/v1/Sync.js +85 -0
  26. package/dist/v1/SyncProgress.d.ts +18 -0
  27. package/dist/v1/SyncProgress.js +23 -0
  28. package/dist/v1/SyncSchema.d.ts +449 -0
  29. package/dist/v1/SyncSchema.js +123 -0
  30. package/dist/v1/Transacting.d.ts +45 -0
  31. package/dist/v1/Transacting.js +252 -0
  32. package/dist/v1/Transaction.d.ts +25 -0
  33. package/dist/v1/Transaction.js +155 -0
  34. package/dist/v1/TransactionHistory.d.ts +12 -0
  35. package/dist/v1/TransactionHistory.js +29 -0
  36. package/dist/v1/TransactionImbalances.d.ts +8 -0
  37. package/dist/v1/TransactionImbalances.js +21 -0
  38. package/dist/v1/UnshieldedState.d.ts +37 -0
  39. package/dist/v1/UnshieldedState.js +67 -0
  40. package/dist/v1/V1Builder.d.ts +97 -0
  41. package/dist/v1/V1Builder.js +160 -0
  42. package/dist/v1/WalletError.d.ts +89 -0
  43. package/dist/v1/WalletError.js +53 -0
  44. package/dist/v1/index.d.ts +15 -0
  45. package/dist/v1/index.js +27 -0
  46. package/package.json +5 -5
  47. package/dist/State.d.ts +0 -21
  48. package/dist/State.js +0 -43
  49. package/dist/SyncService.d.ts +0 -55
  50. package/dist/SyncService.js +0 -88
  51. package/dist/TransactionHistoryService.d.ts +0 -31
  52. package/dist/TransactionHistoryService.js +0 -49
  53. package/dist/TransactionService.d.ts +0 -43
  54. package/dist/TransactionService.js +0 -291
  55. package/dist/WalletBuilder.d.ts +0 -37
  56. package/dist/WalletBuilder.js +0 -137
  57. /package/dist/{tx-history-storage → storage}/InMemoryTransactionHistoryStorage.js +0 -0
  58. /package/dist/{tx-history-storage → storage}/NoOpTransactionHistoryStorage.d.ts +0 -0
  59. /package/dist/{tx-history-storage → storage}/NoOpTransactionHistoryStorage.js +0 -0
@@ -0,0 +1,252 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) 2025 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-v6';
14
+ import { Either, Option, pipe } from 'effect';
15
+ import { CoreWallet } from './CoreWallet.js';
16
+ import { SignError, TransactingError } from './WalletError.js';
17
+ import { getBalanceRecipe, Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
18
+ import { isIntentBound, TransactionTrait } from './Transaction.js';
19
+ import { MidnightBech32m, UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
20
+ const GUARANTEED_SEGMENT = 0;
21
+ const mergeCounterOffer = (counterOffer, currentOffer) => pipe(Option.fromNullable(currentOffer), Option.match({
22
+ onNone: () => Either.right(counterOffer),
23
+ onSome: (currentOffer) => Either.try({
24
+ try: () => ledger.UnshieldedOffer.new([...currentOffer.inputs, ...counterOffer.inputs], [...currentOffer.outputs, ...counterOffer.outputs], [...currentOffer.signatures, ...counterOffer.signatures]),
25
+ catch: (error) => new TransactingError({ message: 'Failed to merge counter offers', cause: error }),
26
+ }),
27
+ }));
28
+ export const makeDefaultTransactingCapability = (config, getContext) => {
29
+ return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionTrait.default);
30
+ };
31
+ export class TransactingCapabilityImplementation {
32
+ networkId;
33
+ getCoinSelection;
34
+ txTrait;
35
+ getCoins;
36
+ getKeys;
37
+ constructor(networkId, getCoinSelection, getCoins, getKeys, txTrait) {
38
+ this.getCoins = getCoins;
39
+ this.networkId = networkId;
40
+ this.getCoinSelection = getCoinSelection;
41
+ this.getKeys = getKeys;
42
+ this.txTrait = txTrait;
43
+ }
44
+ balanceTransaction(wallet, transaction) {
45
+ return Either.gen(this, function* () {
46
+ const segments = TransactionTrait.default.getSegments(transaction);
47
+ if (!transaction.intents || !transaction.intents.size || !segments.length) {
48
+ return {
49
+ newState: wallet,
50
+ transaction,
51
+ };
52
+ }
53
+ const { addressHex, publicKey } = wallet.publicKey;
54
+ for (const segment of [...segments, GUARANTEED_SEGMENT]) {
55
+ const allIntentImbalances = yield* Either.try({
56
+ try: () => transaction.imbalances(segment),
57
+ catch: (error) => new TransactingError({ message: 'Failed to get intent imbalances', cause: error }),
58
+ });
59
+ const imbalances = allIntentImbalances
60
+ .entries()
61
+ .filter(([token, value]) => token.tag === 'unshielded' && value !== 0n)
62
+ .map(([token, value]) => [token, value])
63
+ .map(([token, value]) => {
64
+ return [token.raw, value];
65
+ })
66
+ .toArray();
67
+ // // intent is balanced
68
+ if (!imbalances.length)
69
+ continue;
70
+ const availableCoins = this.getCoins().getAvailableCoins(wallet);
71
+ if (!availableCoins.length) {
72
+ return yield* Either.left(new TransactingError({ message: 'No available coins to spend' }));
73
+ }
74
+ // select inputs, receive the change outputs
75
+ const { inputs, outputs: changeOutputs } = yield* Either.try({
76
+ try: () => getBalanceRecipe({
77
+ coins: availableCoins.map(({ utxo }) => utxo),
78
+ initialImbalances: Imbalances.fromEntries(imbalances),
79
+ feeTokenType: '',
80
+ transactionCostModel: {
81
+ inputFeeOverhead: 0n,
82
+ outputFeeOverhead: 0n,
83
+ },
84
+ createOutput: (coin) => ({
85
+ ...coin,
86
+ owner: addressHex,
87
+ }),
88
+ isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
89
+ }),
90
+ catch: (error) => {
91
+ const message = error instanceof Error ? error.message : error?.toString() || '';
92
+ return new TransactingError({ message });
93
+ },
94
+ });
95
+ // mark the coins as spent
96
+ const [spentInputs] = yield* CoreWallet.spendUtxos(wallet, inputs);
97
+ const ledgerInputs = spentInputs.map((input) => ({
98
+ ...input,
99
+ intentHash: input.intentHash,
100
+ owner: publicKey,
101
+ }));
102
+ const counterOffer = yield* Either.try({
103
+ try: () => ledger.UnshieldedOffer.new(ledgerInputs, changeOutputs, []),
104
+ catch: (error) => new TransactingError({ message: 'Failed to create counter offer', cause: error }),
105
+ });
106
+ // NOTE: for the segment === 0 we insert the counter-offer into any intent's guaranteed section
107
+ if (segment !== GUARANTEED_SEGMENT) {
108
+ const intent = transaction.intents.get(segment);
109
+ const isBound = isIntentBound(intent);
110
+ if (!isBound && intent.fallibleUnshieldedOffer) {
111
+ const mergedOffer = yield* mergeCounterOffer(counterOffer, intent.fallibleUnshieldedOffer);
112
+ intent.fallibleUnshieldedOffer = mergedOffer;
113
+ transaction.intents = transaction.intents.set(segment, intent);
114
+ }
115
+ else {
116
+ // create a new offer if the intent is bound
117
+ const nextSegment = Math.max(...TransactionTrait.default.getSegments(transaction)) + 1;
118
+ const newIntent = ledger.Intent.new(intent.ttl);
119
+ newIntent.fallibleUnshieldedOffer = counterOffer;
120
+ transaction.intents = transaction.intents.set(nextSegment, newIntent);
121
+ }
122
+ }
123
+ else {
124
+ let ttl = new Date();
125
+ let updated = false;
126
+ // try to find and modify any unbound intent first
127
+ const segments = TransactionTrait.default.getSegments(transaction);
128
+ for (const segment of segments) {
129
+ const intent = transaction.intents.get(segment);
130
+ ttl = intent.ttl;
131
+ const isBound = isIntentBound(intent);
132
+ if (!isBound) {
133
+ const mergedOffer = yield* mergeCounterOffer(counterOffer, intent.guaranteedUnshieldedOffer);
134
+ intent.guaranteedUnshieldedOffer = mergedOffer;
135
+ transaction.intents = transaction.intents.set(segment, intent);
136
+ updated = true;
137
+ break;
138
+ }
139
+ }
140
+ // no unbound intents found, insert a new one
141
+ if (!updated) {
142
+ const nextSegment = Math.max(...segments) + 1;
143
+ const newIntent = ledger.Intent.new(ttl);
144
+ newIntent.guaranteedUnshieldedOffer = counterOffer;
145
+ transaction.intents = transaction.intents.set(nextSegment, newIntent);
146
+ }
147
+ }
148
+ }
149
+ return {
150
+ newState: wallet,
151
+ transaction: transaction,
152
+ };
153
+ });
154
+ }
155
+ makeTransfer(wallet, outputs, ttl) {
156
+ const networkId = this.networkId;
157
+ const isValid = outputs.every((output) => output.amount > 0n);
158
+ if (!isValid) {
159
+ throw new TransactingError({ message: 'The amount needs to be positive' });
160
+ }
161
+ const ledgerOutputs = outputs.map((output) => {
162
+ return {
163
+ value: output.amount,
164
+ owner: UnshieldedAddress.codec
165
+ .decode(networkId, MidnightBech32m.parse(output.receiverAddress))
166
+ .data.toString('hex'),
167
+ type: output.type,
168
+ };
169
+ });
170
+ return Either.try({
171
+ try: () => {
172
+ const intent = ledger.Intent.new(ttl);
173
+ intent.guaranteedUnshieldedOffer = ledger.UnshieldedOffer.new([], ledgerOutputs, []);
174
+ return {
175
+ newState: wallet,
176
+ transaction: ledger.Transaction.fromParts(networkId, undefined, undefined, intent),
177
+ };
178
+ },
179
+ catch: (error) => new TransactingError({ message: 'Failed to create transaction', cause: error }),
180
+ });
181
+ }
182
+ initSwap(wallet, desiredInputs, desiredOutputs, ttl) {
183
+ return Either.gen(this, function* () {
184
+ const networkId = this.networkId;
185
+ const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
186
+ if (!outputsValid) {
187
+ return yield* Either.left(new TransactingError({ message: 'The amount needs to be positive' }));
188
+ }
189
+ const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
190
+ if (!inputsValid) {
191
+ return yield* Either.left(new TransactingError({ message: 'The input amounts need to be positive' }));
192
+ }
193
+ const ledgerOutputs = desiredOutputs.map((output) => ({
194
+ value: output.amount,
195
+ owner: UnshieldedAddress.codec
196
+ .decode(networkId, MidnightBech32m.parse(output.receiverAddress))
197
+ .data.toString('hex'),
198
+ type: output.type,
199
+ }));
200
+ const targetImbalances = Imbalances.fromEntries(Object.entries(desiredInputs));
201
+ const availableCoins = this.getCoins().getAvailableCoins(wallet);
202
+ const { inputs, outputs: changeOutputs } = yield* Either.try({
203
+ try: () => getBalanceRecipe({
204
+ coins: availableCoins.map(({ utxo }) => utxo),
205
+ initialImbalances: Imbalances.empty(),
206
+ feeTokenType: '',
207
+ transactionCostModel: {
208
+ inputFeeOverhead: 0n,
209
+ outputFeeOverhead: 0n,
210
+ },
211
+ createOutput: (coin) => ({
212
+ ...coin,
213
+ owner: wallet.publicKey.addressHex,
214
+ }),
215
+ isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
216
+ targetImbalances,
217
+ }),
218
+ catch: (error) => {
219
+ const message = error instanceof Error ? error.message : error?.toString() || '';
220
+ return new TransactingError({ message });
221
+ },
222
+ });
223
+ const [spentInputs, updatedWallet] = yield* CoreWallet.spendUtxos(wallet, inputs);
224
+ const ledgerInputs = spentInputs.map((input) => ({
225
+ ...input,
226
+ owner: wallet.publicKey.publicKey,
227
+ }));
228
+ const offer = ledger.UnshieldedOffer.new(ledgerInputs, [...changeOutputs, ...ledgerOutputs], []);
229
+ const intent = ledger.Intent.new(ttl);
230
+ intent.guaranteedUnshieldedOffer = offer;
231
+ const tx = ledger.Transaction.fromParts(networkId, undefined, undefined, intent);
232
+ return {
233
+ newState: updatedWallet,
234
+ transaction: tx,
235
+ };
236
+ });
237
+ }
238
+ signTransaction(transaction, signSegment) {
239
+ return Either.gen(function* () {
240
+ const segments = TransactionTrait.default.getSegments(transaction);
241
+ if (!segments.length) {
242
+ throw new SignError({ message: 'No segments found in the provided transaction' });
243
+ }
244
+ for (const segment of segments) {
245
+ const signedData = yield* TransactionTrait.default.getOfferSignatureData(transaction, segment);
246
+ const signature = signSegment(signedData);
247
+ transaction = yield* TransactionTrait.default.addOfferSignature(transaction, signature, segment);
248
+ }
249
+ return transaction;
250
+ });
251
+ }
252
+ }
@@ -0,0 +1,25 @@
1
+ import { Either } from 'effect';
2
+ import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
3
+ import { TransactionImbalances } from './TransactionImbalances.js';
4
+ import * as ledger from '@midnight-ntwrk/ledger-v6';
5
+ import { TransactingError, WalletError } from './WalletError.js';
6
+ export declare const isIntentBound: (intent: ledger.Intent<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>) => boolean;
7
+ export type TransactionTrait<Tx> = {
8
+ id(tx: Tx): string;
9
+ getOfferSignatureData: (transaction: Tx, segment: number) => Either.Either<Uint8Array, WalletError>;
10
+ getSegments(transaction: Tx): number[];
11
+ addOfferSignature(transaction: Tx, signature: ledger.Signature, segment: number): Either.Either<Tx, WalletError>;
12
+ bindTransaction(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>): Either.Either<ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Binding>, WalletError>;
13
+ };
14
+ export declare const TransactionTrait: {
15
+ default: TransactionTrait<ledger.UnprovenTransaction>;
16
+ shared: {
17
+ getOfferSignatureData(tx: ledger.UnprovenTransaction, segment: number): Either.Either<Uint8Array<ArrayBufferLike>, WalletError>;
18
+ addOfferSignature(transaction: ledger.UnprovenTransaction, signature: ledger.Signature, segment?: number): Either.Either<ledger.UnprovenTransaction, WalletError>;
19
+ getSegments(transaction: ledger.UnprovenTransaction): number[];
20
+ bindTransaction(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>): Either.Either<ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Binding>, TransactingError>;
21
+ getImbalances(tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction): TransactionImbalances;
22
+ getGuaranteedImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => Imbalances;
23
+ getFallibleImbalances: (tx: ledger.FinalizedTransaction | ledger.UnprovenTransaction | ledger.ProofErasedTransaction) => Imbalances;
24
+ };
25
+ };
@@ -0,0 +1,155 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) 2025 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 } from 'effect';
14
+ import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
15
+ import * as ledger from '@midnight-ntwrk/ledger-v6';
16
+ import { TransactingError } from './WalletError.js';
17
+ export const isIntentBound = (intent) => {
18
+ return intent.binding instanceof ledger.Binding;
19
+ };
20
+ export const TransactionTrait = new (class {
21
+ default = {
22
+ getOfferSignatureData(tx, segment) {
23
+ return TransactionTrait.shared.getOfferSignatureData(tx, segment);
24
+ },
25
+ getSegments(tx) {
26
+ return TransactionTrait.shared.getSegments(tx);
27
+ },
28
+ addOfferSignature(transaction, signature, segment) {
29
+ return TransactionTrait.shared.addOfferSignature(transaction, signature, segment);
30
+ },
31
+ bindTransaction(transaction) {
32
+ return TransactionTrait.shared.bindTransaction(transaction);
33
+ },
34
+ id(tx) {
35
+ return tx.identifiers().at(0);
36
+ },
37
+ };
38
+ shared = {
39
+ getOfferSignatureData(tx, segment) {
40
+ return Either.try({
41
+ try: () => {
42
+ if (!tx.intents) {
43
+ throw new TransactingError({ message: 'Transaction has no intents' });
44
+ }
45
+ const intent = tx.intents.get(segment);
46
+ if (!intent) {
47
+ throw new TransactingError({ message: `Intent with segment ${segment} was not found` });
48
+ }
49
+ return pipe(isIntentBound(intent) ? intent : intent.bind(segment), (boundIntent) => boundIntent.signatureData(segment));
50
+ },
51
+ catch: (error) => error instanceof TransactingError
52
+ ? error
53
+ : new TransactingError({ message: 'Failed to get offer signature data', cause: error }),
54
+ });
55
+ },
56
+ addOfferSignature(transaction, signature, segment = 1) {
57
+ return Either.gen(function* () {
58
+ if (!transaction.intents || !transaction.intents.size) {
59
+ throw new TransactingError({ message: 'No intents found in the provided transaction' });
60
+ }
61
+ const intent = transaction.intents.get(segment);
62
+ if (!intent) {
63
+ throw new TransactingError({ message: 'Intent with a given segment was not found' });
64
+ }
65
+ const isBound = isIntentBound(intent);
66
+ if (isBound)
67
+ return transaction;
68
+ let updatedIntent = intent;
69
+ if (intent.guaranteedUnshieldedOffer) {
70
+ const offer = intent.guaranteedUnshieldedOffer;
71
+ const inputsLen = offer.inputs.length;
72
+ const signatures = [];
73
+ for (let i = 0; i < inputsLen; ++i) {
74
+ signatures.push(offer.signatures.at(i) ?? signature);
75
+ }
76
+ updatedIntent = yield* Either.try({
77
+ try: () => {
78
+ const offerWithSignatures = offer.addSignatures(signatures);
79
+ updatedIntent.guaranteedUnshieldedOffer = offerWithSignatures;
80
+ return updatedIntent;
81
+ },
82
+ catch: (error) => new TransactingError({
83
+ message: `Failed to add guaranteed signature at segment ${segment}`,
84
+ cause: error,
85
+ }),
86
+ });
87
+ }
88
+ if (intent.fallibleUnshieldedOffer) {
89
+ const offer = intent.fallibleUnshieldedOffer;
90
+ const inputsLen = offer.inputs.length;
91
+ const signatures = [];
92
+ for (let i = 0; i < inputsLen; ++i) {
93
+ signatures.push(offer.signatures.at(i) ?? signature);
94
+ }
95
+ updatedIntent = yield* Either.try({
96
+ try: () => {
97
+ const offerWithSignatures = offer.addSignatures(signatures);
98
+ updatedIntent.fallibleUnshieldedOffer = offerWithSignatures;
99
+ return updatedIntent;
100
+ },
101
+ catch: (error) => new TransactingError({
102
+ message: `Failed to add fallible signature at segment ${segment}`,
103
+ cause: error,
104
+ }),
105
+ });
106
+ }
107
+ transaction.intents = transaction.intents.set(segment, updatedIntent);
108
+ return transaction;
109
+ });
110
+ },
111
+ getSegments(transaction) {
112
+ return transaction.intents && transaction.intents.size > 0 ? transaction.intents.keys().toArray() : [];
113
+ },
114
+ bindTransaction(transaction) {
115
+ return Either.try({
116
+ try: () => transaction.bind(),
117
+ catch: (error) => new TransactingError({ message: 'Failed to bind transaction', cause: error }),
118
+ });
119
+ },
120
+ getImbalances(tx) {
121
+ const guaranteedImbalances = TransactionTrait.shared.getGuaranteedImbalances(tx);
122
+ const fallibleImbalances = TransactionTrait.shared.getFallibleImbalances(tx);
123
+ return pipe({
124
+ guaranteed: guaranteedImbalances,
125
+ fallible: fallibleImbalances,
126
+ fees: 0n,
127
+ });
128
+ },
129
+ getGuaranteedImbalances: (tx) => {
130
+ const rawGuaranteedImbalances = tx
131
+ .imbalances(0)
132
+ .entries()
133
+ .filter(([token]) => token.tag === 'shielded')
134
+ .map(([token, value]) => {
135
+ return [token.raw.toString(), value];
136
+ });
137
+ return Imbalances.fromEntries(rawGuaranteedImbalances);
138
+ },
139
+ getFallibleImbalances: (tx) => {
140
+ try {
141
+ const rawFallibleImbalances = tx
142
+ .imbalances(1)
143
+ .entries()
144
+ .filter(([token]) => token.tag === 'shielded')
145
+ .map(([token, value]) => {
146
+ return [token.raw.toString(), value];
147
+ });
148
+ return Imbalances.fromEntries(rawFallibleImbalances);
149
+ }
150
+ catch {
151
+ return Imbalances.empty();
152
+ }
153
+ },
154
+ };
155
+ })();
@@ -0,0 +1,12 @@
1
+ import { TransactionHistoryStorage, TransactionHistoryEntry, TransactionHash } from '../storage/index.js';
2
+ import { UnshieldedUpdate } from './SyncSchema.js';
3
+ export interface TransactionHistoryService<SyncUpdate> {
4
+ create(update: SyncUpdate): Promise<void>;
5
+ get(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
6
+ getAll(): AsyncIterableIterator<TransactionHistoryEntry>;
7
+ delete(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
8
+ }
9
+ export type DefaultTransactionHistoryConfiguration = {
10
+ txHistoryStorage: TransactionHistoryStorage;
11
+ };
12
+ export declare const makeDefaultTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService<UnshieldedUpdate>;
@@ -0,0 +1,29 @@
1
+ const convertUpdateToEntry = ({ transaction, status }) => {
2
+ return {
3
+ id: transaction.id,
4
+ hash: transaction.hash,
5
+ protocolVersion: transaction.protocolVersion,
6
+ identifiers: transaction.identifiers ? transaction.identifiers : [],
7
+ status,
8
+ timestamp: transaction.block?.timestamp ?? null,
9
+ fees: transaction.fees?.paidFees ?? null,
10
+ };
11
+ };
12
+ export const makeDefaultTransactionHistoryService = (config, _getContext) => {
13
+ const { txHistoryStorage } = config;
14
+ return {
15
+ create: async (update) => {
16
+ const entry = convertUpdateToEntry(update);
17
+ await txHistoryStorage.create(entry);
18
+ },
19
+ get: async (hash) => {
20
+ return await txHistoryStorage.get(hash);
21
+ },
22
+ getAll: () => {
23
+ return txHistoryStorage.getAll();
24
+ },
25
+ delete: async (hash) => {
26
+ return txHistoryStorage.delete(hash);
27
+ },
28
+ };
29
+ };
@@ -0,0 +1,8 @@
1
+ import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
2
+ export type TransactionImbalances = Readonly<{
3
+ guaranteed: Imbalances;
4
+ fallible: Imbalances;
5
+ }>;
6
+ export declare const TransactionImbalances: {
7
+ empty: () => TransactionImbalances;
8
+ };
@@ -0,0 +1,21 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) 2025 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 '@midnight-ntwrk/wallet-sdk-capabilities';
14
+ export const TransactionImbalances = new (class {
15
+ empty = () => {
16
+ return {
17
+ guaranteed: Imbalances.empty(),
18
+ fallible: Imbalances.empty(),
19
+ };
20
+ };
21
+ })();
@@ -0,0 +1,37 @@
1
+ import * as ledger from '@midnight-ntwrk/ledger-v6';
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, UtxoNotFoundError>;
29
+ readonly spendByUtxo: (state: UnshieldedState, utxo: ledger.Utxo) => Either.Either<UnshieldedState, UtxoNotFoundError>;
30
+ readonly rollbackSpendByUtxo: (state: UnshieldedState, utxo: ledger.Utxo) => Either.Either<UnshieldedState, UtxoNotFoundError>;
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
+ };
@@ -0,0 +1,67 @@
1
+ import { Data, Either, HashMap } from 'effect';
2
+ import { ApplyTransactionError, UtxoNotFoundError } from './WalletError.js';
3
+ export class UtxoWithMeta extends Data.Class {
4
+ }
5
+ const UtxoHash = (utxo) => `${utxo.intentHash}#${utxo.outputNo}`;
6
+ export const UnshieldedState = {
7
+ empty: () => ({
8
+ availableUtxos: HashMap.empty(),
9
+ pendingUtxos: HashMap.empty(),
10
+ }),
11
+ restore: (availableUtxos, pendingUtxos) => ({
12
+ availableUtxos: HashMap.fromIterable(availableUtxos.map((utxo) => [UtxoHash(utxo.utxo), utxo])),
13
+ pendingUtxos: HashMap.fromIterable(pendingUtxos.map((utxo) => [UtxoHash(utxo.utxo), utxo])),
14
+ }),
15
+ spend: (state, utxo) => Either.gen(function* () {
16
+ const hash = UtxoHash(utxo.utxo);
17
+ if (!HashMap.has(state.availableUtxos, hash)) {
18
+ return yield* Either.left(new UtxoNotFoundError({ utxo: utxo.utxo }));
19
+ }
20
+ return {
21
+ availableUtxos: HashMap.remove(state.availableUtxos, hash),
22
+ pendingUtxos: HashMap.set(state.pendingUtxos, hash, utxo),
23
+ };
24
+ }),
25
+ rollbackSpend: (state, utxo) => Either.gen(function* () {
26
+ const hash = UtxoHash(utxo.utxo);
27
+ if (!HashMap.has(state.pendingUtxos, hash)) {
28
+ return yield* Either.left(new UtxoNotFoundError({ utxo: utxo.utxo }));
29
+ }
30
+ return {
31
+ availableUtxos: HashMap.set(state.availableUtxos, hash, utxo),
32
+ pendingUtxos: HashMap.remove(state.pendingUtxos, hash),
33
+ };
34
+ }),
35
+ spendByUtxo: (state, utxo) => Either.gen(function* () {
36
+ const hash = UtxoHash(utxo);
37
+ const found = yield* Either.fromOption(HashMap.get(state.availableUtxos, hash), () => new UtxoNotFoundError({ utxo }));
38
+ return yield* UnshieldedState.spend(state, found);
39
+ }),
40
+ rollbackSpendByUtxo: (state, utxo) => Either.gen(function* () {
41
+ const hash = UtxoHash(utxo);
42
+ const found = yield* Either.fromOption(HashMap.get(state.pendingUtxos, hash), () => new UtxoNotFoundError({ utxo }));
43
+ return yield* UnshieldedState.rollbackSpend(state, found);
44
+ }),
45
+ applyUpdate: (state, update) => Either.gen(function* () {
46
+ if (!['SUCCESS', 'PARTIAL_SUCCESS'].includes(update.status)) {
47
+ return yield* Either.left(new ApplyTransactionError({ message: `Invalid status: ${update.status}` }));
48
+ }
49
+ return {
50
+ availableUtxos: HashMap.union(HashMap.removeMany(state.availableUtxos, update.spentUtxos.map((utxo) => UtxoHash(utxo.utxo))), HashMap.fromIterable(update.createdUtxos.map((utxo) => [UtxoHash(utxo.utxo), utxo]))),
51
+ pendingUtxos: HashMap.removeMany(state.pendingUtxos, update.spentUtxos.map((utxo) => UtxoHash(utxo.utxo))),
52
+ };
53
+ }),
54
+ applyFailedUpdate: (state, update) => Either.gen(function* () {
55
+ if (update.status !== 'FAILURE') {
56
+ return yield* Either.left(new ApplyTransactionError({ message: `Invalid status: ${update.status}` }));
57
+ }
58
+ return {
59
+ availableUtxos: HashMap.union(state.availableUtxos, HashMap.fromIterable(update.spentUtxos.map((utxo) => [UtxoHash(utxo.utxo), utxo]))),
60
+ pendingUtxos: HashMap.removeMany(state.pendingUtxos, update.spentUtxos.map((utxo) => UtxoHash(utxo.utxo))),
61
+ };
62
+ }),
63
+ toArrays: (state) => ({
64
+ availableUtxos: HashMap.toValues(state.availableUtxos),
65
+ pendingUtxos: HashMap.toValues(state.pendingUtxos),
66
+ }),
67
+ };