applesauce-wallet 6.2.0 → 7.0.0

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.
@@ -6,7 +6,7 @@ import { MINT_RECOMMENDATION_KIND, getRecommendationMintPubkey, isValidMintRecom
6
6
  async function fetchMintPubkey(mintUrl) {
7
7
  // Fetch mint info directly from the /v1/info endpoint
8
8
  const infoUrl = new URL("/v1/info", mintUrl).toString();
9
- const response = await fetch(infoUrl);
9
+ const response = await fetch(infoUrl, { signal: AbortSignal.timeout(10_000) });
10
10
  if (!response.ok) {
11
11
  throw new Error(`Failed to fetch mint info from ${mintUrl}: ${response.statusText}`);
12
12
  }
@@ -11,7 +11,7 @@ export declare class Nutzap extends EventCast<NutzapEvent> {
11
11
  get sender(): import("applesauce-core/casts").User;
12
12
  get comment(): string | undefined;
13
13
  /** The pointer to the event that was zapped */
14
- get zapPointer(): import("nostr-tools/nip19").EventPointer | import("nostr-tools/nip19").AddressPointer | undefined;
14
+ get zapPointer(): import("nostr-tools/nip19").AddressPointer | import("nostr-tools/nip19").EventPointer | undefined;
15
15
  /** The event that was zapped */
16
16
  get zapped$(): import("applesauce-core").ChainableObservable<NostrEvent | undefined>;
17
17
  }
@@ -1,6 +1,5 @@
1
1
  import { getEncodedToken } from "@cashu/cashu-ts";
2
- import { UR, URDecoder, UREncoder } from "@gandlaf21/bc-ur/dist/lib/es6/index.js";
3
- import { defer, filter, interval, map, Observable, shareReplay } from "rxjs";
2
+ import { defer, filter, interval, map, Observable, shareReplay, switchMap } from "rxjs";
4
3
  /** Preset speeds for the animated qr code */
5
4
  export const ANIMATED_QR_INTERVAL = {
6
5
  SLOW: 500,
@@ -16,45 +15,134 @@ export const ANIMATED_QR_FRAGMENTS = {
16
15
  /** Creates an observable that iterates through a multi-part animated qr code */
17
16
  export function sendAnimated(token, options) {
18
17
  // start the stream as soon as there is subscriber
19
- return defer(() => {
18
+ return defer(() => import("@gandlaf21/bc-ur/dist/lib/es6/index.js")).pipe(switchMap(({ UR, UREncoder }) => {
20
19
  let str = typeof token === "string" ? token : getEncodedToken(token);
21
20
  let utf8 = new TextEncoder();
22
21
  let buffer = utf8.encode(str);
23
22
  let ur = UR.from(buffer);
24
23
  let encoder = new UREncoder(ur, options?.fragmentLength ?? 100, 0);
25
24
  return interval(options?.interval ?? ANIMATED_QR_INTERVAL.FAST).pipe(map(() => encoder.nextPart()));
26
- });
25
+ }));
27
26
  }
28
- /** An operator that decodes UR, emits progress percent and completes with final result or error */
27
+ /**
28
+ * An operator that decodes UR, emits progress percent and completes with final result or error.
29
+ * Subscribes to its source synchronously so fragments/terminal signals emitted by a hot source
30
+ * while bc-ur is still loading are buffered in order and drained into one decoder once ready.
31
+ */
29
32
  function urDecoder() {
30
33
  return (source) => new Observable((observer) => {
31
- const decoder = new URDecoder();
32
- return source.subscribe((part) => {
33
- decoder.receivePart(part);
34
- if (decoder.isComplete() && decoder.isSuccess()) {
35
- // emit progress
36
- const progress = decoder.estimatedPercentComplete();
37
- observer.next(progress);
38
- // emit result
39
- const ur = decoder.resultUR();
40
- const decoded = ur.decodeCBOR();
41
- const utf8 = new TextDecoder();
42
- const tokenStr = utf8.decode(decoded);
43
- observer.next(tokenStr);
44
- // complete
45
- observer.complete();
34
+ let cancelled = false;
35
+ let settled = false;
36
+ let decoder = null;
37
+ const queue = [];
38
+ const decode = (part) => {
39
+ if (settled || !decoder)
40
+ return;
41
+ try {
42
+ decoder.receivePart(part);
43
+ if (decoder.isComplete() && decoder.isSuccess()) {
44
+ settled = true;
45
+ // emit progress
46
+ const progress = decoder.estimatedPercentComplete();
47
+ observer.next(progress);
48
+ // emit result
49
+ const ur = decoder.resultUR();
50
+ const decoded = ur.decodeCBOR();
51
+ const utf8 = new TextDecoder();
52
+ const tokenStr = utf8.decode(decoded);
53
+ observer.next(tokenStr);
54
+ // complete
55
+ observer.complete();
56
+ }
57
+ else if (decoder.isError()) {
58
+ settled = true;
59
+ // emit error
60
+ const reason = decoder.resultError();
61
+ observer.error(new Error(reason));
62
+ }
63
+ else {
64
+ // emit progress
65
+ const progress = decoder.estimatedPercentComplete();
66
+ observer.next(progress);
67
+ }
68
+ }
69
+ catch (error) {
70
+ settled = true;
71
+ observer.error(error);
46
72
  }
47
- else if (decoder.isError()) {
48
- // emit error
49
- const reason = decoder.resultError();
50
- observer.error(new Error(reason));
73
+ };
74
+ const fail = (error) => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ observer.error(error);
79
+ };
80
+ const complete = () => {
81
+ if (settled)
82
+ return;
83
+ settled = true;
84
+ observer.error(new Error("Animated QR input completed before a complete UR was decoded"));
85
+ };
86
+ const drain = (signal) => {
87
+ if (signal.kind === "next")
88
+ decode(signal.value);
89
+ else if (signal.kind === "error")
90
+ fail(signal.error);
91
+ else
92
+ complete();
93
+ };
94
+ // subscribe to the (possibly hot) source synchronously, before bc-ur has loaded
95
+ const sourceSubscription = source.subscribe({
96
+ next: (part) => {
97
+ if (cancelled || settled)
98
+ return;
99
+ if (decoder)
100
+ decode(part);
101
+ else
102
+ queue.push({ kind: "next", value: part });
103
+ },
104
+ error: (error) => {
105
+ if (cancelled || settled)
106
+ return;
107
+ if (decoder)
108
+ fail(error);
109
+ else
110
+ queue.push({ kind: "error", error });
111
+ },
112
+ complete: () => {
113
+ if (cancelled || settled)
114
+ return;
115
+ if (decoder)
116
+ complete();
117
+ else
118
+ queue.push({ kind: "complete" });
119
+ },
120
+ });
121
+ // start loading without awaiting, so the source subscription above happens synchronously
122
+ import("@gandlaf21/bc-ur/dist/lib/es6/index.js").then(({ URDecoder }) => {
123
+ if (cancelled || settled)
124
+ return;
125
+ try {
126
+ decoder = new URDecoder();
127
+ // drain buffered fragments/terminal signal in original arrival order
128
+ while (queue.length > 0 && !settled)
129
+ drain(queue.shift());
51
130
  }
52
- else {
53
- // emit progress
54
- const progress = decoder.estimatedPercentComplete();
55
- observer.next(progress);
131
+ catch (error) {
132
+ // a throw here (e.g. a module-shape mismatch) would otherwise escape as an
133
+ // unhandled rejection, since promise.then(onFulfilled) never forwards its own throw
134
+ fail(error);
56
135
  }
136
+ }, (error) => {
137
+ if (cancelled || settled)
138
+ return;
139
+ fail(error);
57
140
  });
141
+ return () => {
142
+ cancelled = true;
143
+ queue.length = 0;
144
+ sourceSubscription.unsubscribe();
145
+ };
58
146
  });
59
147
  }
60
148
  /** Creates an observable that completes with decoded token */
@@ -67,5 +155,5 @@ export function receiveAnimated(input) {
67
155
  // decode UR and complete
68
156
  urDecoder(),
69
157
  // only run one decoder
70
- shareReplay(1));
158
+ shareReplay({ bufferSize: 1, refCount: true }));
71
159
  }
@@ -24,10 +24,11 @@ export declare function getProofP2PKPubkey(proof: ProofLike): string | undefined
24
24
  /** Internal method to filter out duplicate proofs */
25
25
  export declare function ignoreDuplicateProofs(seen?: Set<string>): (proof: ProofLike) => boolean;
26
26
  /**
27
- * Returns a decoded cashu token inside an unicode emoji
27
+ * Returns a decoded cashu token inside a Unicode emoji
28
+ * @param keysetIds Full mint keyset IDs used to resolve shortened v2 IDs
28
29
  * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
29
30
  */
30
- export declare function decodeTokenFromEmojiString(str: string): Token | undefined;
31
+ export declare function decodeTokenFromEmojiString(str: string, keysetIds?: string[]): Token | undefined;
31
32
  /**
32
33
  * Encodes a token into an emoji char
33
34
  * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/SendTokenDialog.vue#L710
@@ -42,10 +42,11 @@ export function ignoreDuplicateProofs(seen = new Set()) {
42
42
  };
43
43
  }
44
44
  /**
45
- * Returns a decoded cashu token inside an unicode emoji
45
+ * Returns a decoded cashu token inside a Unicode emoji
46
+ * @param keysetIds Full mint keyset IDs used to resolve shortened v2 IDs
46
47
  * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
47
48
  */
48
- export function decodeTokenFromEmojiString(str) {
49
+ export function decodeTokenFromEmojiString(str, keysetIds = []) {
49
50
  try {
50
51
  let decoded = [];
51
52
  const chars = Array.from(str);
@@ -83,8 +84,7 @@ export function decodeTokenFromEmojiString(str) {
83
84
  }
84
85
  // Switch out token if we found peanut data
85
86
  let decodedString = decoded.join("");
86
- // Pass an empty keyset list — works for tokens with standard hex keyset ids
87
- return getDecodedToken(decodedString, []);
87
+ return getDecodedToken(decodedString, keysetIds);
88
88
  }
89
89
  catch (error) {
90
90
  return undefined;
@@ -1,4 +1,4 @@
1
- import { getHiddenTags, hasHiddenTags, isETag, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
1
+ import { getHiddenTags, hasHiddenTags, isETag, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, setCachedValue, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
2
2
  export const WALLET_HISTORY_KIND = 7376;
3
3
  /** Checks if an event is a valid wallet history event */
4
4
  export function isValidWalletHistory(event) {
@@ -13,12 +13,11 @@ export function getHistoryRedeemed(history) {
13
13
  }
14
14
  /** Checks if the history contents are locked */
15
15
  export function isHistoryContentUnlocked(history) {
16
- // Wrap in try catch to avoid throwing validation errors
17
- try {
18
- return (HistoryContentSymbol in history || (isHiddenTagsUnlocked(history) && getHistoryContent(history) !== undefined));
19
- }
20
- catch { }
21
- return false;
16
+ // No try/catch needed: `history` is typed so the `in` test cannot throw, getHiddenTags stopped
17
+ // throwing on malformed content, and getHistoryContent now returns undefined rather than
18
+ // raising. The defensive catch this used to carry was masking those throws and silently
19
+ // reporting a malformed history event as merely locked.
20
+ return HistoryContentSymbol in history || (isHiddenTagsUnlocked(history) && getHistoryContent(history) !== undefined);
22
21
  }
23
22
  export function getHistoryContent(history) {
24
23
  // Return cached value if it exists
@@ -28,23 +27,26 @@ export function getHistoryContent(history) {
28
27
  const tags = getHiddenTags(history);
29
28
  if (!tags)
30
29
  return;
31
- // Read tags
30
+ // Read tags. A history event missing its required tags, or carrying an unparseable amount, is
31
+ // unusable — return undefined rather than throwing, consistent with the locked case above and
32
+ // with what this getter's own return type already promises. The rejection is not cached, so the
33
+ // event stays readable if correct tags are decrypted in later.
32
34
  const direction = tags.find((t) => t[0] === "direction")?.[1];
33
35
  if (!direction)
34
- throw new Error("History event missing direction");
36
+ return undefined;
35
37
  const amountStr = tags.find((t) => t[0] === "amount")?.[1];
36
38
  if (!amountStr)
37
- throw new Error("History event missing amount");
39
+ return undefined;
38
40
  const amount = parseInt(amountStr);
39
41
  if (!Number.isFinite(amount))
40
- throw new Error("Failed to parse amount");
42
+ return undefined;
41
43
  const mint = tags.find((t) => t[0] === "mint")?.[1];
42
44
  const feeStr = tags.find((t) => t[0] === "fee")?.[1];
43
45
  const fee = feeStr ? parseInt(feeStr) : undefined;
44
46
  const created = tags.filter((t) => isETag(t) && t[3] === "created").map((t) => t[1]);
45
47
  const content = { direction, amount, created, mint, fee };
46
- // Set the cached value
47
- Reflect.set(history, HistoryContentSymbol, content);
48
+ // Set the cached value (identity memo, non-enumerable so a spread drops it)
49
+ setCachedValue(history, HistoryContentSymbol, content);
48
50
  return content;
49
51
  }
50
52
  /** Decrypts a wallet history event */
@@ -57,10 +57,12 @@ export class IndexedDBCouch {
57
57
  const db = await this.init();
58
58
  const id = this.generateId();
59
59
  const encodedToken = getEncodedToken(token);
60
+ // Token encoding shortens v2 IDs; keep full IDs for offline recovery.
61
+ const keysetIds = [...new Set(token.proofs.map((proof) => proof.id))];
60
62
  return new Promise((resolve, reject) => {
61
63
  const transaction = db.transaction([this.storeName], "readwrite");
62
64
  const store = transaction.objectStore(this.storeName);
63
- const request = store.add({ id, encodedToken });
65
+ const request = store.add({ id, encodedToken, keysetIds });
64
66
  request.onsuccess = () => {
65
67
  // Return a function to remove this specific token by ID
66
68
  resolve(async () => {
@@ -98,7 +100,7 @@ export class IndexedDBCouch {
98
100
  return stored
99
101
  .map((item) => {
100
102
  try {
101
- return getDecodedToken(item.encodedToken, []);
103
+ return getDecodedToken(item.encodedToken, item.keysetIds ?? []);
102
104
  }
103
105
  catch {
104
106
  return null;
@@ -22,8 +22,10 @@ export class LocalStorageCouch {
22
22
  store(token) {
23
23
  const id = this.generateId();
24
24
  const encodedToken = getEncodedToken(token);
25
+ // Token encoding shortens v2 IDs; keep full IDs for offline recovery.
26
+ const keysetIds = [...new Set(token.proofs.map((proof) => proof.id))];
25
27
  const stored = this.getStoredTokens();
26
- stored.push({ id, encodedToken });
28
+ stored.push({ id, encodedToken, keysetIds });
27
29
  this.saveStoredTokens(stored);
28
30
  // Return a function to remove this specific token by ID
29
31
  return () => {
@@ -47,7 +49,7 @@ export class LocalStorageCouch {
47
49
  return this.getStoredTokens()
48
50
  .map((item) => {
49
51
  try {
50
- return getDecodedToken(item.encodedToken, []);
52
+ return getDecodedToken(item.encodedToken, item.keysetIds ?? []);
51
53
  }
52
54
  catch {
53
55
  return null;
@@ -34,15 +34,19 @@ export declare function isNutzapRedeemed(nutzapId: string, history: NostrEvent[]
34
34
  /**
35
35
  * Extracts the P2PK locking pubkey from proofs in a nutzap event
36
36
  * @param nutzap the nutzap event containing P2PK-locked proofs
37
- * @returns the pubkey that the proofs are locked to, or undefined if not found
38
- * @throws {Error} if proofs are not P2PK locked or have inconsistent pubkeys
37
+ * @returns the single pubkey every proof is locked to, or undefined if there is no such pubkey —
38
+ * no proofs, a proof that is not P2PK locked, or proofs locked to more than one pubkey. A nutzap
39
+ * arrives from a stranger, so all three are hostile input rather than programmer errors, and none
40
+ * of them throw. This matches getProofP2PKPubkey one level down, which already returns undefined
41
+ * for every failure.
39
42
  */
40
43
  export declare function getNutzapP2PKPubkey(nutzap: NostrEvent): string | undefined;
41
44
  /**
42
45
  * Finds the matching private key for the P2PK lock in a nutzap event's proofs
43
46
  * @param nutzap the nutzap event containing P2PK-locked proofs
44
47
  * @param privateKeys array of private keys to search through
45
- * @returns the matching private key, or undefined if none match
46
- * @throws {Error} if proofs are not P2PK locked or have inconsistent pubkeys
48
+ * @returns the matching private key, or undefined if none match — including when the nutzap has no
49
+ * single P2PK pubkey to match against, which getNutzapP2PKPubkey now reports as undefined rather
50
+ * than by throwing
47
51
  */
48
52
  export declare function findMatchingPrivateKeyForNutzap(nutzap: NostrEvent, privateKeys: Uint8Array[]): Uint8Array | undefined;
@@ -70,8 +70,11 @@ export function isNutzapRedeemed(nutzapId, history) {
70
70
  /**
71
71
  * Extracts the P2PK locking pubkey from proofs in a nutzap event
72
72
  * @param nutzap the nutzap event containing P2PK-locked proofs
73
- * @returns the pubkey that the proofs are locked to, or undefined if not found
74
- * @throws {Error} if proofs are not P2PK locked or have inconsistent pubkeys
73
+ * @returns the single pubkey every proof is locked to, or undefined if there is no such pubkey —
74
+ * no proofs, a proof that is not P2PK locked, or proofs locked to more than one pubkey. A nutzap
75
+ * arrives from a stranger, so all three are hostile input rather than programmer errors, and none
76
+ * of them throw. This matches getProofP2PKPubkey one level down, which already returns undefined
77
+ * for every failure.
75
78
  */
76
79
  export function getNutzapP2PKPubkey(nutzap) {
77
80
  const proofs = getNutzapProofs(nutzap);
@@ -81,12 +84,14 @@ export function getNutzapP2PKPubkey(nutzap) {
81
84
  for (const proof of proofs) {
82
85
  const proofPubkey = getProofP2PKPubkey(proof);
83
86
  if (!proofPubkey)
84
- throw new Error("Proof is not P2PK locked");
87
+ return undefined;
85
88
  if (!p2pkPubkey) {
86
89
  p2pkPubkey = proofPubkey;
87
90
  }
88
91
  else if (p2pkPubkey !== proofPubkey) {
89
- throw new Error("Proofs are locked to different pubkeys");
92
+ // Mixed locks mean there is no single pubkey to report, so the answer is undefined rather
93
+ // than the first one seen — returning p2pkPubkey here would silently hide the mismatch.
94
+ return undefined;
90
95
  }
91
96
  }
92
97
  return p2pkPubkey;
@@ -95,8 +100,9 @@ export function getNutzapP2PKPubkey(nutzap) {
95
100
  * Finds the matching private key for the P2PK lock in a nutzap event's proofs
96
101
  * @param nutzap the nutzap event containing P2PK-locked proofs
97
102
  * @param privateKeys array of private keys to search through
98
- * @returns the matching private key, or undefined if none match
99
- * @throws {Error} if proofs are not P2PK locked or have inconsistent pubkeys
103
+ * @returns the matching private key, or undefined if none match — including when the nutzap has no
104
+ * single P2PK pubkey to match against, which getNutzapP2PKPubkey now reports as undefined rather
105
+ * than by throwing
100
106
  */
101
107
  export function findMatchingPrivateKeyForNutzap(nutzap, privateKeys) {
102
108
  const p2pkPubkey = getNutzapP2PKPubkey(nutzap);
@@ -38,7 +38,12 @@ export type UnlockedTokenContent = UnlockedHiddenContent & {
38
38
  export declare function isTokenContentUnlocked<T extends NostrEvent>(token: T): token is T & UnlockedTokenContent;
39
39
  /**
40
40
  * Returns the decrypted and parsed details of a 7375 token event
41
- * @throws {Error} If the token content is invalid
41
+ *
42
+ * Returns undefined for anything that does not yield usable content — still locked, not valid
43
+ * JSON, not an object, or missing `mint`/`proofs`. None of these throw: this getter is read from
44
+ * inside RxJS pipes (see WalletToken's `meta$`), where a throw errors the observable and is
45
+ * terminal, killing the wallet's token stream rather than skipping one malformed event.
46
+ * `unlockTokenContent` is the imperative counterpart that does throw.
42
47
  */
43
48
  export declare function getTokenContent(token: UnlockedTokenContent): TokenContent;
44
49
  export declare function getTokenContent(token: NostrEvent): TokenContent | undefined;
@@ -1,4 +1,4 @@
1
- import { getHiddenContent, isHiddenContentUnlocked, lockHiddenContent, notifyEventUpdate, setHiddenContentEncryptionMethod, unlockHiddenContent, } from "applesauce-core/helpers";
1
+ import { getHiddenContent, isHiddenContentUnlocked, lockHiddenContent, notifyEventUpdate, safeParse, setCachedValue, setHiddenContentEncryptionMethod, unlockHiddenContent, } from "applesauce-core/helpers";
2
2
  import { ignoreDuplicateProofs } from "./cashu.js";
3
3
  export const WALLET_TOKEN_KIND = 7375;
4
4
  /** Checks if an event is a valid wallet token event */
@@ -21,12 +21,11 @@ setHiddenContentEncryptionMethod(WALLET_TOKEN_KIND, "nip44");
21
21
  export const TokenContentSymbol = Symbol.for("token-content");
22
22
  /** Returns if token details are locked */
23
23
  export function isTokenContentUnlocked(token) {
24
- // Wrap in try catch to avoid throwing validation errors
25
- try {
26
- return TokenContentSymbol in token || (isHiddenContentUnlocked(token) && getTokenContent(token) !== undefined);
27
- }
28
- catch { }
29
- return false;
24
+ // No try/catch needed: `token` is typed so the `in` test cannot throw, and neither
25
+ // isHiddenContentUnlocked nor getTokenContent throws — getTokenContent returns undefined for
26
+ // malformed content rather than raising. The defensive catch this used to carry was masking
27
+ // that throw and silently reporting a malformed token as merely locked.
28
+ return TokenContentSymbol in token || (isHiddenContentUnlocked(token) && getTokenContent(token) !== undefined);
30
29
  }
31
30
  export function getTokenContent(token) {
32
31
  if (TokenContentSymbol in token)
@@ -35,17 +34,24 @@ export function getTokenContent(token) {
35
34
  const plaintext = getHiddenContent(token);
36
35
  if (!plaintext)
37
36
  return undefined;
38
- // Parse the content as a token content
39
- const details = JSON.parse(plaintext);
40
- // Throw an error if the token content is invalid
37
+ // Parse the content as a token content. safeParse (not JSON.parse) so malformed content
38
+ // returns undefined instead of throwing out of a getter.
39
+ const details = safeParse(plaintext);
40
+ // Shape guard BEFORE any property read: safeParse("null") yields null and `null.mint` throws a
41
+ // TypeError, which would just swap one throw for another.
42
+ if (!details || typeof details !== "object")
43
+ return undefined;
44
+ // Content missing its required fields is unusable. Returning undefined (rather than throwing)
45
+ // keeps this consistent with the locked and unparseable cases above, and the rejection is
46
+ // deliberately not cached — a token stays readable if correct content is decrypted in later.
41
47
  if (!details.mint)
42
- throw new Error("Token missing mint");
48
+ return undefined;
43
49
  if (!details.proofs)
44
- throw new Error("Token missing proofs");
50
+ return undefined;
45
51
  if (!details.del)
46
52
  details.del = [];
47
- // Set the cached value
48
- Reflect.set(token, TokenContentSymbol, details);
53
+ // Set the cached value (identity memo, non-enumerable so a spread drops it)
54
+ setCachedValue(token, TokenContentSymbol, details);
49
55
  return details;
50
56
  }
51
57
  /** Decrypts a k:7375 token event */
@@ -1,4 +1,4 @@
1
- import { getHiddenTags, hasHiddenTags, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, relaySet, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
1
+ import { getHiddenTags, hasHiddenTags, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, relaySet, setCachedValue, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
2
2
  import { hexToBytes } from "applesauce-core/helpers/event";
3
3
  export const WALLET_KIND = 17375;
4
4
  export const WALLET_BACKUP_KIND = 375;
@@ -27,8 +27,8 @@ export function getWalletMints(wallet) {
27
27
  return undefined;
28
28
  // Get mints
29
29
  const mints = tags.filter((t) => t[0] === "mint").map((t) => t[1]);
30
- // Set the cached value
31
- Reflect.set(wallet, WalletMintsSymbol, mints);
30
+ // Set the cached value (identity memo, non-enumerable so a spread drops it)
31
+ setCachedValue(wallet, WalletMintsSymbol, mints);
32
32
  return mints;
33
33
  }
34
34
  export function getWalletPrivateKey(wallet) {
@@ -41,8 +41,11 @@ export function getWalletPrivateKey(wallet) {
41
41
  // Parse private key
42
42
  const privkey = tags.find((t) => t[0] === "privkey" && t[1])?.[1];
43
43
  const key = privkey ? hexToBytes(privkey) : null;
44
- // Set the cached value
45
- Reflect.set(wallet, WalletPrivateKeySymbol, key);
44
+ // Set the cached value (identity memo, non-enumerable so a spread drops it). This is a V8
45
+ // improvement: the decrypted private key no longer rides along on an unrelated plain-object
46
+ // spread of the wallet event. The key itself is still held in memory for this wallet
47
+ // instance's lifetime -- no lock primitive is added here, that stays out of this phase's scope.
48
+ setCachedValue(wallet, WalletPrivateKeySymbol, key);
46
49
  return key;
47
50
  }
48
51
  export function getWalletRelays(wallet) {
@@ -56,8 +59,8 @@ export function getWalletRelays(wallet) {
56
59
  // Get relays
57
60
  const urls = tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]);
58
61
  const relays = relaySet(urls);
59
- // Set the cached value
60
- Reflect.set(wallet, WalletRelaysSymbol, relays);
62
+ // Set the cached value (identity memo, non-enumerable so a spread drops it)
63
+ setCachedValue(wallet, WalletRelaysSymbol, relays);
61
64
  return relays;
62
65
  }
63
66
  /** Unlocks a wallet and returns the hidden tags */
@@ -86,5 +89,6 @@ export async function unlockWallet(wallet, signer) {
86
89
  export function lockWallet(wallet) {
87
90
  Reflect.deleteProperty(wallet, WalletPrivateKeySymbol);
88
91
  Reflect.deleteProperty(wallet, WalletMintsSymbol);
92
+ Reflect.deleteProperty(wallet, WalletRelaysSymbol);
89
93
  lockHiddenTags(wallet);
90
94
  }
@@ -1,6 +1,5 @@
1
- import type { EventStore } from "applesauce-core";
1
+ import type { Debugger, EventStore } from "applesauce-core";
2
2
  import { RelayPool } from "applesauce-relay";
3
- import type { Debugger } from "debug";
4
3
  import { Observable } from "rxjs";
5
4
  /** A status update emitted while loading a wallet's events */
6
5
  export type WalletLoaderStatus = {
@@ -1,14 +1,13 @@
1
1
  import { type MeltProofsResponse, type MeltQuoteBaseResponse, Mint, type MintQuoteBolt11Response, type Proof, Token, Wallet as CashuWallet } from "@cashu/cashu-ts";
2
2
  import { ActionRunner } from "applesauce-actions";
3
- import { ChainableObservable, EventStore } from "applesauce-core";
3
+ import { ChainableObservable, EventStore, type Debugger } from "applesauce-core";
4
4
  import { User } from "applesauce-core/casts";
5
5
  import type { NostrEvent } from "applesauce-core/helpers";
6
6
  import { type EncryptedContentCache } from "applesauce-common/helpers";
7
7
  import type { RelayPool } from "applesauce-relay";
8
8
  import type { ISigner } from "applesauce-signers";
9
- import type { Debugger } from "debug";
10
9
  import { BehaviorSubject, Observable, Subscription } from "rxjs";
11
- import "../casts/__register__";
10
+ import "../casts/__register__.js";
12
11
  import type { WalletHistory } from "../casts/wallet-history.js";
13
12
  import type { WalletToken } from "../casts/wallet-token.js";
14
13
  import type { Wallet } from "../casts/wallet.js";
@@ -1,13 +1,13 @@
1
- import { getDecodedToken, getEncodedToken, Mint, MintQuoteState, Wallet as CashuWallet, } from "@cashu/cashu-ts";
1
+ import { getTokenMetadata, getEncodedToken, Mint, MintQuoteState, Wallet as CashuWallet, } from "@cashu/cashu-ts";
2
2
  import { ActionRunner } from "applesauce-actions";
3
- import { logger as baseLogger, chainable } from "applesauce-core";
3
+ import { logger as baseLogger, chainable, } from "applesauce-core";
4
4
  import { castUser } from "applesauce-core/casts";
5
5
  import { addSeenRelay, canHaveEncryptedContent, getEncryptedContent, getOutboxes, getSeenRelays, isEncryptedContentUnlocked, kinds, normalizeURL, notifyEventUpdate, relaySet, setEncryptedContentCache, } from "applesauce-core/helpers";
6
6
  import { isEncryptedContentFromCache, markEncryptedContentFromCache, } from "applesauce-common/helpers";
7
7
  import { BehaviorSubject, combineLatest, isObservable, of } from "rxjs";
8
8
  import { distinctUntilChanged, map, shareReplay, startWith, switchMap } from "rxjs/operators";
9
9
  // Ensure the User.wallet$ is setup
10
- import "../casts/__register__";
10
+ import "../casts/__register__.js";
11
11
  import { CleanupDeletedTokens, ConsolidateTokens, CreateWallet, MintTokens, ReceiveToken, ReceiveNutzaps, RecoverFromCouch, RolloverTokens, SetWalletMints, SetWalletRelays, TokensOperation, UnlockWallet, } from "../actions/index.js";
12
12
  import { lockHistoryContent, WALLET_HISTORY_KIND } from "../helpers/history.js";
13
13
  import { lockTokenContent, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
@@ -446,11 +446,12 @@ export class NutWallet {
446
446
  /** Receives a Cashu token (encoded string or decoded {@link Token}) into the wallet */
447
447
  async receiveToken(token) {
448
448
  await this.track("receive", async () => {
449
- const decoded = typeof token === "string" ? getDecodedToken(token, []) : token;
450
- if (!decoded)
451
- throw new Error("Failed to decode token");
449
+ const metadata = typeof token === "string" ? getTokenMetadata(token) : token;
450
+ if (metadata.unit && metadata.unit !== "sat")
451
+ throw new Error("Only sat tokens are supported");
452
+ const wallet = await this.getCashuWallet(metadata.mint);
453
+ const decoded = typeof token === "string" ? wallet.decodeToken(token) : token;
452
454
  this.log("Receiving token from mint %s", decoded.mint);
453
- const wallet = await this.getCashuWallet(decoded.mint);
454
455
  await this.actions.run(ReceiveToken, decoded, { couch: this.couch, wallet, getCashuWallet: this.getCashuWallet });
455
456
  await this.refreshCouch();
456
457
  });
@@ -1,9 +1,8 @@
1
1
  import type { MeltProofsResponse, MeltQuoteBolt11Response, MintQuoteBolt11Response } from "@cashu/cashu-ts";
2
- import type { EventStore } from "applesauce-core";
2
+ import type { Debugger, EventStore } from "applesauce-core";
3
3
  import type { EncryptedContentCache } from "applesauce-common/helpers";
4
4
  import type { ISigner } from "applesauce-signers";
5
5
  import type { RelayPool } from "applesauce-relay";
6
- import type { Debugger } from "debug";
7
6
  import type { Observable } from "rxjs";
8
7
  import type { WalletToken } from "../casts/wallet-token.js";
9
8
  import type { Couch } from "../helpers/couch.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-wallet",
3
- "version": "6.2.0",
3
+ "version": "7.0.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -87,27 +87,25 @@
87
87
  }
88
88
  },
89
89
  "dependencies": {
90
- "@cashu/cashu-ts": "^4.5.1",
90
+ "@cashu/cashu-ts": "^4.11.0",
91
91
  "@gandlaf21/bc-ur": "^1.1.12",
92
- "applesauce-actions": "^6.2.0",
93
- "applesauce-common": "^6.2.0",
94
- "applesauce-core": "^6.2.0",
95
- "applesauce-loaders": "^6.2.0",
96
- "debug": "^4.4.3",
92
+ "applesauce-actions": "^7.0.0",
93
+ "applesauce-common": "^7.0.0",
94
+ "applesauce-core": "^7.0.0",
95
+ "applesauce-loaders": "^7.0.0",
97
96
  "rxjs": "^7.8.2"
98
97
  },
99
98
  "peerDependencies": {
100
- "applesauce-relay": "^6.0.3",
101
- "applesauce-signers": "^6.0.0"
99
+ "applesauce-relay": "^7.0.0",
100
+ "applesauce-signers": "^7.0.0"
102
101
  },
103
102
  "devDependencies": {
104
103
  "@hirez_io/observer-spy": "^2.2.0",
105
- "@types/debug": "^4.1.13",
106
- "applesauce-relay": "^6.2.0",
107
- "applesauce-signers": "^6.2.0",
104
+ "applesauce-relay": "^7.0.0",
105
+ "applesauce-signers": "^7.0.0",
108
106
  "rimraf": "^6.1.3",
109
- "typescript": "^5.9.3",
110
- "vitest": "^4.1.8"
107
+ "typescript": "^7.0.2",
108
+ "vitest": "^4.1.11"
111
109
  },
112
110
  "funding": {
113
111
  "type": "lightning",