applesauce-wallet 0.0.0-next-20251209200210 → 0.0.0-next-20251231055351

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 (91) hide show
  1. package/dist/actions/common.d.ts +4 -0
  2. package/dist/actions/common.js +15 -0
  3. package/dist/actions/index.d.ts +3 -2
  4. package/dist/actions/index.js +3 -2
  5. package/dist/actions/mint-recomendation.d.ts +30 -0
  6. package/dist/actions/mint-recomendation.js +96 -0
  7. package/dist/actions/{zap-info.d.ts → nutzap-info.d.ts} +20 -3
  8. package/dist/actions/nutzap-info.js +117 -0
  9. package/dist/actions/nutzaps.d.ts +24 -0
  10. package/dist/actions/nutzaps.js +154 -0
  11. package/dist/actions/tokens.d.ts +77 -7
  12. package/dist/actions/tokens.js +332 -69
  13. package/dist/actions/wallet.d.ts +18 -3
  14. package/dist/actions/wallet.js +74 -32
  15. package/dist/blueprints/history.d.ts +1 -1
  16. package/dist/blueprints/history.js +1 -1
  17. package/dist/blueprints/index.d.ts +1 -0
  18. package/dist/blueprints/index.js +1 -0
  19. package/dist/blueprints/mint-recommendation.d.ts +16 -0
  20. package/dist/blueprints/mint-recommendation.js +11 -0
  21. package/dist/blueprints/wallet.d.ts +5 -1
  22. package/dist/blueprints/wallet.js +6 -3
  23. package/dist/casts/__register__.d.ts +22 -0
  24. package/dist/casts/__register__.js +52 -0
  25. package/dist/casts/index.d.ts +8 -0
  26. package/dist/casts/index.js +8 -0
  27. package/dist/casts/mint-info.d.ts +18 -0
  28. package/dist/casts/mint-info.js +42 -0
  29. package/dist/casts/mint-recommendation.d.ts +16 -0
  30. package/dist/casts/mint-recommendation.js +29 -0
  31. package/dist/casts/nutzap-info.d.ts +14 -0
  32. package/dist/casts/nutzap-info.js +22 -0
  33. package/dist/casts/nutzap.d.ts +16 -0
  34. package/dist/casts/nutzap.js +37 -0
  35. package/dist/casts/wallet-history.d.ts +16 -0
  36. package/dist/casts/wallet-history.js +40 -0
  37. package/dist/casts/wallet-token.d.ts +29 -0
  38. package/dist/casts/wallet-token.js +52 -0
  39. package/dist/casts/wallet.d.ts +27 -0
  40. package/dist/casts/wallet.js +62 -0
  41. package/dist/helpers/cashu.d.ts +21 -0
  42. package/dist/helpers/cashu.js +105 -0
  43. package/dist/helpers/couch.d.ts +11 -0
  44. package/dist/helpers/couch.js +1 -0
  45. package/dist/helpers/history.d.ts +5 -1
  46. package/dist/helpers/history.js +13 -4
  47. package/dist/helpers/index.d.ts +7 -1
  48. package/dist/helpers/index.js +7 -1
  49. package/dist/helpers/indexed-db-couch.d.ts +34 -0
  50. package/dist/helpers/indexed-db-couch.js +119 -0
  51. package/dist/helpers/local-storage-couch.d.ts +29 -0
  52. package/dist/helpers/local-storage-couch.js +78 -0
  53. package/dist/helpers/mint-info.d.ts +40 -0
  54. package/dist/helpers/mint-info.js +80 -0
  55. package/dist/helpers/mint-recommendation.d.ts +41 -0
  56. package/dist/helpers/mint-recommendation.js +54 -0
  57. package/dist/helpers/{zap-info.d.ts → nutzap-info.d.ts} +10 -1
  58. package/dist/helpers/{zap-info.js → nutzap-info.js} +22 -10
  59. package/dist/helpers/nutzap.d.ts +15 -0
  60. package/dist/helpers/nutzap.js +57 -3
  61. package/dist/helpers/tokens.d.ts +9 -18
  62. package/dist/helpers/tokens.js +64 -94
  63. package/dist/helpers/wallet.d.ts +16 -6
  64. package/dist/helpers/wallet.js +40 -14
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/models/history.d.ts +1 -1
  68. package/dist/models/history.js +7 -10
  69. package/dist/models/index.d.ts +0 -1
  70. package/dist/models/index.js +0 -1
  71. package/dist/models/nutzap.d.ts +2 -0
  72. package/dist/models/nutzap.js +8 -0
  73. package/dist/models/tokens.d.ts +2 -2
  74. package/dist/models/tokens.js +14 -17
  75. package/dist/operations/history.js +1 -1
  76. package/dist/operations/index.d.ts +2 -1
  77. package/dist/operations/index.js +2 -1
  78. package/dist/operations/mint-recommendation.d.ts +13 -0
  79. package/dist/operations/mint-recommendation.js +26 -0
  80. package/dist/operations/nutzap-info.d.ts +21 -0
  81. package/dist/operations/nutzap-info.js +71 -0
  82. package/dist/operations/wallet.d.ts +10 -1
  83. package/dist/operations/wallet.js +33 -3
  84. package/package.json +37 -28
  85. package/dist/actions/zap-info.js +0 -83
  86. package/dist/actions/zaps.d.ts +0 -8
  87. package/dist/actions/zaps.js +0 -30
  88. package/dist/models/wallet.d.ts +0 -13
  89. package/dist/models/wallet.js +0 -21
  90. package/dist/operations/zap-info.d.ts +0 -10
  91. package/dist/operations/zap-info.js +0 -17
