@midnightntwrk/wallet-sdk-unshielded-wallet 4.0.0-beta.0 → 4.0.0-beta.2

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.
@@ -18,6 +18,14 @@ export interface UnshieldedKeystore {
18
18
  getBech32Address(): MidnightBech32m;
19
19
  getPublicKey(): SignatureVerifyingKey;
20
20
  getAddress(): UserAddress;
21
+ /** The synchronous in-process signing primitive. */
21
22
  signData(data: Uint8Array): Signature;
23
+ /**
24
+ * Async counterpart of {@link signData} that conforms to the SDK's signer callback shape (`(data) =>
25
+ * Promise<Signature>`), so the keystore can be passed directly to `signRecipe`/`signUnprovenTransaction`/… without
26
+ * wrapping each call site. It simply resolves the synchronous {@link signData}; out-of-process backends (MPC, HSM)
27
+ * supply their own async signer.
28
+ */
29
+ signDataAsync: (data: Uint8Array) => Promise<Signature>;
22
30
  }
23
31
  export declare const createKeystore: (secretKey: UnshieldedSecretKey, networkId: NetworkId.NetworkId) => UnshieldedKeystore;
package/dist/KeyStore.js CHANGED
@@ -36,6 +36,7 @@ export const createKeystore = (secretKey, networkId) => {
36
36
  getPublicKey: () => signatureVerifyingKey(ledgerSigningKey),
37
37
  getAddress: () => addressFromKey(keystore.getPublicKey()),
38
38
  signData: (data) => signData(ledgerSigningKey, data),
39
+ signDataAsync: (data) => Promise.resolve(keystore.signData(data)),
39
40
  };
40
41
  return keystore;
41
42
  };
@@ -8,6 +8,7 @@ import { type CoinsAndBalancesCapability } from './v1/CoinsAndBalances.js';
8
8
  import { type KeysCapability } from './v1/Keys.js';
9
9
  import { type TokenTransfer, type FinalizedTransactionBalanceResult, type UnboundTransactionBalanceResult, type UnprovenTransactionBalanceResult } from './v1/Transacting.js';
10
10
  import { type WalletSyncUpdate } from './v1/SyncSchema.js';
11
+ import { type SignSegment } from './v1/Signing.js';
11
12
  import { type UtxoWithMeta } from './v1/UnshieldedState.js';
12
13
  import { type Variant, type VariantBuilder, type WalletLike } from '@midnightntwrk/wallet-sdk-runtime/abstractions';
13
14
  import { type PublicKey } from './KeyStore.js';
@@ -54,8 +55,8 @@ export type UnshieldedWalletAPI<TSerialized = string> = {
54
55
  */
55
56
  rotateUtxos(guaranteedUtxos: readonly UtxoWithMeta[], fallibleUtxos: readonly UtxoWithMeta[], nightVerifyingKey: ledger.SignatureVerifyingKey, ttl: Date): Promise<ledger.UnprovenTransaction>;
56
57
  initSwap(desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: readonly TokenTransfer[], ttl: Date): Promise<ledger.UnprovenTransaction>;
57
- signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<ledger.UnprovenTransaction>;
58
- signUnboundTransaction(transaction: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<UnboundTransaction>;
58
+ signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: SignSegment): Promise<ledger.UnprovenTransaction>;
59
+ signUnboundTransaction(transaction: UnboundTransaction, signSegment: SignSegment): Promise<UnboundTransaction>;
59
60
  serializeState(): Promise<TSerialized>;
60
61
  waitForSyncedState(allowedGap?: bigint): Promise<UnshieldedWalletState<TSerialized>>;
61
62
  revertTransaction(transaction: ledger.Transaction<ledger.Signaturish, ledger.Proofish, ledger.Bindingish>): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './UnshieldedWallet.js';
2
- export { type UnshieldedTransactionHistoryEntry, UnshieldedSectionSchema } from './v1/TransactionHistory.js';
2
+ export { type UnshieldedTransactionHistoryEntry, UnshieldedSectionSchema, mergeUnshieldedSections, } from './v1/TransactionHistory.js';
3
+ export { type SignSegment } from './v1/Signing.js';
3
4
  export * from './KeyStore.js';
package/dist/index.js CHANGED
@@ -11,5 +11,5 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  export * from './UnshieldedWallet.js';
14
- export { UnshieldedSectionSchema } from './v1/TransactionHistory.js';
14
+ export { UnshieldedSectionSchema, mergeUnshieldedSections, } from './v1/TransactionHistory.js';
15
15
  export * from './KeyStore.js';
@@ -6,6 +6,7 @@ import { type WalletSyncUpdate } from './SyncSchema.js';
6
6
  import { type TransactingCapability, type TokenTransfer, type FinalizedTransactionBalanceResult, type UnboundTransactionBalanceResult, type UnprovenTransactionBalanceResult } from './Transacting.js';
7
7
  import { type UtxoWithMeta } from './UnshieldedState.js';
8
8
  import { type UnboundTransaction } from './TransactionOps.js';
9
+ import { type SignSegment, type SigningService } from './Signing.js';
9
10
  import { type WalletError } from './WalletError.js';
10
11
  import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
11
12
  import { type KeysCapability } from './Keys.js';
@@ -19,6 +20,7 @@ export declare namespace RunningV1Variant {
19
20
  syncService: SyncService<CoreWallet, TSyncUpdate>;
20
21
  syncCapability: SyncCapability<CoreWallet, TSyncUpdate>;
21
22
  transactingCapability: TransactingCapability<CoreWallet>;
23
+ signingService: SigningService;
22
24
  coinsAndBalancesCapability: CoinsAndBalancesCapability<CoreWallet>;
23
25
  keysCapability: KeysCapability<CoreWallet>;
24
26
  coinSelection: CoinSelection<ledger.Utxo>;
@@ -41,8 +43,8 @@ export declare class RunningV1Variant<TSerialized, TSyncUpdate> implements Varia
41
43
  transferTransaction(outputs: ReadonlyArray<TokenTransfer>, ttl: Date): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
42
44
  rotateUtxos(guaranteedUtxos: ReadonlyArray<UtxoWithMeta>, fallibleUtxos: ReadonlyArray<UtxoWithMeta>, nightVerifyingKey: ledger.SignatureVerifyingKey, ttl: Date): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
43
45
  initSwap(desiredInputs: Record<string, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>, ttl: Date): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
44
- signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
45
- signUnboundTransaction(transaction: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Effect.Effect<UnboundTransaction, WalletError>;
46
+ signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: SignSegment): Effect.Effect<ledger.UnprovenTransaction, WalletError>;
47
+ signUnboundTransaction(transaction: UnboundTransaction, signSegment: SignSegment): Effect.Effect<UnboundTransaction, WalletError>;
46
48
  revertTransaction(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): Effect.Effect<void, WalletError>;
47
49
  serializeState(state: CoreWallet): TSerialized;
48
50
  }
@@ -98,10 +98,10 @@ export class RunningV1Variant {
98
98
  });
