applesauce-wallet 0.11.0 → 0.12.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.
Files changed (68) hide show
  1. package/README.md +3 -0
  2. package/dist/__tests__/fake-user.d.ts +10 -0
  3. package/dist/__tests__/fake-user.js +31 -0
  4. package/dist/actions/__tests__/tokens.test.d.ts +1 -0
  5. package/dist/actions/__tests__/tokens.test.js +139 -0
  6. package/dist/actions/__tests__/wallet.test.d.ts +1 -0
  7. package/dist/actions/__tests__/wallet.test.js +56 -0
  8. package/dist/actions/index.d.ts +2 -0
  9. package/dist/actions/index.js +2 -0
  10. package/dist/actions/tokens.d.ts +17 -0
  11. package/dist/actions/tokens.js +110 -0
  12. package/dist/actions/wallet.d.ts +13 -0
  13. package/dist/actions/wallet.js +64 -0
  14. package/dist/blueprints/history.d.ts +5 -0
  15. package/dist/blueprints/history.js +11 -0
  16. package/dist/blueprints/index.d.ts +2 -0
  17. package/dist/blueprints/index.js +2 -0
  18. package/dist/blueprints/tokens.d.ts +8 -0
  19. package/dist/blueprints/tokens.js +11 -0
  20. package/dist/blueprints/wallet.d.ts +6 -0
  21. package/dist/blueprints/wallet.js +13 -0
  22. package/dist/helpers/__tests__/animated-qr.test.d.ts +1 -0
  23. package/dist/helpers/__tests__/animated-qr.test.js +44 -0
  24. package/dist/helpers/__tests__/tokens.test.d.ts +1 -0
  25. package/dist/helpers/__tests__/tokens.test.js +127 -0
  26. package/dist/helpers/animated-qr.d.ts +30 -0
  27. package/dist/helpers/animated-qr.js +71 -0
  28. package/dist/helpers/history.d.ts +26 -0
  29. package/dist/helpers/history.js +45 -0
  30. package/dist/helpers/index.d.ts +3 -0
  31. package/dist/helpers/index.js +3 -0
  32. package/dist/helpers/tokens.d.ts +58 -0
  33. package/dist/helpers/tokens.js +160 -0
  34. package/dist/helpers/wallet.d.ts +3 -1
  35. package/dist/helpers/wallet.js +9 -4
  36. package/dist/index.d.ts +4 -1
  37. package/dist/index.js +4 -1
  38. package/dist/operations/event/__tests__/wallet.test.d.ts +1 -0
  39. package/dist/operations/event/__tests__/wallet.test.js +25 -0
  40. package/dist/operations/event/history.d.ts +7 -0
  41. package/dist/operations/event/history.js +19 -0
  42. package/dist/operations/event/index.d.ts +3 -0
  43. package/dist/operations/event/index.js +3 -0
  44. package/dist/operations/event/tokens.d.ts +4 -0
  45. package/dist/operations/event/tokens.js +24 -0
  46. package/dist/operations/event/wallet.d.ts +4 -0
  47. package/dist/operations/event/wallet.js +14 -0
  48. package/dist/operations/index.d.ts +2 -0
  49. package/dist/operations/index.js +2 -0
  50. package/dist/operations/tag/__tests__/wallet.test.d.ts +1 -0
  51. package/dist/operations/tag/__tests__/wallet.test.js +16 -0
  52. package/dist/operations/tag/history.d.ts +14 -0
  53. package/dist/operations/tag/history.js +34 -0
  54. package/dist/operations/tag/index.d.ts +2 -0
  55. package/dist/operations/tag/index.js +2 -0
  56. package/dist/operations/tag/wallet.d.ts +5 -0
  57. package/dist/operations/tag/wallet.js +15 -0
  58. package/dist/queries/__tests__/wallet.test.d.ts +1 -0
  59. package/dist/queries/__tests__/wallet.test.js +30 -0
  60. package/dist/queries/history.d.ts +6 -0
  61. package/dist/queries/history.js +27 -0
  62. package/dist/queries/index.d.ts +2 -0
  63. package/dist/queries/index.js +2 -0
  64. package/dist/queries/tokens.d.ts +6 -0
  65. package/dist/queries/tokens.js +64 -0
  66. package/dist/queries/wallet.d.ts +4 -1
  67. package/dist/queries/wallet.js +3 -3
  68. package/package.json +43 -2
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { EventFactory } from "applesauce-factory";
3
+ import { FakeUser } from "../../__tests__/fake-user.js";
4
+ import { WalletTokenBlueprint } from "../../blueprints/tokens.js";
5
+ import { decodeTokenFromEmojiString, dumbTokenSelection, encodeTokenToEmoji, unlockTokenContent } from "../tokens.js";
6
+ import { HiddenContentSymbol, unixNow } from "applesauce-core/helpers";
7
+ const user = new FakeUser();
8
+ const factory = new EventFactory({ signer: user });
9
+ describe("dumbTokenSelection", () => {
10
+ it("should select old tokens first", async () => {
11
+ const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
12
+ mint: "https://money.com",
13
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
14
+ }));
15
+ await unlockTokenContent(a, user);
16
+ const bDraft = await factory.create(WalletTokenBlueprint, {
17
+ mint: "https://money.com",
18
+ proofs: [{ secret: "B", C: "B", id: "B", amount: 50 }],
19
+ });
20
+ bDraft.created_at -= 60 * 60 * 7;
21
+ const b = await user.signEvent(bDraft);
22
+ await unlockTokenContent(b, user);
23
+ expect(dumbTokenSelection([a, b], 40).events).toEqual([b]);
24
+ });
25
+ it("should select enough tokens to total min amount", async () => {
26
+ const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
27
+ mint: "https://money.com",
28
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
29
+ }));
30
+ await unlockTokenContent(a, user);
31
+ const bDraft = await factory.create(WalletTokenBlueprint, {
32
+ mint: "https://money.com",
33
+ proofs: [{ secret: "B", C: "B", id: "B", amount: 50 }],
34
+ });
35
+ bDraft.created_at -= 60 * 60 * 7;
36
+ const b = await user.signEvent(bDraft);
37
+ await unlockTokenContent(b, user);
38
+ expect(dumbTokenSelection([a, b], 120).events).toEqual(expect.arrayContaining([a, b]));
39
+ });
40
+ it("should throw if not enough funds", async () => {
41
+ const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
42
+ mint: "https://money.com",
43
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
44
+ }));
45
+ await unlockTokenContent(a, user);
46
+ expect(() => dumbTokenSelection([a], 120)).toThrow();
47
+ });
48
+ it("should ignore locked tokens", async () => {
49
+ const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
50
+ mint: "https://money.com",
51
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
52
+ }));
53
+ await unlockTokenContent(a, user);
54
+ const bDraft = await factory.create(WalletTokenBlueprint, {
55
+ mint: "https://money.com",
56
+ proofs: [{ secret: "B", C: "B", id: "B", amount: 50 }],
57
+ });
58
+ bDraft.created_at -= 60 * 60 * 7;
59
+ const b = await user.signEvent(bDraft);
60
+ // manually remove the hidden content to lock it again
61
+ Reflect.deleteProperty(b, HiddenContentSymbol);
62
+ expect(dumbTokenSelection([a, b], 20).events).toEqual([a]);
63
+ });
64
+ it("should ignore duplicate proofs", async () => {
65
+ const a = await user.signEvent(await factory.create(WalletTokenBlueprint, {
66
+ mint: "https://money.com",
67
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
68
+ }));
69
+ // create a second event with the same proofs
70
+ const b = await user.signEvent(await factory.create(WalletTokenBlueprint, {
71
+ mint: "https://money.com",
72
+ proofs: [{ secret: "A", C: "A", id: "A", amount: 100 }],
73
+ }));
74
+ expect(() => dumbTokenSelection([a, b], 150)).toThrow();
75
+ });
76
+ it("should include duplicate token events and ignore duplicate proofs", async () => {
77
+ const A = { secret: "A", C: "A", id: "A", amount: 100 };
78
+ const a = await user.signEvent({
79
+ ...(await factory.create(WalletTokenBlueprint, {
80
+ mint: "https://money.com",
81
+ proofs: [A],
82
+ })),
83
+ // make event older
84
+ created_at: unixNow() - 100,
85
+ });
86
+ // create a second event with the same proofs
87
+ const a2 = await user.signEvent({
88
+ ...(await factory.create(WalletTokenBlueprint, {
89
+ mint: "https://money.com",
90
+ proofs: [A],
91
+ })),
92
+ // make event older
93
+ created_at: a.created_at - 200,
94
+ });
95
+ const B = { secret: "B", C: "B", id: "B", amount: 50 };
96
+ const b = await user.signEvent(await factory.create(WalletTokenBlueprint, {
97
+ mint: "https://money.com",
98
+ proofs: [B],
99
+ }));
100
+ const result = dumbTokenSelection([a, a2, b], 150);
101
+ expect(result.events.map((e) => e.id)).toEqual(expect.arrayContaining([a.id, a2.id, b.id]));
102
+ expect(result.proofs).toEqual([A, B]);
103
+ });
104
+ });
105
+ describe("encodeTokenToEmoji", () => {
106
+ it("should encode token into emoji string", () => {
107
+ const token = "cashuBo2FteBtodHRwczovL3Rlc3RudXQuY2FzaHUuc3BhY2VhdWNzYXRhdIGiYWlIAJofKTJT5B5hcIGkYWEBYXN4QDdlZDBkMzk3NGQ5ZWM2OTc2YTAzYmZmYjdkMTA4NzIzZTBiMDRjMzRhNDc3MjlmNjMwOGJlODc3OTA2NTY0NDVhY1ghA36iYyOHCe4CnTxzORbcXFVeAbkMUFE6FqPWInujnAOcYWSjYWVYIJmHRwCQ0Uopkd3P5xb0MdcWQEaZz9hXWtcn-FMhZj8LYXNYIF4X9ybXxg5Pp0KSowfu4y_Aovo9iy3TXlLSaKyVJzz2YXJYIC_UFkoC5U9BpSgBTGUQgsjfz_emv5xykDiavZUfRN8E";
108
+ expect(encodeTokenToEmoji(token).length).toBeGreaterThan(token.length);
109
+ });
110
+ });
111
+ const emoji = "🥜󠅓󠅑󠅣󠅘󠅥󠄲󠅟󠄢󠄶󠅤󠅕󠄲󠅤󠅟󠅔󠄸󠅂󠅧󠅓󠅪󠅟󠅦󠄼󠄣󠅂󠅜󠅓󠄣󠅂󠅥󠅔󠅈󠅁󠅥󠅉󠄢󠄶󠅪󠅑󠄸󠅅󠅥󠅓󠄣󠄲󠅘󠅉󠄢󠅆󠅘󠅔󠅇󠄾󠅪󠅉󠅈󠅂󠅘󠅔󠄹󠄷󠅙󠅉󠅇󠅜󠄹󠄱󠄺󠅟󠅖󠄻󠅄󠄺󠅄󠄥󠄲󠄥󠅘󠅓󠄹󠄷󠅛󠅉󠅇󠄵󠄲󠅉󠅈󠄾󠄤󠅁󠄴󠅔󠅜󠅊󠄴󠄲󠅛󠄽󠅪󠅛󠄣󠄾󠄷󠅁󠄥󠅊󠅇󠄽󠄢󠄿󠅄󠅓󠄢󠅉󠅄󠄱󠅪󠅉󠅝󠅊󠅝󠅉󠅚󠅔󠅛󠄽󠅄󠄱󠄤󠄾󠅪󠄹󠅪󠅊󠅄󠄲󠅙󠄽󠄴󠅂󠅚󠄽󠅪󠅂󠅘󠄾󠄴󠅓󠄣󠄽󠅚󠅜󠅝󠄾󠅚󠄽󠅧󠄿󠄷󠄺󠅜󠄿󠄴󠅓󠄣󠄿󠅄󠄱󠄢󠄾󠅄󠅉󠄠󠄾󠄴󠅆󠅘󠅉󠄡󠅗󠅘󠄱󠄣󠄦󠅙󠅉󠅩󠄿󠄸󠄳󠅕󠄤󠄳󠅞󠅄󠅨󠅪󠄿󠅂󠅒󠅓󠅈󠄶󠅆󠅕󠄱󠅒󠅛󠄽󠅅󠄶󠄵󠄦󠄶󠅡󠅀󠅇󠄹󠅞󠅥󠅚󠅞󠄱󠄿󠅓󠅉󠅇󠅃󠅚󠅉󠅇󠅆󠅉󠄹󠄺󠅝󠄸󠅂󠅧󠄳󠅁󠄠󠅅󠅟󠅠󠅛󠅔󠄣󠅀󠄥󠅨󠅒󠄠󠄽󠅔󠅓󠅇󠅁󠄵󠅑󠅊󠅪󠄩󠅘󠅈󠅇󠅤󠅓󠅞󠄝󠄶󠄽󠅘󠅊󠅚󠄨󠄼󠅉󠅈󠄾󠅉󠄹󠄶󠄤󠅈󠄩󠅩󠅒󠅈󠅨󠅗󠄥󠅀󠅠󠄠󠄻󠅃󠅟󠅧󠅖󠅥󠄤󠅩󠅏󠄱󠅟󠅦󠅟󠄩󠅙󠅩󠄣󠅄󠅈󠅜󠄼󠅃󠅑󠄻󠅩󠅆󠄺󠅪󠅪󠄢󠅉󠅈󠄺󠅉󠄹󠄳󠅏󠅅󠄶󠅛󠅟󠄳󠄥󠅅󠄩󠄲󠅠󠅃󠅗󠄲󠅄󠄷󠅅󠅁󠅗󠅣󠅚󠅖󠅪󠅏󠅕󠅝󠅦󠄥󠅨󠅩󠅛󠄴󠅙󠅑󠅦󠅊󠅅󠅖󠅂󠄾󠄨󠄵";
112
+ describe("decodeTokenFromEmojiString", () => {
113
+ it("should decode single emoji", () => {
114
+ expect(decodeTokenFromEmojiString(emoji)).toEqual(expect.objectContaining({
115
+ mint: "https://testnut.cashu.space",
116
+ proofs: [expect.any(Object)],
117
+ unit: "sat",
118
+ }));
119
+ });
120
+ it("should decode an emoji in text", () => {
121
+ expect(decodeTokenFromEmojiString("the money is in the emoji, " + emoji + " you can redeem it using cashu.me")).toEqual(expect.objectContaining({
122
+ mint: "https://testnut.cashu.space",
123
+ proofs: [expect.any(Object)],
124
+ unit: "sat",
125
+ }));
126
+ });
127
+ });
@@ -0,0 +1,30 @@
1
+ import { Token } from "@cashu/cashu-ts";
2
+ import { Observable } from "rxjs";
3
+ /** Preset speeds for the animated qr code */
4
+ export declare const ANIMATED_QR_INTERVAL: {
5
+ SLOW: number;
6
+ MEDIUM: number;
7
+ FAST: number;
8
+ };
9
+ /** Presets for fragment length for animated qr code */
10
+ export declare const ANIMATED_QR_FRAGMENTS: {
11
+ SHORT: number;
12
+ MEDIUM: number;
13
+ LONG: number;
14
+ };
15
+ export type SendAnimatedOptions = {
16
+ /**
17
+ * The interval between the parts ( 150 - 500 )
18
+ * @default 150
19
+ */
20
+ interval?: number;
21
+ /**
22
+ * max fragment length ( 50 - 200 )
23
+ * @default 100
24
+ */
25
+ fragmentLength?: number;
26
+ };
27
+ /** Creates an observable that iterates through a multi-part animated qr code */
28
+ export declare function sendAnimated(token: Token | string, options?: SendAnimatedOptions): Observable<string>;
29
+ /** Creates an observable that completes with decoded token */
30
+ export declare function receiveAnimated(input: Observable<string>): Observable<string | number>;
@@ -0,0 +1,71 @@
1
+ import { getEncodedTokenV4 } 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";
4
+ /** Preset speeds for the animated qr code */
5
+ export const ANIMATED_QR_INTERVAL = {
6
+ SLOW: 500,
7
+ MEDIUM: 250,
8
+ FAST: 150,
9
+ };
10
+ /** Presets for fragment length for animated qr code */
11
+ export const ANIMATED_QR_FRAGMENTS = {
12
+ SHORT: 50,
13
+ MEDIUM: 100,
14
+ LONG: 150,
15
+ };
16
+ /** Creates an observable that iterates through a multi-part animated qr code */
17
+ export function sendAnimated(token, options) {
18
+ // start the stream as soon as there is subscriber
19
+ return defer(() => {
20
+ let str = typeof token === "string" ? token : getEncodedTokenV4(token);
21
+ let utf8 = new TextEncoder();
22
+ let buffer = utf8.encode(str);
23
+ let ur = UR.from(buffer);
24
+ let encoder = new UREncoder(ur, options?.fragmentLength ?? 100, 0);
25
+ return interval(options?.interval ?? ANIMATED_QR_INTERVAL.FAST).pipe(map(() => encoder.nextPart()));
26
+ });
27
+ }
28
+ /** An operator that decodes UR, emits progress percent and completes with final result or error */
29
+ function urDecoder() {
30
+ 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();
46
+ }
47
+ else if (decoder.isError()) {
48
+ // emit error
49
+ const reason = decoder.resultError();
50
+ observer.error(new Error(reason));
51
+ }
52
+ else {
53
+ // emit progress
54
+ const progress = decoder.estimatedPercentComplete();
55
+ observer.next(progress);
56
+ }
57
+ });
58
+ });
59
+ }
60
+ /** Creates an observable that completes with decoded token */
61
+ export function receiveAnimated(input) {
62
+ return input.pipe(
63
+ // convert to lower case
64
+ map((str) => str.toLowerCase()),
65
+ // filter out non UR parts
66
+ filter((str) => str.startsWith("ur:bytes")),
67
+ // decode UR and complete
68
+ urDecoder(),
69
+ // only run one decoder
70
+ shareReplay(1));
71
+ }
@@ -0,0 +1,26 @@
1
+ import { HiddenContentSigner } from "applesauce-core/helpers";
2
+ import { NostrEvent } from "nostr-tools";
3
+ export declare const WALLET_HISTORY_KIND = 7376;
4
+ export type HistoryDirection = "in" | "out";
5
+ export type HistoryContent = {
6
+ /** The direction of the transaction, in = received, out = sent */
7
+ direction: HistoryDirection;
8
+ /** The amount of the transaction */
9
+ amount: number;
10
+ /** An array of token event ids created */
11
+ created: string[];
12
+ /** The mint that was spent from */
13
+ mint?: string;
14
+ /** The fee paid */
15
+ fee?: number;
16
+ };
17
+ export declare const HistoryContentSymbol: unique symbol;
18
+ /** returns an array of redeemed event ids in a history event */
19
+ export declare function getHistoryRedeemed(history: NostrEvent): string[];
20
+ /** Checks if the history contents are locked */
21
+ export declare function isHistoryContentLocked(history: NostrEvent): boolean;
22
+ /** Returns the parsed content of a 7376 history event */
23
+ export declare function getHistoryContent(history: NostrEvent): HistoryContent | undefined;
24
+ /** Decrypts a wallet history event */
25
+ export declare function unlockHistoryContent(history: NostrEvent, signer: HiddenContentSigner): Promise<HistoryContent>;
26
+ export declare function lockHistoryContent(history: NostrEvent): void;
@@ -0,0 +1,45 @@
1
+ import { getHiddenTags, getOrComputeCachedValue, isETag, isHiddenContentLocked, isHiddenTagsLocked, lockHiddenTags, unlockHiddenTags, } from "applesauce-core/helpers";
2
+ export const WALLET_HISTORY_KIND = 7376;
3
+ export const HistoryContentSymbol = Symbol.for("history-content");
4
+ /** returns an array of redeemed event ids in a history event */
5
+ export function getHistoryRedeemed(history) {
6
+ return history.tags.filter((t) => isETag(t) && t[3] === "redeemed").map((t) => t[1]);
7
+ }
8
+ /** Checks if the history contents are locked */
9
+ export function isHistoryContentLocked(history) {
10
+ return isHiddenTagsLocked(history);
11
+ }
12
+ /** Returns the parsed content of a 7376 history event */
13
+ export function getHistoryContent(history) {
14
+ if (isHistoryContentLocked(history))
15
+ return undefined;
16
+ return getOrComputeCachedValue(history, HistoryContentSymbol, () => {
17
+ const tags = getHiddenTags(history);
18
+ if (!tags)
19
+ throw new Error("History event is locked");
20
+ const direction = tags.find((t) => t[0] === "direction")?.[1];
21
+ if (!direction)
22
+ throw new Error("History event missing direction");
23
+ const amountStr = tags.find((t) => t[0] === "amount")?.[1];
24
+ if (!amountStr)
25
+ throw new Error("History event missing amount");
26
+ const amount = parseInt(amountStr);
27
+ if (!Number.isFinite(amount))
28
+ throw new Error("Failed to parse amount");
29
+ const mint = tags.find((t) => t[0] === "mint")?.[1];
30
+ const feeStr = tags.find((t) => t[0] === "fee")?.[1];
31
+ const fee = feeStr ? parseInt(feeStr) : undefined;
32
+ const created = tags.filter((t) => isETag(t) && t[3] === "created").map((t) => t[1]);
33
+ return { direction, amount, created, mint, fee };
34
+ });
35
+ }
36
+ /** Decrypts a wallet history event */
37
+ export async function unlockHistoryContent(history, signer) {
38
+ if (isHiddenContentLocked(history))
39
+ await unlockHiddenTags(history, signer);
40
+ return getHistoryContent(history);
41
+ }
42
+ export function lockHistoryContent(history) {
43
+ Reflect.deleteProperty(history, HistoryContentSymbol);
44
+ lockHiddenTags(history);
45
+ }
@@ -1 +1,4 @@
1
1
  export * from "./wallet.js";
