@midnight-ntwrk/wallet-sdk-unshielded-wallet 1.0.0-beta.17 → 1.0.0-beta.19
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/dist/UnshieldedWallet.d.ts +14 -11
- package/dist/UnshieldedWallet.js +26 -5
- package/dist/v1/RunningV1Variant.d.ts +13 -9
- package/dist/v1/RunningV1Variant.js +18 -5
- package/dist/v1/Transacting.d.ts +67 -12
- package/dist/v1/Transacting.js +258 -179
- package/dist/v1/TransactionOps.d.ts +23 -0
- package/dist/v1/TransactionOps.js +95 -0
- package/dist/v1/V1Builder.d.ts +28 -30
- package/dist/v1/V1Builder.js +1 -12
- package/dist/v1/index.d.ts +1 -1
- package/dist/v1/index.js +1 -1
- package/package.json +8 -7
- package/dist/v1/Transaction.d.ts +0 -25
- package/dist/v1/Transaction.js +0 -155
- package/dist/v1/TransactionImbalances.d.ts +0 -8
- package/dist/v1/TransactionImbalances.js +0 -21
package/dist/v1/Transacting.js
CHANGED
|
@@ -11,184 +11,142 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
import * as ledger from '@midnight-ntwrk/ledger-v7';
|
|
14
|
-
import { Either, Option, pipe } from 'effect';
|
|
14
|
+
import { Either, Option, pipe, Array as Arr } from 'effect';
|
|
15
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 {
|
|
16
|
+
import { InsufficientFundsError, OtherWalletError, SignError, TransactingError } from './WalletError.js';
|
|
17
|
+
import { getBalanceRecipe, Imbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
18
|
+
import { TransactionOps } from './TransactionOps.js';
|
|
19
19
|
import { MidnightBech32m, UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
|
|
20
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
21
|
export const makeDefaultTransactingCapability = (config, getContext) => {
|
|
29
|
-
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability,
|
|
22
|
+
return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionOps);
|
|
30
23
|
};
|
|
31
24
|
export class TransactingCapabilityImplementation {
|
|
32
25
|
networkId;
|
|
33
26
|
getCoinSelection;
|
|
34
|
-
|
|
27
|
+
txOps;
|
|
35
28
|
getCoins;
|
|
36
29
|
getKeys;
|
|
37
|
-
constructor(networkId, getCoinSelection, getCoins, getKeys,
|
|
30
|
+
constructor(networkId, getCoinSelection, getCoins, getKeys, txOps) {
|
|
38
31
|
this.getCoins = getCoins;
|
|
39
32
|
this.networkId = networkId;
|
|
40
33
|
this.getCoinSelection = getCoinSelection;
|
|
41
34
|
this.getKeys = getKeys;
|
|
42
|
-
this.
|
|
35
|
+
this.txOps = txOps;
|
|
43
36
|
}
|
|
44
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Balances an unbound transaction
|
|
39
|
+
* Note: Unbound transactions are balanced in place and returned
|
|
40
|
+
* @param wallet - The wallet to balance the transaction with
|
|
41
|
+
* @param transaction - The transaction to balance
|
|
42
|
+
* @returns The balanced transaction and the new wallet state if successful, otherwise an error
|
|
43
|
+
*/
|
|
44
|
+
balanceUnboundTransaction(wallet, transaction) {
|
|
45
|
+
return this.#balanceUnboundishTransaction(wallet, transaction);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Balances an unproven transaction
|
|
49
|
+
* Note: This method does the same thing as balanceUnboundTransaction but is provided for convenience and type safety
|
|
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
|
|
59
|
+
* Note: In bound transactions we can only balance the guaranteed section in intents
|
|
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 )
|
|
63
|
+
* and the new wallet state if successful, otherwise an error
|
|
64
|
+
*/
|
|
65
|
+
balanceFinalizedTransaction(wallet, transaction) {
|
|
45
66
|
return Either.gen(this, function* () {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
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` }));
|
|
147
74
|
}
|
|
148
75
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
return [ledger.Transaction.fromPartsRandomized(this.networkId, undefined, undefined, balancingIntent), newState];
|
|
153
88
|
});
|
|
154
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Makes a transfer transaction
|
|
92
|
+
* @param wallet - The wallet to make the transfer with
|
|
93
|
+
* @param outputs - The outputs for the transfer
|
|
94
|
+
* @param ttl - The TTL for the transaction
|
|
95
|
+
* @returns The balanced transfer transaction and the new wallet state if successful, otherwise an error
|
|
96
|
+
*/
|
|
155
97
|
makeTransfer(wallet, outputs, ttl) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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, []);
|
|
98
|
+
return Either.gen(this, function* () {
|
|
99
|
+
const { networkId } = this;
|
|
100
|
+
const isValid = outputs.every((output) => output.amount > 0n);
|
|
101
|
+
if (!isValid) {
|
|
102
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all inputs needs to be positive' }));
|
|
103
|
+
}
|
|
104
|
+
const ledgerOutputs = outputs.map((output) => {
|
|
174
105
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
106
|
+
value: output.amount,
|
|
107
|
+
owner: UnshieldedAddress.codec
|
|
108
|
+
.decode(networkId, MidnightBech32m.parse(output.receiverAddress))
|
|
109
|
+
.data.toString('hex'),
|
|
110
|
+
type: output.type,
|
|
177
111
|
};
|
|
178
|
-
}
|
|
179
|
-
|
|
112
|
+
});
|
|
113
|
+
const recipe = yield* this.#balanceSegment(wallet, Imbalances.empty(), Imbalances.fromEntries(ledgerOutputs.map((output) => [output.type, output.value])), this.getCoinSelection());
|
|
114
|
+
const { newState, offer } = yield* this.#prepareOffer(wallet, {
|
|
115
|
+
inputs: recipe.inputs,
|
|
116
|
+
outputs: [...recipe.outputs, ...ledgerOutputs],
|
|
117
|
+
});
|
|
118
|
+
const intent = ledger.Intent.new(ttl);
|
|
119
|
+
const hasNightOutput = ledgerOutputs.some((output) => output.type === ledger.nativeToken().raw);
|
|
120
|
+
if (hasNightOutput) {
|
|
121
|
+
intent.fallibleUnshieldedOffer = offer;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
intent.guaranteedUnshieldedOffer = offer;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
newState,
|
|
128
|
+
transaction: ledger.Transaction.fromParts(networkId, undefined, undefined, intent),
|
|
129
|
+
};
|
|
180
130
|
});
|
|
181
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Initializes a swap transaction
|
|
134
|
+
* @param wallet - The wallet to initialize the swap for
|
|
135
|
+
* @param desiredInputs - The desired inputs for the swap
|
|
136
|
+
* @param desiredOutputs - The desired outputs for the swap
|
|
137
|
+
* @param ttl - The TTL for the swap
|
|
138
|
+
* @returns The initialized swap transaction and the new wallet state if successful, otherwise an error
|
|
139
|
+
*/
|
|
182
140
|
initSwap(wallet, desiredInputs, desiredOutputs, ttl) {
|
|
183
141
|
return Either.gen(this, function* () {
|
|
184
|
-
const networkId = this
|
|
142
|
+
const { networkId } = this;
|
|
185
143
|
const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
|
|
186
144
|
if (!outputsValid) {
|
|
187
|
-
return yield* Either.left(new TransactingError({ message: 'The amount needs to be positive' }));
|
|
145
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all outputs needs to be positive' }));
|
|
188
146
|
}
|
|
189
147
|
const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
|
|
190
148
|
if (!inputsValid) {
|
|
191
|
-
return yield* Either.left(new TransactingError({ message: 'The
|
|
149
|
+
return yield* Either.left(new TransactingError({ message: 'The amount of all inputs needs to be positive' }));
|
|
192
150
|
}
|
|
193
151
|
const ledgerOutputs = desiredOutputs.map((output) => ({
|
|
194
152
|
value: output.amount,
|
|
@@ -198,55 +156,176 @@ export class TransactingCapabilityImplementation {
|
|
|
198
156
|
type: output.type,
|
|
199
157
|
}));
|
|
200
158
|
const targetImbalances = Imbalances.fromEntries(Object.entries(desiredInputs));
|
|
201
|
-
const
|
|
202
|
-
const {
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
},
|
|
159
|
+
const recipe = yield* this.#balanceSegment(wallet, Imbalances.empty(), targetImbalances, this.getCoinSelection());
|
|
160
|
+
const { newState, offer } = yield* this.#prepareOffer(wallet, {
|
|
161
|
+
inputs: recipe.inputs,
|
|
162
|
+
outputs: [...recipe.outputs, ...ledgerOutputs],
|
|
222
163
|
});
|
|
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
164
|
const intent = ledger.Intent.new(ttl);
|
|
230
165
|
intent.guaranteedUnshieldedOffer = offer;
|
|
231
166
|
const tx = ledger.Transaction.fromParts(networkId, undefined, undefined, intent);
|
|
232
167
|
return {
|
|
233
|
-
newState
|
|
168
|
+
newState,
|
|
234
169
|
transaction: tx,
|
|
235
170
|
};
|
|
236
171
|
});
|
|
237
172
|
}
|
|
238
|
-
|
|
239
|
-
return
|
|
240
|
-
|
|
173
|
+
signUnprovenTransaction(transaction, signSegment) {
|
|
174
|
+
return this.#signTransactionInternal(transaction, signSegment);
|
|
175
|
+
}
|
|
176
|
+
signUnboundTransaction(transaction, signSegment) {
|
|
177
|
+
return this.#signTransactionInternal(transaction, signSegment);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Internal method to sign either an unproven or unbound transaction
|
|
181
|
+
* @param transaction - The transaction to sign
|
|
182
|
+
* @param signSegment - The signing function
|
|
183
|
+
* @returns The signed transaction if successful, otherwise an error
|
|
184
|
+
*/
|
|
185
|
+
#signTransactionInternal(transaction, signSegment) {
|
|
186
|
+
return Either.gen(this, function* () {
|
|
187
|
+
const segments = this.txOps.getSegments(transaction);
|
|
241
188
|
if (!segments.length) {
|
|
242
189
|
throw new SignError({ message: 'No segments found in the provided transaction' });
|
|
243
190
|
}
|
|
244
191
|
for (const segment of segments) {
|
|
245
|
-
const signedData = yield*
|
|
192
|
+
const signedData = yield* this.txOps.getSignatureData(transaction, segment);
|
|
246
193
|
const signature = signSegment(signedData);
|
|
247
|
-
transaction = yield*
|
|
194
|
+
transaction = (yield* this.txOps.addSignature(transaction, signature, segment));
|
|
248
195
|
}
|
|
249
196
|
return transaction;
|
|
250
197
|
});
|
|
251
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Reverts a transaction by rolling back all inputs owned by this wallet
|
|
201
|
+
* @param wallet - The wallet to revert the transaction for
|
|
202
|
+
* @param transaction - The transaction to revert (can be FinalizedTransaction, UnboundTransaction, or UnprovenTransaction)
|
|
203
|
+
* @returns The updated wallet with rolled back UTXOs if successful, otherwise an error
|
|
204
|
+
*/
|
|
205
|
+
revert(wallet, transaction) {
|
|
206
|
+
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)))));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Balances a segment of a transaction
|
|
210
|
+
* @param wallet - The wallet to balance the segment for
|
|
211
|
+
* @param imbalances - The imbalances to balance the segment for
|
|
212
|
+
* @param targetImbalances - The target imbalances to balance the segment for
|
|
213
|
+
* @param coinSelection - The coin selection to use for the balance recipe
|
|
214
|
+
* @returns The balance recipe if successful, otherwise an error
|
|
215
|
+
*/
|
|
216
|
+
#balanceSegment(wallet, imbalances, targetImbalances, coinSelection) {
|
|
217
|
+
return Either.try({
|
|
218
|
+
try: () => getBalanceRecipe({
|
|
219
|
+
coins: this.getCoins()
|
|
220
|
+
.getAvailableCoins(wallet)
|
|
221
|
+
.map(({ utxo }) => utxo),
|
|
222
|
+
initialImbalances: imbalances,
|
|
223
|
+
feeTokenType: '',
|
|
224
|
+
transactionCostModel: {
|
|
225
|
+
inputFeeOverhead: 0n,
|
|
226
|
+
outputFeeOverhead: 0n,
|
|
227
|
+
},
|
|
228
|
+
coinSelection,
|
|
229
|
+
createOutput: (coin) => ({
|
|
230
|
+
...coin,
|
|
231
|
+
owner: wallet.publicKey.addressHex,
|
|
232
|
+
}),
|
|
233
|
+
isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
|
|
234
|
+
targetImbalances,
|
|
235
|
+
}),
|
|
236
|
+
catch: (err) => {
|
|
237
|
+
if (err instanceof BalancingInsufficientFundsError) {
|
|
238
|
+
return new InsufficientFundsError({
|
|
239
|
+
message: 'Insufficient funds',
|
|
240
|
+
tokenType: err.tokenType,
|
|
241
|
+
amount: imbalances.get(err.tokenType) ?? 0n,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return new OtherWalletError({
|
|
246
|
+
message: 'Balancing unshielded segment failed',
|
|
247
|
+
cause: err,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Prepares an offer for a given balance recipe
|
|
255
|
+
* @param wallet - The wallet to prepare the offer for
|
|
256
|
+
* @param balanceRecipe - The balance recipe to prepare the offer for
|
|
257
|
+
* @returns The prepared offer and the new wallet state if successful, otherwise an error
|
|
258
|
+
*/
|
|
259
|
+
#prepareOffer(wallet, balanceRecipe) {
|
|
260
|
+
return Either.gen(function* () {
|
|
261
|
+
const [spentInputs, updatedWallet] = yield* CoreWallet.spendUtxos(wallet, balanceRecipe.inputs);
|
|
262
|
+
const { publicKey } = wallet.publicKey;
|
|
263
|
+
const ledgerInputs = spentInputs.map((input) => ({
|
|
264
|
+
...input,
|
|
265
|
+
intentHash: input.intentHash,
|
|
266
|
+
owner: publicKey,
|
|
267
|
+
}));
|
|
268
|
+
const counterOffer = yield* Either.try({
|
|
269
|
+
try: () => ledger.UnshieldedOffer.new(ledgerInputs, [...balanceRecipe.outputs], []),
|
|
270
|
+
catch: (error) => new TransactingError({ message: 'Failed to create counter offer', cause: error }),
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
newState: updatedWallet,
|
|
274
|
+
offer: counterOffer,
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
#mergeOffers(offerA, offerB) {
|
|
279
|
+
return pipe(Option.fromNullable(offerB), Option.match({
|
|
280
|
+
onNone: () => Either.right(offerA),
|
|
281
|
+
onSome: (offerB) => Either.try({
|
|
282
|
+
try: () => ledger.UnshieldedOffer.new([...offerB.inputs, ...offerA.inputs], [...offerB.outputs, ...offerA.outputs], [...offerB.signatures, ...offerA.signatures]),
|
|
283
|
+
catch: (error) => new TransactingError({ message: 'Failed to merge offers', cause: error }),
|
|
284
|
+
}),
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Balances an unboundish (unproven or unbound) transaction
|
|
289
|
+
* @param wallet - The wallet to balance the transaction with
|
|
290
|
+
* @param transaction - The transaction to balance
|
|
291
|
+
* @returns The balanced transaction and the new wallet state if successful, otherwise an error
|
|
292
|
+
*/
|
|
293
|
+
#balanceUnboundishTransaction(wallet, transaction) {
|
|
294
|
+
return Either.gen(this, function* () {
|
|
295
|
+
const segments = this.txOps.getSegments(transaction);
|
|
296
|
+
// no segments to balance
|
|
297
|
+
if (segments.length === 0) {
|
|
298
|
+
return [undefined, wallet];
|
|
299
|
+
}
|
|
300
|
+
for (const segment of [...segments, GUARANTEED_SEGMENT]) {
|
|
301
|
+
const imbalances = this.txOps.getImbalances(transaction, segment);
|
|
302
|
+
// intent is balanced
|
|
303
|
+
if (imbalances.size === 0) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// if segment is GUARANTEED_SEGMENT, use the first intent to place the balancing offer in the guaranteed section
|
|
307
|
+
const intentSegment = segment === GUARANTEED_SEGMENT ? segments[0] : segment;
|
|
308
|
+
const intent = transaction.intents?.get(intentSegment);
|
|
309
|
+
if (!intent) {
|
|
310
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} was not found` }));
|
|
311
|
+
}
|
|
312
|
+
const isBound = this.txOps.isIntentBound(intent);
|
|
313
|
+
if (isBound) {
|
|
314
|
+
return yield* Either.left(new TransactingError({ message: `Intent with id ${segment} is already bound` }));
|
|
315
|
+
}
|
|
316
|
+
const recipe = yield* this.#balanceSegment(wallet, imbalances, Imbalances.empty(), this.getCoinSelection());
|
|
317
|
+
const { offer } = yield* this.#prepareOffer(wallet, recipe);
|
|
318
|
+
const targetOffer = segment !== GUARANTEED_SEGMENT ? intent.fallibleUnshieldedOffer : intent.guaranteedUnshieldedOffer;
|
|
319
|
+
const mergedOffer = yield* this.#mergeOffers(offer, targetOffer);
|
|
320
|
+
if (segment !== GUARANTEED_SEGMENT) {
|
|
321
|
+
intent.fallibleUnshieldedOffer = mergedOffer;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
intent.guaranteedUnshieldedOffer = mergedOffer;
|
|
325
|
+
}
|
|
326
|
+
transaction.intents = transaction.intents.set(intentSegment, intent);
|
|
327
|
+
}
|
|
328
|
+
return [transaction, wallet];
|
|
329
|
+
});
|
|
330
|
+
}
|
|
252
331
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
3
|
+
import * as ledger from '@midnight-ntwrk/ledger-v7';
|
|
4
|
+
import { WalletError } from './WalletError.js';
|
|
5
|
+
/**
|
|
6
|
+
* Unbound transaction type. This is a transaction that has no signatures and is not bound yet.
|
|
7
|
+
*/
|
|
8
|
+
export type UnboundTransaction = ledger.Transaction<ledger.SignatureEnabled, ledger.Proof, ledger.PreBinding>;
|
|
9
|
+
/**
|
|
10
|
+
* Utility type to extract the Intent type from a Transaction type.
|
|
11
|
+
* Maps Transaction<S, P, B> to Intent<S, P, B>.
|
|
12
|
+
*/
|
|
13
|
+
export type IntentOf<T> = T extends ledger.Transaction<infer S, infer P, infer B> ? ledger.Intent<S, P, B> : never;
|
|
14
|
+
export type TransactionOps = {
|
|
15
|
+
getSignatureData: (transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>, segment: number) => Either.Either<Uint8Array, WalletError>;
|
|
16
|
+
getSegments(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): number[];
|
|
17
|
+
addSignature(transaction: ledger.UnprovenTransaction | UnboundTransaction, signature: ledger.Signature, segment: number): Either.Either<ledger.UnprovenTransaction, WalletError>;
|
|
18
|
+
getImbalances(transaction: ledger.FinalizedTransaction | UnboundTransaction | ledger.UnprovenTransaction, segment: number): Imbalances;
|
|
19
|
+
addSignaturesToOffer(offer: ledger.UnshieldedOffer<ledger.SignatureEnabled>, signature: ledger.Signature, segment: number, offerType: 'guaranteed' | 'fallible'): Either.Either<ledger.UnshieldedOffer<ledger.SignatureEnabled>, WalletError>;
|
|
20
|
+
isIntentBound(intent: ledger.Intent<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): boolean;
|
|
21
|
+
extractOwnInputs(transaction: ledger.FinalizedTransaction | UnboundTransaction | ledger.UnprovenTransaction, signatureVerifyingKey: ledger.SignatureVerifyingKey): ledger.Utxo[];
|
|
22
|
+
};
|
|
23
|
+
export declare const TransactionOps: TransactionOps;
|
|
@@ -0,0 +1,95 @@
|
|
|
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, Array as Arr } from 'effect';
|
|
14
|
+
import { Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
15
|
+
import * as ledger from '@midnight-ntwrk/ledger-v7';
|
|
16
|
+
import { TransactingError } from './WalletError.js';
|
|
17
|
+
export const TransactionOps = {
|
|
18
|
+
getSignatureData(tx, segment) {
|
|
19
|
+
if (!tx.intents) {
|
|
20
|
+
return Either.left(new TransactingError({ message: 'Transaction has no intents' }));
|
|
21
|
+
}
|
|
22
|
+
const intent = tx.intents.get(segment);
|
|
23
|
+
if (!intent) {
|
|
24
|
+
return Either.left(new TransactingError({ message: `Intent with segment ${segment} was not found` }));
|
|
25
|
+
}
|
|
26
|
+
return Either.try({
|
|
27
|
+
try: () => intent.signatureData(segment),
|
|
28
|
+
catch: (error) => new TransactingError({ message: 'Failed to get offer signature data', cause: error }),
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
getSegments(transaction) {
|
|
32
|
+
return transaction.intents?.keys().toArray() ?? [];
|
|
33
|
+
},
|
|
34
|
+
addSignature(transaction, signature, segment) {
|
|
35
|
+
return Either.gen(function* () {
|
|
36
|
+
if (!transaction.intents || transaction.intents.size === 0) {
|
|
37
|
+
return yield* Either.left(new TransactingError({ message: 'No intents found in the transaction' }));
|
|
38
|
+
}
|
|
39
|
+
const originalIntent = yield* Either.fromNullable(transaction.intents?.get(segment), () => new TransactingError({ message: 'Intent with a given segment was not found' }));
|
|
40
|
+
if (TransactionOps.isIntentBound(originalIntent)) {
|
|
41
|
+
return yield* Either.left(new TransactingError({ message: `Intent at segment ${segment} is already bound` }));
|
|
42
|
+
}
|
|
43
|
+
const clonedIntent = yield* Either.try({
|
|
44
|
+
try: () => ledger.Intent.deserialize('signature', 'pre-proof', 'pre-binding', originalIntent.serialize()),
|
|
45
|
+
catch: (error) => new TransactingError({ message: 'Failed to clone intent', cause: error }),
|
|
46
|
+
});
|
|
47
|
+
if (clonedIntent.fallibleUnshieldedOffer) {
|
|
48
|
+
clonedIntent.fallibleUnshieldedOffer = yield* TransactionOps.addSignaturesToOffer(clonedIntent.fallibleUnshieldedOffer, signature, segment, 'fallible');
|
|
49
|
+
}
|
|
50
|
+
if (clonedIntent.guaranteedUnshieldedOffer) {
|
|
51
|
+
clonedIntent.guaranteedUnshieldedOffer = yield* TransactionOps.addSignaturesToOffer(clonedIntent.guaranteedUnshieldedOffer, signature, segment, 'guaranteed');
|
|
52
|
+
}
|
|
53
|
+
transaction.intents = transaction.intents?.set(segment, clonedIntent);
|
|
54
|
+
return transaction;
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
getImbalances(transaction, segment) {
|
|
58
|
+
const imbalances = transaction
|
|
59
|
+
.imbalances(segment)
|
|
60
|
+
.entries()
|
|
61
|
+
.filter(([token, value]) => token.tag === 'unshielded' && value !== 0n)
|
|
62
|
+
.map(([token, value]) => [token.raw.toString(), value])
|
|
63
|
+
.toArray();
|
|
64
|
+
return Imbalances.fromEntries(imbalances);
|
|
65
|
+
},
|
|
66
|
+
addSignaturesToOffer(offer, signature, segment, offerType) {
|
|
67
|
+
return pipe(offer.inputs, Arr.map((_, i) => offer.signatures.at(i) ?? signature), (signatures) => Either.try({
|
|
68
|
+
try: () => offer.addSignatures(signatures),
|
|
69
|
+
catch: (error) => new TransactingError({
|
|
70
|
+
message: `Failed to add ${offerType} signature at segment ${segment}`,
|
|
71
|
+
cause: error,
|
|
72
|
+
}),
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
isIntentBound(intent) {
|
|
76
|
+
return intent.binding.instance === 'binding';
|
|
77
|
+
},
|
|
78
|
+
extractOwnInputs(transaction, signatureVerifyingKey) {
|
|
79
|
+
const segments = TransactionOps.getSegments(transaction);
|
|
80
|
+
return pipe(segments, Arr.flatMap((segment) => {
|
|
81
|
+
const intent = transaction.intents?.get(segment);
|
|
82
|
+
if (!intent) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const { guaranteedUnshieldedOffer, fallibleUnshieldedOffer } = intent;
|
|
86
|
+
const ownedInputsfromGuaranteedSection = guaranteedUnshieldedOffer?.inputs
|
|
87
|
+
? guaranteedUnshieldedOffer.inputs.filter((input) => input.owner === signatureVerifyingKey)
|
|
88
|
+
: [];
|
|
89
|
+
const ownedInputsfromFallibleSection = fallibleUnshieldedOffer
|
|
90
|
+
? fallibleUnshieldedOffer.inputs.filter((input) => input.owner === signatureVerifyingKey)
|
|
91
|
+
: [];
|
|
92
|
+
return [...ownedInputsfromGuaranteedSection, ...ownedInputsfromFallibleSection];
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
95
|
+
};
|