@midnight-ntwrk/wallet-sdk-unshielded-wallet 1.0.0-beta.11

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Wallet SDK Unshielded Wallet
2
+
3
+ TBD
@@ -0,0 +1,23 @@
1
+ import { UnshieldedAddress, MidnightBech32m } from '@midnight-ntwrk/wallet-sdk-address-format';
2
+ import { Signature, SignatureVerifyingKey, UserAddress } from '@midnight-ntwrk/ledger-v6';
3
+ import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
4
+ export type PublicKey = {
5
+ publicKey: SignatureVerifyingKey;
6
+ address: UnshieldedAddress;
7
+ };
8
+ export declare const PublicKey: {
9
+ fromKeyStore: (keystore: UnshieldedKeystore) => PublicKey;
10
+ };
11
+ export interface UnshieldedKeystore {
12
+ getSecretKey(): Buffer;
13
+ getBech32Address(): MidnightBech32m;
14
+ getPublicKey(): SignatureVerifyingKey;
15
+ getAddress(): UserAddress;
16
+ signData(data: Uint8Array): Signature;
17
+ }
18
+ export interface Keystore {
19
+ keystore: UnshieldedKeystore;
20
+ getBech32Address(): MidnightBech32m;
21
+ getPublicKey(): SignatureVerifyingKey;
22
+ }
23
+ export declare const createKeystore: (secretKey: Uint8Array<ArrayBufferLike>, networkId: NetworkId.NetworkId) => UnshieldedKeystore;
@@ -0,0 +1,25 @@
1
+ import { UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
2
+ import { addressFromKey, signData, signatureVerifyingKey, } from '@midnight-ntwrk/ledger-v6';
3
+ import { pipe } from 'effect';
4
+ export const PublicKey = {
5
+ fromKeyStore: (keystore) => {
6
+ return {
7
+ publicKey: keystore.getPublicKey(),
8
+ address: pipe(keystore.getAddress(), (str) => Buffer.from(str, 'hex'), (bytes) => new UnshieldedAddress(bytes)),
9
+ };
10
+ },
11
+ };
12
+ export const createKeystore = (secretKey, networkId) => {
13
+ const keystore = {
14
+ getSecretKey: () => Buffer.from(secretKey),
15
+ getBech32Address: () => {
16
+ const address = keystore.getAddress();
17
+ const addressBuffer = Buffer.from(address, 'hex');
18
+ return UnshieldedAddress.codec.encode(networkId, new UnshieldedAddress(addressBuffer));
19
+ },
20
+ getPublicKey: () => signatureVerifyingKey(keystore.getSecretKey().toString('hex')),
21
+ getAddress: () => addressFromKey(keystore.getPublicKey()),
22
+ signData: (data) => signData(keystore.getSecretKey().toString('hex'), data),
23
+ };
24
+ return keystore;
25
+ };
@@ -0,0 +1,21 @@
1
+ import { Effect, Stream } from 'effect';
2
+ import { UnshieldedStateAPI, Utxo } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
3
+ import { ParseError } from 'effect/ParseResult';
4
+ export interface State {
5
+ address: string;
6
+ balances: Map<string, bigint>;
7
+ pendingCoins: readonly Utxo[];
8
+ availableCoins: readonly Utxo[];
9
+ totalCoins: readonly Utxo[];
10
+ syncProgress: {
11
+ applyGap: number;
12
+ synced: boolean;
13
+ } | undefined;
14
+ }
15
+ export declare class StateImpl {
16
+ unshieldedState: UnshieldedStateAPI;
17
+ address: string;
18
+ constructor(unshieldedState: UnshieldedStateAPI, address: string);
19
+ updates(): Stream.Stream<State>;
20
+ serialize(): Effect.Effect<string, ParseError>;
21
+ }
package/dist/State.js ADDED
@@ -0,0 +1,31 @@
1
+ import { Effect, HashSet, pipe, Stream } from 'effect';
2
+ import { UnshieldedStateEncoder } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
3
+ export class StateImpl {
4
+ unshieldedState;
5
+ address;
6
+ constructor(unshieldedState, address) {
7
+ this.unshieldedState = unshieldedState;
8
+ this.address = address;
9
+ }
10
+ updates() {
11
+ return this.unshieldedState.state.pipe(Stream.map((state) => ({
12
+ address: this.address,
13
+ balances: HashSet.reduce(state.utxos, new Map(), (acc, utxo) => {
14
+ acc.set(utxo.type, (acc.get(utxo.type) || 0n) + utxo.value);
15
+ return acc;
16
+ }),
17
+ pendingCoins: HashSet.toValues(state.pendingUtxos),
18
+ availableCoins: HashSet.toValues(state.utxos),
19
+ totalCoins: HashSet.toValues(HashSet.union(state.utxos, state.pendingUtxos)),
20
+ syncProgress: state.syncProgress
21
+ ? {
22
+ applyGap: (state.syncProgress?.highestTransactionId ?? 0) - (state.syncProgress?.currentTransactionId ?? 0),
23
+ synced: state.syncProgress?.highestTransactionId === state.syncProgress?.currentTransactionId,
24
+ }
25
+ : undefined,
26
+ })));
27
+ }
28
+ serialize() {
29
+ return pipe(this.unshieldedState.getLatestState(), Effect.flatMap((state) => UnshieldedStateEncoder(state)), Effect.map((encoded) => JSON.stringify(encoded)));
30
+ }
31
+ }
@@ -0,0 +1,55 @@
1
+ import { Layer, Context, Stream, Schema, Scope } from 'effect';
2
+ export declare const UnshieldedUpdateSchema: Schema.Union<[Schema.Struct<{
3
+ type: Schema.Literal<["UnshieldedTransaction"]>;
4
+ transaction: Schema.Data<Schema.Struct<{
5
+ id: typeof Schema.Number;
6
+ hash: typeof Schema.String;
7
+ type: Schema.Literal<["RegularTransaction", "SystemTransaction"]>;
8
+ protocolVersion: typeof Schema.Number;
9
+ identifiers: Schema.Array$<typeof Schema.String>;
10
+ transactionResult: Schema.NullOr<Schema.Struct<{
11
+ status: typeof Schema.String;
12
+ segments: Schema.NullOr<Schema.Array$<Schema.Struct<{
13
+ id: typeof Schema.String;
14
+ success: typeof Schema.Boolean;
15
+ }>>>;
16
+ }>>;
17
+ createdUtxos: Schema.Array$<Schema.Data<Schema.Struct<{
18
+ value: Schema.Schema<bigint, string, never>;
19
+ owner: typeof Schema.String;
20
+ type: typeof Schema.String;
21
+ intentHash: typeof Schema.String;
22
+ outputNo: typeof Schema.Number;
23
+ ctime: typeof Schema.Number;
24
+ registeredForDustGeneration: typeof Schema.Boolean;
25
+ }>>>;
26
+ spentUtxos: Schema.Array$<Schema.Data<Schema.Struct<{
27
+ value: Schema.Schema<bigint, string, never>;
28
+ owner: typeof Schema.String;
29
+ type: typeof Schema.String;
30
+ intentHash: typeof Schema.String;
31
+ outputNo: typeof Schema.Number;
32
+ ctime: typeof Schema.Number;
33
+ registeredForDustGeneration: typeof Schema.Boolean;
34
+ }>>>;
35
+ }>>;
36
+ }>, Schema.Struct<{
37
+ type: Schema.Literal<["UnshieldedTransactionsProgress"]>;
38
+ highestTransactionId: typeof Schema.Number;
39
+ }>]>;
40
+ export type UnshieldedUpdate = Schema.Schema.Type<typeof UnshieldedUpdateSchema>;
41
+ declare const SyncServiceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
42
+ readonly _tag: "SyncServiceError";
43
+ } & Readonly<A>;
44
+ export declare class SyncServiceError extends SyncServiceError_base<{
45
+ readonly error?: unknown;
46
+ }> {
47
+ }
48
+ export interface SyncServiceLive {
49
+ readonly startSync: (address: string, transactionId: number) => Stream.Stream<UnshieldedUpdate, SyncServiceError, Scope.Scope>;
50
+ }
51
+ declare const SyncService_base: Context.TagClass<SyncService, "@midnight-ntwrk/wallet-sdk-unshielded-wallet/SyncService", SyncServiceLive>;
52
+ export declare class SyncService extends SyncService_base {
53
+ static readonly LiveWithIndexer: (indexerUrl: string) => Layer.Layer<SyncService>;
54
+ }
55
+ export {};
@@ -0,0 +1,76 @@
1
+ import { Effect, Layer, Context, Stream, pipe, Schema, Data } from 'effect';
2
+ import { UnshieldedTransactions } from '@midnight-ntwrk/wallet-sdk-indexer-client';
3
+ import { UnshieldedTransactionSchema } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
4
+ import { WsSubscriptionClient } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
5
+ const TransactionSchema = Schema.Struct({
6
+ type: Schema.Literal('UnshieldedTransaction'),
7
+ transaction: UnshieldedTransactionSchema,
8
+ });
9
+ const ProgressSchema = Schema.Struct({
10
+ type: Schema.Literal('UnshieldedTransactionsProgress'),
11
+ highestTransactionId: Schema.Number,
12
+ });
13
+ export const UnshieldedUpdateSchema = Schema.Union(TransactionSchema, ProgressSchema);
14
+ const UnshieldedUpdateDecoder = Schema.decodeUnknown(UnshieldedUpdateSchema);
15
+ export class SyncServiceError extends Data.TaggedError('SyncServiceError') {
16
+ }
17
+ export class SyncService extends Context.Tag('@midnight-ntwrk/wallet-sdk-unshielded-wallet/SyncService')() {
18
+ static LiveWithIndexer = (indexerUrl) => {
19
+ const make = Effect.gen(function* () {
20
+ const indexerClient = yield* UnshieldedTransactions;
21
+ const startSync = (address, transactionId) => pipe(indexerClient({ address, transactionId }), Stream.provideLayer(WsSubscriptionClient.layer({ url: indexerUrl })), Stream.mapEffect((message) => {
22
+ const { type } = message.unshieldedTransactions;
23
+ if (type === 'UnshieldedTransactionsProgress') {
24
+ return UnshieldedUpdateDecoder({
25
+ type,
26
+ highestTransactionId: message.unshieldedTransactions.highestTransactionId,
27
+ });
28
+ }
29
+ else {
30
+ const { transaction, createdUtxos, spentUtxos } = message.unshieldedTransactions;
31
+ const isRegularTransaction = transaction.type === 'RegularTransaction';
32
+ const transactionResult = isRegularTransaction
33
+ ? {
34
+ status: transaction.transactionResult.status,
35
+ segments: transaction.transactionResult.segments?.map((segment) => ({
36
+ id: segment.id.toString(),
37
+ success: segment.success,
38
+ })) ?? null,
39
+ }
40
+ : null;
41
+ return UnshieldedUpdateDecoder({
42
+ type,
43
+ transaction: {
44
+ type: transaction.type,
45
+ id: transaction.id,
46
+ hash: transaction.hash,
47
+ identifiers: isRegularTransaction ? transaction.identifiers : [],
48
+ protocolVersion: transaction.protocolVersion,
49
+ transactionResult,
50
+ createdUtxos: createdUtxos.map((utxo) => ({
51
+ value: utxo.value,
52
+ owner: utxo.owner,
53
+ type: utxo.tokenType,
54
+ intentHash: utxo.intentHash,
55
+ outputNo: utxo.outputIndex,
56
+ registeredForDustGeneration: utxo.registeredForDustGeneration,
57
+ ctime: utxo.ctime ? utxo.ctime * 1000 : undefined,
58
+ })),
59
+ spentUtxos: spentUtxos.map((utxo) => ({
60
+ value: utxo.value,
61
+ owner: utxo.owner,
62
+ type: utxo.tokenType,
63
+ intentHash: utxo.intentHash,
64
+ outputNo: utxo.outputIndex,
65
+ registeredForDustGeneration: utxo.registeredForDustGeneration,
66
+ ctime: utxo.ctime ? utxo.ctime * 1000 : undefined,
67
+ })),
68
+ },
69
+ });
70
+ }
71
+ }), Stream.mapError((error) => new SyncServiceError({ error })));
72
+ return SyncService.of({ startSync });
73
+ });
74
+ return Layer.effect(SyncService, make);
75
+ };
76
+ }
@@ -0,0 +1,31 @@
1
+ import { Effect, Layer, Context, Stream } from 'effect';
2
+ import { TransactionHash, TransactionHistoryEntry, TransactionHistoryStorage } from './tx-history-storage/index.js';
3
+ declare const TransactionHistoryServiceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
4
+ readonly _tag: "TransactionHistoryServiceError";
5
+ } & Readonly<A>;
6
+ export declare class TransactionHistoryServiceError extends TransactionHistoryServiceError_base<{
7
+ readonly error?: unknown;
8
+ }> {
9
+ toString(): string;
10
+ }
11
+ export type TransactionHistoryChange = {
12
+ type: 'created' | 'updated' | 'deleted';
13
+ entry: TransactionHistoryEntry;
14
+ };
15
+ /**
16
+ * TransactionHistoryService API
17
+ *
18
+ * Extend with tx lifecycle methods when needed in the future.
19
+ */
20
+ export interface TransactionHistoryServiceAPI {
21
+ create: (item: TransactionHistoryEntry) => Effect.Effect<void, TransactionHistoryServiceError>;
22
+ delete: (hash: TransactionHash) => Effect.Effect<void, TransactionHistoryServiceError>;
23
+ getAll: () => Stream.Stream<TransactionHistoryEntry, TransactionHistoryServiceError>;
24
+ get: (hash: TransactionHash) => Effect.Effect<TransactionHistoryEntry | undefined, TransactionHistoryServiceError>;
25
+ changes: Stream.Stream<TransactionHistoryChange | undefined>;
26
+ }
27
+ declare const TransactionHistoryService_base: Context.TagClass<TransactionHistoryService, "@midnight-ntwrk/wallet-sdk-unshielded-wallet/TransactionHistoryService", TransactionHistoryServiceAPI>;
28
+ export declare class TransactionHistoryService extends TransactionHistoryService_base {
29
+ static readonly Live: (storage: TransactionHistoryStorage) => Layer.Layer<TransactionHistoryService>;
30
+ }
31
+ export {};
@@ -0,0 +1,37 @@
1
+ import { Effect, Layer, Context, Data, SubscriptionRef, Stream } from 'effect';
2
+ export class TransactionHistoryServiceError extends Data.TaggedError('TransactionHistoryServiceError') {
3
+ toString() {
4
+ return `TransactionHistoryServiceError: ${this.error instanceof Error ? this.error.toString() : 'Unknown error'}`;
5
+ }
6
+ }
7
+ export class TransactionHistoryService extends Context.Tag('@midnight-ntwrk/wallet-sdk-unshielded-wallet/TransactionHistoryService')() {
8
+ static Live = (storage) => Layer.effect(TransactionHistoryService, Effect.gen(function* () {
9
+ const txHistoryRef = yield* SubscriptionRef.make(undefined);
10
+ return {
11
+ create: (entry) => Effect.tryPromise({
12
+ try: async () => storage.create(entry),
13
+ catch: (error) => new TransactionHistoryServiceError({ error }),
14
+ }).pipe(Effect.tap(() => SubscriptionRef.set(txHistoryRef, {
15
+ type: 'created',
16
+ entry,
17
+ }))),
18
+ delete: (hash) => Effect.tryPromise({
19
+ try: async () => {
20
+ const deletedEntry = await storage.get(hash);
21
+ await storage.delete(hash);
22
+ SubscriptionRef.set(txHistoryRef, {
23
+ type: 'deleted',
24
+ entry: deletedEntry,
25
+ });
26
+ },
27
+ catch: (error) => new TransactionHistoryServiceError({ error }),
28
+ }),
29
+ getAll: () => Stream.fromAsyncIterable(storage.getAll(), () => new TransactionHistoryServiceError({ error: 'Failed to get all transactions' })),
30
+ get: (hash) => Effect.tryPromise({
31
+ try: async () => await storage.get(hash),
32
+ catch: (error) => new TransactionHistoryServiceError({ error }),
33
+ }),
34
+ changes: txHistoryRef.changes,
35
+ };
36
+ }));
37
+ }
@@ -0,0 +1,43 @@
1
+ import { Effect, Layer, Context } from 'effect';
2
+ import { ParseError } from 'effect/ParseResult';
3
+ import { UnshieldedStateAPI, UtxoNotFoundError } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
4
+ import { Binding, PreBinding, type Bindingish, type PreProof, type Proofish, type SignatureEnabled, type Signaturish, Transaction, type RawTokenType, type UserAddress } from '@midnight-ntwrk/ledger-v6';
5
+ import { SignatureVerifyingKey } from '@midnight-ntwrk/ledger-v6';
6
+ import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
7
+ export type TokenTransfer = {
8
+ readonly amount: bigint;
9
+ readonly type: string;
10
+ readonly receiverAddress: string;
11
+ };
12
+ declare const DeserializationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
13
+ readonly _tag: "DeserializationError";
14
+ } & Readonly<A>;
15
+ export declare class DeserializationError extends DeserializationError_base<{
16
+ readonly message: string;
17
+ readonly internal?: unknown;
18
+ }> {
19
+ }
20
+ declare const TransactionServiceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
21
+ readonly _tag: "TransactionServiceError";
22
+ } & Readonly<A>;
23
+ export declare class TransactionServiceError extends TransactionServiceError_base<{
24
+ readonly message: string;
25
+ readonly cause?: unknown;
26
+ }> {
27
+ }
28
+ export interface TransactionServiceLive {
29
+ readonly transferTransaction: (outputs: TokenTransfer[], ttl: Date, networkId: NetworkId.NetworkId) => Effect.Effect<Transaction<SignatureEnabled, PreProof, PreBinding>, TransactionServiceError>;
30
+ readonly initSwap: (desiredInputs: Record<RawTokenType, bigint>, desiredOutputs: TokenTransfer[], ttl: Date, networkId: NetworkId.NetworkId, state: UnshieldedStateAPI, myAddress: UserAddress, publicKey: SignatureVerifyingKey) => Effect.Effect<Transaction<SignatureEnabled, PreProof, PreBinding>, TransactionServiceError | ParseError | UtxoNotFoundError>;
31
+ readonly deserializeTransaction: <S extends Signaturish, P extends Proofish, B extends Bindingish>(markerS: S['instance'], markerP: P['instance'], markerB: B['instance'], tx: string) => Effect.Effect<Transaction<S, P, B>, DeserializationError>;
32
+ readonly serializeTransaction: (transaction: Transaction<Signaturish, Proofish, Bindingish>) => Effect.Effect<string, TransactionServiceError>;
33
+ readonly balanceTransaction: (transaction: Transaction<SignatureEnabled, Proofish, Bindingish>, state: UnshieldedStateAPI, myAddress: UserAddress, publicKey: SignatureVerifyingKey) => Effect.Effect<Transaction<SignatureEnabled, Proofish, Bindingish>, TransactionServiceError | ParseError | UtxoNotFoundError>;
34
+ readonly getOfferSignatureData: (transaction: Transaction<Signaturish, Proofish, Bindingish>, segment: number) => Effect.Effect<Uint8Array, TransactionServiceError>;
35
+ readonly addOfferSignature: <S extends Signaturish, P extends Proofish, B extends Bindingish>(transaction: Transaction<S, P, B>, signature: string, segment: number) => Effect.Effect<Transaction<S, P, B>, TransactionServiceError>;
36
+ readonly bindTransaction: <S extends Signaturish, P extends Proofish, B extends Bindingish>(transaction: Transaction<S, P, B>) => Effect.Effect<Transaction<S, P, Binding>, TransactionServiceError>;
37
+ readonly getSegments: (transaction: Transaction<Signaturish, Proofish, Bindingish>) => number[];
38
+ }
39
+ declare const TransactionService_base: Context.TagClass<TransactionService, "@midnight-ntwrk/wallet-sdk-unshielded-wallet/TransactionService", TransactionServiceLive>;
40
+ export declare class TransactionService extends TransactionService_base {
41
+ static readonly Live: Layer.Layer<TransactionService>;
42
+ }
43
+ export {};
@@ -0,0 +1,279 @@
1
+ /* temporarily disable eslint until we upgrade to ledger 6 */
2
+ import { Effect, Layer, Context, Data, HashSet, pipe, Option, Either } from 'effect';
3
+ import { getBalanceRecipe, Imbalances } from '@midnight-ntwrk/wallet-sdk-capabilities';
4
+ import { Binding, Transaction, Intent, UnshieldedOffer, } from '@midnight-ntwrk/ledger-v6';
5
+ export class DeserializationError extends Data.TaggedError('DeserializationError') {
6
+ }
7
+ export class TransactionServiceError extends Data.TaggedError('TransactionServiceError') {
8
+ }
9
+ const GUARANTEED_SEGMENT = 0;
10
+ const ledgerTry = (fn) => {
11
+ return Either.try({
12
+ try: fn,
13
+ catch: (error) => {
14
+ // eslint-disable-next-line no-console
15
+ console.log('Error from ledger', error);
16
+ const message = error instanceof Error ? error.message : `${error?.toString()}`;
17
+ return new TransactionServiceError({ message: `Error from ledger: ${message}`, cause: error });
18
+ },
19
+ });
20
+ };
21
+ const isIntentBound = (intent) => {
22
+ return ledgerTry(() => intent.binding instanceof Binding);
23
+ };
24
+ const mergeCounterOffer = (counterOffer, currentOffer) => pipe(Option.fromNullable(currentOffer), Option.match({
25
+ onNone: () => Either.right(counterOffer),
26
+ onSome: (currentOffer) => ledgerTry(() => UnshieldedOffer.new([...currentOffer.inputs, ...counterOffer.inputs], [...currentOffer.outputs, ...counterOffer.outputs], [...currentOffer.signatures, ...counterOffer.signatures])),
27
+ }));
28
+ export class TransactionService extends Context.Tag('@midnight-ntwrk/wallet-sdk-unshielded-wallet/TransactionService')() {
29
+ static Live = Layer.succeed(TransactionService, (() => {
30
+ const transferTransaction = (desiredOutputs, ttl, networkId) => Effect.gen(function* () {
31
+ const isValid = desiredOutputs.every((output) => output.amount > 0n);
32
+ if (!isValid) {
33
+ return yield* Effect.fail(new TransactionServiceError({ message: 'The amount needs to be positive' }));
34
+ }
35
+ const ledgerOutputs = desiredOutputs.map((output) => {
36
+ return {
37
+ value: output.amount,
38
+ owner: output.receiverAddress,
39
+ type: output.type,
40
+ };
41
+ });
42
+ return yield* ledgerTry(() => {
43
+ const intent = Intent.new(ttl);
44
+ intent.guaranteedUnshieldedOffer = UnshieldedOffer.new([], ledgerOutputs, []);
45
+ return Transaction.fromParts(networkId, undefined, undefined, intent);
46
+ });
47
+ });
48
+ const initSwap = (desiredInputs, desiredOutputs, ttl, networkId, state, myAddress, publicKey) => Effect.gen(function* () {
49
+ const outputsValid = desiredOutputs.every((output) => output.amount > 0n);
50
+ if (!outputsValid) {
51
+ return yield* Effect.fail(new TransactionServiceError({ message: 'The amount needs to be positive' }));
52
+ }
53
+ const inputsValid = Object.entries(desiredInputs).every(([, amount]) => amount > 0n);
54
+ if (!inputsValid) {
55
+ return yield* Effect.fail(new TransactionServiceError({ message: 'The input amounts need to be positive' }));
56
+ }
57
+ const ledgerOutputs = desiredOutputs.map((output) => ({
58
+ value: output.amount,
59
+ owner: output.receiverAddress,
60
+ type: output.type,
61
+ }));
62
+ const targetImbalances = Imbalances.fromEntries(Object.entries(desiredInputs));
63
+ const latestState = yield* state.getLatestState();
64
+ const availableCoins = HashSet.toValues(latestState.utxos);
65
+ const { inputs, outputs: changeOutputs } = yield* Effect.try({
66
+ try: () => getBalanceRecipe({
67
+ coins: availableCoins,
68
+ initialImbalances: Imbalances.empty(),
69
+ feeTokenType: '',
70
+ transactionCostModel: {
71
+ inputFeeOverhead: 0n,
72
+ outputFeeOverhead: 0n,
73
+ },
74
+ createOutput: (coin) => ({
75
+ ...coin,
76
+ owner: myAddress,
77
+ }),
78
+ isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
79
+ targetImbalances,
80
+ }),
81
+ catch: (error) => {
82
+ const message = error instanceof Error ? error.message : error?.toString() || '';
83
+ return new TransactionServiceError({ message });
84
+ },
85
+ });
86
+ for (const input of inputs) {
87
+ yield* state.spend(input);
88
+ }
89
+ const ledgerInputs = inputs.map((input) => ({
90
+ ...input,
91
+ owner: publicKey,
92
+ }));
93
+ const offer = yield* ledgerTry(() => UnshieldedOffer.new(ledgerInputs, [...changeOutputs, ...ledgerOutputs], []));
94
+ const intent = Intent.new(ttl);
95
+ intent.guaranteedUnshieldedOffer = offer;
96
+ return yield* ledgerTry(() => Transaction.fromParts(networkId, undefined, undefined, intent));
97
+ });
98
+ const deserializeTransaction = (markerS, markerP, markerB, tx) =>
99
+ // NOTE: ledger's deserialization error is too of a low-level and doesn't tell us what exactly was wrong
100
+ Effect.mapError(ledgerTry(() => {
101
+ const data = Buffer.from(tx, 'hex');
102
+ return Transaction.deserialize(markerS, markerP, markerB, data);
103
+ }), (e) => new DeserializationError({ message: 'Unable to deserialize transaction', internal: e.message }));
104
+ const serializeTransaction = (transaction) => Effect.map(ledgerTry(() => transaction.serialize()), (res) => Buffer.from(res).toString('hex'));
105
+ const balanceTransaction = (transaction, state, myAddress, publicKey) => Effect.gen(function* () {
106
+ const segments = getSegments(transaction);
107
+ if (!transaction.intents || !transaction.intents.size || !segments.length) {
108
+ return transaction;
109
+ }
110
+ for (const segment of [...segments, GUARANTEED_SEGMENT]) {
111
+ const allIntentImbalances = yield* ledgerTry(() => transaction.imbalances(segment));
112
+ const imbalances = allIntentImbalances
113
+ .entries()
114
+ .filter(([token, value]) => token.tag === 'unshielded' && value !== 0n)
115
+ .map(([token, value]) => {
116
+ return [token.raw.toString(), value];
117
+ })
118
+ .toArray();
119
+ // intent is balanced
120
+ if (!imbalances.length)
121
+ continue;
122
+ const latestState = yield* state.getLatestState();
123
+ const availableCoins = HashSet.toValues(latestState.utxos);
124
+ // select inputs, receive the change outputs
125
+ const { inputs, outputs: changeOutputs } = yield* Effect.try({
126
+ try: () => getBalanceRecipe({
127
+ coins: availableCoins,
128
+ initialImbalances: Imbalances.fromEntries(imbalances),
129
+ feeTokenType: '',
130
+ transactionCostModel: {
131
+ inputFeeOverhead: 0n,
132
+ outputFeeOverhead: 0n,
133
+ },
134
+ createOutput: (coin) => ({
135
+ ...coin,
136
+ owner: myAddress,
137
+ }),
138
+ isCoinEqual: (a, b) => a.intentHash === b.intentHash && a.outputNo === b.outputNo,
139
+ }),
140
+ catch: (error) => {
141
+ const message = error instanceof Error ? error.message : error?.toString() || '';
142
+ return new TransactionServiceError({ message });
143
+ },
144
+ });
145
+ if (!inputs.length) {
146
+ return yield* Effect.fail(new TransactionServiceError({ message: 'No coins found to spend' }));
147
+ }
148
+ // mark the coins as spent
149
+ for (const input of inputs) {
150
+ yield* state.spend(input);
151
+ }
152
+ const ledgerInputs = inputs.map((input) => ({
153
+ ...input,
154
+ intentHash: input.intentHash,
155
+ owner: publicKey,
156
+ }));
157
+ const counterOffer = yield* ledgerTry(() => UnshieldedOffer.new(ledgerInputs, changeOutputs, []));
158
+ // NOTE: for the segment === 0 we insert the counter-offer into any intent's guaranteed section
159
+ if (segment !== GUARANTEED_SEGMENT) {
160
+ const intent = transaction.intents.get(segment);
161
+ const isBound = yield* isIntentBound(intent);
162
+ if (!isBound && intent.fallibleUnshieldedOffer) {
163
+ const mergedOffer = yield* mergeCounterOffer(counterOffer, intent.fallibleUnshieldedOffer);
164
+ yield* ledgerTry(() => {
165
+ intent.fallibleUnshieldedOffer = mergedOffer;
166
+ transaction.intents = transaction.intents.set(segment, intent);
167
+ });
168
+ }
169
+ else {
170
+ // create a new offer if the intent is bound
171
+ yield* ledgerTry(() => {
172
+ const nextSegment = Math.max(...getSegments(transaction)) + 1;
173
+ const newIntent = Intent.new(intent.ttl);
174
+ newIntent.fallibleUnshieldedOffer = counterOffer;
175
+ transaction.intents = transaction.intents.set(nextSegment, newIntent);
176
+ });
177
+ }
178
+ }
179
+ else {
180
+ let ttl;
181
+ let updated = false;
182
+ // try to find and modify any unbound intent first
183
+ const segments = getSegments(transaction);
184
+ for (const segment of segments) {
185
+ const intent = transaction.intents.get(segment);
186
+ ttl = intent.ttl;
187
+ const isBound = yield* isIntentBound(intent);
188
+ if (!isBound) {
189
+ const mergedOffer = yield* mergeCounterOffer(counterOffer, intent.guaranteedUnshieldedOffer);
190
+ yield* ledgerTry(() => {
191
+ intent.guaranteedUnshieldedOffer = mergedOffer;
192
+ transaction.intents = transaction.intents.set(segment, intent);
193
+ });
194
+ updated = true;
195
+ break;
196
+ }
197
+ }
198
+ // no unbound intents found, insert a new one
199
+ if (!updated) {
200
+ yield* ledgerTry(() => {
201
+ const nextSegment = Math.max(...segments) + 1;
202
+ const newIntent = Intent.new(ttl);
203
+ newIntent.guaranteedUnshieldedOffer = counterOffer;
204
+ transaction.intents = transaction.intents.set(nextSegment, newIntent);
205
+ });
206
+ }
207
+ }
208
+ }
209
+ return transaction;
210
+ });
211
+ const getOfferSignatureData = (transaction, segment = 1) => {
212
+ if (!transaction.intents) {
213
+ return Effect.fail(new TransactionServiceError({ message: 'No intents found in the provided transaction' }));
214
+ }
215
+ const intent = transaction.intents.get(segment);
216
+ if (!intent) {
217
+ return Effect.fail(new TransactionServiceError({ message: 'Intent with a given segment was not found' }));
218
+ }
219
+ return pipe(ledgerTry(() => (isIntentBound(intent) ? intent : intent.bind(segment))), Effect.andThen((boundIntent) => ledgerTry(() => boundIntent.signatureData(segment))));
220
+ };
221
+ const addOfferSignature = (transaction, signature, segment = 1) => Effect.gen(function* () {
222
+ if (!transaction.intents || !transaction.intents.size) {
223
+ return yield* Effect.fail(new TransactionServiceError({ message: 'No intents found in the provided transaction' }));
224
+ }
225
+ const intent = transaction.intents.get(segment);
226
+ if (!intent) {
227
+ return yield* Effect.fail(new TransactionServiceError({ message: 'Intent with a given segment was not found' }));
228
+ }
229
+ // skip if it's locked
230
+ const isBound = yield* isIntentBound(intent);
231
+ if (isBound)
232
+ return transaction;
233
+ let updatedIntent = intent;
234
+ if (intent.guaranteedUnshieldedOffer) {
235
+ const offer = intent.guaranteedUnshieldedOffer;
236
+ const inputsLen = offer.inputs.length;
237
+ const signatures = [];
238
+ for (let i = 0; i < inputsLen; ++i) {
239
+ signatures.push(offer.signatures.at(i) ?? signature);
240
+ }
241
+ const updatedOffer = yield* ledgerTry(() => offer.addSignatures(signatures));
242
+ updatedIntent = yield* ledgerTry(() => {
243
+ updatedIntent.guaranteedUnshieldedOffer = updatedOffer;
244
+ return updatedIntent;
245
+ });
246
+ }
247
+ if (intent.fallibleUnshieldedOffer) {
248
+ const offer = intent.fallibleUnshieldedOffer;
249
+ const inputsLen = offer.inputs.length;
250
+ const signatures = [];
251
+ for (let i = 0; i < inputsLen; ++i) {
252
+ signatures.push(offer.signatures.at(i) ?? signature);
253
+ }
254
+ const updatedOffer = yield* ledgerTry(() => offer.addSignatures(signatures));
255
+ updatedIntent = yield* ledgerTry(() => {
256
+ updatedIntent.fallibleUnshieldedOffer = updatedOffer;
257
+ return updatedIntent;
258
+ });
259
+ }
260
+ transaction.intents = yield* ledgerTry(() => transaction.intents.set(segment, updatedIntent));
261
+ return transaction;
262
+ });
263
+ const bindTransaction = (transaction) => ledgerTry(() => transaction.bind());
264
+ const getSegments = (transaction) => {
265
+ return transaction.intents ? [...transaction.intents.keys()] : [];
266
+ };
267
+ return TransactionService.of({
268
+ transferTransaction,
269
+ initSwap,
270
+ deserializeTransaction,
271
+ serializeTransaction,
272
+ balanceTransaction,
273
+ getOfferSignatureData,
274
+ addOfferSignature,
275
+ bindTransaction,
276
+ getSegments,
277
+ });
278
+ })());
279
+ }
@@ -0,0 +1,37 @@
1
+ import * as ledger from '@midnight-ntwrk/ledger-v6';
2
+ import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
3
+ import { Observable } from 'rxjs';
4
+ import { PublicKey } from './KeyStore.js';
5
+ import { State } from './State.js';
6
+ import { TransactionHistoryChange } from './TransactionHistoryService.js';
7
+ import { TokenTransfer } from './TransactionService.js';
8
+ import { TransactionHash, TransactionHistoryEntry, TransactionHistoryStorage } from './tx-history-storage/index.js';
9
+ interface WalletConfig {
10
+ publicKey: PublicKey;
11
+ networkId: NetworkId.NetworkId;
12
+ indexerUrl: string;
13
+ txHistoryStorage?: TransactionHistoryStorage | undefined;
14
+ }
15
+ export interface UnshieldedWallet {
16
+ start(): Promise<void>;
17
+ stop(): Promise<void>;
18
+ serializeState(): Promise<string>;
19
+ state: () => Observable<State>;
20
+ transferTransaction(outputs: TokenTransfer[], ttl: Date): Promise<ledger.UnprovenTransaction>;
21
+ initSwap(desiredInputs: Record<string, bigint>, desiredOutputs: TokenTransfer[], ttl: Date): Promise<ledger.UnprovenTransaction>;
22
+ balanceTransaction(tx: ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>): Promise<ledger.Transaction<ledger.SignatureEnabled, ledger.Proofish, ledger.Bindingish>>;
23
+ signTransaction(tx: ledger.UnprovenTransaction, signSegment: (data: Uint8Array) => ledger.Signature): Promise<ledger.UnprovenTransaction>;
24
+ transactionHistory: undefined | {
25
+ get: (item: TransactionHash) => Promise<TransactionHistoryEntry | undefined>;
26
+ getAll: () => Observable<TransactionHistoryEntry>;
27
+ changes: () => Observable<TransactionHistoryChange | undefined>;
28
+ };
29
+ }
30
+ interface RestorableWalletConfig extends WalletConfig {
31
+ serializedState: string;
32
+ }
33
+ export declare class WalletBuilder {
34
+ static build({ publicKey, networkId, indexerUrl, txHistoryStorage }: WalletConfig): Promise<UnshieldedWallet>;
35
+ static restore({ publicKey, networkId, indexerUrl, serializedState, txHistoryStorage, }: RestorableWalletConfig): Promise<UnshieldedWallet>;
36
+ }
37
+ export {};
@@ -0,0 +1,137 @@
1
+ import { MidnightBech32m, UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
2
+ import { UnshieldedStateDecoder, UnshieldedStateService } from '@midnight-ntwrk/wallet-sdk-unshielded-state';
3
+ import { ObservableOps } from '@midnight-ntwrk/wallet-sdk-utilities';
4
+ import { Deferred, Effect, Either, Fiber, Layer, pipe, Stream } from 'effect';
5
+ import { StateImpl } from './State.js';
6
+ import { SyncService } from './SyncService.js';
7
+ import { TransactionHistoryService } from './TransactionHistoryService.js';
8
+ import { TransactionService } from './TransactionService.js';
9
+ import { NoOpTransactionHistoryStorage } from './tx-history-storage/NoOpTransactionHistoryStorage.js';
10
+ const makeWallet = ({ publicKey, networkId, txHistoryStorage, }) => Effect.gen(function* () {
11
+ const syncService = yield* SyncService;
12
+ const transactionHistoryService = yield* TransactionHistoryService;
13
+ const unshieldedState = yield* UnshieldedStateService;
14
+ const transactionService = yield* TransactionService;
15
+ // TODO: Scope would be a preferred way to handle controlled stop
16
+ const stopLatch = yield* Deferred.make();
17
+ const bech32mAddress = UnshieldedAddress.codec.encode(networkId, publicKey.address);
18
+ const state = new StateImpl(unshieldedState, bech32mAddress.asString());
19
+ const applyUpdate = (update) => Effect.gen(function* () {
20
+ const { type } = update;
21
+ if (type === 'UnshieldedTransaction') {
22
+ const { transaction } = update;
23
+ yield* unshieldedState.applyTx(update.transaction);
24
+ yield* transactionHistoryService.create({
25
+ id: transaction.id,
26
+ hash: transaction.hash,
27
+ protocolVersion: transaction.protocolVersion,
28
+ identifiers: transaction.identifiers ?? [],
29
+ transactionResult: transaction.transactionResult
30
+ ? {
31
+ status: transaction.transactionResult.status,
32
+ segments: transaction.transactionResult.segments ?? [],
33
+ }
34
+ : null,
35
+ });
36
+ }
37
+ if (type === 'UnshieldedTransactionsProgress') {
38
+ yield* unshieldedState.updateSyncProgress(update.highestTransactionId);
39
+ }
40
+ });
41
+ const start = () => Effect.gen(function* () {
42
+ const latestState = yield* unshieldedState.getLatestState();
43
+ const fiber = yield* pipe(syncService.startSync(bech32mAddress.asString(), latestState.syncProgress?.highestTransactionId ?? 0), Stream.tapError((error) => Effect.gen(function* () {
44
+ // eslint-disable-next-line no-console
45
+ yield* Effect.sync(() => console.error(error));
46
+ yield* Deferred.die(stopLatch, error);
47
+ })), Stream.runForEach(applyUpdate), Effect.fork);
48
+ yield* Deferred.await(stopLatch).pipe(Effect.andThen(Fiber.interrupt(fiber)));
49
+ });
50
+ const stop = () => Deferred.succeed(stopLatch, undefined).pipe(Effect.asVoid);
51
+ const transferTransaction = (outputs, ttl) => Effect.gen(function* () {
52
+ const latestState = yield* unshieldedState.getLatestState();
53
+ if (!latestState.syncProgress) {
54
+ return yield* Effect.fail('Unable to get the latest block number');
55
+ }
56
+ const mappedOutputs = outputs.map((output) => ({
57
+ ...output,
58
+ receiverAddress: UnshieldedAddress.codec
59
+ .decode(networkId, MidnightBech32m.parse(output.receiverAddress))
60
+ .data.toString('hex'),
61
+ }));
62
+ const transaction = yield* transactionService.transferTransaction(mappedOutputs, ttl, networkId);
63
+ return (yield* transactionService.balanceTransaction(transaction, unshieldedState, publicKey.address.hexString, publicKey.publicKey));
64
+ });
65
+ const initSwap = (desiredInputs, outputs, ttl) => Effect.gen(function* () {
66
+ const latestState = yield* unshieldedState.getLatestState();
67
+ if (!latestState.syncProgress) {
68
+ return yield* Effect.fail('Unable to get the latest block number');
69
+ }
70
+ const mappedOutputs = outputs.map((output) => ({
71
+ ...output,
72
+ receiverAddress: UnshieldedAddress.codec
73
+ .decode(networkId, MidnightBech32m.parse(output.receiverAddress))
74
+ .data.toString('hex'),
75
+ }));
76
+ return yield* transactionService.initSwap(desiredInputs, mappedOutputs, ttl, networkId, unshieldedState, publicKey.address.hexString, publicKey.publicKey);
77
+ });
78
+ const balanceTransaction = (tx) => transactionService.balanceTransaction(tx, unshieldedState, publicKey.address.hexString, publicKey.publicKey);
79
+ const signTransaction = (tx, signSegment) => Effect.gen(function* () {
80
+ const segments = transactionService.getSegments(tx);
81
+ if (!segments.length) {
82
+ return yield* Effect.fail('No segments found in the provided transaction');
83
+ }
84
+ for (const segment of segments) {
85
+ const data = yield* transactionService.getOfferSignatureData(tx, segment);
86
+ const signature = yield* signSegment(data);
87
+ tx = yield* transactionService.addOfferSignature(tx, signature, segment);
88
+ }
89
+ // return yield* transactionService.bindTransaction(tx);
90
+ return tx;
91
+ });
92
+ const transactionHistory = txHistoryStorage
93
+ ? {
94
+ get: (hash) => Effect.runPromise(transactionHistoryService.get(hash)),
95
+ getAll: () => ObservableOps.fromStream(transactionHistoryService.getAll()),
96
+ changes: () => ObservableOps.fromStream(transactionHistoryService.changes),
97
+ }
98
+ : undefined;
99
+ const result = {
100
+ state: () => ObservableOps.fromStream(state.updates()),
101
+ start: () => {
102
+ return new Promise((resolve) => {
103
+ Effect.runFork(Effect.scoped(start()));
104
+ resolve(void 0);
105
+ });
106
+ },
107
+ stop: () => Effect.runPromise(stop()),
108
+ transferTransaction: (outputs, ttl) => Effect.runPromise(transferTransaction(outputs, ttl)),
109
+ initSwap: (desiredInputs, desiredOutputs, ttl) => Effect.runPromise(initSwap(desiredInputs, desiredOutputs, ttl)),
110
+ balanceTransaction: (tx) => Effect.runPromise(balanceTransaction(tx)),
111
+ signTransaction: (tx, signSegment) => Effect.runPromise(signTransaction(tx, (data) => Effect.try(() => signSegment(data)))),
112
+ serializeState: () => Effect.runPromise(state.serialize()),
113
+ transactionHistory,
114
+ };
115
+ return result;
116
+ });
117
+ export class WalletBuilder {
118
+ static async build({ publicKey, networkId, indexerUrl, txHistoryStorage }) {
119
+ const txHistoryService = TransactionHistoryService.Live(txHistoryStorage ? txHistoryStorage : new NoOpTransactionHistoryStorage());
120
+ const layers = Layer.mergeAll(SyncService.LiveWithIndexer(indexerUrl), UnshieldedStateService.Live(), TransactionService.Live, txHistoryService);
121
+ const walletService = makeWallet({ publicKey, networkId, txHistoryStorage });
122
+ const wallet = walletService.pipe(Effect.provide(layers));
123
+ return Effect.runPromise(wallet);
124
+ }
125
+ static async restore({ publicKey, networkId, indexerUrl, serializedState, txHistoryStorage, }) {
126
+ const parsedState = JSON.parse(serializedState);
127
+ const decodedState = UnshieldedStateDecoder(parsedState);
128
+ if (Either.isLeft(decodedState)) {
129
+ throw new Error(`Failed to decode unshielded state: ${decodedState.left.message}`);
130
+ }
131
+ const txHistoryService = TransactionHistoryService.Live(txHistoryStorage ? txHistoryStorage : new NoOpTransactionHistoryStorage());
132
+ const layer = Layer.mergeAll(SyncService.LiveWithIndexer(indexerUrl), UnshieldedStateService.LiveWithState(decodedState.right), TransactionService.Live, txHistoryService);
133
+ const walletService = makeWallet({ publicKey, networkId, txHistoryStorage });
134
+ const wallet = walletService.pipe(Effect.provide(layer));
135
+ return Effect.runPromise(wallet);
136
+ }
137
+ }
@@ -0,0 +1,3 @@
1
+ export * from './WalletBuilder.js';
2
+ export { State as UnshieldedWalletState } from './State.js';
3
+ export { PublicKey, createKeystore, UnshieldedKeystore, Keystore } from './KeyStore.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './WalletBuilder.js';
2
+ export { PublicKey, createKeystore } from './KeyStore.js';
@@ -0,0 +1,33 @@
1
+ import { Schema } from 'effect';
2
+ import { TransactionHistoryStorage, TransactionHistoryEntry, TransactionHash } from './TransactionHistoryStorage.js';
3
+ declare const TransactionHistorySchema: Schema.Map$<typeof Schema.String, Schema.Struct<{
4
+ id: typeof Schema.Number;
5
+ hash: typeof Schema.String;
6
+ protocolVersion: typeof Schema.Number;
7
+ identifiers: Schema.Array$<typeof Schema.String>;
8
+ transactionResult: Schema.NullOr<Schema.Struct<{
9
+ status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
10
+ segments: Schema.Array$<Schema.Struct<{
11
+ id: typeof Schema.String;
12
+ success: typeof Schema.Boolean;
13
+ }>>;
14
+ }>>;
15
+ }>>;
16
+ export type TransactionHistory = Schema.Schema.Type<typeof TransactionHistorySchema>;
17
+ /**
18
+ * In-memory implementation of the TransactionHistoryStorage interface.
19
+ *
20
+ * TODO: Implement update method with callback api when needed in the future
21
+ */
22
+ export declare class InMemoryTransactionHistoryStorage implements TransactionHistoryStorage {
23
+ private entries;
24
+ constructor(entries?: TransactionHistory);
25
+ create(entry: TransactionHistoryEntry): Promise<void>;
26
+ delete(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
27
+ getAll(): AsyncIterableIterator<TransactionHistoryEntry>;
28
+ get(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
29
+ serialize(): string;
30
+ reset(): void;
31
+ static fromSerialized(serializedHistory: string): InMemoryTransactionHistoryStorage;
32
+ }
33
+ export {};
@@ -0,0 +1,53 @@
1
+ import { Either, Schema } from 'effect';
2
+ import { TransactionHistoryEntrySchema, } from './TransactionHistoryStorage.js';
3
+ const TransactionHistorySchema = Schema.Map({
4
+ key: Schema.String,
5
+ value: TransactionHistoryEntrySchema,
6
+ });
7
+ const TransactionHistoryEncoder = Schema.encodeSync(TransactionHistorySchema);
8
+ const TransactionHistoryDecoder = Schema.decodeUnknownEither(TransactionHistorySchema);
9
+ /**
10
+ * In-memory implementation of the TransactionHistoryStorage interface.
11
+ *
12
+ * TODO: Implement update method with callback api when needed in the future
13
+ */
14
+ export class InMemoryTransactionHistoryStorage {
15
+ entries;
16
+ constructor(entries) {
17
+ this.entries = entries || new Map();
18
+ }
19
+ create(entry) {
20
+ this.entries.set(entry.hash, entry);
21
+ return Promise.resolve();
22
+ }
23
+ delete(hash) {
24
+ const existingEntry = this.entries.get(hash);
25
+ if (!existingEntry) {
26
+ return Promise.resolve(undefined);
27
+ }
28
+ this.entries.delete(hash);
29
+ return Promise.resolve(existingEntry);
30
+ }
31
+ async *getAll() {
32
+ for (const entry of this.entries.values()) {
33
+ yield await Promise.resolve(entry);
34
+ }
35
+ }
36
+ get(hash) {
37
+ return Promise.resolve(this.entries.get(hash));
38
+ }
39
+ serialize() {
40
+ const result = TransactionHistoryEncoder(this.entries);
41
+ return JSON.stringify(result);
42
+ }
43
+ reset() {
44
+ this.entries.clear();
45
+ }
46
+ static fromSerialized(serializedHistory) {
47
+ const schema = JSON.parse(serializedHistory);
48
+ const decoded = Either.getOrElse(TransactionHistoryDecoder(schema), (error) => {
49
+ throw new Error(`Failed to decode transaction history: ${error.message}`);
50
+ });
51
+ return new InMemoryTransactionHistoryStorage(decoded);
52
+ }
53
+ }
@@ -0,0 +1,9 @@
1
+ import { TransactionHistoryStorage, TransactionHash, TransactionHistoryEntry } from './TransactionHistoryStorage.js';
2
+ export declare class NoOpTransactionHistoryStorage implements TransactionHistoryStorage {
3
+ create(_entry: TransactionHistoryEntry): Promise<void>;
4
+ delete(_hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
5
+ getAll(): AsyncIterableIterator<TransactionHistoryEntry>;
6
+ get(_hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
7
+ serialize(): string;
8
+ static deserialize(_serialized: string): NoOpTransactionHistoryStorage;
9
+ }
@@ -0,0 +1,20 @@
1
+ export class NoOpTransactionHistoryStorage {
2
+ create(_entry) {
3
+ return Promise.resolve();
4
+ }
5
+ delete(_hash) {
6
+ return Promise.resolve(undefined);
7
+ }
8
+ async *getAll() {
9
+ return Promise.resolve(yield* []);
10
+ }
11
+ get(_hash) {
12
+ return Promise.resolve(undefined);
13
+ }
14
+ serialize() {
15
+ return JSON.stringify({});
16
+ }
17
+ static deserialize(_serialized) {
18
+ return new NoOpTransactionHistoryStorage();
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ import { Schema } from 'effect';
2
+ declare const TransactionHashSchema: typeof Schema.String;
3
+ export type TransactionHash = Schema.Schema.Type<typeof TransactionHashSchema>;
4
+ export declare const TransactionHistoryEntrySchema: Schema.Struct<{
5
+ id: typeof Schema.Number;
6
+ hash: typeof Schema.String;
7
+ protocolVersion: typeof Schema.Number;
8
+ identifiers: Schema.Array$<typeof Schema.String>;
9
+ transactionResult: Schema.NullOr<Schema.Struct<{
10
+ status: Schema.Literal<["SUCCESS", "FAILURE", "PARTIAL_SUCCESS"]>;
11
+ segments: Schema.Array$<Schema.Struct<{
12
+ id: typeof Schema.String;
13
+ success: typeof Schema.Boolean;
14
+ }>>;
15
+ }>>;
16
+ }>;
17
+ export type TransactionHistoryEntry = Schema.Schema.Type<typeof TransactionHistoryEntrySchema>;
18
+ export interface TransactionHistoryStorage {
19
+ create(entry: TransactionHistoryEntry): Promise<void>;
20
+ delete(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
21
+ getAll(): AsyncIterableIterator<TransactionHistoryEntry>;
22
+ get(hash: TransactionHash): Promise<TransactionHistoryEntry | undefined>;
23
+ }
24
+ export {};
@@ -0,0 +1,15 @@
1
+ import { Schema } from 'effect';
2
+ const TransactionHashSchema = Schema.String;
3
+ export const TransactionHistoryEntrySchema = Schema.Struct({
4
+ id: Schema.Number,
5
+ hash: TransactionHashSchema,
6
+ protocolVersion: Schema.Number,
7
+ identifiers: Schema.Array(Schema.String),
8
+ transactionResult: Schema.NullOr(Schema.Struct({
9
+ status: Schema.Literal('SUCCESS', 'FAILURE', 'PARTIAL_SUCCESS'),
10
+ segments: Schema.Array(Schema.Struct({
11
+ id: Schema.String,
12
+ success: Schema.Boolean,
13
+ })),
14
+ })),
15
+ });
@@ -0,0 +1,2 @@
1
+ export * from './InMemoryTransactionHistoryStorage.js';
2
+ export * from './TransactionHistoryStorage.js';
@@ -0,0 +1,2 @@
1
+ export * from './InMemoryTransactionHistoryStorage.js';
2
+ export * from './TransactionHistoryStorage.js';
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@midnight-ntwrk/wallet-sdk-unshielded-wallet",
3
+ "version": "1.0.0-beta.11",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "author": "IOHK",
8
+ "license": "Apache-2.0",
9
+ "publishConfig": {
10
+ "registry": "https://npm.pkg.github.com/"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/midnight-ntwrk/artifacts.git"
15
+ },
16
+ "files": [
17
+ "dist/"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "@midnight-ntwrk/ledger-v6": "6.1.0-alpha.5",
27
+ "@midnight-ntwrk/wallet-sdk-abstractions": "1.0.0-beta.8",
28
+ "@midnight-ntwrk/wallet-sdk-address-format": "3.0.0-beta.7",
29
+ "@midnight-ntwrk/wallet-sdk-capabilities": "3.0.0-beta.7",
30
+ "@midnight-ntwrk/wallet-sdk-hd": "3.0.0-beta.6",
31
+ "@midnight-ntwrk/wallet-sdk-indexer-client": "1.0.0-beta.11",
32
+ "@midnight-ntwrk/wallet-sdk-unshielded-state": "1.0.0-beta.10",
33
+ "@midnight-ntwrk/wallet-sdk-utilities": "1.0.0-beta.7",
34
+ "effect": "^3.17.3",
35
+ "rxjs": "^7.5"
36
+ },
37
+ "scripts": {
38
+ "typecheck": "tsc -b ./tsconfig.json --noEmit",
39
+ "test": "vitest run",
40
+ "lint": "eslint --max-warnings 0",
41
+ "format": "prettier --write \"**/*.{ts,js,json,yaml,yml}\"",
42
+ "dist": "tsc -b ./tsconfig.build.json",
43
+ "dist:publish": "tsc -b ./tsconfig.publish.json",
44
+ "clean": "rimraf --glob dist 'tsconfig.*.tsbuildinfo' && date +%s > .clean-timestamp",
45
+ "publint": "publint --strict"
46
+ },
47
+ "devDependencies": {
48
+ "eslint": "^9.37.0",
49
+ "publint": "~0.3.14",
50
+ "rimraf": "^6.0.1",
51
+ "testcontainers": "^11.0.3",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^3.2.4"
54
+ }
55
+ }