2
+ export * from "./tokens.js";
3
+ export * from "./history.js";
4
+ export * from "./animated-qr.js";
@@ -1 +1,4 @@
1
1
  export * from "./wallet.js";
2
+ export * from "./tokens.js";
3
+ export * from "./history.js";
4
+ export * from "./animated-qr.js";
@@ -0,0 +1,58 @@
1
+ import { Proof, Token } from "@cashu/cashu-ts";
2
+ import { HiddenContentSigner } from "applesauce-core/helpers";
3
+ import { NostrEvent } from "nostr-tools";
4
+ /** Internal method for creating a unique id for each proof */
5
+ export declare function getProofUID(proof: Proof): string;
6
+ /** Internal method to filter out duplicate proofs */
7
+ export declare function ignoreDuplicateProofs(seen?: Set<string>): (proof: Proof) => boolean;
8
+ export declare const WALLET_TOKEN_KIND = 7375;
9
+ export type TokenContent = {
10
+ /** Cashu mint for the proofs */
11
+ mint: string;
12
+ /** Cashu proofs */
13
+ proofs: {
14
+ amount: number;
15
+ secret: string;
16
+ C: string;
17
+ id: string;
18
+ }[];
19
+ /** The cashu unit */
20
+ unit?: string;
21
+ /** tokens that were destroyed in the creation of this token (helps on wallet state transitions) */
22
+ del: string[];
23
+ };
24
+ export declare const TokenContentSymbol: unique symbol;
25
+ /**
26
+ * Returns the decrypted and parsed details of a 7375 token event
27
+ * @throws
28
+ */
29
+ export declare function getTokenContent(token: NostrEvent): TokenContent | undefined;
30
+ /** Returns if token details are locked */
31
+ export declare function isTokenContentLocked(token: NostrEvent): boolean;
32
+ /** Decrypts a k:7375 token event */
33
+ export declare function unlockTokenContent(token: NostrEvent, signer: HiddenContentSigner): Promise<TokenContent>;
34
+ /** Removes the unencrypted hidden content */
35
+ export declare function lockTokenContent(token: NostrEvent): void;
36
+ /**
37
+ * Gets the totaled amount of proofs in a token event
38
+ * @param token The token event to calculate the total
39
+ */
40
+ export declare function getTokenProofsTotal(token: NostrEvent): number | undefined;
41
+ /**
42
+ * Selects oldest tokens and proofs that total up to more than the min amount
43
+ * @throws
44
+ */
45
+ export declare function dumbTokenSelection(tokens: NostrEvent[], minAmount: number, mint?: string): {
46
+ events: NostrEvent[];
47
+ proofs: Proof[];
48
+ };
49
+ /**
50
+ * Returns a decoded cashu token inside an unicode emoji
51
+ * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
52
+ */
53
+ export declare function decodeTokenFromEmojiString(str: string): Token | undefined;
54
+ /**
55
+ * Encodes a token into an emoji char
56
+ * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/SendTokenDialog.vue#L710
57
+ */
58
+ export declare function encodeTokenToEmoji(token: Token | string, emoji?: string): string;
@@ -0,0 +1,160 @@
1
+ import { getDecodedToken, getEncodedToken } from "@cashu/cashu-ts";
2
+ import { getHiddenContent, getOrComputeCachedValue, isHiddenContentLocked, isHiddenTagsLocked, lockHiddenContent, unlockHiddenContent, } from "applesauce-core/helpers";
3
+ /** Internal method for creating a unique id for each proof */
4
+ export function getProofUID(proof) {
5
+ return proof.id + proof.amount + proof.C + proof.secret;
6
+ }
7
+ /** Internal method to filter out duplicate proofs */
8
+ export function ignoreDuplicateProofs(seen = new Set()) {
9
+ return (proof) => {
10
+ const id = getProofUID(proof);
11
+ if (seen.has(id))
12
+ return false;
13
+ else {
14
+ seen.add(id);
15
+ return true;
16
+ }
17
+ };
18
+ }
19
+ export const WALLET_TOKEN_KIND = 7375;
20
+ export const TokenContentSymbol = Symbol.for("token-content");
21
+ /**
22
+ * Returns the decrypted and parsed details of a 7375 token event
23
+ * @throws
24
+ */
25
+ export function getTokenContent(token) {
26
+ if (isHiddenTagsLocked(token))
27
+ return undefined;
28
+ return getOrComputeCachedValue(token, TokenContentSymbol, () => {
29
+ const plaintext = getHiddenContent(token);
30
+ if (!plaintext)
31
+ throw new Error("Token is locked");
32
+ const details = JSON.parse(plaintext);
33
+ if (!details.mint)
34
+ throw new Error("Token missing mint");
35
+ if (!details.proofs)
36
+ throw new Error("Token missing proofs");
37
+ if (!details.del)
38
+ details.del = [];
39
+ return details;
40
+ });
41
+ }
42
+ /** Returns if token details are locked */
43
+ export function isTokenContentLocked(token) {
44
+ return isHiddenContentLocked(token);
45
+ }
46
+ /** Decrypts a k:7375 token event */
47
+ export async function unlockTokenContent(token, signer) {
48
+ if (isHiddenContentLocked(token))
49
+ await unlockHiddenContent(token, signer);
50
+ return getTokenContent(token);
51
+ }
52
+ /** Removes the unencrypted hidden content */
53
+ export function lockTokenContent(token) {
54
+ Reflect.deleteProperty(token, TokenContentSymbol);
55
+ lockHiddenContent(token);
56
+ }
57
+ /**
58
+ * Gets the totaled amount of proofs in a token event
59
+ * @param token The token event to calculate the total
60
+ */
61
+ export function getTokenProofsTotal(token) {
62
+ if (isTokenContentLocked(token))
63
+ return undefined;
64
+ const content = getTokenContent(token);
65
+ return content.proofs.reduce((t, p) => t + p.amount, 0);
66
+ }
67
+ /**
68
+ * Selects oldest tokens and proofs that total up to more than the min amount
69
+ * @throws
70
+ */
71
+ export function dumbTokenSelection(tokens, minAmount, mint) {
72
+ // sort newest to oldest
73
+ const sorted = tokens
74
+ .filter((token) => !isTokenContentLocked(token) && (mint ? getTokenContent(token).mint === mint : true))
75
+ .sort((a, b) => b.created_at - a.created_at);
76
+ let amount = 0;
77
+ const seen = new Set();
78
+ const selectedTokens = [];
79
+ const selectedProofs = [];
80
+ while (amount < minAmount) {
81
+ const token = sorted.pop();
82
+ if (!token)
83
+ throw new Error("Insufficient funds");
84
+ const proofs = getTokenContent(token).proofs.filter(ignoreDuplicateProofs(seen));
85
+ const total = proofs.reduce((t, p) => t + p.amount, 0);
86
+ selectedTokens.push(token);
87
+ selectedProofs.push(...proofs);
88
+ amount += total;
89
+ }
90
+ return { events: selectedTokens, proofs: selectedProofs };
91
+ }
92
+ /**
93
+ * Returns a decoded cashu token inside an unicode emoji
94
+ * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
95
+ */
96
+ export function decodeTokenFromEmojiString(str) {
97
+ try {
98
+ let decoded = [];
99
+ const chars = Array.from(str);
100
+ if (!chars.length)
101
+ return undefined;
102
+ const fromVariationSelector = function (char) {
103
+ const codePoint = char.codePointAt(0);
104
+ if (codePoint === undefined)
105
+ return null;
106
+ // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F
107
+ if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {
108
+ // Maps FE00->0, FE01->1, ..., FE0F->15
109
+ const byteValue = codePoint - 0xfe00;
110
+ return String.fromCharCode(byteValue);
111
+ }
112
+ // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF
113
+ if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) {
114
+ // Maps E0100->16, E0101->17, ..., E01EF->255
115
+ const byteValue = codePoint - 0xe0100 + 16;
116
+ return String.fromCharCode(byteValue);
117
+ }
118
+ // No Variation Selector
119
+ return null;
120
+ };
121
+ // Check all input chars for peanut data
122
+ for (const char of chars) {
123
+ let byte = fromVariationSelector(char);
124
+ if (byte === null && decoded.length > 0) {
125
+ break;
126
+ }
127
+ else if (byte === null) {
128
+ continue;
129
+ }
130
+ decoded.push(byte); // got some
131
+ }
132
+ // Switch out token if we found peanut data
133
+ let decodedString = decoded.join("");
134
+ return getDecodedToken(decodedString);
135
+ }
136
+ catch (error) {
137
+ return undefined;
138
+ }
139
+ }
140
+ /**
141
+ * Encodes a token into an emoji char
142
+ * @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/SendTokenDialog.vue#L710
143
+ */
144
+ export function encodeTokenToEmoji(token, emoji = "🥜") {
145
+ return (emoji +
146
+ Array.from(typeof token === "string" ? token : getEncodedToken(token))
147
+ .map((char) => {
148
+ const byteValue = char.charCodeAt(0);
149
+ // For byte values 0-15, use Variation Selectors (VS1-VS16): U+FE00 to U+FE0F
150
+ if (byteValue >= 0 && byteValue <= 15) {
151
+ return String.fromCodePoint(0xfe00 + byteValue);
152
+ }
153
+ // For byte values 16-255, use Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF
154
+ if (byteValue >= 16 && byteValue <= 255) {
155
+ return String.fromCodePoint(0xe0100 + (byteValue - 16));
156
+ }
157
+ return "";
158
+ })
159
+ .join(""));
160
+ }
@@ -1,13 +1,15 @@
1
1
  import { HiddenContentSigner } from "applesauce-core/helpers";