@@ -0,0 +1,4 @@
1
+ import { User } from "applesauce-common/casts";
2
+ import { EventSigner } from "applesauce-core";
3
+ import "../casts/__register__.js";
4
+ export declare function getUnlockedWallet(user: User, signer?: EventSigner): Promise<import("../casts/wallet.js").Wallet>;
@@ -0,0 +1,15 @@
1
+ import { unlockWallet } from "../helpers/wallet.js";
2
+ // Make sure the wallet$ is registered on the user class
3
+ import "../casts/__register__.js";
4
+ export async function getUnlockedWallet(user, signer) {
5
+ // NOTE: hard coding the timeout here isn't ideal, but no idea where else to put it
6
+ const wallet = await user.wallet$.$first(5000, undefined);
7
+ if (!wallet)
8
+ throw new Error("Unable to find wallet");
9
+ if (!wallet.unlocked) {
10
+ if (!signer)
11
+ throw new Error("Missing signer");
12
+ await unlockWallet(wallet.event, signer);
13
+ }
14
+ return wallet;
15
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./wallet.js";
2
2
  export * from "./tokens.js";
3
- export * from "./zaps.js";
4
- export * from "./zap-info.js";
3
+ export * from "./nutzaps.js";
4
+ export * from "./nutzap-info.js";
5
+ export * from "./mint-recomendation.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./wallet.js";
2
2
  export * from "./tokens.js";
3
- export * from "./zaps.js";
4
- export * from "./zap-info.js";
3
+ export * from "./nutzaps.js";
4
+ export * from "./nutzap-info.js";
5
+ export * from "./mint-recomendation.js";
@@ -0,0 +1,30 @@
1
+ import { Wallet } from "@cashu/cashu-ts";
2
+ import { Action } from "applesauce-actions";
3
+ import { NostrEvent } from "applesauce-core/helpers/event";
4
+ import "../casts/__register__.js";
5
+ /**
6
+ * Options for reviewing a mint
7
+ */
8
+ export type ReviewMintOptions = {
9
+ /** Optional review/comment content */
10
+ comment?: string;
11
+ /** Optional relays to publish to */
12
+ relays?: string[];
13
+ };
14
+ /**
15
+ * An action that creates and publishes a mint recommendation event.
16
+ * Can accept a mint URL, Wallet instance, or MintInfo event.
17
+ */
18
+ export declare function RecommendMint(input: string | Wallet | NostrEvent, options?: ReviewMintOptions): Action;
19
+ /**
20
+ * Options for updating a mint recommendation
21
+ */
22
+ export type UpdateMintRecommendationOptions = {
23
+ /** Optional relays to publish to */
24
+ relays?: string[];
25
+ };
26
+ /**
27
+ * An action that updates the comment on an existing mint recommendation event.
28
+ * Can accept a mint pubkey (to find the recommendation) or the recommendation event directly.
29
+ */
30
+ export declare function UpdateMintRecommendation(input: string | NostrEvent, comment: string, options?: UpdateMintRecommendationOptions): Action;
@@ -0,0 +1,96 @@
1
+ import { Wallet } from "@cashu/cashu-ts";
2
+ import { MintRecommendationBlueprint } from "../blueprints/mint-recommendation.js";
3
+ import { getCashuMintPubkey, getCashuMintURL, isValidCashuMintInfo } from "../helpers/mint-info.js";
4
+ import { MINT_RECOMMENDATION_KIND, getRecommendationMintPubkey, isValidMintRecommendation, } from "../helpers/mint-recommendation.js";
5
+ import { setComment } from "../operations/mint-recommendation.js";
6
+ // Helper function to fetch mint pubkey from URL
7
+ async function fetchMintPubkey(mintUrl) {
8
+ // Fetch mint info directly from the /v1/info endpoint
9
+ const infoUrl = new URL("/v1/info", mintUrl).toString();
10
+ const response = await fetch(infoUrl);
11
+ if (!response.ok) {
12
+ throw new Error(`Failed to fetch mint info from ${mintUrl}: ${response.statusText}`);
13
+ }
14
+ const mintInfo = (await response.json());
15
+ return mintInfo.pubkey;
16
+ }
17
+ // Make sure the mint recommendation cast is registered
18
+ import "../casts/__register__.js";
19
+ /**
20
+ * An action that creates and publishes a mint recommendation event.
21
+ * Can accept a mint URL, Wallet instance, or MintInfo event.
22
+ */
23
+ export function RecommendMint(input, options) {
24
+ return async ({ factory, sign, publish }) => {
25
+ let mintPubkey;
26
+ let url;
27
+ let addressPointer = undefined;
28
+ // Handle different input types
29
+ if (typeof input === "string") {
30
+ if (!URL.canParse(input))
31
+ throw new Error("Invalid mint URL");
32
+ // Input is a mint URL - fetch info directly
33
+ url = input;
34
+ mintPubkey = await fetchMintPubkey(url);
35
+ }
36
+ else if (input instanceof Wallet) {
37
+ const info = input.getMintInfo();
38
+ mintPubkey = info.pubkey;
39
+ }
40
+ else {
41
+ // Input is a MintInfo event (kind:38172)
42
+ if (!isValidCashuMintInfo(input))
43
+ throw new Error("Invalid mint info event. Expected kind:38172 with required `d` and `u` tags.");
44
+ mintPubkey = getCashuMintPubkey(input);
45
+ url = getCashuMintURL(input);
46
+ // Use the event itself as address pointer (blueprint will convert it)
47
+ addressPointer = input;
48
+ }
49
+ // Create the mint recommendation event
50
+ const recommendation = await factory
51
+ .create(MintRecommendationBlueprint, {
52
+ mintPubkey,
53
+ url,
54
+ addressPointer,
55
+ comment: options?.comment,
56
+ })
57
+ .then(sign);
58
+ // Publish the event
59
+ await publish(recommendation, options?.relays);
60
+ };
61
+ }
62
+ /**
63
+ * An action that updates the comment on an existing mint recommendation event.
64
+ * Can accept a mint pubkey (to find the recommendation) or the recommendation event directly.
65
+ */
66
+ export function UpdateMintRecommendation(input, comment, options) {
67
+ return async ({ events, factory, self, sign, publish }) => {
68
+ let recommendation;
69
+ // Handle different input types
70
+ if (typeof input === "string") {
71
+ // Input is a mint pubkey - find the recommendation event
72
+ const recommendations = events.getTimeline({ kinds: [MINT_RECOMMENDATION_KIND], authors: [self] });
73
+ recommendation = recommendations.find((rec) => {
74
+ if (!isValidMintRecommendation(rec))
75
+ return false;
76
+ const recMintPubkey = getRecommendationMintPubkey(rec);
77
+ return recMintPubkey === input;
78
+ });
79
+ if (!recommendation)
80
+ throw new Error(`No mint recommendation found for mint pubkey: ${input}`);
81
+ }
82
+ else {
83
+ // Input is a recommendation event
84
+ if (!isValidMintRecommendation(input))
85
+ throw new Error("Invalid mint recommendation event");
86
+ // Verify the event belongs to the current user
87
+ if (input.pubkey !== self)
88
+ throw new Error("Cannot update a mint recommendation that belongs to another user");
89
+ recommendation = input;
90
+ }
91
+ // Update the comment
92
+ const updated = await factory.modify(recommendation, setComment(comment)).then(sign);
93
+ // Publish the updated event
94
+ await publish(updated, options?.relays);
95
+ };
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { Action } from "applesauce-actions";
2
+ import "../casts/__register__.js";
2
3
  /** An action to add a relay to the kind 10019 nutzap info event */
3
4
  export declare function AddNutzapInfoRelay(relay: string | string[]): Action;
4
5
  /** An action to remove a relay from the kind 10019 nutzap info event */
@@ -13,10 +14,26 @@ export declare function AddNutzapInfoMint(mint: {
13
14
  }>): Action;
14
15
  /** An action to remove a mint from the kind 10019 nutzap info event */
15
16
  export declare function RemoveNutzapInfoMint(mint: string | string[]): Action;
16
- /** An action to set the pubkey for the kind 10019 nutzap info event */
17
- export declare function SetNutzapInfoPubkey(pubkey: string): Action;
18
17
  /** An action to update the entire nutzap info event */
19
18
  export declare function UpdateNutzapInfo(relays: string[], mints: Array<{
20
19
  url: string;
21
20
  units?: string[];
22
- }>, pubkey: string): Action;
21
+ }>): Action;
22
+ /**
23
+ * Sets the mints on a nutzap info event
24
+ * @throws if the nutzap info does not exist
25
+ */
26
+ export declare function SetNutzapInfoMints(mints: Array<{
27
+ url: string;
28
+ units?: string[];
29
+ }>): Action;
30
+ /**
31
+ * Sets the relays on a nutzap info event
32
+ * @throws if the nutzap info does not exist
33
+ */
34
+ export declare function SetNutzapInfoRelays(relays: string[]): Action;
35
+ /**
36
+ * Sets the pubkey on a nutzap info event
37
+ * @throws if the nutzap info does not exist
38
+ */
39
+ export declare function SetNutzapInfoPubkey(privateKey: Uint8Array): Action;
@@ -0,0 +1,117 @@
1
+ import { getNutzapInfoRelays, NUTZAP_INFO_KIND } from "../helpers/nutzap-info.js";
2
+ import { addNutzapInfoMint, addNutzapInfoRelay, removeNutzapInfoMint, removeNutzapInfoRelay, setNutzapInfoMints, setNutzapInfoPubkey, setNutzapInfoRelays, } from "../operations/nutzap-info.js";
3
+ // Make sure the nutzap$ is registered on the user class
4
+ import "../casts/__register__.js";
5
+ /** An action to add a relay to the kind 10019 nutzap info event */
6
+ export function AddNutzapInfoRelay(relay) {
7
+ return async ({ events, factory, self, sign, publish }) => {
8
+ if (typeof relay === "string")
9
+ relay = [relay];
10
+ const operations = relay.map((r) => addNutzapInfoRelay(r));
11
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
12
+ const signed = nutzapInfo
13
+ ? await factory.modify(nutzapInfo, ...operations).then(sign)
14
+ : await factory.build({ kind: NUTZAP_INFO_KIND }, ...operations).then(sign);
15
+ // Use relays from the updated event
16
+ const relays = getNutzapInfoRelays(signed);
17
+ await publish(signed, relays);
18
+ };
19
+ }
20
+ /** An action to remove a relay from the kind 10019 nutzap info event */
21
+ export function RemoveNutzapInfoRelay(relay) {
22
+ return async ({ events, factory, self, sign, publish }) => {
23
+ if (typeof relay === "string")
24
+ relay = [relay];
25
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
26
+ if (!nutzapInfo)
27
+ return;
28
+ const operations = relay.map((r) => removeNutzapInfoRelay(r));
29
+ const signed = await factory.modify(nutzapInfo, ...operations).then(sign);
30
+ // Use relays from the updated event
31
+ const relays = getNutzapInfoRelays(signed);
32
+ await publish(signed, relays);
33
+ };
34
+ }
35
+ /** An action to add a mint to the kind 10019 nutzap info event */
36
+ export function AddNutzapInfoMint(mint) {
37
+ return async ({ events, factory, self, sign, publish }) => {
38
+ const mints = Array.isArray(mint) ? mint : [mint];
39
+ const operations = mints.map((m) => addNutzapInfoMint(m));
40
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
41
+ const signed = nutzapInfo
42
+ ? await factory.modify(nutzapInfo, ...operations).then(sign)
43
+ : await factory.build({ kind: NUTZAP_INFO_KIND }, ...operations).then(sign);
44
+ // Use relays from the updated event
45
+ const relays = getNutzapInfoRelays(signed);
46
+ await publish(signed, relays);
47
+ };
48
+ }
49
+ /** An action to remove a mint from the kind 10019 nutzap info event */
50
+ export function RemoveNutzapInfoMint(mint) {
51
+ return async ({ events, factory, self, sign, publish }) => {
52
+ if (typeof mint === "string")
53
+ mint = [mint];
54
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
55
+ if (!nutzapInfo)
56
+ return;
57
+ const operations = mint.map((m) => removeNutzapInfoMint(m));
58
+ const signed = await factory.modify(nutzapInfo, ...operations).then(sign);
59
+ // Use relays from the updated event
60
+ const relays = getNutzapInfoRelays(signed);
61
+ await publish(signed, relays);
62
+ };
63
+ }
64
+ /** An action to update the entire nutzap info event */
65
+ export function UpdateNutzapInfo(relays, mints) {
66
+ return async ({ events, factory, self, sign, publish }) => {
67
+ const operations = [setNutzapInfoRelays(relays), setNutzapInfoMints(mints)];
68
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
69
+ const signed = nutzapInfo
70
+ ? await factory.modify(nutzapInfo, ...operations).then(sign)
71
+ : await factory.build({ kind: NUTZAP_INFO_KIND }, ...operations).then(sign);
72
+ await publish(signed, relays);
73
+ };
74
+ }
75
+ /**
76
+ * Sets the mints on a nutzap info event
77
+ * @throws if the nutzap info does not exist
78
+ */
79
+ export function SetNutzapInfoMints(mints) {
80
+ return async ({ events, self, factory, sign, publish }) => {
81
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
82
+ if (!nutzapInfo)
83
+ throw new Error("Nutzap info does not exist");
84
+ const signed = await factory.modify(nutzapInfo, setNutzapInfoMints(mints)).then(sign);
85
+ // Use relays from the updated event
86
+ const relays = getNutzapInfoRelays(signed);
87
+ await publish(signed, relays);
88
+ };
89
+ }
90
+ /**
91
+ * Sets the relays on a nutzap info event
92
+ * @throws if the nutzap info does not exist
93
+ */
94
+ export function SetNutzapInfoRelays(relays) {
95
+ return async ({ events, self, factory, sign, publish }) => {
96
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
97
+ if (!nutzapInfo)
98
+ throw new Error("Nutzap info does not exist");
99
+ const signed = await factory.modify(nutzapInfo, setNutzapInfoRelays(relays)).then(sign);
100
+ await publish(signed, relays);
101
+ };
102
+ }
103
+ /**
104
+ * Sets the pubkey on a nutzap info event
105
+ * @throws if the nutzap info does not exist
106
+ */
107
+ export function SetNutzapInfoPubkey(privateKey) {
108
+ return async ({ events, self, factory, sign, publish }) => {
109
+ const nutzapInfo = events.getReplaceable(NUTZAP_INFO_KIND, self);
110
+ if (!nutzapInfo)
111
+ throw new Error("Nutzap info does not exist");
112
+ const signed = await factory.modify(nutzapInfo, setNutzapInfoPubkey(privateKey)).then(sign);
113
+ // Use relays from the updated event
114
+ const relays = getNutzapInfoRelays(signed);
115
+ await publish(signed, relays);
116
+ };
117
+ }
@@ -0,0 +1,24 @@
1
+ import { Token } from "@cashu/cashu-ts";
2
+ import { Action } from "applesauce-actions";
3
+ import { NostrEvent } from "applesauce-core/helpers/event";
4
+ import { ProfilePointer } from "applesauce-core/helpers/pointers";
5
+ import { Couch } from "../helpers/couch.js";
6
+ import "../casts/__register__.js";
7
+ /** Creates a NIP-61 nutzap event for an event with a token */
8
+ export declare function NutzapEvent(event: NostrEvent, token: Token, options?: {
9
+ comment?: string;
10
+ couch?: Couch;
11
+ }): Action;
12
+ /** Creates a NIP-61 nutzap event to a users profile */
13
+ export declare function NutzapProfile(user: string | ProfilePointer, token: Token, options?: {
14
+ comment?: string;
15
+ couch?: Couch;
16
+ }): Action;
17
+ /**
18
+ * Receives a P2PK-locked cashu token from a nutzap event(s) by unlocking it with the wallet's private key
19
+ * and marks the nutzap event(s) as redeemed
20
+ * Supports nutzaps with different mints by grouping them by mint and redeeming each group separately
21
+ * @param nutzaps single nutzap event or array of nutzap events
22
+ * @param couch optional couch interface for temporarily storing tokens during the operation
23
+ */
24
+ export declare function ReceiveNutzaps(nutzaps: NostrEvent | NostrEvent[], couch?: Couch): Action;
@@ -0,0 +1,154 @@
1
+ import { sumProofs, Wallet } from "@cashu/cashu-ts";
2
+ import { castUser } from "applesauce-common/casts";
3
+ import { bytesToHex } from "applesauce-core/helpers/event";
4
+ import { WalletHistoryBlueprint } from "../blueprints/history.js";
5
+ import { WalletTokenBlueprint } from "../blueprints/tokens.js";
6
+ import { NutzapBlueprint, ProfileNutzapBlueprint } from "../blueprints/zaps.js";
7
+ import { verifyProofsLocked } from "../helpers/nutzap-info.js";
8
+ import { getNutzapMint, getNutzapProofs, isValidNutzap } from "../helpers/nutzap.js";
9
+ import { getUnlockedWallet } from "./common.js";
10
+ // Make sure the nutzap$ is registered on the user class
11
+ import "../casts/__register__.js";
12
+ /** Creates a NIP-61 nutzap event for an event with a token */
13
+ export function NutzapEvent(event, token, options) {
14
+ return async ({ events, factory, user, signer, sign, publish }) => {
15
+ const { comment, couch } = options ?? {};
16
+ const clearStoredToken = await couch?.store(token);
17
+ try {
18
+ const recipient = castUser(event.pubkey, events);
19
+ // Get the recipient's nutzap info
20
+ const info = await recipient.nutzap$.$first(5000, undefined);
21
+ if (!info)
22
+ throw new Error("Nutzap info not found");
23
+ // Get the users wallet
24
+ const wallet = await getUnlockedWallet(user, signer);
25
+ // Verify all tokens are p2pk locked
26
+ verifyProofsLocked(token.proofs, info.event);
27
+ // Create the nutzap event
28
+ const nutzap = await factory.create(NutzapBlueprint, event, token, comment || token.memo).then(sign);
29
+ // Publish the nutzap event
30
+ await publish(nutzap, wallet.relays);
31
+ }
32
+ catch { }
33
+ await clearStoredToken?.();
34
+ };
35
+ }
36
+ /** Creates a NIP-61 nutzap event to a users profile */
37
+ export function NutzapProfile(user, token, options) {
38
+ return async ({ events, factory, sign, publish }) => {
39
+ const { comment, couch } = options ?? {};
40
+ const clearStoredToken = await couch?.store(token);
41
+ try {
42
+ const recipient = castUser(user, events);
43
+ // Get the target's nutzap info
44
+ const info = await recipient.nutzap$.$first(5000, undefined);
45
+ if (!info)
46
+ throw new Error("Nutzap info not found");
47
+ // Verify all tokens are p2pk locked
48
+ verifyProofsLocked(token.proofs, info.event);
49
+ // Create the nutzap event
50
+ const nutzap = await factory.create(ProfileNutzapBlueprint, recipient, token, comment || token.memo).then(sign);
51
+ // Publish the nutzap event
52
+ await publish(nutzap, info.relays);
53
+ }
54
+ catch { }
55
+ await clearStoredToken?.();
56
+ };
57
+ }
58
+ /**
59
+ * Receives a P2PK-locked cashu token from a nutzap event(s) by unlocking it with the wallet's private key
60
+ * and marks the nutzap event(s) as redeemed
61
+ * Supports nutzaps with different mints by grouping them by mint and redeeming each group separately
62
+ * @param nutzaps single nutzap event or array of nutzap events
63
+ * @param couch optional couch interface for temporarily storing tokens during the operation
64
+ */
65
+ export function ReceiveNutzaps(nutzaps, couch) {
66
+ return async ({ factory, user, signer, sign, publish }) => {
67
+ if (!signer)
68
+ throw new Error("Missing signer");
69
+ // Normalize to array
70
+ nutzaps = Array.isArray(nutzaps) ? nutzaps : [nutzaps];
71
+ if (nutzaps.length === 0)
72
+ throw new Error("No nutzap events provided");
73
+ // Filter out nutzaps without mints or proofs (ignore them)
74
+ const validNutzaps = nutzaps.filter((n) => isValidNutzap(n));
75
+ if (validNutzaps.length === 0)
76
+ throw new Error("No valid nutzaps with mints and proofs found");
77
+ // Get private key from current wallet event
78
+ const wallet = await getUnlockedWallet(user, signer);
79
+ // Group nutzaps by mint
80
+ const nutzapsByMint = new Map();
81
+ for (const nutzap of validNutzaps) {
82
+ const mint = getNutzapMint(nutzap);
83
+ if (!mint)
84
+ continue; // Should not happen after filtering, but TypeScript needs this
85
+ if (!nutzapsByMint.has(mint)) {
86
+ nutzapsByMint.set(mint, []);
87
+ }
88
+ nutzapsByMint.get(mint).push(nutzap);
89
+ }
90
+ if (nutzapsByMint.size === 0)
91
+ throw new Error("No valid nutzaps with mints found");
92
+ if (!wallet.privateKey)
93
+ throw new Error("No private key found in wallet");
94
+ // Convert private key to hex string for cashu-ts
95
+ const privkeyHex = bytesToHex(wallet.privateKey);
96
+ // Track clear methods for all stored tokens
97
+ const clearMethods = [];
98
+ try {
99
+ // Process each mint group separately
100
+ for (const [mint, mintNutzaps] of nutzapsByMint) {
101
+ // Extract all proofs from nutzaps for this mint
102
+ const allProofs = mintNutzaps.flatMap(getNutzapProofs);
103
+ if (allProofs.length === 0)
104
+ continue;
105
+ // Construct token from nutzap proofs
106
+ const token = {
107
+ mint,
108
+ proofs: allProofs,
109
+ unit: "sat",
110
+ };
111
+ // Use cashu-ts to receive/unlock the P2PK-locked token
112
+ const cashuWallet = new Wallet(mint);
113
+ await cashuWallet.loadMint();
114
+ // Receive the token using the new wallet.ops API
115
+ // This will swap P2PK-locked proofs with unlocked proofs
116
+ let receivedProofs;
117
+ try {
118
+ receivedProofs = await cashuWallet.ops.receive(token).privkey(privkeyHex).run();
119
+ }
120
+ catch (error) {
121
+ throw new Error(`Failed to receive token for mint ${mint}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ // Calculate total amount
124
+ const amount = sumProofs(receivedProofs);
125
+ // Create token event with received unlocked proofs
126
+ const receivedToken = {
127
+ mint,
128
+ proofs: receivedProofs,
129
+ unit: "sat",
130
+ };
131
+ // Store token in couch immediately after receiving it
132
+ const clearStoredToken = await couch?.store(receivedToken);
133
+ if (clearStoredToken) {
134
+ clearMethods.push(clearStoredToken);
135
+ }
136
+ const tokenEvent = await factory.create(WalletTokenBlueprint, receivedToken, []).then(sign);
137
+ // Create history event marking nutzap events as redeemed
138
+ const nutzapIds = mintNutzaps.map((n) => n.id);
139
+ const history = await factory
140
+ .create(WalletHistoryBlueprint, { direction: "in", amount, mint, created: [tokenEvent.id] }, nutzapIds)
141
+ .then(sign);
142
+ // Publish events
143
+ await publish(tokenEvent, wallet.relays);
144
+ await publish(history, wallet.relays);
145
+ }
146
+ // Clear all stored tokens from the couch after successfully publishing all events for all mints
147
+ await Promise.all(clearMethods.map((clear) => clear()));
148
+ }
149
+ catch (error) {
150
+ // If an error occurs, don't clear the couch (tokens remain for recovery)
151
+ throw error;
152
+ }
153
+ };
154
+ }
@@ -1,17 +1,87 @@
1
- import { Token } from "@cashu/cashu-ts";
1
+ import { Proof, Token, Wallet } from "@cashu/cashu-ts";
2
2
  import { Action } from "applesauce-actions";
3
3
  import { NostrEvent } from "applesauce-core/helpers/event";
4
+ import { Couch } from "../helpers/couch.js";
5
+ import "../casts/__register__.js";
4
6
  /**
5
- * Adds a cashu token to the wallet and marks a list of nutzaps as redeemed
7
+ * Adds a cashu token to the wallet and creates a history event
6
8
  * @param token the cashu token to add
7
- * @param redeemed an array of nutzap event ids to mark as redeemed
9
+ * @param redeemed an array of event ids to mark as redeemed
8
10
  */
9
- export declare function ReceiveToken(token: Token, redeemed?: string[], fee?: number): Action;
11
+ export declare function AddToken(token: Token, options?: {
12
+ redeemed?: string[];
13
+ fee?: number;
14
+ addHistory?: boolean;
15
+ }): Action;
16
+ /** Similar to the AddToken action but swaps the tokens before receiving them */
17
+ export declare function ReceiveToken(token: Token, options?: {
18
+ addHistory?: boolean;
19
+ couch?: Couch;
20
+ }): Action;
10
21
  /** An action that deletes old tokens and creates a new one but does not add a history event */
11
22
  export declare function RolloverTokens(tokens: NostrEvent[], token: Token): Action;
12
23
  /** An action that deletes old token events and adds a spend history item */
13
- export declare function CompleteSpend(spent: NostrEvent[], change: Token): Action;
24
+ export declare function CompleteSpend(spent: NostrEvent[], change: Token, couch?: Couch): Action;
14
25
  /** Combines all unlocked token events into a single event per mint */
15
- export declare function ConsolidateTokens(opts?: {
16
- ignoreLocked?: boolean;
26
+ export declare function ConsolidateTokens(options?: {
27
+ unlockTokens?: boolean;
28
+ }): Action;
29
+ /**
30
+ * Recovers tokens from a couch by checking if they exist in the wallet,
31
+ * verifying they are unspent, and creating token events for any recoverable tokens
32
+ * @param couch the couch interface to recover tokens from
33
+ */
34
+ export declare function RecoverFromCouch(couch: Couch): Action;
35
+ /**
36
+ * Token selection function type that matches dumbTokenSelection signature.
37
+ * Must return tokens from a single mint and ensure all selected tokens are from that mint.
38
+ * If mint is undefined, the function should find a mint with sufficient balance.
39
+ */
40
+ export type TokenSelectionFunction = (tokens: NostrEvent[], minAmount: number, mint?: string) => {
41
+ events: NostrEvent[];
42
+ proofs: Proof[];
43
+ };
44
+ /**
45
+ * A generic action that safely selects tokens, performs an async operation, and handles change.
46
+ * This action requires a couch for safety - tokens are stored in the couch before the operation
47
+ * and can be recovered if something goes wrong.
48
+ *
49
+ * @param minAmount The minimum amount of tokens to select (in sats)
50
+ * @param operation An async function that receives selected proofs and performs the operation.
51
+ * Should return any change proofs. All selected proofs are considered used.
52
+ * @param options Configuration options including mint filter, required couch, and optional custom token selection
53
+ *
54
+ * @example
55
+ * // Use with NutzapProfile
56
+ * await run(TokensOperation, 100, async ({ selectedProofs, mint, cashuWallet }) => {
57
+ * const { keep, send } = await cashuWallet.ops.send(100, selectedProofs).asP2PK({ pubkey }).run();
58
+ * await run(NutzapProfile, recipient, { mint, proofs: send, unit: "sat" });
59
+ * return { change: keep };
60
+ * }, { couch });
61
+ *
62
+ * @example
63
+ * // Use with melt
64
+ * await run(TokensOperation, meltAmount + feeReserve, async ({ selectedProofs, mint, cashuWallet }) => {
65
+ * const meltQuote = await cashuWallet.createMeltQuoteBolt11(invoice);
66
+ * const { keep, send } = await cashuWallet.send(meltAmount + meltQuote.fee_reserve, selectedProofs, { includeFees: true });
67
+ * const meltResponse = await cashuWallet.meltProofs(meltQuote, send);
68
+ * return { change: meltResponse.change };
69
+ * }, { couch });
70
+ *
71
+ * @example
72
+ * // Use with custom token selection
73
+ * await run(TokensOperation, 100, async ({ selectedProofs, mint, cashuWallet }) => {
74
+ * // ... operation
75
+ * }, { couch, tokenSelection: myCustomSelectionFunction });
76
+ */
77
+ export declare function TokensOperation(minAmount: number, operation: (params: {
78
+ selectedProofs: Proof[];
79
+ mint: string;
80
+ cashuWallet: Wallet;
81
+ }) => Promise<{
82
+ change?: Proof[];
83
+ }>, options: {
84
+ mint?: string;
85
+ couch: Couch;
86
+ tokenSelection?: TokenSelectionFunction;
17
87
  }): Action;