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.
- package/dist/actions/common.d.ts +4 -0
- package/dist/actions/common.js +15 -0
- package/dist/actions/index.d.ts +3 -2
- package/dist/actions/index.js +3 -2
- package/dist/actions/mint-recomendation.d.ts +30 -0
- package/dist/actions/mint-recomendation.js +96 -0
- package/dist/actions/{zap-info.d.ts → nutzap-info.d.ts} +20 -3
- package/dist/actions/nutzap-info.js +117 -0
- package/dist/actions/nutzaps.d.ts +24 -0
- package/dist/actions/nutzaps.js +154 -0
- package/dist/actions/tokens.d.ts +77 -7
- package/dist/actions/tokens.js +332 -69
- package/dist/actions/wallet.d.ts +18 -3
- package/dist/actions/wallet.js +74 -32
- package/dist/blueprints/history.d.ts +1 -1
- package/dist/blueprints/history.js +1 -1
- package/dist/blueprints/index.d.ts +1 -0
- package/dist/blueprints/index.js +1 -0
- package/dist/blueprints/mint-recommendation.d.ts +16 -0
- package/dist/blueprints/mint-recommendation.js +11 -0
- package/dist/blueprints/wallet.d.ts +5 -1
- package/dist/blueprints/wallet.js +6 -3
- package/dist/casts/__register__.d.ts +22 -0
- package/dist/casts/__register__.js +52 -0
- package/dist/casts/index.d.ts +8 -0
- package/dist/casts/index.js +8 -0
- package/dist/casts/mint-info.d.ts +18 -0
- package/dist/casts/mint-info.js +42 -0
- package/dist/casts/mint-recommendation.d.ts +16 -0
- package/dist/casts/mint-recommendation.js +29 -0
- package/dist/casts/nutzap-info.d.ts +14 -0
- package/dist/casts/nutzap-info.js +22 -0
- package/dist/casts/nutzap.d.ts +16 -0
- package/dist/casts/nutzap.js +37 -0
- package/dist/casts/wallet-history.d.ts +16 -0
- package/dist/casts/wallet-history.js +40 -0
- package/dist/casts/wallet-token.d.ts +29 -0
- package/dist/casts/wallet-token.js +52 -0
- package/dist/casts/wallet.d.ts +27 -0
- package/dist/casts/wallet.js +62 -0
- package/dist/helpers/cashu.d.ts +21 -0
- package/dist/helpers/cashu.js +105 -0
- package/dist/helpers/couch.d.ts +11 -0
- package/dist/helpers/couch.js +1 -0
- package/dist/helpers/history.d.ts +5 -1
- package/dist/helpers/history.js +13 -4
- package/dist/helpers/index.d.ts +7 -1
- package/dist/helpers/index.js +7 -1
- package/dist/helpers/indexed-db-couch.d.ts +34 -0
- package/dist/helpers/indexed-db-couch.js +119 -0
- package/dist/helpers/local-storage-couch.d.ts +29 -0
- package/dist/helpers/local-storage-couch.js +78 -0
- package/dist/helpers/mint-info.d.ts +40 -0
- package/dist/helpers/mint-info.js +80 -0
- package/dist/helpers/mint-recommendation.d.ts +41 -0
- package/dist/helpers/mint-recommendation.js +54 -0
- package/dist/helpers/{zap-info.d.ts → nutzap-info.d.ts} +10 -1
- package/dist/helpers/{zap-info.js → nutzap-info.js} +22 -10
- package/dist/helpers/nutzap.d.ts +15 -0
- package/dist/helpers/nutzap.js +57 -3
- package/dist/helpers/tokens.d.ts +9 -18
- package/dist/helpers/tokens.js +64 -94
- package/dist/helpers/wallet.d.ts +16 -6
- package/dist/helpers/wallet.js +40 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/history.d.ts +1 -1
- package/dist/models/history.js +7 -10
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.js +0 -1
- package/dist/models/nutzap.d.ts +2 -0
- package/dist/models/nutzap.js +8 -0
- package/dist/models/tokens.d.ts +2 -2
- package/dist/models/tokens.js +14 -17
- package/dist/operations/history.js +1 -1
- package/dist/operations/index.d.ts +2 -1
- package/dist/operations/index.js +2 -1
- package/dist/operations/mint-recommendation.d.ts +13 -0
- package/dist/operations/mint-recommendation.js +26 -0
- package/dist/operations/nutzap-info.d.ts +21 -0
- package/dist/operations/nutzap-info.js +71 -0
- package/dist/operations/wallet.d.ts +10 -1
- package/dist/operations/wallet.js +33 -3
- package/package.json +37 -28
- package/dist/actions/zap-info.js +0 -83
- package/dist/actions/zaps.d.ts +0 -8
- package/dist/actions/zaps.js +0 -30
- package/dist/models/wallet.d.ts +0 -13
- package/dist/models/wallet.js +0 -21
- package/dist/operations/zap-info.d.ts +0 -10
- package/dist/operations/zap-info.js +0 -17
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getAddressPointerFromATag, getOrComputeCachedValue, getTagValue, isATag, isHex, } from "applesauce-core/helpers";
|
|
2
|
+
import { getReplaceableIdentifier } from "applesauce-core/helpers/event";
|
|
3
|
+
import { CASHU_MINT_INFO_KIND } from "./mint-info.js";
|
|
4
|
+
// NIP-87 Recommendation Event Kind
|
|
5
|
+
export const MINT_RECOMMENDATION_KIND = 38000;
|
|
6
|
+
// Symbols for caching computed values
|
|
7
|
+
export const RecommendationKindSymbol = Symbol.for("mint-recommendation-kind");
|
|
8
|
+
export const RecommendationURLSymbol = Symbol.for("mint-recommendation-url");
|
|
9
|
+
export const RecommendationAddressPointerSymbol = Symbol.for("mint-recommendation-address-pointer");
|
|
10
|
+
export function getRecommendationKind(event) {
|
|
11
|
+
return getOrComputeCachedValue(event, RecommendationKindSymbol, () => {
|
|
12
|
+
const kindStr = getTagValue(event, "k");
|
|
13
|
+
if (!kindStr)
|
|
14
|
+
return undefined;
|
|
15
|
+
const kind = parseInt(kindStr, 10);
|
|
16
|
+
return isNaN(kind) ? undefined : kind;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns the d-identifier from a kind:38000 recommendation event
|
|
21
|
+
* This is the kind:38172 event identifier this event is recommending
|
|
22
|
+
*/
|
|
23
|
+
export function getRecommendationMintPubkey(event) {
|
|
24
|
+
const identifier = getReplaceableIdentifier(event);
|
|
25
|
+
if (!isHex(identifier))
|
|
26
|
+
return undefined;
|
|
27
|
+
return identifier || undefined;
|
|
28
|
+
}
|
|
29
|
+
export function getRecommendationURL(event) {
|
|
30
|
+
return getOrComputeCachedValue(event, RecommendationURLSymbol, () => {
|
|
31
|
+
return getTagValue(event, "u");
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function getRecommendationAddressPointer(event) {
|
|
35
|
+
return getOrComputeCachedValue(event, RecommendationAddressPointerSymbol, () => {
|
|
36
|
+
const tag = event.tags.find(isATag);
|
|
37
|
+
return tag ? (getAddressPointerFromATag(tag) ?? undefined) : undefined;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates that an event is a proper kind:38000 recommendation event
|
|
42
|
+
* Checks that the event is kind 38000, has the required `k` and `d` tags,
|
|
43
|
+
* and that the `k` tag is 38172 (cashu mint kind)
|
|
44
|
+
* Each recommendation event recommends a single cashu mint
|
|
45
|
+
*/
|
|
46
|
+
export function isValidMintRecommendation(event) {
|
|
47
|
+
if (!event)
|
|
48
|
+
return false;
|
|
49
|
+
if (event.kind !== MINT_RECOMMENDATION_KIND)
|
|
50
|
+
return false;
|
|
51
|
+
const kind = getRecommendationKind(event);
|
|
52
|
+
// Only cashu mints (kind 38172) are supported
|
|
53
|
+
return kind === CASHU_MINT_INFO_KIND;
|
|
54
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Proof } from "@cashu/cashu-ts";
|
|
2
|
-
import {
|
|
2
|
+
import { NameValueTag } from "applesauce-core/helpers";
|
|
3
|
+
import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
|
|
3
4
|
export declare const NUTZAP_INFO_KIND = 10019;
|
|
5
|
+
/** Validated nutzap info event */
|
|
6
|
+
export type NutzapInfoEvent = KnownEvent<typeof NUTZAP_INFO_KIND>;
|
|
7
|
+
/** Checks if an event is a valid nutzap info event */
|
|
8
|
+
export declare function isValidNutzapInfo(event: NostrEvent): event is NutzapInfoEvent;
|
|
4
9
|
export declare const NutzapMintsSymbol: unique symbol;
|
|
5
10
|
export declare const NutzapRelaysSymbol: unique symbol;
|
|
6
11
|
/** Returns the relay URLs from a kind:10019 nutzap info event */
|
|
@@ -12,8 +17,12 @@ export declare function getNutzapInfoMints(event: NostrEvent): {
|
|
|
12
17
|
}[];
|
|
13
18
|
/** Returns the pubkey for P2PK-locking from a kind:10019 nutzap info event */
|
|
14
19
|
export declare function getNutzapInfoPubkey(event: NostrEvent): string | undefined;
|
|
20
|
+
/** Returns the P2PK pubkey from a kind:10019 nutzap info event */
|
|
21
|
+
export declare function getNutzapInfoP2PKPubkey(event: NostrEvent): string | undefined;
|
|
15
22
|
/**
|
|
16
23
|
* verfies if proofs are locked to nutzap info
|
|
17
24
|
* @throws {Error} if proofs are not locked to nutzap info
|
|
18
25
|
*/
|
|
19
26
|
export declare function verifyProofsLocked(proofs: Proof[], info: NostrEvent): void;
|
|
27
|
+
/** Creates a pubkey tag for a kind:10019 nutzap info event from a private key */
|
|
28
|
+
export declare function createNutzapInfoPubkeyTag(key: Uint8Array): NameValueTag<"pubkey">;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { getOrComputeCachedValue,
|
|
1
|
+
import { getOrComputeCachedValue, getPublicKey, mergeRelaySets } from "applesauce-core/helpers";
|
|
2
|
+
import { getProofP2PKPubkey } from "./cashu.js";
|
|
2
3
|
export const NUTZAP_INFO_KIND = 10019;
|
|
4
|
+
/** Checks if an event is a valid nutzap info event */
|
|
5
|
+
export function isValidNutzapInfo(event) {
|
|
6
|
+
return event.kind === NUTZAP_INFO_KIND;
|
|
7
|
+
}
|
|
3
8
|
// Symbols for caching computed values
|
|
4
9
|
export const NutzapMintsSymbol = Symbol.for("nutzap-mints");
|
|
5
10
|
export const NutzapRelaysSymbol = Symbol.for("nutzap-relays");
|
|
@@ -17,7 +22,14 @@ export function getNutzapInfoMints(event) {
|
|
|
17
22
|
}
|
|
18
23
|
/** Returns the pubkey for P2PK-locking from a kind:10019 nutzap info event */
|
|
19
24
|
export function getNutzapInfoPubkey(event) {
|
|
20
|
-
|
|
25
|
+
const pubkey = event.tags.find((t) => t[0] === "pubkey")?.[1];
|
|
26
|
+
if (!pubkey)
|
|
27
|
+
return undefined;
|
|
28
|
+
return pubkey.length === 64 ? `02${pubkey}` : pubkey;
|
|
29
|
+
}
|
|
30
|
+
/** Returns the P2PK pubkey from a kind:10019 nutzap info event */
|
|
31
|
+
export function getNutzapInfoP2PKPubkey(event) {
|
|
32
|
+
return getNutzapInfoPubkey(event);
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* verfies if proofs are locked to nutzap info
|
|
@@ -27,16 +39,16 @@ export function verifyProofsLocked(proofs, info) {
|
|
|
27
39
|
const pubkey = getNutzapInfoPubkey(info);
|
|
28
40
|
if (!pubkey)
|
|
29
41
|
throw new Error("Nutzap info must have a pubkey");
|
|
30
|
-
const fullPubkey = pubkey.length === 64 ? `02${pubkey}` : pubkey;
|
|
31
42
|
for (const proof of proofs) {
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
34
|
-
throw new Error(`Cashu token must have a spending condition`);
|
|
35
|
-
if (!Array.isArray(secret))
|
|
36
|
-
throw new Error("Invalid spending condition");
|
|
37
|
-
if (secret[0] !== "P2PK")
|
|
43
|
+
const proofPubkey = getProofP2PKPubkey(proof);
|
|
44
|
+
if (!proofPubkey)
|
|
38
45
|
throw new Error("Token proofs must be P2PK locked");
|
|
39
|
-
if (
|
|
46
|
+
if (proofPubkey !== pubkey)
|
|
40
47
|
throw new Error("Token proofs must be P2PK locked to the recipient's nutzap pubkey");
|
|
41
48
|
}
|
|
42
49
|
}
|
|
50
|
+
/** Creates a pubkey tag for a kind:10019 nutzap info event from a private key */
|
|
51
|
+
export function createNutzapInfoPubkeyTag(key) {
|
|
52
|
+
const pubkey = "02" + getPublicKey(key);
|
|
53
|
+
return ["pubkey", pubkey];
|
|
54
|
+
}
|
package/dist/helpers/nutzap.d.ts
CHANGED
|
@@ -31,3 +31,18 @@ export declare function getNutzapAmount(event: NostrEvent): number | undefined;
|
|
|
31
31
|
export declare function isValidNutzap(nutzap: NostrEvent): nutzap is NutzapEvent;
|
|
32
32
|
/** Checks if a nutzap event has already been redeemed based on kind:7376 wallet history events */
|
|
33
33
|
export declare function isNutzapRedeemed(nutzapId: string, history: NostrEvent[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Extracts the P2PK locking pubkey from proofs in a nutzap event
|
|
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
|
|
39
|
+
*/
|
|
40
|
+
export declare function getNutzapP2PKPubkey(nutzap: NostrEvent): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Finds the matching private key for the P2PK lock in a nutzap event's proofs
|
|
43
|
+
* @param nutzap the nutzap event containing P2PK-locked proofs
|
|
44
|
+
* @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
|
|
47
|
+
*/
|
|
48
|
+
export declare function findMatchingPrivateKeyForNutzap(nutzap: NostrEvent, privateKeys: Uint8Array[]): Uint8Array | undefined;
|
package/dist/helpers/nutzap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getAddressPointerFromATag, getEventPointerFromETag, getOrComputeCachedValue, getProfilePointerFromPTag, getTagValue, isPTag, processTags, safeParse, } from "applesauce-core/helpers";
|
|
1
|
+
import { getAddressPointerFromATag, getEventPointerFromETag, getOrComputeCachedValue, getProfilePointerFromPTag, getPublicKey, getTagValue, isPTag, processTags, safeParse, } from "applesauce-core/helpers";
|
|
2
2
|
import { getHistoryRedeemed } from "./history.js";
|
|
3
|
+
import { getProofP2PKPubkey } from "./cashu.js";
|
|
3
4
|
export const NUTZAP_KIND = 9321;
|
|
4
5
|
// Symbols for caching computed values
|
|
5
6
|
export const NutzapProofsSymbol = Symbol.for("nutzap-proofs");
|
|
@@ -54,8 +55,6 @@ export function isValidNutzap(nutzap) {
|
|
|
54
55
|
if (nutzap.kind !== NUTZAP_KIND)
|
|
55
56
|
return false;
|
|
56
57
|
// Check if the nutzap has a mint, recipient, and proofs
|
|
57
|
-
if (getNutzapPointer(nutzap) === undefined)
|
|
58
|
-
return false;
|
|
59
58
|
if (getNutzapMint(nutzap) === undefined)
|
|
60
59
|
return false;
|
|
61
60
|
if (getNutzapRecipient(nutzap) === undefined)
|
|
@@ -68,3 +67,58 @@ export function isValidNutzap(nutzap) {
|
|
|
68
67
|
export function isNutzapRedeemed(nutzapId, history) {
|
|
69
68
|
return history.some((entry) => getHistoryRedeemed(entry).includes(nutzapId));
|
|
70
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Extracts the P2PK locking pubkey from proofs in a nutzap event
|
|
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
|
|
75
|
+
*/
|
|
76
|
+
export function getNutzapP2PKPubkey(nutzap) {
|
|
77
|
+
const proofs = getNutzapProofs(nutzap);
|
|
78
|
+
if (proofs.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
let p2pkPubkey;
|
|
81
|
+
for (const proof of proofs) {
|
|
82
|
+
const proofPubkey = getProofP2PKPubkey(proof);
|
|
83
|
+
if (!proofPubkey)
|
|
84
|
+
throw new Error("Proof is not P2PK locked");
|
|
85
|
+
if (!p2pkPubkey) {
|
|
86
|
+
p2pkPubkey = proofPubkey;
|
|
87
|
+
}
|
|
88
|
+
else if (p2pkPubkey !== proofPubkey) {
|
|
89
|
+
throw new Error("Proofs are locked to different pubkeys");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return p2pkPubkey;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Finds the matching private key for the P2PK lock in a nutzap event's proofs
|
|
96
|
+
* @param nutzap the nutzap event containing P2PK-locked proofs
|
|
97
|
+
* @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
|
|
100
|
+
*/
|
|
101
|
+
export function findMatchingPrivateKeyForNutzap(nutzap, privateKeys) {
|
|
102
|
+
const p2pkPubkey = getNutzapP2PKPubkey(nutzap);
|
|
103
|
+
if (!p2pkPubkey)
|
|
104
|
+
return undefined;
|
|
105
|
+
// Normalize target pubkey to full format (with 02 prefix) for comparison
|
|
106
|
+
// getNutzapP2PKPubkey already normalizes to full format, so p2pkPubkey is 66 chars
|
|
107
|
+
const targetPubkeyFull = p2pkPubkey;
|
|
108
|
+
const targetPubkeyXOnly = p2pkPubkey.length === 66 ? p2pkPubkey.slice(2) : p2pkPubkey;
|
|
109
|
+
for (const privateKey of privateKeys) {
|
|
110
|
+
try {
|
|
111
|
+
// Derive public key from private key (returns 64-char x-only format)
|
|
112
|
+
const derivedPubkey = getPublicKey(privateKey);
|
|
113
|
+
// Compare: derived pubkey is x-only, so compare both formats
|
|
114
|
+
if (derivedPubkey === targetPubkeyXOnly || `02${derivedPubkey}` === targetPubkeyFull) {
|
|
115
|
+
return privateKey;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// Skip invalid private keys
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
package/dist/helpers/tokens.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Proof
|
|
1
|
+
import { Proof } from "@cashu/cashu-ts";
|
|
2
2
|
import { HiddenContentSigner, UnlockedHiddenContent } from "applesauce-core/helpers";
|
|
3
|
-
import { NostrEvent } from "applesauce-core/helpers/event";
|
|
3
|
+
import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
|
|
4
4
|
export declare const WALLET_TOKEN_KIND = 7375;
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
7
|
-
/**
|
|
8
|
-
export declare function
|
|
5
|
+
/** Validated wallet token event */
|
|
6
|
+
export type WalletTokenEvent = KnownEvent<typeof WALLET_TOKEN_KIND>;
|
|
7
|
+
/** Checks if an event is a valid wallet token event */
|
|
8
|
+
export declare function isValidWalletToken(event: NostrEvent): event is WalletTokenEvent;
|
|
9
9
|
export type TokenContent = {
|
|
10
10
|
/** Cashu mint for the proofs */
|
|
11
11
|
mint: string;
|
|
@@ -27,14 +27,14 @@ export declare const TokenContentSymbol: unique symbol;
|
|
|
27
27
|
export type UnlockedTokenContent = UnlockedHiddenContent & {
|
|
28
28
|
[TokenContentSymbol]: TokenContent;
|
|
29
29
|
};
|
|
30
|
+
/** Returns if token details are locked */
|
|
31
|
+
export declare function isTokenContentUnlocked<T extends NostrEvent>(token: T): token is T & UnlockedTokenContent;
|
|
30
32
|
/**
|
|
31
33
|
* Returns the decrypted and parsed details of a 7375 token event
|
|
32
34
|
* @throws {Error} If the token content is invalid
|
|
33
35
|
*/
|
|
34
36
|
export declare function getTokenContent(token: UnlockedTokenContent): TokenContent;
|
|
35
37
|
export declare function getTokenContent(token: NostrEvent): TokenContent | undefined;
|
|
36
|
-
/** Returns if token details are locked */
|
|
37
|
-
export declare function isTokenContentUnlocked<T extends NostrEvent>(token: T): token is T & UnlockedTokenContent;
|
|
38
38
|
/** Decrypts a k:7375 token event */
|
|
39
39
|
export declare function unlockTokenContent(token: NostrEvent, signer: HiddenContentSigner): Promise<TokenContent>;
|
|
40
40
|
/** Removes the unencrypted hidden content */
|
|
@@ -47,19 +47,10 @@ export declare function getTokenProofsTotal(token: UnlockedTokenContent): number
|
|
|
47
47
|
export declare function getTokenProofsTotal(token: NostrEvent): number | undefined;
|
|
48
48
|
/**
|
|
49
49
|
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
50
|
+
* If mint is undefined, finds a mint with sufficient balance and selects only from that mint
|
|
50
51
|
* @throws {Error} If there are insufficient funds
|
|
51
52
|
*/
|
|
52
53
|
export declare function dumbTokenSelection(tokens: NostrEvent[], minAmount: number, mint?: string): {
|
|
53
54
|
events: NostrEvent[];
|
|
54
55
|
proofs: Proof[];
|
|
55
56
|
};
|
|
56
|
-
/**
|
|
57
|
-
* Returns a decoded cashu token inside an unicode emoji
|
|
58
|
-
* @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
|
|
59
|
-
*/
|
|
60
|
-
export declare function decodeTokenFromEmojiString(str: string): Token | undefined;
|
|
61
|
-
/**
|
|
62
|
-
* Encodes a token into an emoji char
|
|
63
|
-
* @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/SendTokenDialog.vue#L710
|
|
64
|
-
*/
|
|
65
|
-
export declare function encodeTokenToEmoji(token: Token | string, emoji?: string): string;
|
package/dist/helpers/tokens.js
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
import { getDecodedToken, getEncodedToken } from "@cashu/cashu-ts";
|
|
2
1
|
import { getHiddenContent, isHiddenContentUnlocked, lockHiddenContent, notifyEventUpdate, setHiddenContentEncryptionMethod, unlockHiddenContent, } from "applesauce-core/helpers";
|
|
2
|
+
import { ignoreDuplicateProofs } from "./cashu.js";
|
|
3
3
|
export const WALLET_TOKEN_KIND = 7375;
|
|
4
|
+
/** Checks if an event is a valid wallet token event */
|
|
5
|
+
export function isValidWalletToken(event) {
|
|
6
|
+
return event.kind === WALLET_TOKEN_KIND;
|
|
7
|
+
}
|
|
4
8
|
// Enable hidden content for wallet token kind
|
|
5
9
|
setHiddenContentEncryptionMethod(WALLET_TOKEN_KIND, "nip44");
|
|
6
|
-
/** Internal method for creating a unique id for each proof */
|
|
7
|
-
export function getProofUID(proof) {
|
|
8
|
-
return proof.id + proof.amount + proof.C + proof.secret;
|
|
9
|
-
}
|
|
10
|
-
/** Internal method to filter out duplicate proofs */
|
|
11
|
-
export function ignoreDuplicateProofs(seen = new Set()) {
|
|
12
|
-
return (proof) => {
|
|
13
|
-
const id = getProofUID(proof);
|
|
14
|
-
if (seen.has(id))
|
|
15
|
-
return false;
|
|
16
|
-
else {
|
|
17
|
-
seen.add(id);
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
10
|
/** Symbol for caching token content */
|
|
23
11
|
export const TokenContentSymbol = Symbol.for("token-content");
|
|
12
|
+
/** Returns if token details are locked */
|
|
13
|
+
export function isTokenContentUnlocked(token) {
|
|
14
|
+
// Wrap in try catch to avoid throwing validation errors
|
|
15
|
+
try {
|
|
16
|
+
return TokenContentSymbol in token || (isHiddenContentUnlocked(token) && getTokenContent(token) !== undefined);
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
24
21
|
export function getTokenContent(token) {
|
|
25
|
-
if (
|
|
22
|
+
if (TokenContentSymbol in token)
|
|
26
23
|
return token[TokenContentSymbol];
|
|
27
24
|
// Get the hidden content
|
|
28
25
|
const plaintext = getHiddenContent(token);
|
|
@@ -41,13 +38,9 @@ export function getTokenContent(token) {
|
|
|
41
38
|
Reflect.set(token, TokenContentSymbol, details);
|
|
42
39
|
return details;
|
|
43
40
|
}
|
|
44
|
-
/** Returns if token details are locked */
|
|
45
|
-
export function isTokenContentUnlocked(token) {
|
|
46
|
-
return isHiddenContentUnlocked(token) && Reflect.has(token, TokenContentSymbol) === true;
|
|
47
|
-
}
|
|
48
41
|
/** Decrypts a k:7375 token event */
|
|
49
42
|
export async function unlockTokenContent(token, signer) {
|
|
50
|
-
if (
|
|
43
|
+
if (TokenContentSymbol in token)
|
|
51
44
|
return token[TokenContentSymbol];
|
|
52
45
|
// Unlock the hidden content
|
|
53
46
|
await unlockHiddenContent(token, signer);
|
|
@@ -72,12 +65,54 @@ export function getTokenProofsTotal(token) {
|
|
|
72
65
|
}
|
|
73
66
|
/**
|
|
74
67
|
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
68
|
+
* If mint is undefined, finds a mint with sufficient balance and selects only from that mint
|
|
75
69
|
* @throws {Error} If there are insufficient funds
|
|
76
70
|
*/
|
|
77
71
|
export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
78
|
-
//
|
|
72
|
+
// If mint is not specified, find a mint with sufficient balance
|
|
73
|
+
let targetMint = mint;
|
|
74
|
+
if (!targetMint) {
|
|
75
|
+
// Group tokens by mint and calculate total balance per mint
|
|
76
|
+
const tokensByMint = new Map();
|
|
77
|
+
for (const token of tokens) {
|
|
78
|
+
const content = getTokenContent(token);
|
|
79
|
+
if (!content)
|
|
80
|
+
continue; // Skip locked tokens
|
|
81
|
+
const tokenMint = content.mint;
|
|
82
|
+
if (!tokenMint)
|
|
83
|
+
continue;
|
|
84
|
+
if (!tokensByMint.has(tokenMint)) {
|
|
85
|
+
tokensByMint.set(tokenMint, []);
|
|
86
|
+
}
|
|
87
|
+
tokensByMint.get(tokenMint).push(token);
|
|
88
|
+
}
|
|
89
|
+
// Find a mint with sufficient balance
|
|
90
|
+
let foundMint;
|
|
91
|
+
for (const [mintKey, mintTokens] of tokensByMint) {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const token of mintTokens) {
|
|
95
|
+
const content = getTokenContent(token);
|
|
96
|
+
if (!content)
|
|
97
|
+
continue;
|
|
98
|
+
const proofs = content.proofs.filter(ignoreDuplicateProofs(seen));
|
|
99
|
+
total += proofs.reduce((t, p) => t + p.amount, 0);
|
|
100
|
+
}
|
|
101
|
+
if (total >= minAmount) {
|
|
102
|
+
foundMint = mintKey;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!foundMint)
|
|
107
|
+
throw new Error("Insufficient funds in any mint");
|
|
108
|
+
targetMint = foundMint;
|
|
109
|
+
}
|
|
110
|
+
// Filter tokens by the target mint and sort newest to oldest
|
|
79
111
|
const sorted = tokens
|
|
80
|
-
.filter((token) =>
|
|
112
|
+
.filter((token) => {
|
|
113
|
+
const content = getTokenContent(token);
|
|
114
|
+
return content?.mint === targetMint;
|
|
115
|
+
})
|
|
81
116
|
.sort((a, b) => b.created_at - a.created_at);
|
|
82
117
|
let amount = 0;
|
|
83
118
|
const seen = new Set();
|
|
@@ -91,6 +126,10 @@ export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
|
91
126
|
// Skip locked tokens
|
|
92
127
|
if (!content)
|
|
93
128
|
continue;
|
|
129
|
+
// Verify mint matches (should always match due to filter, but double-check for safety)
|
|
130
|
+
if (content.mint !== targetMint) {
|
|
131
|
+
throw new Error(`Token mint mismatch: expected ${targetMint}, got ${content.mint}`);
|
|
132
|
+
}
|
|
94
133
|
// Get proofs and total
|
|
95
134
|
const proofs = content.proofs.filter(ignoreDuplicateProofs(seen));
|
|
96
135
|
const total = proofs.reduce((t, p) => t + p.amount, 0);
|
|
@@ -100,72 +139,3 @@ export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
|
100
139
|
}
|
|
101
140
|
return { events: selectedTokens, proofs: selectedProofs };
|
|
102
141
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Returns a decoded cashu token inside an unicode emoji
|
|
105
|
-
* @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/ReceiveTokenDialog.vue#L387
|
|
106
|
-
*/
|
|
107
|
-
export function decodeTokenFromEmojiString(str) {
|
|
108
|
-
try {
|
|
109
|
-
let decoded = [];
|
|
110
|
-
const chars = Array.from(str);
|
|
111
|
-
if (!chars.length)
|
|
112
|
-
return undefined;
|
|
113
|
-
const fromVariationSelector = function (char) {
|
|
114
|
-
const codePoint = char.codePointAt(0);
|
|
115
|
-
if (codePoint === undefined)
|
|
116
|
-
return null;
|
|
117
|
-
// Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F
|
|
118
|
-
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {
|
|
119
|
-
// Maps FE00->0, FE01->1, ..., FE0F->15
|
|
120
|
-
const byteValue = codePoint - 0xfe00;
|
|
121
|
-
return String.fromCharCode(byteValue);
|
|
122
|
-
}
|
|
123
|
-
// Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF
|
|
124
|
-
if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) {
|
|
125
|
-
// Maps E0100->16, E0101->17, ..., E01EF->255
|
|
126
|
-
const byteValue = codePoint - 0xe0100 + 16;
|
|
127
|
-
return String.fromCharCode(byteValue);
|
|
128
|
-
}
|
|
129
|
-
// No Variation Selector
|
|
130
|
-
return null;
|
|
131
|
-
};
|
|
132
|
-
// Check all input chars for peanut data
|
|
133
|
-
for (const char of chars) {
|
|
134
|
-
let byte = fromVariationSelector(char);
|
|
135
|
-
if (byte === null && decoded.length > 0) {
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
else if (byte === null) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
decoded.push(byte); // got some
|
|
142
|
-
}
|
|
143
|
-
// Switch out token if we found peanut data
|
|
144
|
-
let decodedString = decoded.join("");
|
|
145
|
-
return getDecodedToken(decodedString);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Encodes a token into an emoji char
|
|
153
|
-
* @see https://github.com/cashubtc/cashu.me/blob/1194a7b9ee2f43305e38304de7bef8839601ff4d/src/components/SendTokenDialog.vue#L710
|
|
154
|
-
*/
|
|
155
|
-
export function encodeTokenToEmoji(token, emoji = "🥜") {
|
|
156
|
-
return (emoji +
|
|
157
|
-
Array.from(typeof token === "string" ? token : getEncodedToken(token))
|
|
158
|
-
.map((char) => {
|
|
159
|
-
const byteValue = char.charCodeAt(0);
|
|
160
|
-
// For byte values 0-15, use Variation Selectors (VS1-VS16): U+FE00 to U+FE0F
|
|
161
|
-
if (byteValue >= 0 && byteValue <= 15) {
|
|
162
|
-
return String.fromCodePoint(0xfe00 + byteValue);
|
|
163
|
-
}
|
|
164
|
-
// For byte values 16-255, use Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF
|
|
165
|
-
if (byteValue >= 16 && byteValue <= 255) {
|
|
166
|
-
return String.fromCodePoint(0xe0100 + (byteValue - 16));
|
|
167
|
-
}
|
|
168
|
-
return "";
|
|
169
|
-
})
|
|
170
|
-
.join(""));
|
|
171
|
-
}
|
package/dist/helpers/wallet.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { HiddenContentSigner, UnlockedHiddenTags } from "applesauce-core/helpers";
|
|
2
|
-
import { NostrEvent } from "applesauce-core/helpers/event";
|
|
2
|
+
import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
|
|
3
3
|
export declare const WALLET_KIND = 17375;
|
|
4
4
|
export declare const WALLET_BACKUP_KIND = 375;
|
|
5
|
+
/** Validated wallet event */
|
|
6
|
+
export type WalletEvent = KnownEvent<typeof WALLET_KIND>;
|
|
7
|
+
/** Checks if an event is a valid wallet event */
|
|
8
|
+
export declare function isValidWallet(event: NostrEvent): event is WalletEvent;
|
|
5
9
|
export declare const WalletPrivateKeySymbol: unique symbol;
|
|
6
10
|
export declare const WalletMintsSymbol: unique symbol;
|
|
11
|
+
export declare const WalletRelaysSymbol: unique symbol;
|
|
7
12
|
/** Type for unlocked wallet events */
|
|
8
13
|
export type UnlockedWallet = UnlockedHiddenTags & {
|
|
9
|
-
[WalletPrivateKeySymbol]
|
|
10
|
-
[WalletMintsSymbol]
|
|
14
|
+
[WalletPrivateKeySymbol]?: Uint8Array | null;
|
|
15
|
+
[WalletMintsSymbol]?: string[];
|
|
16
|
+
[WalletRelaysSymbol]?: string[];
|
|
11
17
|
};
|
|
12
18
|
/** Returns if a wallet is unlocked */
|
|
13
19
|
export declare function isWalletUnlocked<T extends NostrEvent>(wallet: T): wallet is T & UnlockedWallet;
|
|
@@ -15,12 +21,16 @@ export declare function isWalletUnlocked<T extends NostrEvent>(wallet: T): walle
|
|
|
15
21
|
export declare function getWalletMints(wallet: UnlockedWallet): string[];
|
|
16
22
|
export declare function getWalletMints(wallet: NostrEvent): string[];
|
|
17
23
|
/** Returns the wallets private key as a string */
|
|
18
|
-
export declare function getWalletPrivateKey(wallet: UnlockedWallet): Uint8Array;
|
|
19
|
-
export declare function getWalletPrivateKey(wallet: NostrEvent): Uint8Array | undefined;
|
|
24
|
+
export declare function getWalletPrivateKey(wallet: UnlockedWallet): Uint8Array | null;
|
|
25
|
+
export declare function getWalletPrivateKey(wallet: NostrEvent): Uint8Array | undefined | null;
|
|
26
|
+
/** Returns the wallets relays */
|
|
27
|
+
export declare function getWalletRelays(wallet: UnlockedWallet): string[];
|
|
28
|
+
export declare function getWalletRelays(wallet: NostrEvent): string[] | undefined;
|
|
20
29
|
/** Unlocks a wallet and returns the hidden tags */
|
|
21
30
|
export declare function unlockWallet(wallet: NostrEvent, signer: HiddenContentSigner): Promise<{
|
|
22
31
|
mints: string[];
|
|
23
|
-
privateKey
|
|
32
|
+
privateKey: Uint8Array | null;
|
|
33
|
+
relays: string[];
|
|
24
34
|
}>;
|
|
25
35
|
/** Locks a wallet event */
|
|
26
36
|
export declare function lockWallet(wallet: NostrEvent): void;
|
package/dist/helpers/wallet.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getHiddenTags, hasHiddenTags, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, relaySet, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
|
|
2
|
+
import { hexToBytes } from "applesauce-core/helpers/event";
|
|
3
3
|
export const WALLET_KIND = 17375;
|
|
4
4
|
export const WALLET_BACKUP_KIND = 375;
|
|
5
|
+
/** Checks if an event is a valid wallet event */
|
|
6
|
+
export function isValidWallet(event) {
|
|
7
|
+
return event.kind === WALLET_KIND && hasHiddenTags(event);
|
|
8
|
+
}
|
|
5
9
|
// Enable hidden content for wallet kinds
|
|
6
10
|
setHiddenTagsEncryptionMethod(WALLET_KIND, "nip44");
|
|
7
11
|
setHiddenTagsEncryptionMethod(WALLET_BACKUP_KIND, "nip44");
|
|
8
12
|
export const WalletPrivateKeySymbol = Symbol.for("wallet-private-key");
|
|
9
13
|
export const WalletMintsSymbol = Symbol.for("wallet-mints");
|
|
14
|
+
export const WalletRelaysSymbol = Symbol.for("wallet-relays");
|
|
10
15
|
/** Returns if a wallet is unlocked */
|
|
11
16
|
export function isWalletUnlocked(wallet) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Reflect.has(wallet, WalletMintsSymbol));
|
|
17
|
+
// No need for try catch or proactivly parsing here since it only depends on hidden tags
|
|
18
|
+
return isHiddenTagsUnlocked(wallet);
|
|
15
19
|
}
|
|
16
20
|
export function getWalletMints(wallet) {
|
|
17
21
|
// Return cached value if it exists
|
|
18
|
-
if (
|
|
19
|
-
return
|
|
22
|
+
if (WalletMintsSymbol in wallet)
|
|
23
|
+
return wallet[WalletMintsSymbol];
|
|
20
24
|
// Get hidden tags
|
|
21
25
|
const tags = getHiddenTags(wallet);
|
|
22
26
|
if (!tags)
|
|
@@ -28,33 +32,55 @@ export function getWalletMints(wallet) {
|
|
|
28
32
|
return mints;
|
|
29
33
|
}
|
|
30
34
|
export function getWalletPrivateKey(wallet) {
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
35
|
+
if (WalletPrivateKeySymbol in wallet)
|
|
36
|
+
return wallet[WalletPrivateKeySymbol];
|
|
33
37
|
// Get hidden tags
|
|
34
38
|
const tags = getHiddenTags(wallet);
|
|
35
39
|
if (!tags)
|
|
36
40
|
return undefined;
|
|
37
41
|
// Parse private key
|
|
38
42
|
const privkey = tags.find((t) => t[0] === "privkey" && t[1])?.[1];
|
|
39
|
-
const key = privkey ? hexToBytes(privkey) :
|
|
43
|
+
const key = privkey ? hexToBytes(privkey) : null;
|
|
40
44
|
// Set the cached value
|
|
41
45
|
Reflect.set(wallet, WalletPrivateKeySymbol, key);
|
|
42
46
|
return key;
|
|
43
47
|
}
|
|
48
|
+
export function getWalletRelays(wallet) {
|
|
49
|
+
// Return cached value if it exists
|
|
50
|
+
if (WalletRelaysSymbol in wallet)
|
|
51
|
+
return wallet[WalletRelaysSymbol];
|
|
52
|
+
// Get hidden tags
|
|
53
|
+
const tags = getHiddenTags(wallet);
|
|
54
|
+
if (!tags)
|
|
55
|
+
return undefined;
|
|
56
|
+
// Get relays
|
|
57
|
+
const urls = tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]);
|
|
58
|
+
const relays = relaySet(urls);
|
|
59
|
+
// Set the cached value
|
|
60
|
+
Reflect.set(wallet, WalletRelaysSymbol, relays);
|
|
61
|
+
return relays;
|
|
62
|
+
}
|
|
44
63
|
/** Unlocks a wallet and returns the hidden tags */
|
|
45
64
|
export async function unlockWallet(wallet, signer) {
|
|
46
|
-
if (
|
|
47
|
-
return {
|
|
65
|
+
if (WalletPrivateKeySymbol in wallet && WalletMintsSymbol in wallet && WalletRelaysSymbol in wallet)
|
|
66
|
+
return {
|
|
67
|
+
mints: wallet[WalletMintsSymbol],
|
|
68
|
+
privateKey: wallet[WalletPrivateKeySymbol],
|
|
69
|
+
relays: wallet[WalletRelaysSymbol],
|
|
70
|
+
};
|
|
48
71
|
// Unlock hidden tags if needed
|
|
49
72
|
await unlockHiddenTags(wallet, signer);
|
|
50
73
|
// Read the wallet mints and private key
|
|
51
74
|
const mints = getWalletMints(wallet);
|
|
52
75
|
if (!mints)
|
|
53
76
|
throw new Error("Failed to unlock wallet mints");
|
|
54
|
-
const
|
|
77
|
+
const relays = getWalletRelays(wallet);
|
|
78
|
+
if (!relays)
|
|
79
|
+
throw new Error("Failed to unlock wallet relays");
|
|
80
|
+
const key = getWalletPrivateKey(wallet) ?? null;
|
|
55
81
|
// Notify the event store
|
|
56
82
|
notifyEventUpdate(wallet);
|
|
57
|
-
return { mints, privateKey: key };
|
|
83
|
+
return { mints, privateKey: key, relays };
|
|
58
84
|
}
|
|
59
85
|
/** Locks a wallet event */
|
|
60
86
|
export function lockWallet(wallet) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * as Actions from "./actions/index.js";
|
|
2
2
|
export * as Blueprints from "./blueprints/index.js";
|
|
3
|
+
export * as Casts from "./casts/index.js";
|
|
3
4
|
export * as Helpers from "./helpers/index.js";
|
|
4
5
|
export * as Models from "./models/index.js";
|
|
5
6
|
export * as Operations from "./operations/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * as Actions from "./actions/index.js";
|
|
2
2
|
export * as Blueprints from "./blueprints/index.js";
|
|
3
|
+
export * as Casts from "./casts/index.js";
|
|
3
4
|
export * as Helpers from "./helpers/index.js";
|
|
4
5
|
export * as Models from "./models/index.js";
|
|
5
6
|
export * as Operations from "./operations/index.js";
|
package/dist/models/history.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import { NostrEvent } from "applesauce-core/helpers/event";
|
|
|
3
3
|
/** A model that returns an array of redeemed event ids for a wallet */
|
|
4
4
|
export declare function WalletRedeemedModel(pubkey: string): Model<string[]>;
|
|
5
5
|
/** A model that returns a timeline of wallet history events */
|
|
6
|
-
export declare function WalletHistoryModel(pubkey: string,
|
|
6
|
+
export declare function WalletHistoryModel(pubkey: string, unlocked?: boolean | undefined): Model<NostrEvent[]>;
|