2
2
  import { NostrEvent } from "nostr-tools";
3
3
  export declare const WALLET_KIND = 17375;
4
+ export declare const WALLET_BACKUP_KIND = 375;
4
5
  export declare const WalletPrivateKeySymbol: unique symbol;
5
6
  export declare const WalletMintsSymbol: unique symbol;
6
7
  /** Returns if a wallet is locked */
7
8
  export declare function isWalletLocked(wallet: NostrEvent): boolean;
8
9
  /** Unlocks a wallet and returns the hidden tags */
9
10
  export declare function unlockWallet(wallet: NostrEvent, signer: HiddenContentSigner): Promise<string[][]>;
11
+ export declare function lockWallet(wallet: NostrEvent): void;
10
12
  /** Returns the wallets mints */
11
13
  export declare function getWalletMints(wallet: NostrEvent): string[];
12
14
  /** Returns the wallets private key as a string */
13
- export declare function getWalletPrivateKey(wallet: NostrEvent): string;
15
+ export declare function getWalletPrivateKey(wallet: NostrEvent): Uint8Array | undefined;
@@ -1,5 +1,7 @@
1
- import { getHiddenTags, getOrComputeCachedValue, isHiddenTagsLocked, unlockHiddenTags, } from "applesauce-core/helpers";
1
+ import { hexToBytes } from "@noble/hashes/utils";
2
+ import { getHiddenTags, getOrComputeCachedValue, isHiddenTagsLocked, lockHiddenTags, unlockHiddenTags, } from "applesauce-core/helpers";
2
3
  export const WALLET_KIND = 17375;
