applesauce-wallet 0.0.0-next-20250313084132 → 0.0.0-next-20250313104750

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getDecodedToken } from "@cashu/cashu-ts";
3
+ import { lastValueFrom, take, timer } from "rxjs";
4
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
5
+ import { receiveAnimated, sendAnimated } from "../animated-qr.js";
6
+ const tokenStr = "cashuBo2FteBtodHRwczovL3Rlc3RudXQuY2FzaHUuc3BhY2VhdWNzYXRhdIGiYWlIAJofKTJT5B5hcIOkYWEQYXN4QDA2M2VlYjgwZDZjODM4NWU4NzYwODVhN2E4NzdkOTkyY2U4N2QwNTRmY2RjYzNiODMwMzhjOWY3MmNmMDY1ZGVhY1ghAynsxZ-OuZfZDcqYTLPYfCqHO7jkGjn97aolgtSYhYykYWSjYWVYIGbp_9B9aztSlZxz7g6Tqx5M_1PuFzJCVEMOVd8XAF8wYXNYINTOU77ODcj28v04pq7ektdf6sq2XuxvMjVE0wK6jFolYXJYIM_gZnUGT5jDOyZiQ-2vG9zYnuWaY8vPoWGe_3sXvrvbpGFhBGFzeEA0MWMwZDk1YTU5ZjkxNTdlYTc5NTJlNGFlMzYzYTI3NTMxNTllNmQ1NGJiNzExMTg5ZDk5YjU1MmYzYjIzZTJiYWNYIQM-49gen_1nPchxbaAiKprVr78VmMRVpHH_Tu9P8TO5mGFko2FlWCB9j7rlpdBH_m7tNYnLpzPhn-nGmS1CcbUfnPzjxy6G92FzWCDdsby7fGM5324T5UEoV858YWzZ9MCY59KgKP362fJDfmFyWCDL73v4FRo7iMe83bfMuEy3RJPtC1Vr1jdOpw2-x-7EAaRhYQFhc3hANjRhZDI3NmExOGNmNDhiMDZmYjdiMGYwOWFiMTU4ZTA0ZmM0NmIxYzA4YzMyNjJlODUxNzZkYTMzMTgyYzQ3YWFjWCECMDpCbNbrgA9FcQEIYxobU7ik_pTl8sByPqHDmkY4azxhZKNhZVggqHGaff9M270EU8LGxRpG_G4rn2bMgjyk3hFFg78ZXRVhc1ggP6DsNsWykwKE94yZF23gpCyapcoqh6DDZdVu0lKn2Z5hclggmPKig-lObsuxi_1XCm7_Y_tqaCcqEDz8eCwVhJ8gq9M";
7
+ const token = getDecodedToken(tokenStr);
8
+ describe("sendAnimated", () => {
9
+ it("should loop", async () => {
10
+ const qr$ = sendAnimated(token, { interval: 0 });
11
+ const spy = subscribeSpyTo(qr$);
12
+ // wait 100ms
13
+ await lastValueFrom(timer(100));
14
+ // should not have competed
15
+ expect(spy.receivedComplete()).toBeFalsy();
16
+ spy.unsubscribe();
17
+ });
18
+ it("should emit parts", async () => {
19
+ const qr$ = sendAnimated(token, { interval: 0 }).pipe(take(6));
20
+ const spy = subscribeSpyTo(qr$);
21
+ // wait 100ms
22
+ await lastValueFrom(qr$);
23
+ // should not have competed
24
+ expect(spy.getValues()).toEqual(Array(6)
25
+ .fill(0)
26
+ .map((_, i) => expect.stringContaining(`ur:bytes/${i + 1}-6/`)));
27
+ });
28
+ });
29
+ describe("receiveAnimated", () => {
30
+ it("should decode animated qr", async () => {
31
+ const qr$ = sendAnimated(token, { interval: 0 }).pipe(receiveAnimated);
32
+ const spy = subscribeSpyTo(qr$);
33
+ await lastValueFrom(qr$);
34
+ expect(spy.getValues()).toEqual([
35
+ expect.any(Number),
36
+ expect.any(Number),
37
+ expect.any(Number),
38
+ expect.any(Number),
39
+ expect.any(Number),
40
+ expect.any(Number),
41
+ token,
42
+ ]);
43
+ });
44
+ });
@@ -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<Token | number>;
@@ -0,0 +1,77 @@
1
+ import { getDecodedToken, 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 encoded = typeof token === "string" ? token : getEncodedTokenV4(token);
21
+ let buffer = Buffer.from(encoded);
22
+ let ur = UR.fromBuffer(buffer);
23
+ let encoder = new UREncoder(ur, options?.fragmentLength ?? 100, 0);
24
+ return interval(options?.interval ?? ANIMATED_QR_INTERVAL.FAST).pipe(map(() => encoder.nextPart()));
25
+ });
26
+ }
27
+ /** An operator that decodes UR, emits progress percent and completes with final result or error */
28
+ function urDecoder() {
29
+ return (source) => new Observable((observer) => {
30
+ const decoder = new URDecoder();
31
+ return source.subscribe((part) => {
32
+ decoder.receivePart(part);
33
+ if (decoder.isComplete() && decoder.isSuccess()) {
34
+ // emit progress
35
+ const progress = decoder.estimatedPercentComplete();
36
+ observer.next(progress);
37
+ // emit result
38
+ const ur = decoder.resultUR();
39
+ const decoded = ur.decodeCBOR();
40
+ const utf8 = new TextDecoder();
41
+ const tokenStr = utf8.decode(decoded);
42
+ observer.next(tokenStr);
43
+ // complete
44
+ observer.complete();
45
+ }
46
+ else if (decoder.isError()) {
47
+ // emit error
48
+ const reason = decoder.resultError();
49
+ observer.error(new Error(reason));
50
+ }
51
+ else {
52
+ // emit progress
53
+ const progress = decoder.estimatedPercentComplete();
54
+ observer.next(progress);
55
+ }
56
+ });
57
+ });
58
+ }
59
+ /** Creates an observable that completes with decoded token */
60
+ export function receiveAnimated(input) {
61
+ return input.pipe(
62
+ // convert to lower case
63
+ map((str) => str.toLowerCase()),
64
+ // filter out non UR parts
65
+ filter((str) => str.startsWith("ur:")),
66
+ // decode UR and complete
67
+ urDecoder(),
68
+ // decode cashu token
69
+ map((part) => {
70
+ if (typeof part === "string")
71
+ return getDecodedToken(part);
72
+ else
73
+ return part;
74
+ }),
75
+ // only run one decoder
76
+ shareReplay(1));
77
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./wallet.js";
2
2
  export * from "./tokens.js";
3
3
  export * from "./history.js";
4
+ export * from "./animated-qr.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./wallet.js";
2
2
  export * from "./tokens.js";
3
3
  export * from "./history.js";
4
+ export * from "./animated-qr.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-wallet",
3
- "version": "0.0.0-next-20250313084132",
3
+ "version": "0.0.0-next-20250313104750",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -78,17 +78,19 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@cashu/cashu-ts": "2.0.0-rc1",
81
+ "@gandlaf21/bc-ur": "^1.1.12",
81
82
  "@noble/hashes": "^1.7.1",
82
- "applesauce-actions": "0.0.0-next-20250313084132",
83
- "applesauce-core": "0.0.0-next-20250313084132",
84
- "applesauce-factory": "0.0.0-next-20250313084132",
83
+ "applesauce-actions": "0.0.0-next-20250313104750",
84
+ "applesauce-core": "0.0.0-next-20250313104750",
85
+ "applesauce-factory": "0.0.0-next-20250313104750",
86
+ "buffer": "^6.0.3",
85
87
  "nostr-tools": "^2.10.4",
86
88
  "rxjs": "^7.8.1"
87
89
  },
88
90
  "devDependencies": {
89
91
  "@hirez_io/observer-spy": "^2.2.0",
90
92
  "@types/debug": "^4.1.12",
91
- "applesauce-signers": "0.0.0-next-20250313084132",
93
+ "applesauce-signers": "0.0.0-next-20250313104750",
92
94
  "typescript": "^5.7.3",
93
95
  "vitest": "^3.0.5"
94
96
  },