applesauce-wallet 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/tokens.js +3 -3
- package/dist/actions/wallet.js +7 -7
- package/dist/helpers/history.d.ts +8 -3
- package/dist/helpers/history.js +40 -28
- package/dist/helpers/nutzap.d.ts +10 -4
- package/dist/helpers/nutzap.js +13 -9
- package/dist/helpers/tokens.d.ts +11 -4
- package/dist/helpers/tokens.js +42 -33
- package/dist/helpers/wallet.d.ts +17 -6
- package/dist/helpers/wallet.js +47 -24
- package/dist/models/history.js +2 -2
- package/dist/models/nutzap.d.ts +4 -2
- package/dist/models/nutzap.js +10 -7
- package/dist/models/tokens.js +4 -4
- package/dist/models/wallet.js +8 -5
- package/package.json +10 -8
package/dist/actions/tokens.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CashuMint, CashuWallet, CheckStateEnum } from "@cashu/cashu-ts";
|
|
2
2
|
import { DeleteBlueprint } from "applesauce-factory/blueprints";
|
|
3
|
-
import { getTokenContent, ignoreDuplicateProofs,
|
|
3
|
+
import { getTokenContent, ignoreDuplicateProofs, isTokenContentUnlocked, WALLET_TOKEN_KIND, } from "../helpers/tokens.js";
|
|
4
4
|
import { WalletTokenBlueprint } from "../blueprints/tokens.js";
|
|
5
5
|
import { WalletHistoryBlueprint } from "../blueprints/history.js";
|
|
6
6
|
/**
|
|
@@ -37,7 +37,7 @@ export function CompleteSpend(spent, change) {
|
|
|
37
37
|
return async function* ({ factory }) {
|
|
38
38
|
if (spent.length === 0)
|
|
39
39
|
throw new Error("Cant complete spent with no token events");
|
|
40
|
-
if (spent.some((s) =>
|
|
40
|
+
if (spent.some((s) => isTokenContentUnlocked(s)))
|
|
41
41
|
throw new Error("Cant complete spend with locked tokens");
|
|
42
42
|
// create the nip-09 delete event for previous events
|
|
43
43
|
const deleteDraft = await factory.create(DeleteBlueprint, spent);
|
|
@@ -67,7 +67,7 @@ export function CompleteSpend(spent, change) {
|
|
|
67
67
|
export function ConsolidateTokens(opts) {
|
|
68
68
|
return async function* ({ events, factory, self }) {
|
|
69
69
|
const tokens = Array.from(events.getByFilters({ kinds: [WALLET_TOKEN_KIND], authors: [self] })).filter((token) => {
|
|
70
|
-
if (
|
|
70
|
+
if (isTokenContentUnlocked(token)) {
|
|
71
71
|
if (opts?.ignoreLocked)
|
|
72
72
|
return false;
|
|
73
73
|
else
|
package/dist/actions/wallet.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getWalletMints, getWalletPrivateKey,
|
|
1
|
+
import { getWalletMints, getWalletPrivateKey, isWalletUnlocked, unlockWallet, WALLET_KIND } from "../helpers/wallet.js";
|
|
2
2
|
import { WalletBackupBlueprint, WalletBlueprint } from "../blueprints/wallet.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { isTokenContentUnlocked, unlockTokenContent, WALLET_TOKEN_KIND } from "../helpers/tokens.js";
|
|
4
|
+
import { isHistoryContentUnlocked, unlockHistoryContent, WALLET_HISTORY_KIND } from "../helpers/history.js";
|
|
5
5
|
/** An action that creates a new 17375 wallet event and 375 wallet backup */
|
|
6
6
|
export function CreateWallet(mints, privateKey) {
|
|
7
7
|
return async function* ({ events, factory, self }) {
|
|
@@ -24,7 +24,7 @@ export function WalletAddPrivateKey(privateKey) {
|
|
|
24
24
|
const wallet = events.getReplaceable(WALLET_KIND, self);
|
|
25
25
|
if (!wallet)
|
|
26
26
|
throw new Error("Wallet does not exist");
|
|
27
|
-
if (
|
|
27
|
+
if (isWalletUnlocked(wallet))
|
|
28
28
|
throw new Error("Wallet is locked");
|
|
29
29
|
if (getWalletPrivateKey(wallet))
|
|
30
30
|
throw new Error("Wallet already has a private key");
|
|
@@ -46,18 +46,18 @@ export function UnlockWallet(unlock) {
|
|
|
46
46
|
const wallet = events.getReplaceable(WALLET_KIND, self);
|
|
47
47
|
if (!wallet)
|
|
48
48
|
throw new Error("Wallet does not exist");
|
|
49
|
-
if (
|
|
49
|
+
if (isWalletUnlocked(wallet))
|
|
50
50
|
await unlockWallet(wallet, signer);
|
|
51
51
|
if (unlock?.tokens) {
|
|
52
52
|
const tokens = events.getTimeline({ kinds: [WALLET_TOKEN_KIND], authors: [self] });
|
|
53
53
|
for (const token of tokens)
|
|
54
|
-
if (
|
|
54
|
+
if (isTokenContentUnlocked(token))
|
|
55
55
|
await unlockTokenContent(token, signer);
|
|
56
56
|
}
|
|
57
57
|
if (unlock?.history) {
|
|
58
58
|
const history = events.getTimeline({ kinds: [WALLET_HISTORY_KIND], authors: [self] });
|
|
59
59
|
for (const entry of history)
|
|
60
|
-
if (
|
|
60
|
+
if (isHistoryContentUnlocked(entry))
|
|
61
61
|
await unlockHistoryContent(entry, signer);
|
|
62
62
|
}
|
|
63
63
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HiddenContentSigner } from "applesauce-core/helpers";
|
|
1
|
+
import { HiddenContentSigner, UnlockedHiddenTags } from "applesauce-core/helpers";
|
|
2
2
|
import { NostrEvent } from "nostr-tools";
|
|
3
3
|
export declare const WALLET_HISTORY_KIND = 7376;
|
|
4
4
|
export type HistoryDirection = "in" | "out";
|
|
@@ -15,12 +15,17 @@ export type HistoryContent = {
|
|
|
15
15
|
fee?: number;
|
|
16
16
|
};
|
|
17
17
|
export declare const HistoryContentSymbol: unique symbol;
|
|
18
|
+
/** Type for unlocked history events */
|
|
19
|
+
export type UnlockedHistoryContent = UnlockedHiddenTags & {
|
|
20
|
+
[HistoryContentSymbol]: HistoryContent;
|
|
21
|
+
};
|
|
18
22
|
/** returns an array of redeemed event ids in a history event */
|
|
19
23
|
export declare function getHistoryRedeemed(history: NostrEvent): string[];
|
|
20
24
|
/** Checks if the history contents are locked */
|
|
21
|
-
export declare function
|
|
25
|
+
export declare function isHistoryContentUnlocked<T extends NostrEvent>(history: T): history is T & UnlockedHistoryContent;
|
|
22
26
|
/** Returns the parsed content of a 7376 history event */
|
|
23
|
-
export declare function getHistoryContent(history:
|
|
27
|
+
export declare function getHistoryContent<T extends UnlockedHiddenTags>(history: T): HistoryContent;
|
|
28
|
+
export declare function getHistoryContent<T extends NostrEvent>(history: T): HistoryContent | undefined;
|
|
24
29
|
/** Decrypts a wallet history event */
|
|
25
30
|
export declare function unlockHistoryContent(history: NostrEvent, signer: HiddenContentSigner): Promise<HistoryContent>;
|
|
26
31
|
export declare function lockHistoryContent(history: NostrEvent): void;
|
package/dist/helpers/history.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getHiddenTags,
|
|
1
|
+
import { getHiddenTags, isETag, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
|
|
2
2
|
export const WALLET_HISTORY_KIND = 7376;
|
|
3
3
|
// Enable hidden content for wallet history kind
|
|
4
4
|
setHiddenTagsEncryptionMethod(WALLET_HISTORY_KIND, "nip44");
|
|
@@ -8,38 +8,50 @@ export function getHistoryRedeemed(history) {
|
|
|
8
8
|
return history.tags.filter((t) => isETag(t) && t[3] === "redeemed").map((t) => t[1]);
|
|
9
9
|
}
|
|
10
10
|
/** Checks if the history contents are locked */
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
11
|
+
export function isHistoryContentUnlocked(history) {
|
|
12
|
+
return isHiddenTagsUnlocked(history) && Reflect.has(history, HistoryContentSymbol) === true;
|
|
13
13
|
}
|
|
14
|
-
/** Returns the parsed content of a 7376 history event */
|
|
15
14
|
export function getHistoryContent(history) {
|
|
16
|
-
if
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
// Return cached value if it exists
|
|
16
|
+
if (isHistoryContentUnlocked(history))
|
|
17
|
+
return history[HistoryContentSymbol];
|
|
18
|
+
// Get hidden tags
|
|
19
|
+
const tags = getHiddenTags(history);
|
|
20
|
+
if (!tags)
|
|
21
|
+
return;
|
|
22
|
+
// Read tags
|
|
23
|
+
const direction = tags.find((t) => t[0] === "direction")?.[1];
|
|
24
|
+
if (!direction)
|
|
25
|
+
throw new Error("History event missing direction");
|
|
26
|
+
const amountStr = tags.find((t) => t[0] === "amount")?.[1];
|
|
27
|
+
if (!amountStr)
|
|
28
|
+
throw new Error("History event missing amount");
|
|
29
|
+
const amount = parseInt(amountStr);
|
|
30
|
+
if (!Number.isFinite(amount))
|
|
31
|
+
throw new Error("Failed to parse amount");
|
|
32
|
+
const mint = tags.find((t) => t[0] === "mint")?.[1];
|
|
33
|
+
const feeStr = tags.find((t) => t[0] === "fee")?.[1];
|
|
34
|
+
const fee = feeStr ? parseInt(feeStr) : undefined;
|
|
35
|
+
const created = tags.filter((t) => isETag(t) && t[3] === "created").map((t) => t[1]);
|
|
36
|
+
const content = { direction, amount, created, mint, fee };
|
|
37
|
+
// Set the cached value
|
|
38
|
+
Reflect.set(history, HistoryContentSymbol, content);
|
|
39
|
+
return content;
|
|
37
40
|
}
|
|
38
41
|
/** Decrypts a wallet history event */
|
|
39
42
|
export async function unlockHistoryContent(history, signer) {
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// Return cached value if it exists
|
|
44
|
+
if (isHistoryContentUnlocked(history))
|
|
45
|
+
return history[HistoryContentSymbol];
|
|
46
|
+
// Unlock hidden tags if needed
|
|
47
|
+
await unlockHiddenTags(history, signer);
|
|
48
|
+
// Get the history content
|
|
49
|
+
const content = getHistoryContent(history);
|
|
50
|
+
if (!content)
|
|
51
|
+
throw new Error("Failed to unlock history content");
|
|
52
|
+
// Notify the event store
|
|
53
|
+
notifyEventUpdate(history);
|
|
54
|
+
return content;
|
|
43
55
|
}
|
|
44
56
|
export function lockHistoryContent(history) {
|
|
45
57
|
Reflect.deleteProperty(history, HistoryContentSymbol);
|
package/dist/helpers/nutzap.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Proof } from "@cashu/cashu-ts";
|
|
2
|
+
import { KnownEvent } from "applesauce-core/helpers";
|
|
2
3
|
import { NostrEvent } from "nostr-tools";
|
|
3
|
-
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
|
4
|
+
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
|
|
4
5
|
export declare const NUTZAP_KIND = 9321;
|
|
6
|
+
/** Validated NIP-61 nutzap event */
|
|
7
|
+
export type NutzapEvent = KnownEvent<typeof NUTZAP_KIND>;
|
|
5
8
|
export declare const NutzapProofsSymbol: unique symbol;
|
|
6
9
|
export declare const NutzapAmountSymbol: unique symbol;
|
|
7
10
|
export declare const NutzapMintSymbol: unique symbol;
|
|
8
11
|
/** Returns the cashu proofs from a kind:9321 nutzap event */
|
|
9
12
|
export declare function getNutzapProofs(event: NostrEvent): Proof[];
|
|
10
13
|
/** Returns the mint URL from a kind:9321 nutzap event */
|
|
14
|
+
export declare function getNutzapMint(event: NutzapEvent): string;
|
|
11
15
|
export declare function getNutzapMint(event: NostrEvent): string | undefined;
|
|
12
16
|
/** Returns the recipient pubkey from a kind:9321 nutzap event */
|
|
13
|
-
export declare function getNutzapRecipient(event:
|
|
17
|
+
export declare function getNutzapRecipient(event: NutzapEvent): ProfilePointer;
|
|
18
|
+
export declare function getNutzapRecipient(event: NostrEvent): ProfilePointer | undefined;
|
|
14
19
|
/** Returns the event ID being nutzapped from a kind:9321 nutzap event */
|
|
15
20
|
export declare function getNutzapEventPointer(event: NostrEvent): EventPointer | undefined;
|
|
16
21
|
/** Returns the event ID being nutzapped from a kind:9321 nutzap event */
|
|
@@ -20,8 +25,9 @@ export declare function getNutzapPointer(event: NostrEvent): EventPointer | Addr
|
|
|
20
25
|
/** Returns the comment from a kind:9321 nutzap event */
|
|
21
26
|
export declare function getNutzapComment(event: NostrEvent): string | undefined;
|
|
22
27
|
/** Calculates the total amount of sats in a kind:9321 nutzap event */
|
|
23
|
-
export declare function getNutzapAmount(event:
|
|
28
|
+
export declare function getNutzapAmount(event: NutzapEvent): number;
|
|
29
|
+
export declare function getNutzapAmount(event: NostrEvent): number | undefined;
|
|
24
30
|
/** Checks if a nutzap is valid according to NIP-61 requirements */
|
|
25
|
-
export declare function isValidNutzap(nutzap: NostrEvent):
|
|
31
|
+
export declare function isValidNutzap(nutzap: NostrEvent): nutzap is NutzapEvent;
|
|
26
32
|
/** Checks if a nutzap event has already been redeemed based on kind:7376 wallet history events */
|
|
27
33
|
export declare function isNutzapRedeemed(nutzapId: string, history: NostrEvent[]): boolean;
|
package/dist/helpers/nutzap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAddressPointerFromATag, getEventPointerFromETag, getOrComputeCachedValue, getTagValue, processTags, safeParse, } from "applesauce-core/helpers";
|
|
1
|
+
import { getAddressPointerFromATag, getEventPointerFromETag, getOrComputeCachedValue, getProfilePointerFromPTag, getTagValue, isPTag, processTags, safeParse, } from "applesauce-core/helpers";
|
|
2
2
|
import { getHistoryRedeemed } from "./history.js";
|
|
3
3
|
export const NUTZAP_KIND = 9321;
|
|
4
4
|
// Symbols for caching computed values
|
|
@@ -11,16 +11,15 @@ export function getNutzapProofs(event) {
|
|
|
11
11
|
return processTags(event.tags, (tag) => (tag[0] === "proof" ? safeParse(tag[1]) : undefined));
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
-
/** Returns the mint URL from a kind:9321 nutzap event */
|
|
15
14
|
export function getNutzapMint(event) {
|
|
16
15
|
return getOrComputeCachedValue(event, NutzapMintSymbol, () => {
|
|
17
16
|
const url = getTagValue(event, "u");
|
|
18
17
|
return url && URL.canParse(url) ? url : undefined;
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
|
-
/** Returns the recipient pubkey from a kind:9321 nutzap event */
|
|
22
20
|
export function getNutzapRecipient(event) {
|
|
23
|
-
|
|
21
|
+
const tag = event.tags.find(isPTag);
|
|
22
|
+
return tag && getProfilePointerFromPTag(tag);
|
|
24
23
|
}
|
|
25
24
|
/** Returns the event ID being nutzapped from a kind:9321 nutzap event */
|
|
26
25
|
export function getNutzapEventPointer(event) {
|
|
@@ -44,7 +43,6 @@ export function getNutzapPointer(event) {
|
|
|
44
43
|
export function getNutzapComment(event) {
|
|
45
44
|
return event.content || undefined;
|
|
46
45
|
}
|
|
47
|
-
/** Calculates the total amount of sats in a kind:9321 nutzap event */
|
|
48
46
|
export function getNutzapAmount(event) {
|
|
49
47
|
return getOrComputeCachedValue(event, NutzapAmountSymbol, () => {
|
|
50
48
|
const proofs = getNutzapProofs(event);
|
|
@@ -53,10 +51,16 @@ export function getNutzapAmount(event) {
|
|
|
53
51
|
}
|
|
54
52
|
/** Checks if a nutzap is valid according to NIP-61 requirements */
|
|
55
53
|
export function isValidNutzap(nutzap) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
54
|
+
if (nutzap.kind !== NUTZAP_KIND)
|
|
55
|
+
return false;
|
|
56
|
+
// Check if the nutzap has a mint, recipient, and proofs
|
|
57
|
+
if (getNutzapPointer(nutzap) === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
if (getNutzapMint(nutzap) === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
if (getNutzapRecipient(nutzap) === undefined)
|
|
62
|
+
return false;
|
|
63
|
+
if (getNutzapProofs(nutzap).length === 0)
|
|
60
64
|
return false;
|
|
61
65
|
return true;
|
|
62
66
|
}
|
package/dist/helpers/tokens.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Proof, Token } from "@cashu/cashu-ts";
|
|
2
|
-
import { HiddenContentSigner } from "applesauce-core/helpers";
|
|
2
|
+
import { HiddenContentSigner, UnlockedHiddenContent } from "applesauce-core/helpers";
|
|
3
3
|
import { NostrEvent } from "nostr-tools";
|
|
4
4
|
export declare const WALLET_TOKEN_KIND = 7375;
|
|
5
5
|
/** Internal method for creating a unique id for each proof */
|
|
@@ -21,14 +21,20 @@ export type TokenContent = {
|
|
|
21
21
|
/** tokens that were destroyed in the creation of this token (helps on wallet state transitions) */
|
|
22
22
|
del: string[];
|
|
23
23
|
};
|
|
24
|
+
/** Symbol for caching token content */
|
|
24
25
|
export declare const TokenContentSymbol: unique symbol;
|
|
26
|
+
/** Type for token events with unlocked content */
|
|
27
|
+
export type UnlockedTokenContent = UnlockedHiddenContent & {
|
|
28
|
+
[TokenContentSymbol]: TokenContent;
|
|
29
|
+
};
|
|
25
30
|
/**
|
|
26
31
|
* Returns the decrypted and parsed details of a 7375 token event
|
|
27
|
-
* @throws
|
|
32
|
+
* @throws {Error} If the token content is invalid
|
|
28
33
|
*/
|
|
34
|
+
export declare function getTokenContent(token: UnlockedTokenContent): TokenContent;
|
|
29
35
|
export declare function getTokenContent(token: NostrEvent): TokenContent | undefined;
|
|
30
36
|
/** Returns if token details are locked */
|
|
31
|
-
export declare function
|
|
37
|
+
export declare function isTokenContentUnlocked<T extends NostrEvent>(token: T): token is T & UnlockedTokenContent;
|
|
32
38
|
/** Decrypts a k:7375 token event */
|
|
33
39
|
export declare function unlockTokenContent(token: NostrEvent, signer: HiddenContentSigner): Promise<TokenContent>;
|
|
34
40
|
/** Removes the unencrypted hidden content */
|
|
@@ -37,10 +43,11 @@ export declare function lockTokenContent(token: NostrEvent): void;
|
|
|
37
43
|
* Gets the totaled amount of proofs in a token event
|
|
38
44
|
* @param token The token event to calculate the total
|
|
39
45
|
*/
|
|
46
|
+
export declare function getTokenProofsTotal(token: UnlockedTokenContent): number;
|
|
40
47
|
export declare function getTokenProofsTotal(token: NostrEvent): number | undefined;
|
|
41
48
|
/**
|
|
42
49
|
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
43
|
-
* @throws
|
|
50
|
+
* @throws {Error} If there are insufficient funds
|
|
44
51
|
*/
|
|
45
52
|
export declare function dumbTokenSelection(tokens: NostrEvent[], minAmount: number, mint?: string): {
|
|
46
53
|
events: NostrEvent[];
|
package/dist/helpers/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDecodedToken, getEncodedToken } from "@cashu/cashu-ts";
|
|
2
|
-
import { getHiddenContent,
|
|
2
|
+
import { getHiddenContent, isHiddenContentUnlocked, lockHiddenContent, notifyEventUpdate, setHiddenContentEncryptionMethod, unlockHiddenContent, } from "applesauce-core/helpers";
|
|
3
3
|
export const WALLET_TOKEN_KIND = 7375;
|
|
4
4
|
// Enable hidden content for wallet token kind
|
|
5
5
|
setHiddenContentEncryptionMethod(WALLET_TOKEN_KIND, "nip44");
|
|
@@ -19,61 +19,65 @@ export function ignoreDuplicateProofs(seen = new Set()) {
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
/** Symbol for caching token content */
|
|
22
23
|
export const TokenContentSymbol = Symbol.for("token-content");
|
|
23
|
-
/**
|
|
24
|
-
* Returns the decrypted and parsed details of a 7375 token event
|
|
25
|
-
* @throws
|
|
26
|
-
*/
|
|
27
24
|
export function getTokenContent(token) {
|
|
28
|
-
if (
|
|
25
|
+
if (isTokenContentUnlocked(token))
|
|
26
|
+
return token[TokenContentSymbol];
|
|
27
|
+
// Get the hidden content
|
|
28
|
+
const plaintext = getHiddenContent(token);
|
|
29
|
+
if (!plaintext)
|
|
29
30
|
return undefined;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
31
|
+
// Parse the content as a token content
|
|
32
|
+
const details = JSON.parse(plaintext);
|
|
33
|
+
// Throw an error if the token content is invalid
|
|
34
|
+
if (!details.mint)
|
|
35
|
+
throw new Error("Token missing mint");
|
|
36
|
+
if (!details.proofs)
|
|
37
|
+
throw new Error("Token missing proofs");
|
|
38
|
+
if (!details.del)
|
|
39
|
+
details.del = [];
|
|
40
|
+
// Set the cached value
|
|
41
|
+
Reflect.set(token, TokenContentSymbol, details);
|
|
42
|
+
return details;
|
|
43
43
|
}
|
|
44
44
|
/** Returns if token details are locked */
|
|
45
|
-
export function
|
|
46
|
-
return
|
|
45
|
+
export function isTokenContentUnlocked(token) {
|
|
46
|
+
return isHiddenContentUnlocked(token) && Reflect.has(token, TokenContentSymbol) === true;
|
|
47
47
|
}
|
|
48
48
|
/** Decrypts a k:7375 token event */
|
|
49
49
|
export async function unlockTokenContent(token, signer) {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if (isTokenContentUnlocked(token))
|
|
51
|
+
return token[TokenContentSymbol];
|
|
52
|
+
// Unlock the hidden content
|
|
53
|
+
await unlockHiddenContent(token, signer);
|
|
54
|
+
// Parse the content as a token content
|
|
55
|
+
const parsed = getTokenContent(token);
|
|
56
|
+
if (!parsed)
|
|
57
|
+
throw new Error("Failed to unlock token content");
|
|
58
|
+
// Trigger update for event
|
|
59
|
+
notifyEventUpdate(token);
|
|
60
|
+
return parsed;
|
|
53
61
|
}
|
|
54
62
|
/** Removes the unencrypted hidden content */
|
|
55
63
|
export function lockTokenContent(token) {
|
|
56
64
|
Reflect.deleteProperty(token, TokenContentSymbol);
|
|
57
65
|
lockHiddenContent(token);
|
|
58
66
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Gets the totaled amount of proofs in a token event
|
|
61
|
-
* @param token The token event to calculate the total
|
|
62
|
-
*/
|
|
63
67
|
export function getTokenProofsTotal(token) {
|
|
64
|
-
if (isTokenContentLocked(token))
|
|
65
|
-
return undefined;
|
|
66
68
|
const content = getTokenContent(token);
|
|
69
|
+
if (!content)
|
|
70
|
+
return undefined;
|
|
67
71
|
return content.proofs.reduce((t, p) => t + p.amount, 0);
|
|
68
72
|
}
|
|
69
73
|
/**
|
|
70
74
|
* Selects oldest tokens and proofs that total up to more than the min amount
|
|
71
|
-
* @throws
|
|
75
|
+
* @throws {Error} If there are insufficient funds
|
|
72
76
|
*/
|
|
73
77
|
export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
74
78
|
// sort newest to oldest
|
|
75
79
|
const sorted = tokens
|
|
76
|
-
.filter((token) =>
|
|
80
|
+
.filter((token) => (mint ? getTokenContent(token)?.mint === mint : true))
|
|
77
81
|
.sort((a, b) => b.created_at - a.created_at);
|
|
78
82
|
let amount = 0;
|
|
79
83
|
const seen = new Set();
|
|
@@ -83,7 +87,12 @@ export function dumbTokenSelection(tokens, minAmount, mint) {
|
|
|
83
87
|
const token = sorted.pop();
|
|
84
88
|
if (!token)
|
|
85
89
|
throw new Error("Insufficient funds");
|
|
86
|
-
const
|
|
90
|
+
const content = getTokenContent(token);
|
|
91
|
+
// Skip locked tokens
|
|
92
|
+
if (!content)
|
|
93
|
+
continue;
|
|
94
|
+
// Get proofs and total
|
|
95
|
+
const proofs = content.proofs.filter(ignoreDuplicateProofs(seen));
|
|
87
96
|
const total = proofs.reduce((t, p) => t + p.amount, 0);
|
|
88
97
|
selectedTokens.push(token);
|
|
89
98
|
selectedProofs.push(...proofs);
|
package/dist/helpers/wallet.d.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { HiddenContentSigner } from "applesauce-core/helpers";
|
|
1
|
+
import { HiddenContentSigner, UnlockedHiddenTags } from "applesauce-core/helpers";
|
|
2
2
|
import { NostrEvent } from "nostr-tools";
|
|
3
3
|
export declare const WALLET_KIND = 17375;
|
|
4
4
|
export declare const WALLET_BACKUP_KIND = 375;
|
|
5
5
|
export declare const WalletPrivateKeySymbol: unique symbol;
|
|
6
6
|
export declare const WalletMintsSymbol: unique symbol;
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/** Type for unlocked wallet events */
|
|
8
|
+
export type UnlockedWallet = UnlockedHiddenTags & {
|
|
9
|
+
[WalletPrivateKeySymbol]: Uint8Array;
|
|
10
|
+
[WalletMintsSymbol]: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Returns if a wallet is unlocked */
|
|
13
|
+
export declare function isWalletUnlocked<T extends NostrEvent>(wallet: T): wallet is T & UnlockedWallet;
|
|
12
14
|
/** Returns the wallets mints */
|
|
15
|
+
export declare function getWalletMints(wallet: UnlockedWallet): string[];
|
|
13
16
|
export declare function getWalletMints(wallet: NostrEvent): string[];
|
|
14
17
|
/** Returns the wallets private key as a string */
|
|
18
|
+
export declare function getWalletPrivateKey(wallet: UnlockedWallet): Uint8Array;
|
|
15
19
|
export declare function getWalletPrivateKey(wallet: NostrEvent): Uint8Array | undefined;
|
|
20
|
+
/** Unlocks a wallet and returns the hidden tags */
|
|
21
|
+
export declare function unlockWallet(wallet: NostrEvent, signer: HiddenContentSigner): Promise<{
|
|
22
|
+
mints: string[];
|
|
23
|
+
privateKey?: Uint8Array;
|
|
24
|
+
}>;
|
|
25
|
+
/** Locks a wallet event */
|
|
26
|
+
export declare function lockWallet(wallet: NostrEvent): void;
|
package/dist/helpers/wallet.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hexToBytes } from "@noble/hashes/utils";
|
|
2
|
-
import { getHiddenTags,
|
|
2
|
+
import { getHiddenTags, isHiddenTagsUnlocked, lockHiddenTags, notifyEventUpdate, setHiddenTagsEncryptionMethod, unlockHiddenTags, } from "applesauce-core/helpers";
|
|
3
3
|
export const WALLET_KIND = 17375;
|
|
4
4
|
export const WALLET_BACKUP_KIND = 375;
|
|
5
5
|
// Enable hidden content for wallet kinds
|
|
@@ -7,35 +7,58 @@ setHiddenTagsEncryptionMethod(WALLET_KIND, "nip44");
|
|
|
7
7
|
setHiddenTagsEncryptionMethod(WALLET_BACKUP_KIND, "nip44");
|
|
8
8
|
export const WalletPrivateKeySymbol = Symbol.for("wallet-private-key");
|
|
9
9
|
export const WalletMintsSymbol = Symbol.for("wallet-mints");
|
|
10
|
-
/** Returns if a wallet is
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
10
|
+
/** Returns if a wallet is unlocked */
|
|
11
|
+
export function isWalletUnlocked(wallet) {
|
|
12
|
+
return (isHiddenTagsUnlocked(wallet) &&
|
|
13
|
+
Reflect.has(wallet, WalletPrivateKeySymbol) &&
|
|
14
|
+
Reflect.has(wallet, WalletMintsSymbol));
|
|
15
|
+
}
|
|
16
|
+
export function getWalletMints(wallet) {
|
|
17
|
+
// Return cached value if it exists
|
|
18
|
+
if (Reflect.has(wallet, WalletMintsSymbol))
|
|
19
|
+
return Reflect.get(wallet, WalletMintsSymbol);
|
|
20
|
+
// Get hidden tags
|
|
21
|
+
const tags = getHiddenTags(wallet);
|
|
22
|
+
if (!tags)
|
|
23
|
+
return undefined;
|
|
24
|
+
// Get mints
|
|
25
|
+
const mints = tags.filter((t) => t[0] === "mint").map((t) => t[1]);
|
|
26
|
+
// Set the cached value
|
|
27
|
+
Reflect.set(wallet, WalletMintsSymbol, mints);
|
|
28
|
+
return mints;
|
|
29
|
+
}
|
|
30
|
+
export function getWalletPrivateKey(wallet) {
|
|
31
|
+
if (Reflect.has(wallet, WalletPrivateKeySymbol))
|
|
32
|
+
return Reflect.get(wallet, WalletPrivateKeySymbol);
|
|
33
|
+
// Get hidden tags
|
|
34
|
+
const tags = getHiddenTags(wallet);
|
|
35
|
+
if (!tags)
|
|
36
|
+
return undefined;
|
|
37
|
+
// Parse private key
|
|
38
|
+
const privkey = tags.find((t) => t[0] === "privkey" && t[1])?.[1];
|
|
39
|
+
const key = privkey ? hexToBytes(privkey) : undefined;
|
|
40
|
+
// Set the cached value
|
|
41
|
+
Reflect.set(wallet, WalletPrivateKeySymbol, key);
|
|
42
|
+
return key;
|
|
13
43
|
}
|
|
14
44
|
/** Unlocks a wallet and returns the hidden tags */
|
|
15
45
|
export async function unlockWallet(wallet, signer) {
|
|
16
|
-
|
|
46
|
+
if (isWalletUnlocked(wallet))
|
|
47
|
+
return { mints: getWalletMints(wallet), privateKey: getWalletPrivateKey(wallet) };
|
|
48
|
+
// Unlock hidden tags if needed
|
|
49
|
+
await unlockHiddenTags(wallet, signer);
|
|
50
|
+
// Read the wallet mints and private key
|
|
51
|
+
const mints = getWalletMints(wallet);
|
|
52
|
+
if (!mints)
|
|
53
|
+
throw new Error("Failed to unlock wallet mints");
|
|
54
|
+
const key = getWalletPrivateKey(wallet);
|
|
55
|
+
// Notify the event store
|
|
56
|
+
notifyEventUpdate(wallet);
|
|
57
|
+
return { mints, privateKey: key };
|
|
17
58
|
}
|
|
59
|
+
/** Locks a wallet event */
|
|
18
60
|
export function lockWallet(wallet) {
|
|
19
61
|
Reflect.deleteProperty(wallet, WalletPrivateKeySymbol);
|
|
20
62
|
Reflect.deleteProperty(wallet, WalletMintsSymbol);
|
|
21
63
|
lockHiddenTags(wallet);
|
|
22
64
|
}
|
|
23
|
-
/** Returns the wallets mints */
|
|
24
|
-
export function getWalletMints(wallet) {
|
|
25
|
-
return getOrComputeCachedValue(wallet, WalletMintsSymbol, () => {
|
|
26
|
-
const tags = getHiddenTags(wallet);
|
|
27
|
-
if (!tags)
|
|
28
|
-
throw new Error("Wallet is locked");
|
|
29
|
-
return tags.filter((t) => t[0] === "mint").map((t) => t[1]);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
/** Returns the wallets private key as a string */
|
|
33
|
-
export function getWalletPrivateKey(wallet) {
|
|
34
|
-
return getOrComputeCachedValue(wallet, WalletPrivateKeySymbol, () => {
|
|
35
|
-
const tags = getHiddenTags(wallet);
|
|
36
|
-
if (!tags)
|
|
37
|
-
throw new Error("Wallet is locked");
|
|
38
|
-
const key = tags.find((t) => t[0] === "privkey" && t[1])?.[1];
|
|
39
|
-
return key ? hexToBytes(key) : undefined;
|
|
40
|
-
});
|
|
41
|
-
}
|
package/dist/models/history.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { combineLatest, filter, map, scan, startWith } from "rxjs";
|
|
2
|
-
import { getHistoryRedeemed,
|
|
2
|
+
import { getHistoryRedeemed, isHistoryContentUnlocked, WALLET_HISTORY_KIND } from "../helpers/history.js";
|
|
3
3
|
/** A model that returns an array of redeemed event ids for a wallet */
|
|
4
4
|
export function WalletRedeemedModel(pubkey) {
|
|
5
5
|
return (events) => events
|
|
@@ -15,7 +15,7 @@ export function WalletHistoryModel(pubkey, locked) {
|
|
|
15
15
|
if (locked === undefined)
|
|
16
16
|
return history;
|
|
17
17
|
else
|
|
18
|
-
return history.filter((entry) =>
|
|
18
|
+
return history.filter((entry) => isHistoryContentUnlocked(entry) === locked);
|
|
19
19
|
}));
|
|
20
20
|
};
|
|
21
21
|
}
|
package/dist/models/nutzap.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Model } from "applesauce-core";
|
|
2
|
+
import { KnownEvent } from "applesauce-core/helpers";
|
|
2
3
|
import { NostrEvent } from "nostr-tools";
|
|
4
|
+
import { NUTZAP_KIND } from "../helpers/nutzap.js";
|
|
3
5
|
/** A model that returns all nutzap events for an event */
|
|
4
|
-
export declare function EventNutZapzModel(event: NostrEvent): Model<
|
|
6
|
+
export declare function EventNutZapzModel(event: NostrEvent): Model<KnownEvent<typeof NUTZAP_KIND>[]>;
|
|
5
7
|
/** A model that returns all nutzaps for a users profile */
|
|
6
|
-
export declare function ProfileNutZapzModel(pubkey: string): Model<
|
|
8
|
+
export declare function ProfileNutZapzModel(pubkey: string): Model<KnownEvent<typeof NUTZAP_KIND>[]>;
|
package/dist/models/nutzap.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { getReplaceableAddress, isReplaceable } from "applesauce-core/helpers";
|
|
2
2
|
import { map } from "rxjs";
|
|
3
|
-
import { getNutzapPointer, NUTZAP_KIND } from "../helpers/nutzap.js";
|
|
3
|
+
import { getNutzapPointer, isValidNutzap, NUTZAP_KIND } from "../helpers/nutzap.js";
|
|
4
4
|
/** A model that returns all nutzap events for an event */
|
|
5
5
|
export function EventNutZapzModel(event) {
|
|
6
|
-
return (events) => isReplaceable(event.kind)
|
|
6
|
+
return (events) => (isReplaceable(event.kind)
|
|
7
7
|
? events.timeline({ kinds: [NUTZAP_KIND], "#e": [event.id] })
|
|
8
|
-
: events.timeline({ kinds: [NUTZAP_KIND], "#a": [getReplaceableAddress(event)] })
|
|
8
|
+
: events.timeline({ kinds: [NUTZAP_KIND], "#a": [getReplaceableAddress(event)] })).pipe(map((events) =>
|
|
9
|
+
// Validate nutzap events
|
|
10
|
+
events.filter(isValidNutzap)));
|
|
9
11
|
}
|
|
10
12
|
/** A model that returns all nutzaps for a users profile */
|
|
11
13
|
export function ProfileNutZapzModel(pubkey) {
|
|
12
|
-
return (events) => events
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
return (events) => events.timeline({ kinds: [NUTZAP_KIND], "#p": [pubkey] }).pipe(
|
|
15
|
+
// Validate nutzap events
|
|
16
|
+
map((zaps) => zaps.filter(isValidNutzap)),
|
|
17
|
+
// filter out nutzaps that are for events
|
|
18
|
+
map((zaps) => zaps.filter((zap) => getNutzapPointer(zap) === undefined)));
|
|
16
19
|
}
|
package/dist/models/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { combineLatest, filter, map, startWith } from "rxjs";
|
|
2
|
-
import { getTokenContent, ignoreDuplicateProofs,
|
|
2
|
+
import { getTokenContent, ignoreDuplicateProofs, isTokenContentUnlocked, WALLET_TOKEN_KIND, } from "../helpers/tokens.js";
|
|
3
3
|
/** removes deleted events from sorted array */
|
|
4
4
|
function filterDeleted(tokens) {
|
|
5
5
|
const deleted = new Set();
|
|
@@ -10,7 +10,7 @@ function filterDeleted(tokens) {
|
|
|
10
10
|
if (deleted.has(token.id))
|
|
11
11
|
return false;
|
|
12
12
|
// add ids to deleted array
|
|
13
|
-
if (!
|
|
13
|
+
if (!isTokenContentUnlocked(token)) {
|
|
14
14
|
const details = getTokenContent(token);
|
|
15
15
|
for (const id of details.del)
|
|
16
16
|
deleted.add(id);
|
|
@@ -30,7 +30,7 @@ export function WalletTokensModel(pubkey, locked) {
|
|
|
30
30
|
if (locked === undefined)
|
|
31
31
|
return tokens;
|
|
32
32
|
else
|
|
33
|
-
return tokens.filter((t) =>
|
|
33
|
+
return tokens.filter((t) => isTokenContentUnlocked(t) === locked);
|
|
34
34
|
}),
|
|
35
35
|
// remove deleted events
|
|
36
36
|
map(filterDeleted));
|
|
@@ -41,7 +41,7 @@ export function WalletBalanceModel(pubkey) {
|
|
|
41
41
|
return (events) => {
|
|
42
42
|
const updates = events.update$.pipe(filter((e) => e.kind === WALLET_TOKEN_KIND && e.pubkey === pubkey), startWith(undefined));
|
|
43
43
|
const timeline = events.timeline({ kinds: [WALLET_TOKEN_KIND], authors: [pubkey] });
|
|
44
|
-
return combineLatest([updates, timeline]).pipe(map(([_, tokens]) => tokens.filter((t) => !
|
|
44
|
+
return combineLatest([updates, timeline]).pipe(map(([_, tokens]) => tokens.filter((t) => !isTokenContentUnlocked(t))),
|
|
45
45
|
// filter out deleted tokens
|
|
46
46
|
map(filterDeleted),
|
|
47
47
|
// map tokens to totals
|
package/dist/models/wallet.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { filter, map, merge } from "rxjs";
|
|
2
|
-
import { getWalletMints, getWalletPrivateKey,
|
|
2
|
+
import { getWalletMints, getWalletPrivateKey, isWalletUnlocked, WALLET_KIND } from "../helpers/wallet.js";
|
|
3
3
|
/** A model to get the state of a NIP-60 wallet */
|
|
4
4
|
export function WalletModel(pubkey) {
|
|
5
5
|
return (events) => merge(
|
|
@@ -9,10 +9,13 @@ export function WalletModel(pubkey) {
|
|
|
9
9
|
events.update$.pipe(filter((e) => e.kind === WALLET_KIND && e.pubkey === pubkey))).pipe(map((wallet) => {
|
|
10
10
|
if (!wallet)
|
|
11
11
|
return;
|
|
12
|
-
if (
|
|
12
|
+
if (isWalletUnlocked(wallet)) {
|
|
13
|
+
const mints = getWalletMints(wallet);
|
|
14
|
+
const privateKey = getWalletPrivateKey(wallet);
|
|
15
|
+
return { locked: false, mints, privateKey, event: wallet };
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
13
18
|
return { locked: true, event: wallet };
|
|
14
|
-
|
|
15
|
-
const privateKey = getWalletPrivateKey(wallet);
|
|
16
|
-
return { locked: false, mints, privateKey, event: wallet };
|
|
19
|
+
}
|
|
17
20
|
}));
|
|
18
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-wallet",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,27 +72,29 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@cashu/cashu-ts": "2.
|
|
75
|
+
"@cashu/cashu-ts": "^2.7.2",
|
|
76
76
|
"@gandlaf21/bc-ur": "^1.1.12",
|
|
77
77
|
"@noble/hashes": "^1.7.1",
|
|
78
|
-
"applesauce-actions": "^
|
|
79
|
-
"applesauce-core": "^
|
|
80
|
-
"applesauce-factory": "^
|
|
81
|
-
"nostr-tools": "~2.
|
|
78
|
+
"applesauce-actions": "^4.0.0",
|
|
79
|
+
"applesauce-core": "^4.0.0",
|
|
80
|
+
"applesauce-factory": "^4.0.0",
|
|
81
|
+
"nostr-tools": "~2.17",
|
|
82
82
|
"rxjs": "^7.8.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
86
86
|
"@types/debug": "^4.1.12",
|
|
87
|
-
"applesauce-signers": "^
|
|
87
|
+
"applesauce-signers": "^4.0.0",
|
|
88
|
+
"rimraf": "^6.0.1",
|
|
88
89
|
"typescript": "^5.8.3",
|
|
89
|
-
"vitest": "^3.2.
|
|
90
|
+
"vitest": "^3.2.4"
|
|
90
91
|
},
|
|
91
92
|
"funding": {
|
|
92
93
|
"type": "lightning",
|
|
93
94
|
"url": "lightning:nostrudel@geyser.fund"
|
|
94
95
|
},
|
|
95
96
|
"scripts": {
|
|
97
|
+
"prebuild": "rimraf dist",
|
|
96
98
|
"build": "tsc",
|
|
97
99
|
"watch:build": "tsc --watch > /dev/null",
|
|
98
100
|
"test": "vitest run --passWithNoTests",
|