4
+ export const WALLET_BACKUP_KIND = 375;
3
5
  export const WalletPrivateKeySymbol = Symbol.for("wallet-private-key");
4
6
  export const WalletMintsSymbol = Symbol.for("wallet-mints");
5
7
  /** Returns if a wallet is locked */
@@ -10,6 +12,11 @@ export function isWalletLocked(wallet) {
10
12
  export async function unlockWallet(wallet, signer) {
11
13
  return await unlockHiddenTags(wallet, signer);
12
14
  }
15
+ export function lockWallet(wallet) {
16
+ Reflect.deleteProperty(wallet, WalletPrivateKeySymbol);
17
+ Reflect.deleteProperty(wallet, WalletMintsSymbol);
18
+ lockHiddenTags(wallet);
19
+ }
13
20
  /** Returns the wallets mints */
14
21
  export function getWalletMints(wallet) {
15
22
  return getOrComputeCachedValue(wallet, WalletMintsSymbol, () => {
@@ -26,8 +33,6 @@ export function getWalletPrivateKey(wallet) {
26
33
  if (!tags)
27
34
  throw new Error("Wallet is locked");
28
35
  const key = tags.find((t) => t[0] === "privkey" && t[1])?.[1];
29
- if (!key)
30
- throw new Error("Wallet missing private key");
31
- return key;
36
+ return key ? hexToBytes(key) : undefined;
32
37
  });
33
38
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- export * as Queries from "./queries/index.js";
1
+ export * as Actions from "./actions/index.js";
2
+ export * as Blueprints from "./blueprints/index.js";
2
3
  export * as Helpers from "./helpers/index.js";
4
+ export * as Queries from "./queries/index.js";
5
+ export * from "./operations/index.js";
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
- export * as Queries from "./queries/index.js";
1
+ export * as Actions from "./actions/index.js";
2
+ export * as Blueprints from "./blueprints/index.js";
2
3
  export * as Helpers from "./helpers/index.js";
4
+ export * as Queries from "./queries/index.js";
5
+ export * from "./operations/index.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { setWalletBackupContent } from "../wallet.js";
3
+ import { EventFactory } from "applesauce-factory";
4
+ import { generateSecretKey } from "nostr-tools";
5
+ import { unixNow } from "applesauce-core/helpers";
6
+ import { WALLET_BACKUP_KIND } from "../../../helpers/wallet.js";
7
+ import { FakeUser } from "../../../__tests__/fake-user.js";
8
+ import { WalletBlueprint } from "../../../blueprints/wallet.js";
9
+ const user = new FakeUser();
10
+ const factory = new EventFactory({ signer: user });
11
+ describe("setWalletBackupContent", () => {
12
+ it("should throw if kind is not wallet kind", async () => {
13
+ const note = user.note();
14
+ await expect(setWalletBackupContent(note)({ kind: WALLET_BACKUP_KIND, tags: [], created_at: unixNow(), content: "" }, factory.context)).rejects.toThrow();
15
+ });
16
+ it("should throw if pubkey does not match", async () => {
17
+ const wallet = await factory.sign(await factory.create(WalletBlueprint, [], generateSecretKey()));
18
+ const user2 = new FakeUser();
19
+ await expect(setWalletBackupContent(wallet)({ kind: WALLET_BACKUP_KIND, tags: [], created_at: unixNow(), content: "" }, { signer: user2 })).rejects.toThrow();
20
+ });
21
+ it("should copy the content of the wallet event", async () => {
22
+ const wallet = await factory.sign(await factory.create(WalletBlueprint, [], generateSecretKey()));
23
+ expect(await setWalletBackupContent(wallet)({ kind: WALLET_BACKUP_KIND, tags: [], created_at: unixNow(), content: "" }, factory.context)).toEqual(expect.objectContaining({ content: wallet.content }));
24
+ });
25
+ });
@@ -0,0 +1,7 @@
1
+ import { EventOperation } from "applesauce-factory";
2
+ import { HistoryContent } from "../../helpers/history.js";
3
+ import { EventPointer } from "nostr-tools/nip19";
4
+ /** Sets the encrypted tags of a wallet history event */
5
+ export declare function setHistoryContent(content: HistoryContent): EventOperation;
6
+ /** Sets the "redeemed" tags on a wallet history event */
7
+ export declare function setHistoryRedeemed(redeemed: (string | EventPointer)[]): EventOperation;
@@ -0,0 +1,19 @@
1
+ import { modifyHiddenTags, modifyPublicTags } from "applesauce-factory/operations/event";
2
+ import { includeHistoryCreatedTags, includeHistoryRedeemedTags, setHistoryAmountTag, setHistoryDirectionTag, setHistoryFeeTag, setHistoryMintTag, } from "../tag/history.js";
3
+ /** Sets the encrypted tags of a wallet history event */
4
+ export function setHistoryContent(content) {
5
+ const operations = [
6
+ setHistoryDirectionTag(content.direction),
7
+ setHistoryAmountTag(content.amount),
8
+ includeHistoryCreatedTags(content.created),
9
+ ];
10
+ if (content.fee !== undefined)
11
+ operations.push(setHistoryFeeTag(content.fee));
12
+ if (content.mint !== undefined)
13
+ operations.push(setHistoryMintTag(content.mint));
14
+ return modifyHiddenTags(...operations);
15
+ }
16
+ /** Sets the "redeemed" tags on a wallet history event */
17
+ export function setHistoryRedeemed(redeemed) {
18
+ return modifyPublicTags(includeHistoryRedeemedTags(redeemed));
19
+ }