@midnight-ntwrk/wallet-sdk-facade 5.0.0-rc.0 → 5.0.0-rc.1
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/index.d.ts +37 -3
- package/dist/index.js +195 -13
- package/dist/transaction.d.ts +12 -0
- package/dist/transaction.js +26 -32
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -7,13 +7,18 @@ import { type DefaultShieldedConfiguration, type ShieldedWalletAPI, type Shielde
|
|
|
7
7
|
import type { DefaultUnshieldedConfiguration, SignSegment, UnshieldedWalletAPI } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
|
|
8
8
|
import { type UnshieldedWalletState } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
|
|
9
9
|
import { Clock } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
10
|
-
import {
|
|
10
|
+
import { DateTime, Option, Schema } from 'effect';
|
|
11
11
|
import { type AnyTx, type FinalizedTx, ProtocolVersion, TransactionHistoryStorage, type UnboundTx, type UnprovenTx, WalletTransaction } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
12
12
|
import * as ledgerV8 from '@midnight-ntwrk/ledger-v8';
|
|
13
13
|
import { type WalletSeeds } from '@midnight-ntwrk/wallet-sdk-hd';
|
|
14
14
|
import { type Observable } from 'rxjs';
|
|
15
15
|
import { type DefaultPendingTransactionsServiceConfiguration, PendingTransactions, type PendingTransactionsService } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
16
16
|
import { type AnyLedgerParameters, type AnyVersionValidatableTransaction, type BlockData, type BlockDataFetcher, type ValidateTxOptions, ValidationFetchError, type VersionedValidationService, WellFormedError, type WellFormedStrictnessFlags } from '@midnight-ntwrk/wallet-sdk-capabilities/validation';
|
|
17
|
+
/**
|
|
18
|
+
* The traits pending transactions are read with, exported so a test that restores a stored pending-transaction payload
|
|
19
|
+
* reads it exactly as the wallet does. Building a second trait for that would be testing the copy, not the wallet.
|
|
20
|
+
*/
|
|
21
|
+
export { finalizedTransactionTraits } from './transaction.js';
|
|
17
22
|
import { type DustAddress, type ShieldedAddress, type UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
|
|
18
23
|
/**
|
|
19
24
|
* Full entry schema for transaction history. The common entry data and wallet-specific sections (`shielded`,
|
|
@@ -32,11 +37,11 @@ export declare const WalletEntrySchema: Schema.Struct<Readonly<{
|
|
|
32
37
|
submittedAt: typeof Schema.Date;
|
|
33
38
|
}>, Schema.Struct<{
|
|
34
39
|
status: Schema.Literal<["finalized"]>;
|
|
35
|
-
finalizedBlock: Schema.Struct<{
|
|
40
|
+
finalizedBlock: Schema.optional<Schema.Struct<{
|
|
36
41
|
hash: typeof Schema.String;
|
|
37
42
|
height: typeof Schema.Number;
|
|
38
43
|
timestamp: typeof Schema.Date;
|
|
39
|
-
}
|
|
44
|
+
}>>;
|
|
40
45
|
}>, Schema.Struct<{
|
|
41
46
|
status: Schema.Literal<["rejected"]>;
|
|
42
47
|
rejectedAt: typeof Schema.Date;
|
|
@@ -786,6 +791,35 @@ export declare class WalletFacade {
|
|
|
786
791
|
ttl: Date;
|
|
787
792
|
payFees?: boolean;
|
|
788
793
|
}): Promise<UnprovenTransactionRecipe>;
|
|
794
|
+
/**
|
|
795
|
+
* Builds and signs a transaction registering `nightUtxos` to generate dust for `dustReceiverAddress`.
|
|
796
|
+
*
|
|
797
|
+
* @remarks
|
|
798
|
+
* A first-time registration pays its own fee out of the dust its Night has generated so far. When that falls short,
|
|
799
|
+
* the Night UTxOs booked for the transaction are released before the call rejects, so it can be retried as is.
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* const register = () => wallet.registerNightUtxosForDustGeneration(nightUtxos, verifyingKey, signData);
|
|
803
|
+
* const recipe = await register().catch(async (error: unknown) => {
|
|
804
|
+
* // An unreachable fee will not be covered by waiting, so only a reachable one is waited out.
|
|
805
|
+
* if (!isInsufficientDustForFeeError(error) || error.estimate._tag !== 'Reachable') throw error;
|
|
806
|
+
* const timeoutMs = Number(error.estimate.seconds) * 1000 + 60_000;
|
|
807
|
+
* await wallet.waitForGeneratedDust(nightUtxos, error.fee, { timeoutMs });
|
|
808
|
+
* return register();
|
|
809
|
+
* });
|
|
810
|
+
* ```;
|
|
811
|
+
*
|
|
812
|
+
* @param nightUtxos - The Night UTxOs to register. Must be non-empty.
|
|
813
|
+
* @param nightVerifyingKey - The verifying key that owns them.
|
|
814
|
+
* @param signDustRegistration - Signs the registration and the unshielded offers.
|
|
815
|
+
* @param dustReceiverAddress - Where the generated dust goes. Defaults to this wallet's own dust address.
|
|
816
|
+
* @returns The unproven registration transaction, signed.
|
|
817
|
+
* @throws Error if `nightUtxos` is empty.
|
|
818
|
+
* @throws InsufficientDustForFeeError (`_tag` `'Wallet.InsufficientDustForFee'`, recognised by
|
|
819
|
+
* `isInsufficientDustForFeeError` from `@midnight-ntwrk/wallet-sdk`) when the generated dust does not yet cover the
|
|
820
|
+
* fee. It carries `claimableFeePayment`, `fee`, `shortfall` and an `estimate` of when it will; when it is
|
|
821
|
+
* reachable, wait with {@link waitForGeneratedDust} for at least that long and retry.
|
|
822
|
+
*/
|
|
789
823
|
registerNightUtxosForDustGeneration(nightUtxos: readonly UtxoWithMeta[], nightVerifyingKey: ledgerV9.SignatureVerifyingKey, signDustRegistration: SignSegment, dustReceiverAddress?: DustAddress): Promise<UnprovenTransactionRecipe>;
|
|
790
824
|
/**
|
|
791
825
|
* Waits until the dust projected to be generated by the given Night UTxOs reaches `requiredAmount`, re-checking every
|
package/dist/index.js
CHANGED
|
@@ -15,18 +15,25 @@ import { makeDefaultSubmissionService, } from '@midnight-ntwrk/wallet-sdk-capabi
|
|
|
15
15
|
import { makeDefaultVersionedProvingService, } from '@midnight-ntwrk/wallet-sdk-capabilities/proving';
|
|
16
16
|
import { ShieldedSectionSchema, mergeShieldedSections, } from '@midnight-ntwrk/wallet-sdk-shielded';
|
|
17
17
|
import { UnshieldedSectionSchema, mergeUnshieldedSections, } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
|
|
18
|
+
import { TransactionOps as V1UnshieldedTransactionOps } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet/v1';
|
|
19
|
+
import { TransactionOps as V2UnshieldedTransactionOps } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet/v2';
|
|
18
20
|
import { DustSectionSchema, mergeDustSections } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
|
|
19
21
|
import { Clock } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
20
22
|
import { FetchTermsAndConditions as FetchTermsAndConditionsQuery } from '@midnight-ntwrk/wallet-sdk-indexer-client';
|
|
21
23
|
import { QueryRunner } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
|
|
22
|
-
import { Array as Arr, Either, Option, pipe, Schema } from 'effect';
|
|
24
|
+
import { Array as Arr, DateTime, Either, Option, pipe, Schema } from 'effect';
|
|
23
25
|
import { ProtocolVersion, TransactionHistoryStorage, WalletTransaction, WireFormatError, } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
24
26
|
import * as ledgerV8 from '@midnight-ntwrk/ledger-v8';
|
|
25
27
|
import * as Signatures from '@midnight-ntwrk/wallet-sdk-capabilities/signatures';
|
|
26
|
-
import { BehaviorSubject, combineLatest, concatMap, distinctUntilChanged, firstValueFrom, map, tap, } from 'rxjs';
|
|
28
|
+
import { BehaviorSubject, combineLatest, concatMap, distinctUntilChanged, filter, firstValueFrom, map, take, tap, } from 'rxjs';
|
|
27
29
|
import { PendingTransactions, PendingTransactionsServiceImpl, } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
28
30
|
import { makeDefaultBlockDataFetcher, makeDefaultVersionedValidationService, ValidationFetchError, WellFormedError, } from '@midnight-ntwrk/wallet-sdk-capabilities/validation';
|
|
29
31
|
import { finalizedTransactionTraits, txHistoryHash } from './transaction.js';
|
|
32
|
+
/**
|
|
33
|
+
* The traits pending transactions are read with, exported so a test that restores a stored pending-transaction payload
|
|
34
|
+
* reads it exactly as the wallet does. Building a second trait for that would be testing the copy, not the wallet.
|
|
35
|
+
*/
|
|
36
|
+
export { finalizedTransactionTraits } from './transaction.js';
|
|
30
37
|
/**
|
|
31
38
|
* Why the wallet gave up on a transaction, for the history entry.
|
|
32
39
|
*
|
|
@@ -292,6 +299,8 @@ export class FacadeState {
|
|
|
292
299
|
}
|
|
293
300
|
}
|
|
294
301
|
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
302
|
+
/** How a coin is named wherever it is referred to without being carried: in a reservation, and in a release by id. */
|
|
303
|
+
const coinIdsOf = (utxos) => utxos.map((utxo) => `${utxo.intentHash}#${utxo.outputNo}`);
|
|
295
304
|
/**
|
|
296
305
|
* Clock abstraction for obtaining the current time. By default, the facade uses the system clock
|
|
297
306
|
* ({@link Clock.systemClock}); for testing with a simulator, inject a custom clock (e.g. one backed by the simulator's
|
|
@@ -447,6 +456,8 @@ export class WalletFacade {
|
|
|
447
456
|
#txHistoryStorage;
|
|
448
457
|
clock;
|
|
449
458
|
#pendingSubscription;
|
|
459
|
+
#expiredReservationSubscription;
|
|
460
|
+
#restoredBookingSubscription;
|
|
450
461
|
#protocolVersionSubscription;
|
|
451
462
|
/**
|
|
452
463
|
* The protocol version the wallets have all reached, as last observed.
|
|
@@ -483,6 +494,38 @@ export class WalletFacade {
|
|
|
483
494
|
}
|
|
484
495
|
}))
|
|
485
496
|
.subscribe();
|
|
497
|
+
// A reservation past its TTL stands for a transaction the ledger will not accept, so the record has done its job
|
|
498
|
+
// and is dropped. It does not release the coins: the wallet books each one with the same TTL and sweeps them
|
|
499
|
+
// itself, so one mechanism owns release.
|
|
500
|
+
//
|
|
501
|
+
// Releasing from here as well would be a second sweep on a different clock, and a coin the wallet has already
|
|
502
|
+
// freed can be booked again for another transaction before this one fires. The ids a reservation holds say
|
|
503
|
+
// nothing about which booking currently holds them, so that release would free the wrong one.
|
|
504
|
+
this.#expiredReservationSubscription = this.pendingTransactionsService
|
|
505
|
+
.state()
|
|
506
|
+
.pipe(concatMap((pending) => PendingTransactions.allExpiredReservations(pending)), concatMap((reservation) => this.pendingTransactionsService.clearReservation(reservation.identifiers)))
|
|
507
|
+
.subscribe();
|
|
508
|
+
// Coins a previous process left booked would otherwise sit until their transactions' TTLs, which can be an hour.
|
|
509
|
+
// Once sync reaches the tip, every transaction this address is party to has been applied, so a coin still booked
|
|
510
|
+
// was never spent — unless a reservation or a transaction being tracked says the spend is still out there. Both
|
|
511
|
+
// halves matter: registering a transaction drops the reservation standing in for it, so from submission onwards
|
|
512
|
+
// the tracked transaction is the only thing saying its coins are spoken for.
|
|
513
|
+
//
|
|
514
|
+
// This only sees what the services were given. A pending-transactions service restored from storage covers the
|
|
515
|
+
// spends a previous process submitted; one started empty covers none of them, and a restored booking whose
|
|
516
|
+
// transaction is still in the mempool is released here.
|
|
517
|
+
this.#restoredBookingSubscription = this.unshielded.state
|
|
518
|
+
.pipe(filter((unshielded) => unshielded.state.progress.isStrictlyComplete()),
|
|
519
|
+
// Only bookings restored at startup are candidates, and this releases all of them at once, so the first time
|
|
520
|
+
// sync reaches the tip is the only time there is anything to do. Acting once also keeps the reaction from
|
|
521
|
+
// feeding itself: releasing publishes new state, which would otherwise satisfy this same condition again.
|
|
522
|
+
take(1), concatMap(async () => {
|
|
523
|
+
const { publicKey } = (await firstValueFrom(this.unshielded.state)).state;
|
|
524
|
+
const pending = await firstValueFrom(this.pendingTransactionsService.state());
|
|
525
|
+
const stillSpokenFor = PendingTransactions.coveredUnshieldedIds(pending, (tx) => this.#ownInputIds(tx, publicKey.publicKey));
|
|
526
|
+
await this.unshielded.releaseRestoredPending(stillSpokenFor);
|
|
527
|
+
}))
|
|
528
|
+
.subscribe();
|
|
486
529
|
// Deliberately built from the wallets' own states rather than from `state()`: `state()` includes the pending set,
|
|
487
530
|
// and orphaning writes to it, so feeding that back here would be a cycle.
|
|
488
531
|
this.#protocolVersionSubscription = combineLatest([this.shielded.state, this.unshielded.state, this.dust.state])
|
|
@@ -593,6 +636,107 @@ export class WalletFacade {
|
|
|
593
636
|
}),
|
|
594
637
|
}).pipe(Either.map((transaction) => WalletTransaction.adopt(stage, transaction, protocolVersion))), (error) => error);
|
|
595
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Reads the transaction a handle carries, with the ledger version the handle itself names.
|
|
641
|
+
*
|
|
642
|
+
* @remarks
|
|
643
|
+
* The counterpart of {@link accept}, which reads at the version the facade is acting at and refuses the rest. This one
|
|
644
|
+
* asks nothing of the facade's position: the handle was stamped by whichever ledger version built it, and that
|
|
645
|
+
* version is the one that can read its bytes. It is how a transaction tracked across a crossing can still name its
|
|
646
|
+
* coins afterwards.
|
|
647
|
+
* @param handle The handle to read.
|
|
648
|
+
* @returns The carried transaction.
|
|
649
|
+
*/
|
|
650
|
+
#carried(handle) {
|
|
651
|
+
return Either.getOrThrowWith(WalletTransaction.unwrapWithin(handle, this.epochOf(handle.protocolVersion)), (error) => error);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The ids of this wallet's own coins that `handle` spends, read with the ledger version that built it.
|
|
655
|
+
*
|
|
656
|
+
* @remarks
|
|
657
|
+
* The identity is this wallet's in ledger-v9's shape; a transaction built below the boundary is read with ledger-v8,
|
|
658
|
+
* whose verifying key is the bare bytes of the one scheme it has, so the key is narrowed to match. Only a schnorr
|
|
659
|
+
* identity ever has a ledger-v8 transaction to read, because the wallet refuses to start that variant with any
|
|
660
|
+
* other, so the narrowing cannot fail here and a failure is reported as the defect it would be.
|
|
661
|
+
* @param handle The transaction to read.
|
|
662
|
+
* @param publicKey This wallet's verifying key, in the shape of whichever variant is running: ledger-v8's bare hex
|
|
663
|
+
* below the boundary, ledger-v9's tagged record from it.
|
|
664
|
+
* @returns Every coin of this wallet's the transaction spends, each `intentHash#outputNo`.
|
|
665
|
+
*/
|
|
666
|
+
#ownInputIds(handle, publicKey) {
|
|
667
|
+
// Lifted once so both branches start from ledger-v9's shape; ledger-v8's key is the bare string, so the check is exact.
|
|
668
|
+
const verifyingKey = typeof publicKey === 'string' ? Signatures.liftSignatureVerifyingKey(publicKey) : publicKey;
|
|
669
|
+
const ownInputs = handle.protocolVersion < this.#forkVersion
|
|
670
|
+
? V1UnshieldedTransactionOps.extractOwnInputs(this.#carried(handle), Either.getOrThrowWith(Signatures.lowerSignatureVerifyingKey(verifyingKey), (error) => error))
|
|
671
|
+
: V2UnshieldedTransactionOps.extractOwnInputs(this.#carried(handle), verifyingKey);
|
|
672
|
+
return coinIdsOf(ownInputs);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Records that a just-balanced transaction has unshielded coins reserved.
|
|
676
|
+
*
|
|
677
|
+
* Balancing books those coins, but nothing tracks the transaction until it is submitted, so a caller that abandons it
|
|
678
|
+
* in between leaves the coins spoken for with no record saying so. The reservation is that record, and it is matched
|
|
679
|
+
* to the transaction that eventually arrives by identifier, which proving and binding leave unchanged.
|
|
680
|
+
*
|
|
681
|
+
* Takes the transaction the unshielded wallet produced rather than the merged one: its inputs are exactly the coins
|
|
682
|
+
* booked here, and its identifiers are a subset of whatever the merged transaction ends up carrying.
|
|
683
|
+
*/
|
|
684
|
+
async #reserve(unshieldedTx, ttl) {
|
|
685
|
+
if (!unshieldedTx) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const { publicKey } = (await firstValueFrom(this.unshielded.state)).state;
|
|
689
|
+
await this.#reserveIds(unshieldedTx, this.#ownInputIds(unshieldedTx, publicKey.publicKey), ttl);
|
|
690
|
+
}
|
|
691
|
+
/** Records `bookedIds` against `unshieldedTx`, or nothing at all when the transaction booked no coins. */
|
|
692
|
+
async #reserveIds(unshieldedTx, bookedIds, ttl) {
|
|
693
|
+
if (bookedIds.length === 0) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const identity = this.#carried(unshieldedTx);
|
|
697
|
+
await this.pendingTransactionsService.addReservation({
|
|
698
|
+
identifiers: identity.identifiers(),
|
|
699
|
+
intentHashes: [...(identity.intents?.entries() ?? [])].map(([segment, intent]) => intent.intentHash(segment)),
|
|
700
|
+
inputs: { unshielded: bookedIds },
|
|
701
|
+
ttl,
|
|
702
|
+
createdAt: DateTime.unsafeFromDate(this.clock.now()),
|
|
703
|
+
expired: false,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Balances a transaction in place and records only the coins the wallet itself booked doing so.
|
|
708
|
+
*
|
|
709
|
+
* In-place balancing hands back the caller's own transaction with the wallet's inputs added, so the result names
|
|
710
|
+
* coins from two sources: the ones coin selection just took, and any the caller had already put there. Only the first
|
|
711
|
+
* are this wallet's booking, and they are exactly the inputs of this wallet's that the balanced transaction carries
|
|
712
|
+
* and the caller's did not.
|
|
713
|
+
*
|
|
714
|
+
* Read off the two transactions, not off the wallet's published state: the wallet publishes its state on its own
|
|
715
|
+
* fiber, so a read taken as soon as the balancing call returns can still show the coins as available, and the booking
|
|
716
|
+
* would go unrecorded.
|
|
717
|
+
*/
|
|
718
|
+
async #balanceInPlaceAndReserve(tx, balance, ttl) {
|
|
719
|
+
const { publicKey } = (await firstValueFrom(this.unshielded.state)).state;
|
|
720
|
+
const inputsBefore = new Set(this.#ownInputIds(tx, publicKey.publicKey));
|
|
721
|
+
const balanced = await balance();
|
|
722
|
+
if (balanced === undefined) {
|
|
723
|
+
return balanced;
|
|
724
|
+
}
|
|
725
|
+
const newlyBooked = this.#ownInputIds(balanced, publicKey.publicKey).filter((id) => !inputsBefore.has(id));
|
|
726
|
+
await this.#reserveIds(balanced, newlyBooked, ttl);
|
|
727
|
+
return balanced;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Undoes a booking taken while balancing, together with the record that stood for it.
|
|
731
|
+
*
|
|
732
|
+
* The two always go together. A record left behind outlives the coins it named, and until its TTL passes it tells
|
|
733
|
+
* anything that reads it that a spend is still out there — holding coins at the next reconciliation that nothing is
|
|
734
|
+
* actually holding.
|
|
735
|
+
*/
|
|
736
|
+
async #revertUnshieldedBooking(tx) {
|
|
737
|
+
await this.unshielded.revertTransaction(tx);
|
|
738
|
+
await this.pendingTransactionsService.clearReservation([...this.#carried(tx).identifiers()]);
|
|
739
|
+
}
|
|
596
740
|
defaultTtl() {
|
|
597
741
|
return new Date(this.clock.now().getTime() + DEFAULT_TTL_MS);
|
|
598
742
|
}
|
|
@@ -666,11 +810,12 @@ export class WalletFacade {
|
|
|
666
810
|
const dustReceiverAddress = isRegistration ? action.dustReceiverAddress : undefined;
|
|
667
811
|
// Step 1 — Dust decides which Night UTxO belongs in the guaranteed slot (the one whose dust
|
|
668
812
|
// generation can pay the fee) and computes the fee-payment allowance.
|
|
669
|
-
const
|
|
813
|
+
const nightUtxosWithMeta = nightUtxos.map(({ utxo, meta }) => ({
|
|
670
814
|
...utxo,
|
|
671
815
|
ctime: meta.ctime,
|
|
672
816
|
registeredForDustGeneration: meta.registeredForDustGeneration,
|
|
673
|
-
}))
|
|
817
|
+
}));
|
|
818
|
+
const split = await this.dust.splitNightUtxosForDustRegistration(now, nightUtxosWithMeta, isRegistration);
|
|
674
819
|
const toUnshieldedUtxoWithMeta = (u) => ({
|
|
675
820
|
utxo: {
|
|
676
821
|
value: u.utxo.value,
|
|
@@ -690,6 +835,7 @@ export class WalletFacade {
|
|
|
690
835
|
// with the two offers. After this point, a concurrent build call that wants any of these UTxOs
|
|
691
836
|
// will fail fast.
|
|
692
837
|
const txWithOffers = await this.unshielded.rotateUtxos(guaranteedForUnshielded, fallibleForUnshielded, nightVerifyingKey, ttl);
|
|
838
|
+
await this.#reserve(txWithOffers, ttl);
|
|
693
839
|
// Step 3 — Dust attaches its DustActions onto the intent the unshielded wallet just built.
|
|
694
840
|
// If this fails we must unbook the UTxOs so the caller can retry.
|
|
695
841
|
let txWithDustActions;
|
|
@@ -697,7 +843,7 @@ export class WalletFacade {
|
|
|
697
843
|
txWithDustActions = await this.dust.attachDustRegistration(txWithOffers, now, nightVerifyingKey, dustReceiverAddress, split.feePayment);
|
|
698
844
|
}
|
|
699
845
|
catch (error) {
|
|
700
|
-
await this
|
|
846
|
+
await this.#revertUnshieldedBooking(txWithOffers);
|
|
701
847
|
throw error;
|
|
702
848
|
}
|
|
703
849
|
// Step 4 (first-time registration only) — Fail fast if the dust generated so far by the
|
|
@@ -713,10 +859,12 @@ export class WalletFacade {
|
|
|
713
859
|
const hasUnregisteredGuaranteed = split.guaranteedUtxos.some((u) => !u.utxo.registeredForDustGeneration);
|
|
714
860
|
if (isRegistration && hasUnregisteredGuaranteed) {
|
|
715
861
|
const fee = await this.dust.calculateFee([txWithDustActions]);
|
|
716
|
-
|
|
717
|
-
await this.
|
|
718
|
-
|
|
719
|
-
|
|
862
|
+
try {
|
|
863
|
+
await this.dust.ensureFeeCoverage(now, nightUtxosWithMeta, split.feePayment, fee);
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
await this.#revertUnshieldedBooking(txWithOffers);
|
|
867
|
+
throw error;
|
|
720
868
|
}
|
|
721
869
|
}
|
|
722
870
|
// Step 5 — Sign via the standard signRecipe pathway, which now stamps both the unshielded
|
|
@@ -729,7 +877,7 @@ export class WalletFacade {
|
|
|
729
877
|
return signedRecipe.transaction;
|
|
730
878
|
}
|
|
731
879
|
catch (error) {
|
|
732
|
-
await this
|
|
880
|
+
await this.#revertUnshieldedBooking(txWithOffers);
|
|
733
881
|
throw error;
|
|
734
882
|
}
|
|
735
883
|
}
|
|
@@ -796,6 +944,7 @@ export class WalletFacade {
|
|
|
796
944
|
const unshieldedBalancingTx = shouldBalanceUnshielded
|
|
797
945
|
? await this.unshielded.balanceFinalizedTransaction(tx)
|
|
798
946
|
: undefined;
|
|
947
|
+
await this.#reserve(unshieldedBalancingTx, ttl);
|
|
799
948
|
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
800
949
|
// Step 2: Merge unshielded and shielded balancing
|
|
801
950
|
const mergedBalancingTx = this.mergeUnprovenTransactions(shieldedBalancingTx, unshieldedBalancingTx);
|
|
@@ -836,7 +985,7 @@ export class WalletFacade {
|
|
|
836
985
|
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
837
986
|
// For unbound transactions, unshielded balancing happens in place not with a balancing transaction
|
|
838
987
|
const balancedUnshieldedTx = shouldBalanceUnshielded
|
|
839
|
-
? await this.unshielded.balanceUnboundTransaction(tx)
|
|
988
|
+
? await this.#balanceInPlaceAndReserve(tx, () => this.unshielded.balanceUnboundTransaction(tx), ttl)
|
|
840
989
|
: undefined;
|
|
841
990
|
// Step 2: Unbound unshielded tx are balanced in place, use it as base tx if present
|
|
842
991
|
const baseTx = balancedUnshieldedTx ?? tx;
|
|
@@ -878,7 +1027,7 @@ export class WalletFacade {
|
|
|
878
1027
|
const shieldedBalancingTx = shouldBalanceShielded ? await this.shielded.balanceTransaction(tx) : undefined;
|
|
879
1028
|
// For unproven transactions, unshielded balancing happens in place
|
|
880
1029
|
const balancedUnshieldedTx = shouldBalanceUnshielded
|
|
881
|
-
? await this.unshielded.balanceUnprovenTransaction(tx)
|
|
1030
|
+
? await this.#balanceInPlaceAndReserve(tx, () => this.unshielded.balanceUnprovenTransaction(tx), ttl)
|
|
882
1031
|
: undefined;
|
|
883
1032
|
// Step 2: Use the balanced unshielded tx if present, otherwise use the original tx
|
|
884
1033
|
const baseTx = balancedUnshieldedTx ?? tx;
|
|
@@ -1009,7 +1158,7 @@ export class WalletFacade {
|
|
|
1009
1158
|
catch (error) {
|
|
1010
1159
|
await Promise.allSettled([
|
|
1011
1160
|
this.shielded.revertTransaction(tx),
|
|
1012
|
-
this
|
|
1161
|
+
this.#revertUnshieldedBooking(tx),
|
|
1013
1162
|
this.dust.revertTransaction(tx),
|
|
1014
1163
|
]);
|
|
1015
1164
|
throw error;
|
|
@@ -1035,6 +1184,7 @@ export class WalletFacade {
|
|
|
1035
1184
|
}
|
|
1036
1185
|
const shieldedTx = shieldedOutputs.length > 0 ? await this.shielded.transferTransaction(shieldedOutputs) : undefined;
|
|
1037
1186
|
const unshieldedTx = unshieldedOutputs.length > 0 ? await this.unshielded.transferTransaction(unshieldedOutputs, ttl) : undefined;
|
|
1187
|
+
await this.#reserve(unshieldedTx, ttl);
|
|
1038
1188
|
const mergedTxs = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
|
|
1039
1189
|
// Add fee payment
|
|
1040
1190
|
const dustResult = payFees ? await this.dust.balanceTransactions([mergedTxs], ttl) : undefined;
|
|
@@ -1110,6 +1260,7 @@ export class WalletFacade {
|
|
|
1110
1260
|
const unshieldedTx = hasUnshieldedPart
|
|
1111
1261
|
? await this.unshielded.initSwap(unshieldedInputs ?? {}, unshieldedOutputs, ttl)
|
|
1112
1262
|
: undefined;
|
|
1263
|
+
await this.#reserve(unshieldedTx, ttl);
|
|
1113
1264
|
const combinedTx = this.mergeUnprovenTransactions(shieldedTx, unshieldedTx);
|
|
1114
1265
|
if (!combinedTx) {
|
|
1115
1266
|
throw Error('Unexpected transaction state.');
|
|
@@ -1124,6 +1275,35 @@ export class WalletFacade {
|
|
|
1124
1275
|
...(dustResult ? { blockData: dustResult.blockData } : {}),
|
|
1125
1276
|
};
|
|
1126
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Builds and signs a transaction registering `nightUtxos` to generate dust for `dustReceiverAddress`.
|
|
1280
|
+
*
|
|
1281
|
+
* @remarks
|
|
1282
|
+
* A first-time registration pays its own fee out of the dust its Night has generated so far. When that falls short,
|
|
1283
|
+
* the Night UTxOs booked for the transaction are released before the call rejects, so it can be retried as is.
|
|
1284
|
+
* @example
|
|
1285
|
+
* ```ts
|
|
1286
|
+
* const register = () => wallet.registerNightUtxosForDustGeneration(nightUtxos, verifyingKey, signData);
|
|
1287
|
+
* const recipe = await register().catch(async (error: unknown) => {
|
|
1288
|
+
* // An unreachable fee will not be covered by waiting, so only a reachable one is waited out.
|
|
1289
|
+
* if (!isInsufficientDustForFeeError(error) || error.estimate._tag !== 'Reachable') throw error;
|
|
1290
|
+
* const timeoutMs = Number(error.estimate.seconds) * 1000 + 60_000;
|
|
1291
|
+
* await wallet.waitForGeneratedDust(nightUtxos, error.fee, { timeoutMs });
|
|
1292
|
+
* return register();
|
|
1293
|
+
* });
|
|
1294
|
+
* ```;
|
|
1295
|
+
*
|
|
1296
|
+
* @param nightUtxos - The Night UTxOs to register. Must be non-empty.
|
|
1297
|
+
* @param nightVerifyingKey - The verifying key that owns them.
|
|
1298
|
+
* @param signDustRegistration - Signs the registration and the unshielded offers.
|
|
1299
|
+
* @param dustReceiverAddress - Where the generated dust goes. Defaults to this wallet's own dust address.
|
|
1300
|
+
* @returns The unproven registration transaction, signed.
|
|
1301
|
+
* @throws Error if `nightUtxos` is empty.
|
|
1302
|
+
* @throws InsufficientDustForFeeError (`_tag` `'Wallet.InsufficientDustForFee'`, recognised by
|
|
1303
|
+
* `isInsufficientDustForFeeError` from `@midnight-ntwrk/wallet-sdk`) when the generated dust does not yet cover the
|
|
1304
|
+
* fee. It carries `claimableFeePayment`, `fee`, `shortfall` and an `estimate` of when it will; when it is
|
|
1305
|
+
* reachable, wait with {@link waitForGeneratedDust} for at least that long and retry.
|
|
1306
|
+
*/
|
|
1127
1307
|
async registerNightUtxosForDustGeneration(nightUtxos, nightVerifyingKey, signDustRegistration, dustReceiverAddress) {
|
|
1128
1308
|
if (nightUtxos.length === 0) {
|
|
1129
1309
|
throw Error('At least one Night UTXO is required.');
|
|
@@ -1271,6 +1451,8 @@ export class WalletFacade {
|
|
|
1271
1451
|
this.pendingTransactionsService.stop(),
|
|
1272
1452
|
Promise.resolve(this.#pendingSubscription?.unsubscribe()),
|
|
1273
1453
|
Promise.resolve(this.#protocolVersionSubscription?.unsubscribe()),
|
|
1454
|
+
Promise.resolve(this.#expiredReservationSubscription?.unsubscribe()),
|
|
1455
|
+
Promise.resolve(this.#restoredBookingSubscription?.unsubscribe()),
|
|
1274
1456
|
]);
|
|
1275
1457
|
}
|
|
1276
1458
|
async queryTxHistoryByHash(hash) {
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type PendingTransactions } from '@midnight-ntwrk/wallet-sdk-capabilities';
|
|
2
2
|
import { ProtocolVersion, type FinalizedTx } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
3
|
+
import * as ledgerV8 from '@midnight-ntwrk/ledger-v8';
|
|
4
|
+
import * as ledgerV9 from '@midnightntwrk/ledger-v9';
|
|
3
5
|
/**
|
|
4
6
|
* The key under which a transaction's history entry is stored.
|
|
5
7
|
*
|
|
@@ -20,6 +22,16 @@ export declare const txHistoryHash: (tx: {
|
|
|
20
22
|
transactionHash: () => unknown;
|
|
21
23
|
serialize: () => Uint8Array;
|
|
22
24
|
}) => string;
|
|
25
|
+
export declare const v9FinalizedTransactionTrait: PendingTransactions.TransactionTrait<ledgerV9.FinalizedTransaction>;
|
|
26
|
+
/**
|
|
27
|
+
* The ledger-v8's reading of a pending transaction.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Structurally the same as ledger-v9's — identifiers, a TTL, bytes — against a different ledger version's classes. It
|
|
31
|
+
* is the classes that make this a separate trait: `instanceof` distinguishes them, each deserializer refuses the
|
|
32
|
+
* other's bytes, and a grace period is read off that version's own initial parameters.
|
|
33
|
+
*/
|
|
34
|
+
export declare const v8FinalizedTransactionTrait: PendingTransactions.TransactionTrait<ledgerV8.FinalizedTransaction>;
|
|
23
35
|
/**
|
|
24
36
|
* The traits pending transactions are read with, split at the protocol version this chain forks at.
|
|
25
37
|
*
|
package/dist/transaction.js
CHANGED
|
@@ -40,7 +40,29 @@ export const txHistoryHash = (tx) => {
|
|
|
40
40
|
return Buffer.from(tx.serialize()).toString('hex');
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The one TTL rule, closed over the ledger whose grace period it reads: a transaction has expired once the earliest of
|
|
45
|
+
* its intent TTLs has passed, or, when it spends dust, carries shielded offers, or carries no intent at all, once the
|
|
46
|
+
* dust grace period after its creation has. The grace period is the wallet's own backstop rather than a ledger rule; it
|
|
47
|
+
* is what lets a rewards or bridge claim, which has no intents and no offers and which the indexer never reports a
|
|
48
|
+
* status for, leave the pending set at all.
|
|
49
|
+
*/
|
|
50
|
+
const hasTTLExpiredUnder = (dustGracePeriodSeconds) => (tx, creationTime, now) => {
|
|
51
|
+
const intentTTLs = pipe(tx.intents?.values().toArray() ?? [], Arr.map((i) => i.ttl), Arr.filterMap(DateTime.make));
|
|
52
|
+
const hasDustPayments = pipe(tx.intents?.values().toArray() ?? [], Arr.flatMap((i) => i.dustActions?.spends ?? []), Arr.isNonEmptyArray);
|
|
53
|
+
const hasShieldedOffers = tx.guaranteedOffer != null || (tx.fallibleOffer?.size ?? 0) > 0;
|
|
54
|
+
const hasNoDeadlineOfItsOwn = Arr.isEmptyReadonlyArray(intentTTLs);
|
|
55
|
+
const maybeShieldedTTL = hasDustPayments || hasShieldedOffers || hasNoDeadlineOfItsOwn
|
|
56
|
+
? pipe(creationTime, DateTime.addDuration(Duration.seconds(Number(dustGracePeriodSeconds()))), Arr.of)
|
|
57
|
+
: Arr.empty();
|
|
58
|
+
return pipe(intentTTLs, Arr.appendAll(maybeShieldedTTL), (arr) => Arr.isNonEmptyReadonlyArray(arr)
|
|
59
|
+
? Option.some(Arr.min(arr, Order.mapInput(Order.Date, DateTime.toDate)))
|
|
60
|
+
: Option.none(), Option.match({
|
|
61
|
+
onNone: () => false,
|
|
62
|
+
onSome: (finalTTL) => DateTime.distance(finalTTL, now) > 0,
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
export const v9FinalizedTransactionTrait = {
|
|
44
66
|
areAllTxIdsIncluded(tx, txIds) {
|
|
45
67
|
const txIdsSet = HashSet.fromIterable(tx.identifiers());
|
|
46
68
|
const expectedIdSet = HashSet.fromIterable(txIds);
|
|
@@ -52,21 +74,7 @@ const v9FinalizedTransactionTrait = {
|
|
|
52
74
|
firstId(tx) {
|
|
53
75
|
return tx.identifiers()[0];
|
|
54
76
|
},
|
|
55
|
-
hasTTLExpired(
|
|
56
|
-
const defaultShieldedGracePeriod = ledgerV9.LedgerParameters.initialParameters().dust.dustGracePeriodSeconds;
|
|
57
|
-
const intentTTLs = pipe(tx.intents?.values().toArray() ?? [], Arr.map((i) => i.ttl), Arr.filterMap(DateTime.make));
|
|
58
|
-
const hasDustPayments = pipe(tx.intents?.values().toArray() ?? [], Arr.flatMap((i) => i.dustActions?.spends ?? []), Arr.isNonEmptyArray);
|
|
59
|
-
const hasShieldedOffers = tx.guaranteedOffer != null || (tx.fallibleOffer?.size ?? 0) == 0;
|
|
60
|
-
const maybeShieldedTTL = hasDustPayments || hasShieldedOffers
|
|
61
|
-
? pipe(creationTime, DateTime.addDuration(Duration.seconds(Number(defaultShieldedGracePeriod))), Arr.of)
|
|
62
|
-
: Arr.empty();
|
|
63
|
-
return pipe(intentTTLs, Arr.appendAll(maybeShieldedTTL), (arr) => Arr.isNonEmptyReadonlyArray(arr)
|
|
64
|
-
? Option.some(Arr.min(arr, Order.mapInput(Order.Date, DateTime.toDate)))
|
|
65
|
-
: Option.none(), Option.match({
|
|
66
|
-
onNone: () => false,
|
|
67
|
-
onSome: (finalTTL) => DateTime.distance(finalTTL, now) > 0,
|
|
68
|
-
}));
|
|
69
|
-
},
|
|
77
|
+
hasTTLExpired: hasTTLExpiredUnder(() => ledgerV9.LedgerParameters.initialParameters().dust.dustGracePeriodSeconds),
|
|
70
78
|
ids(tx) {
|
|
71
79
|
return tx.identifiers();
|
|
72
80
|
},
|
|
@@ -92,7 +100,7 @@ const v9FinalizedTransactionTrait = {
|
|
|
92
100
|
* is the classes that make this a separate trait: `instanceof` distinguishes them, each deserializer refuses the
|
|
93
101
|
* other's bytes, and a grace period is read off that version's own initial parameters.
|
|
94
102
|
*/
|
|
95
|
-
const v8FinalizedTransactionTrait = {
|
|
103
|
+
export const v8FinalizedTransactionTrait = {
|
|
96
104
|
areAllTxIdsIncluded(tx, txIds) {
|
|
97
105
|
return HashSet.isSubset(HashSet.fromIterable(tx.identifiers()), HashSet.fromIterable(txIds));
|
|
98
106
|
},
|
|
@@ -102,21 +110,7 @@ const v8FinalizedTransactionTrait = {
|
|
|
102
110
|
firstId(tx) {
|
|
103
111
|
return tx.identifiers()[0];
|
|
104
112
|
},
|
|
105
|
-
hasTTLExpired(
|
|
106
|
-
const defaultShieldedGracePeriod = ledgerV8.LedgerParameters.initialParameters().dust.dustGracePeriodSeconds;
|
|
107
|
-
const intentTTLs = pipe(tx.intents?.values().toArray() ?? [], Arr.map((i) => i.ttl), Arr.filterMap(DateTime.make));
|
|
108
|
-
const hasDustPayments = pipe(tx.intents?.values().toArray() ?? [], Arr.flatMap((i) => i.dustActions?.spends ?? []), Arr.isNonEmptyArray);
|
|
109
|
-
const hasShieldedOffers = tx.guaranteedOffer != null || (tx.fallibleOffer?.size ?? 0) == 0;
|
|
110
|
-
const maybeShieldedTTL = hasDustPayments || hasShieldedOffers
|
|
111
|
-
? pipe(creationTime, DateTime.addDuration(Duration.seconds(Number(defaultShieldedGracePeriod))), Arr.of)
|
|
112
|
-
: Arr.empty();
|
|
113
|
-
return pipe(intentTTLs, Arr.appendAll(maybeShieldedTTL), (arr) => Arr.isNonEmptyReadonlyArray(arr)
|
|
114
|
-
? Option.some(Arr.min(arr, Order.mapInput(Order.Date, DateTime.toDate)))
|
|
115
|
-
: Option.none(), Option.match({
|
|
116
|
-
onNone: () => false,
|
|
117
|
-
onSome: (finalTTL) => DateTime.distance(finalTTL, now) > 0,
|
|
118
|
-
}));
|
|
119
|
-
},
|
|
113
|
+
hasTTLExpired: hasTTLExpiredUnder(() => ledgerV8.LedgerParameters.initialParameters().dust.dustGracePeriodSeconds),
|
|
120
114
|
ids(tx) {
|
|
121
115
|
return tx.identifiers();
|
|
122
116
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midnight-ntwrk/wallet-sdk-facade",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@midnightntwrk/ledger-v9": "1.0.0-rc.5",
|
|
30
|
-
"@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-rc.
|
|
30
|
+
"@midnight-ntwrk/wallet-sdk-abstractions": "3.0.0-rc.1",
|
|
31
31
|
"@midnight-ntwrk/wallet-sdk-address-format": "4.0.0-rc.0",
|
|
32
|
-
"@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-rc.
|
|
33
|
-
"@midnight-ntwrk/wallet-sdk-dust-wallet": "5.0.0-rc.
|
|
34
|
-
"@midnight-ntwrk/wallet-sdk-indexer-client": "2.0.0-rc.
|
|
35
|
-
"@midnight-ntwrk/wallet-sdk-shielded": "4.0.0-rc.
|
|
36
|
-
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "4.0.0-rc.
|
|
32
|
+
"@midnight-ntwrk/wallet-sdk-capabilities": "4.0.0-rc.1",
|
|
33
|
+
"@midnight-ntwrk/wallet-sdk-dust-wallet": "5.0.0-rc.1",
|
|
34
|
+
"@midnight-ntwrk/wallet-sdk-indexer-client": "2.0.0-rc.1",
|
|
35
|
+
"@midnight-ntwrk/wallet-sdk-shielded": "4.0.0-rc.1",
|
|
36
|
+
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "4.0.0-rc.1",
|
|
37
37
|
"effect": "^3.19.19",
|
|
38
38
|
"rxjs": "^7.8.2"
|
|
39
39
|
},
|