99
99
  }
100
100
  signUnprovenTransaction(transaction, signSegment) {
101
- return this.#v1Context.transactingCapability.signUnprovenTransaction(transaction, signSegment);
101
+ return this.#v1Context.signingService.sign(transaction, signSegment);
102
102
  }
103
103
  signUnboundTransaction(transaction, signSegment) {
104
- return this.#v1Context.transactingCapability.signUnboundTransaction(transaction, signSegment);
104
+ return this.#v1Context.signingService.sign(transaction, signSegment);
105
105
  }
106
106
  revertTransaction(transaction) {
107
107
  return SubscriptionRef.updateEffect(this.#context.stateRef, (state) => {
@@ -0,0 +1,25 @@
1
+ import { Effect } from 'effect';
2
+ import type * as ledger from '@midnightntwrk/ledger-v9';
3
+ import { type UnboundTransaction } from './TransactionOps.js';
4
+ import { type WalletError } from './WalletError.js';
5
+ /**
6
+ * Produces a {@link ledger.Signature} over the supplied bytes. Asynchronous so that out-of-process signers (MPC, HSM) —
7
+ * whose whole purpose is that the secret never materializes in-process — can be plugged in directly. A synchronous
8
+ * in-process keystore satisfies this by resolving immediately: `keystore.signDataAsync`.
9
+ */
10
+ export type SignSegment = (data: Uint8Array) => Promise<ledger.Signature>;
11
+ /**
12
+ * Authorizes a transaction by signing each of its signable segments with the supplied async {@link SignSegment}. The
13
+ * service is the imperative shell: it drives the async signer and maps its failures into the typed error channel; the
14
+ * pure work is delegated to {@link TransactionOps.collectSignableData} and {@link TransactionOps.attachSignatures}.
15
+ */
16
+ export interface SigningService {
17
+ sign<TTransaction extends ledger.UnprovenTransaction | UnboundTransaction>(transaction: TTransaction, signSegment: SignSegment): Effect.Effect<TTransaction, WalletError>;
18
+ }
19
+ /**
20
+ * The default signing service: collect each segment's signable data (pure), invoke the async signer once per segment
21
+ * (sequentially — segment counts are tiny), then attach the signatures (pure, with scheme validation). A signer
22
+ * rejection or throw is wrapped in a {@link SignError}; a scheme mismatch short-circuits inside `attachSignatures`
23
+ * before anything is attached, so no partially-signed transaction can escape.
24
+ */
25
+ export declare const makeDefaultSigningService: () => SigningService;
@@ -0,0 +1,40 @@
1
+ // This file is part of MIDNIGHT-WALLET-SDK.
2
+ // Copyright (C) Midnight Foundation
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // You may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ //
14
+ // Signing as a service (#504). Authorizing a transaction means producing a `Signature` over each signable segment.
15
+ // For an in-process keystore that is instantaneous, but MPC (threshold protocols with network round-trips) and HSM
16
+ // (on-device PKCS#11) signers are inherently asynchronous. The signer is therefore an async callback, and the
17
+ // orchestration that invokes it lives here — in the Effect (imperative-shell) layer — while the pure transformations
18
+ // (which segments to sign, scheme validation, signature attachment) stay in `TransactionOps`.
19
+ import { EitherOps } from '@midnightntwrk/wallet-sdk-utilities';
20
+ import { Effect, pipe } from 'effect';
21
+ import { TransactionOps } from './TransactionOps.js';
22
+ import { SignError } from './WalletError.js';
23
+ /**
24
+ * The default signing service: collect each segment's signable data (pure), invoke the async signer once per segment
25
+ * (sequentially — segment counts are tiny), then attach the signatures (pure, with scheme validation). A signer
26
+ * rejection or throw is wrapped in a {@link SignError}; a scheme mismatch short-circuits inside `attachSignatures`
27
+ * before anything is attached, so no partially-signed transaction can escape.
28
+ */
29
+ export const makeDefaultSigningService = () => ({
30
+ sign(transaction, signSegment) {
31
+ return Effect.gen(function* () {
32
+ const segments = yield* EitherOps.toEffect(TransactionOps.collectSignableData(transaction));
33
+ const signatures = yield* Effect.forEach(segments, (segment) => pipe(Effect.tryPromise({
34
+ try: () => signSegment(segment.data),
35
+ catch: (cause) => new SignError({ message: 'Signer callback failed', cause }),
36
+ }), Effect.map((signature) => ({ segment: segment.segment, signature }))));
37
+ return yield* EitherOps.toEffect(TransactionOps.attachSignatures(transaction, signatures));
38
+ });
39
+ },
40
+ });
@@ -40,6 +40,8 @@ export declare const UnshieldedTransactionSchema: Schema.Data<Schema.Struct<{
40
40
  protocolVersion: typeof Schema.Number;
41
41
  identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
42
42
  block: Schema.Struct<{
43
+ hash: typeof Schema.String;
44
+ height: typeof Schema.Number;
43
45
  timestamp: Schema.transform<typeof Schema.Number, typeof Schema.DateFromSelf>;
44
46
  }>;
45
47
  fees: Schema.optional<Schema.Struct<{
@@ -64,6 +66,8 @@ export declare const UnshieldedUpdateSchema: Schema.transform<Schema.Struct<{
64
66
  protocolVersion: typeof Schema.Number;
65
67
  identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
66
68
  block: Schema.Struct<{
69
+ hash: typeof Schema.String;
70
+ height: typeof Schema.Number;
67
71
  timestamp: Schema.transform<typeof Schema.Number, typeof Schema.DateFromSelf>;
68
72
  }>;
69
73
  fees: Schema.optional<Schema.Struct<{
@@ -167,6 +171,8 @@ export declare const UnshieldedUpdateSchema: Schema.transform<Schema.Struct<{
167
171
  readonly hash: string;
168
172
  readonly identifiers?: readonly string[] | undefined;
169
173
  readonly block: {
174
+ readonly hash: string;
175
+ readonly height: number;
170
176
  readonly timestamp: Date;
171
177
  };
172
178
  readonly fees?: {
@@ -217,6 +223,8 @@ export declare const UnshieldedUpdateSchema: Schema.transform<Schema.Struct<{
217
223
  readonly hash: string;
218
224
  readonly identifiers?: readonly string[] | undefined;
219
225
  readonly block: {
226
+ readonly hash: string;
227
+ readonly height: number;
220
228
  readonly timestamp: Date;
221
229
  };
222
230
  readonly fees?: {
@@ -259,6 +267,8 @@ export declare const WalletSyncUpdateSchema: Schema.Union<[Schema.transform<Sche
259
267
  protocolVersion: typeof Schema.Number;
260
268
  identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
261
269
  block: Schema.Struct<{
270
+ hash: typeof Schema.String;
271
+ height: typeof Schema.Number;
262
272
  timestamp: Schema.transform<typeof Schema.Number, typeof Schema.DateFromSelf>;
263
273
  }>;
264
274
  fees: Schema.optional<Schema.Struct<{
@@ -362,6 +372,8 @@ export declare const WalletSyncUpdateSchema: Schema.Union<[Schema.transform<Sche
362
372
  readonly hash: string;
363
373
  readonly identifiers?: readonly string[] | undefined;
364
374
  readonly block: {
375
+ readonly hash: string;
376
+ readonly height: number;
365
377
  readonly timestamp: Date;
366
378
  };
367
379
  readonly fees?: {
@@ -412,6 +424,8 @@ export declare const WalletSyncUpdateSchema: Schema.Union<[Schema.transform<Sche
412
424
  readonly hash: string;
413
425
  readonly identifiers?: readonly string[] | undefined;
414
426
  readonly block: {
427
+ readonly hash: string;
428
+ readonly height: number;
415
429
  readonly timestamp: Date;
416
430
  };
417
431
  readonly fees?: {
@@ -70,6 +70,8 @@ export const UnshieldedTransactionSchema = Schema.Data(Schema.Struct({
70
70
  protocolVersion: Schema.Number,
71
71
  identifiers: Schema.optional(Schema.Array(Schema.String)),
72
72
  block: Schema.Struct({
73
+ hash: Schema.String,
74
+ height: Schema.Number,
73
75
  timestamp: DateFromMillis,
74
76
  }),
75
77
  fees: Schema.optional(Schema.Struct({
@@ -44,9 +44,7 @@ export interface TransactingCapability<TState> {
44
44
  balanceFinalizedTransaction(wallet: CoreWallet, transaction: ledger.FinalizedTransaction): Either.Either<[FinalizedTransactionBalanceResult, CoreWallet], WalletError>;
45
45
  balanceUnboundTransaction(wallet: CoreWallet, transaction: UnboundTransaction): Either.Either<[UnboundTransactionBalanceResult, CoreWallet], WalletError>;
46
46
  balanceUnprovenTransaction(wallet: CoreWallet, transaction: ledger.UnprovenTransaction): Either.Either<[UnprovenTransactionBalanceResult, CoreWallet], WalletError>;
47
- signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Either.Either<ledger.UnprovenTransaction, WalletError>;
48
47
  revertTransaction(wallet: CoreWallet, transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): Either.Either<CoreWallet, WalletError>;
49
- signUnboundTransaction(transaction: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Either.Either<UnboundTransaction, WalletError>;
50
48
  }
51
49
  export type DefaultTransactingConfiguration = {
52
50
  networkId: NetworkId.NetworkId;
@@ -111,8 +109,6 @@ export declare class TransactingCapabilityImplementation implements TransactingC
111
109
  * @returns The initialized swap transaction and the new wallet state if successful, otherwise an error
112
110
  */
113
111
  initSwap(wallet: CoreWallet, desiredInputs: Record<ledger.RawTokenType, bigint>, desiredOutputs: ReadonlyArray<TokenTransfer>, ttl: Date): Either.Either<TransactingResult<ledger.UnprovenTransaction, CoreWallet>, WalletError>;
114
- signUnprovenTransaction(transaction: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Either.Either<ledger.UnprovenTransaction, WalletError>;
115
- signUnboundTransaction(transaction: UnboundTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Either.Either<UnboundTransaction, WalletError>;
116
112
  /**
117
113
  * Reverts a transaction by rolling back all inputs owned by this wallet
118
114
  *
@@ -16,7 +16,6 @@ import { CoreWallet } from './CoreWallet.js';
16
16
  import { InsufficientFundsError, OtherWalletError, TransactingError } from './WalletError.js';
17
17
  import { getBalanceRecipe, Imbalances, InsufficientFundsError as BalancingInsufficientFundsError, } from '@midnightntwrk/wallet-sdk-capabilities';
18
18
  import { TransactionOps } from './TransactionOps.js';
19
- import { assertSignatureMatchesKey } from '../SchemeConsistency.js';
20
19
  const GUARANTEED_SEGMENT = 0;
21
20
  export const makeDefaultTransactingCapability = (config, getContext) => {
22
21
  return new TransactingCapabilityImplementation(config.networkId, () => getContext().coinSelection, () => getContext().coinsAndBalancesCapability, () => getContext().keysCapability, TransactionOps);
@@ -221,47 +220,6 @@ export class TransactingCapabilityImplementation {
221
220
  };
222
221
  });
223
222
  }
224
- signUnprovenTransaction(transaction, signSegment) {
225
- return this.#signTransactionInternal(transaction, signSegment);
226
- }
227
- signUnboundTransaction(transaction, signSegment) {
228
- return this.#signTransactionInternal(transaction, signSegment);
229
- }
230
- /**
231
- * Internal method to sign either an unproven or unbound transaction
232
- *
233
- * @param transaction - The transaction to sign
234
- * @param signSegment - The signing function
235
- * @returns The signed transaction if successful, otherwise an error
236
- */
237
- #signTransactionInternal(transaction, signSegment) {
238
- return Either.gen(this, function* () {
239
- const segments = this.txOps.getSegments(transaction);
240
- for (const segment of segments) {
241
- const signedData = yield* this.txOps.getSignatureData(transaction, segment);
242
- const signature = signSegment(signedData);
243
- // Reject a wrong-scheme signature before it is attached, so a mismatch
244
- // never reaches the network (no partially-signed transaction escapes).
245
- yield* this.#assertSignatureMatchesSegmentOwners(transaction, segment, signature);
246
- transaction = yield* this.txOps.addSignature(transaction, signature, segment);
247
- }
248
- return transaction;
249
- });
250
- }
251
- /**
252
- * Asserts that a freshly produced signature shares the scheme (`schnorr` vs `ecdsa`) of every input owner it will
253
- * authorize in the given segment. Pure and synchronous: a mismatch short-circuits signing with a typed
254
- * `SchemeMismatchError` before the signature is attached.
255
- */
256
- #assertSignatureMatchesSegmentOwners(transaction, segment, signature) {
257
- const intent = transaction.intents?.get(segment);
258
- const owners = [
259
- ...(intent?.guaranteedUnshieldedOffer?.inputs ?? []),
260
- ...(intent?.fallibleUnshieldedOffer?.inputs ?? []),
261
- ].map((input) => input.owner);
262
- const ok = Either.right(signature);
263
- return pipe(owners, Arr.reduce(ok, (acc, owner) => Either.flatMap(acc, () => assertSignatureMatchesKey(owner, signature))));
264
- }
265
223
  /**
266
224
  * Reverts a transaction by rolling back all inputs owned by this wallet
267
225
  *
@@ -19,8 +19,37 @@ export declare const UnshieldedSectionSchema: Schema.Struct<{
19
19
  outputIndex: typeof Schema.Number;
20
20
  }>>;
21
21
  }>;
22
- export declare const UnshieldedTransactionHistoryEntrySchema: Schema.Struct<{
23
- unshielded: Schema.Struct<{
22
+ type UnshieldedSection = Schema.Schema.Type<typeof UnshieldedSectionSchema>;
23
+ export declare const mergeUnshieldedSections: (existing: UnshieldedSection, incoming: UnshieldedSection) => UnshieldedSection;
24
+ /**
25
+ * Unshielded entry schema. Extends the common entry shape with an optional `unshielded` section. Tightening — required
26
+ * `unshielded` plus required `protocolVersion`/`status`/`timestamp` — happens at the writer-input type, not on the
27
+ * stored shape. Fees are not unshielded's concern (dust pays them); unshielded never writes `fees`.
28
+ */
29
+ export declare const UnshieldedTransactionHistoryEntrySchema: Schema.Struct<Readonly<{
30
+ hash: typeof Schema.String;
31
+ identifiers: Schema.Array$<typeof Schema.String>;
32
+ protocolVersion: Schema.optional<typeof Schema.Number>;
33
+ status: Schema.optional<Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>>;
34
+ timestamp: Schema.optional<typeof Schema.Date>;
35
+ fees: Schema.optional<Schema.NullOr<typeof Schema.BigInt>>;
36
+ lifecycle: Schema.Union<[Schema.Struct<{
37
+ status: Schema.Literal<["pending"]>;
38
+ submittedAt: typeof Schema.Date;
39
+ }>, Schema.Struct<{
40
+ status: Schema.Literal<["finalized"]>;
41
+ finalizedBlock: Schema.Struct<{
42
+ hash: typeof Schema.String;
43
+ height: typeof Schema.Number;
44
+ timestamp: typeof Schema.Date;
45
+ }>;
46
+ }>, Schema.Struct<{
47
+ status: Schema.Literal<["rejected"]>;
48
+ rejectedAt: typeof Schema.Date;
49
+ reason: Schema.optional<typeof Schema.String>;
50
+ }>]>;
51
+ }> & {
52
+ unshielded: Schema.optional<Schema.Struct<{
24
53
  id: typeof Schema.Number;
25
54
  createdUtxos: Schema.Array$<Schema.Struct<{
26
55
  value: Schema.Schema<bigint, string, never>;
@@ -36,19 +65,15 @@ export declare const UnshieldedTransactionHistoryEntrySchema: Schema.Struct<{
36
65
  intentHash: typeof Schema.String;
37
66
  outputIndex: typeof Schema.Number;
38
67
  }>>;
39
- }>;
40
- hash: typeof Schema.String;
41
- protocolVersion: typeof Schema.Number;
42
- status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
43
- identifiers: Schema.optional<Schema.Array$<typeof Schema.String>>;
44
- timestamp: Schema.optional<typeof Schema.Date>;
45
- fees: Schema.optional<Schema.NullOr<typeof Schema.BigInt>>;
68
+ }>>;
46
69
  }>;
47
70
  export type UnshieldedTransactionHistoryEntry = Schema.Schema.Type<typeof UnshieldedTransactionHistoryEntrySchema>;
48
71
  export type TransactionHistoryService = {
49
72
  put(update: UnshieldedUpdate): Effect.Effect<void, TransactionHistoryError>;
50
73
  };
74
+ export type UnshieldedHistoryStorage = TransactionHistoryStorage.TransactionHistoryReader<TransactionHistoryStorage.TransactionHistoryEntryWithHash> & TransactionHistoryStorage.TransactionHistoryWriter<UnshieldedTransactionHistoryEntry>;
51
75
  export type DefaultTransactionHistoryConfiguration = {
52
- txHistoryStorage: TransactionHistoryStorage.TransactionHistoryStorage<TransactionHistoryStorage.TransactionHistoryEntryWithHash>;
76
+ txHistoryStorage: UnshieldedHistoryStorage;
53
77
  };
54
78
  export declare const makeDefaultTransactionHistoryService: (config: DefaultTransactionHistoryConfiguration, _getContext: () => unknown) => TransactionHistoryService;
79
+ export {};
@@ -26,17 +26,29 @@ export const UnshieldedSectionSchema = Schema.Struct({
26
26
  createdUtxos: Schema.Array(UtxoSchema),
27
27
  spentUtxos: Schema.Array(UtxoSchema),
28
28
  });
29
- export const UnshieldedTransactionHistoryEntrySchema = Schema.Struct({
30
- ...TransactionHistoryStorage.TransactionHistoryCommonSchema.fields,
31
- unshielded: UnshieldedSectionSchema,
29
+ export const mergeUnshieldedSections = (existing, incoming) => ({
30
+ ...existing,
31
+ ...incoming,
32
32
  });
33
- const convertUpdateToStorageEntry = ({ transaction, createdUtxos, spentUtxos, status, }) => ({
33
+ /**
34
+ * Unshielded entry schema. Extends the common entry shape with an optional `unshielded` section. Tightening — required
35
+ * `unshielded` plus required `protocolVersion`/`status`/`timestamp` — happens at the writer-input type, not on the
36
+ * stored shape. Fees are not unshielded's concern (dust pays them); unshielded never writes `fees`.
37
+ */
38
+ export const UnshieldedTransactionHistoryEntrySchema = TransactionHistoryStorage.extendEntrySchema({
39
+ unshielded: Schema.optional(UnshieldedSectionSchema),
40
+ });
41
+ const convertUpdateToFinalizedInput = ({ transaction, createdUtxos, spentUtxos, status, }) => ({
34
42
  hash: transaction.hash,
35
43
  protocolVersion: transaction.protocolVersion,
36
44
  status,
37
45
  identifiers: transaction.identifiers ?? [],
38
46
  timestamp: transaction.block.timestamp,
39
- fees: transaction.fees?.paidFees ?? null,
47
+ finalizedBlock: {
48
+ hash: transaction.block.hash,
49
+ height: transaction.block.height,
50
+ timestamp: transaction.block.timestamp,
51
+ },
40
52
  unshielded: {
41
53
  id: transaction.id,
42
54
  createdUtxos: createdUtxos.map(({ utxo }) => ({
@@ -59,8 +71,8 @@ export const makeDefaultTransactionHistoryService = (config, _getContext) => {
59
71
  const txHistoryStorage = config.txHistoryStorage;
60
72
  return {
61
73
  put: (update) => Effect.tryPromise({
62
- try: () => txHistoryStorage.upsert(convertUpdateToStorageEntry(update)),
63
- catch: (e) => new TransactionHistoryError({ message: 'Failed to put transaction history entry', cause: e }),
74
+ try: () => txHistoryStorage.gotFinalized(convertUpdateToFinalizedInput(update)),
75
+ catch: (e) => new TransactionHistoryError({ message: 'Failed to record finalized history entry', cause: e }),
64
76
  }),
65
77
  };
66
78
  };
@@ -6,11 +6,23 @@ import { type WalletError } from './WalletError.js';
6
6
  export type UnboundTransaction = ledger.Transaction<ledger.SignatureEnabled, ledger.Proof, ledger.PreBinding>;
7
7
  /** Utility type to extract the Intent type from a Transaction type. Maps Transaction<S, P, B> to Intent<S, P, B>. */
8
8
  export type IntentOf<T> = T extends ledger.Transaction<infer S, infer P, infer B> ? ledger.Intent<S, P, B> : never;
9
+ /** A transaction segment paired with the bytes that must be signed to authorize it. */
10
+ export type SignableSegment = {
11
+ readonly segment: number;
12
+ readonly data: Uint8Array;
13
+ };
14
+ /** A signature produced for a specific transaction segment, ready to be attached. */
15
+ export type SegmentSignature = {
16
+ readonly segment: number;
17
+ readonly signature: ledger.Signature;
18
+ };
9
19
  export type TransactionOps = {
10
20
  getSignatureData: (transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>, segment: number) => Either.Either<Uint8Array, WalletError>;
11
21
  getSegments(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): number[];
12
22
  findAvailableSegmentId(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): Option.Option<number>;
13
23
  addSignature<TTransaction extends ledger.UnprovenTransaction | UnboundTransaction>(transaction: TTransaction, signature: ledger.Signature, segment: number): Either.Either<TTransaction, WalletError>;
24
+ collectSignableData(transaction: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding>): Either.Either<readonly SignableSegment[], WalletError>;
25
+ attachSignatures<TTransaction extends ledger.UnprovenTransaction | UnboundTransaction>(transaction: TTransaction, signatures: readonly SegmentSignature[]): Either.Either<TTransaction, WalletError>;
14
26
  getImbalances(transaction: ledger.FinalizedTransaction | UnboundTransaction | ledger.UnprovenTransaction, segment: number): Imbalances;
15
27
  addSignaturesToOffer(offer: ledger.UnshieldedOffer<ledger.SignatureEnabled>, signature: ledger.Signature, segment: number, offerType: 'guaranteed' | 'fallible'): Either.Either<ledger.UnshieldedOffer<ledger.SignatureEnabled>, WalletError>;
16
28
  isIntentBound(intent: ledger.Intent<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): boolean;
@@ -13,7 +13,21 @@
13
13
  import { Either, pipe, Array as Arr, Iterable as IterableOps } from 'effect';
14
14
  import { Imbalances } from '@midnightntwrk/wallet-sdk-capabilities';
15
15
  import { addressFromKey, SignatureEnabled } from '@midnightntwrk/ledger-v9';
16
+ import { assertSignatureMatchesKey } from '../SchemeConsistency.js';
16
17
  import { TransactingError } from './WalletError.js';
18
+ /**
19
+ * Asserts that `signature` shares the scheme of every input owner authorized in `segment`. Pure and synchronous: a
20
+ * mismatch short-circuits with a typed `SchemeMismatchError` so a wrong-scheme signature is never attached.
21
+ */
22
+ const assertSignatureMatchesSegmentOwners = (transaction, segment, signature) => {
23
+ const intent = transaction.intents?.get(segment);
24
+ const owners = [
25
+ ...(intent?.guaranteedUnshieldedOffer?.inputs ?? []),
26
+ ...(intent?.fallibleUnshieldedOffer?.inputs ?? []),
27
+ ].map((input) => input.owner);
28
+ const seed = Either.right(signature);
29
+ return pipe(owners, Arr.reduce(seed, (acc, owner) => Either.flatMap(acc, () => assertSignatureMatchesKey(owner, signature))));
30
+ };
17
31
  export const TransactionOps = {
18
32
  getSignatureData(tx, segment) {
19
33
  if (!tx.intents) {
@@ -58,6 +72,19 @@ export const TransactionOps = {
58
72
  return transaction;
59
73
  });
60
74
  },
75
+ collectSignableData(transaction) {
76
+ // A transaction with no intents has no signable segments — yields an empty list, never an error.
77
+ return Either.all(TransactionOps.getSegments(transaction).map((segment) => Either.map(TransactionOps.getSignatureData(transaction, segment), (data) => ({ segment, data }))));
78
+ },
79
+ attachSignatures(transaction, signatures) {
80
+ return Either.gen(function* () {
81
+ // Validate every signature's scheme against its segment's owners BEFORE attaching any of them, so a
82
+ // mismatch leaves the transaction untouched (no partially-signed transaction can escape toward the network).
83
+ yield* Either.all(signatures.map(({ segment, signature }) => assertSignatureMatchesSegmentOwners(transaction, segment, signature)));
84
+ const seed = Either.right(transaction);
85
+ return yield* pipe(signatures, Arr.reduce(seed, (acc, { segment, signature }) => Either.flatMap(acc, (tx) => TransactionOps.addSignature(tx, signature, segment))));
86
+ });
87
+ },
61
88
  getImbalances(transaction, segment) {
62
89
  const imbalances = transaction
63
90
  .imbalances(segment)
@@ -8,6 +8,7 @@ import { type DefaultSyncContext, type DefaultSyncConfiguration, type SyncCapabi
8
8
  import { type WalletSyncUpdate } from './SyncSchema.js';
9
9
  import { type DefaultTransactingConfiguration, type DefaultTransactingContext, type TransactingCapability } from './Transacting.js';
10
10
  import { type WalletError } from './WalletError.js';
11
+ import { type SigningService } from './Signing.js';
11
12
  import { type CoinsAndBalancesCapability } from './CoinsAndBalances.js';
12
13
  import { type KeysCapability } from './Keys.js';
13
14
  import { type CoinSelection } from '@midnightntwrk/wallet-sdk-capabilities';
@@ -42,6 +43,8 @@ export declare class V1Builder<TConfig extends BaseV1Configuration = BaseV1Confi
42
43
  withSerialization<TSerializationConfig, TSerializationContext extends Partial<RunningV1Variant.AnyContext>, TSerialized>(serializationCapability: (configuration: TSerializationConfig, getContext: () => TSerializationContext) => SerializationCapability<CoreWallet, TSerialized>): V1Builder<TConfig & TSerializationConfig, TContext & TSerializationContext, TSerialized, TSyncUpdate>;
43
44
  withTransactingDefaults(this: V1Builder<TConfig, TContext, TSerialized, TSyncUpdate>): V1Builder<TConfig & DefaultTransactingConfiguration, TContext & DefaultTransactingContext, TSerialized, TSyncUpdate>;
44
45
  withTransacting<TTransactingConfig, TTransactingContext extends Partial<RunningV1Variant.AnyContext>>(transactingCapability: (config: TTransactingConfig, getContext: () => TTransactingContext) => TransactingCapability<CoreWallet>): V1Builder<TConfig & TTransactingConfig, TContext & TTransactingContext, TSerialized, TSyncUpdate>;
46
+ withSigningDefaults(): V1Builder<TConfig, TContext, TSerialized, TSyncUpdate>;
47
+ withSigning<TSigningConfig, TSigningContext extends Partial<RunningV1Variant.AnyContext>>(signingService: (configuration: TSigningConfig, getContext: () => TSigningContext) => SigningService): V1Builder<TConfig & TSigningConfig, TContext & TSigningContext, TSerialized, TSyncUpdate>;
45
48
  withCoinSelection<TCoinSelectionConfig, TCoinSelectionContext extends Partial<RunningV1Variant.AnyContext>>(coinSelection: (config: TCoinSelectionConfig, getContext: () => TCoinSelectionContext) => CoinSelection<ledger.Utxo>): V1Builder<TConfig & TCoinSelectionConfig, TContext & TCoinSelectionContext, TSerialized, TSyncUpdate>;
46
49
  withCoinSelectionDefaults(): V1Builder<TConfig, TContext, TSerialized, TSyncUpdate>;
47
50
  withCoinsAndBalancesDefaults(): V1Builder<TConfig, TContext, TSerialized, TSyncUpdate>;
@@ -61,6 +64,9 @@ declare namespace V1Builder {
61
64
  type HasTransacting<TConfig, TContext> = {
62
65
  readonly transactingCapability: (configuration: TConfig, getContext: () => TContext) => TransactingCapability<CoreWallet>;
63
66
  };
67
+ type HasSigning<TConfig, TContext> = {
68
+ readonly signingService: (configuration: TConfig, getContext: () => TContext) => SigningService;
69
+ };
64
70
  type HasCoinSelection<TConfig, TContext> = {
65
71
  readonly coinSelection: (configuration: TConfig, getContext: () => TContext) => CoinSelection<ledger.Utxo>;
66
72
  };
@@ -77,7 +83,7 @@ declare namespace V1Builder {
77
83
  readonly keysCapability: (configuration: TConfig, getContext: () => TContext) => KeysCapability<CoreWallet>;
78
84
  };
79
85
  /** The internal build state of {@link V1Builder}. */
80
- type FullBuildState<TConfig, TContext, TSerialized, TSyncUpdate> = Types.Simplify<HasSync<TConfig, TContext, TSyncUpdate> & HasSerialization<TConfig, TContext, TSerialized> & HasTransacting<TConfig, TContext> & HasCoinSelection<TConfig, TContext> & HasCoinsAndBalances<TConfig, TContext> & HasKeys<TConfig, TContext> & HasTransactionHistory<TConfig, TContext>>;
86
+ type FullBuildState<TConfig, TContext, TSerialized, TSyncUpdate> = Types.Simplify<HasSync<TConfig, TContext, TSyncUpdate> & HasSerialization<TConfig, TContext, TSerialized> & HasTransacting<TConfig, TContext> & HasSigning<TConfig, TContext> & HasCoinSelection<TConfig, TContext> & HasCoinsAndBalances<TConfig, TContext> & HasKeys<TConfig, TContext> & HasTransactionHistory<TConfig, TContext>>;
81
87
  type PartialBuildState<TConfig = object, TContext = object, TSerialized = never, TSyncUpdate = never> = {
82
88
  [K in keyof FullBuildState<never, never, never, never>]?: FullBuildState<TConfig, TContext, TSerialized, TSyncUpdate>[K] | undefined;
83
89
  };
@@ -4,6 +4,7 @@ import { RunningV1Variant, V1Tag } from './RunningV1Variant.js';
4
4
  import { makeDefaultV1SerializationCapability } from './Serialization.js';
5
5
  import { makeDefaultSyncService, makeDefaultSyncCapability, } from './Sync.js';
6
6
  import { makeDefaultTransactingCapability, } from './Transacting.js';
7
+ import { makeDefaultSigningService } from './Signing.js';
7
8
  import { makeDefaultCoinsAndBalancesCapability } from './CoinsAndBalances.js';
8
9
  import { makeDefaultKeysCapability } from './Keys.js';
9
10
  import { chooseCoin } from '@midnightntwrk/wallet-sdk-capabilities';
@@ -22,6 +23,7 @@ export class V1Builder {
22
23
  return this.withSyncDefaults()
23
24
  .withSerializationDefaults()
24
25
  .withTransactingDefaults()
26
+ .withSigningDefaults()
25
27
  .withCoinsAndBalancesDefaults()
26
28
  .withTransactionHistoryDefaults()
27
29
  .withKeysDefaults()
@@ -55,6 +57,15 @@ export class V1Builder {
55
57
  transactingCapability,
56
58
  });
57
59
  }
60
+ withSigningDefaults() {
61
+ return this.withSigning(makeDefaultSigningService);
62
+ }
63
+ withSigning(signingService) {
64
+ return new V1Builder({
65
+ ...this.#buildState,
66
+ signingService,
67
+ });
68
+ }
58
69
  withCoinSelection(coinSelection) {
59
70
  return new V1Builder({
60
71
  ...this.#buildState,
@@ -119,13 +130,14 @@ export class V1Builder {
119
130
  if (!isBuildStateFull(this.#buildState)) {
120
131
  throw new Error('Not all components are configured in V1 Builder');
121
132
  }
122
- const { syncCapability, syncService, transactingCapability, serializationCapability, coinSelection, coinsAndBalancesCapability, keysCapability, transactionHistoryService, } = this.#buildState;
133
+ const { syncCapability, syncService, transactingCapability, signingService, serializationCapability, coinSelection, coinsAndBalancesCapability, keysCapability, transactionHistoryService, } = this.#buildState;
123
134
  const getContext = () => context;
124
135
  const context = {
125
136
  serializationCapability: serializationCapability(configuration, getContext),
126
137
  syncCapability: syncCapability(configuration, getContext),
127
138
  syncService: syncService(configuration, getContext),
128
139
  transactingCapability: transactingCapability(configuration, getContext),
140
+ signingService: signingService(configuration, getContext),
129
141
  coinsAndBalancesCapability: coinsAndBalancesCapability(configuration, getContext),
130
142
  keysCapability: keysCapability(configuration, getContext),
131
143
  coinSelection: coinSelection(configuration, getContext),
@@ -139,6 +151,7 @@ const isBuildStateFull = (buildState) => {
139
151
  'syncService',
140
152
  'syncCapability',
141
153
  'transactingCapability',
154
+ 'signingService',
142
155
  'coinSelection',
143
156
  'serializationCapability',
144
157
  'coinsAndBalancesCapability',
@@ -2,6 +2,7 @@ export * from './V1Builder.js';
2
2
  export * as Sync from './Sync.js';
3
3
  export * as SyncProgress from './SyncProgress.js';
4
4
  export * as Transacting from './Transacting.js';
5
+ export * as Signing from './Signing.js';
5
6
  export * as TransactionHistory from './TransactionHistory.js';
6
7
  export * as Serialization from './Serialization.js';
7
8
  export * as CoinsAndBalances from './CoinsAndBalances.js';
package/dist/v1/index.js CHANGED
@@ -14,6 +14,7 @@ export * from './V1Builder.js';
14
14
  export * as Sync from './Sync.js';
15
15
  export * as SyncProgress from './SyncProgress.js';
16
16
  export * as Transacting from './Transacting.js';
17
+ export * as Signing from './Signing.js';
17
18
  export * as TransactionHistory from './TransactionHistory.js';
18
19
  export * as Serialization from './Serialization.js';
19
20
  export * as CoinsAndBalances from './CoinsAndBalances.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midnightntwrk/wallet-sdk-unshielded-wallet",
3
- "version": "4.0.0-beta.0",
3
+ "version": "4.0.0-beta.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -30,13 +30,13 @@
30
30
  }
31
31
  },
32
32
  "dependencies": {
33
- "@midnightntwrk/ledger-v9": "1.0.0-rc.2",
34
- "@midnightntwrk/wallet-sdk-abstractions": "^2.1.0",
35
- "@midnightntwrk/wallet-sdk-address-format": "^4.0.0-beta.0",
36
- "@midnightntwrk/wallet-sdk-capabilities": "^4.0.0-beta.0",
37
- "@midnightntwrk/wallet-sdk-indexer-client": "^1.2.4-beta.0",
38
- "@midnightntwrk/wallet-sdk-runtime": "^1.0.4",
39
- "@midnightntwrk/wallet-sdk-utilities": "^1.2.0",
33
+ "@midnightntwrk/ledger-v9": "1.0.0-rc.3",
34
+ "@midnightntwrk/wallet-sdk-abstractions": "3.0.0-beta.0",
35
+ "@midnightntwrk/wallet-sdk-address-format": "4.0.0-beta.2",
36
+ "@midnightntwrk/wallet-sdk-capabilities": "4.0.0-beta.2",
37
+ "@midnightntwrk/wallet-sdk-indexer-client": "1.3.0-beta.1",
38
+ "@midnightntwrk/wallet-sdk-runtime": "1.0.6-beta.0",
39
+ "@midnightntwrk/wallet-sdk-utilities": "1.2.0",
40
40
  "effect": "^3.19.19",
41
41
  "rxjs": "^7.8.2"
42
42
  },
@@ -54,7 +54,7 @@
54
54
  "publint": "publint --strict"
55
55
  },
56
56
  "devDependencies": {
57
- "@midnightntwrk/wallet-sdk-hd": "^3.1.0-beta.0",
57
+ "@midnightntwrk/wallet-sdk-hd": "^3.1.0-beta.1",
58
58
  "@noble/curves": "^1.9.7",
59
59
  "@noble/hashes": "^1.8.0"
60
60
  }