Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lightning address via npub.cash #201

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/components/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<a href="https://v2alpha.nutstash.app/" target="_blank">
this tool
</a>
> to recover from seed phrase.
to recover from seed phrase.
</q-item-label>
<div class="row q-pt-md">
<div class="col-12">
Expand Down Expand Up @@ -241,6 +241,45 @@
<!-- nostr -->
<div class="q-py-sm q-px-xs text-left" on-left>
<q-list padding>
<q-item>
<q-item-section>
<q-item-label overline>Lightning address</q-item-label>
<q-item-label caption
>This is your lightning address. Enable it to check for incoming
payments at startup.</q-item-label
>
</q-item-section>
</q-item>
<q-item>
<q-item-section class="q-mx-none q-pl-none">
<div class="row q-pt-md">
<div class="col-12">
<q-input outlined v-model="npcAddress" dense rounded readonly>
<template v-slot:append>
<q-icon
name="content_copy"
@click="copyText(npcAddress)"
size="0.8em"
color="grey"
class="q-mr-sm cursor-pointer"
>
<q-tooltip>Copy Lightning address</q-tooltip>
</q-icon>
</template>
</q-input>
</div>
</div>
<!-- toggle to turn Lightning address on and off in new row -->
<div class="row q-pt-md">
<q-toggle
v-model="npcEnabled"
label="Enable Lightning address"
color="primary"
/>
</div>
</q-item-section>
</q-item>

<q-item>
<q-item-section>
<q-item-label overline>Link wallet</q-item-label>
Expand Down Expand Up @@ -311,7 +350,7 @@
<q-tooltip>Show QR code</q-tooltip>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-section style="max-width: 10rem">
<!-- <q-item-label
caption
clickable
Expand Down Expand Up @@ -598,6 +637,7 @@ import { map } from "underscore";
import { currentDateStr } from "src/js/utils";
import { useSettingsStore } from "src/stores/settings";
import { useNostrStore } from "src/stores/nostr";
import { useNPCStore } from "src/stores/npubcash";
import { useP2PKStore } from "src/stores/p2pk";
import { useNWCStore } from "src/stores/nwc";
import { useWorkersStore } from "src/stores/workers";
Expand Down Expand Up @@ -666,8 +706,10 @@ export default defineComponent({
"activeProofs",
"proofs",
]),
...mapState(useNPCStore, ["npcAddress"]),
...mapState(useNostrStore, ["pubkey", "mintRecommendations"]),
...mapState(useWalletStore, ["mnemonic"]),
...mapWritableState(useNPCStore, ["npcEnabled"]),
...mapWritableState(useWalletStore, ["keysetCounters"]),
...mapWritableState(useMintsStore, [
"addMintData",
Expand Down
6 changes: 6 additions & 0 deletions src/pages/WalletPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ import { useProofsStore } from "src/stores/proofs";
import { useCameraStore } from "src/stores/camera";
import { useP2PKStore } from "src/stores/p2pk";
import { useNWCStore } from "src/stores/nwc";
import { useNPCStore } from "src/stores/npubcash";

import ReceiveTokenDialog from "src/components/ReceiveTokenDialog.vue";

Expand Down Expand Up @@ -354,6 +355,7 @@ export default {
]),
...mapActions(useCameraStore, ["closeCamera", "showCamera"]),
...mapActions(useNWCStore, ["listenToNWCCommands"]),
...mapActions(useNPCStore, ["generateNPCConnection", "claimAllTokens"]),
// TOKEN METHODS
decodeToken: function (encoded_token) {
try {
Expand Down Expand Up @@ -636,6 +638,10 @@ export default {
if (this.nwcEnabled) {
this.listenToNWCCommands();
}

// generate NPC connection
this.generateNPCConnection();
this.claimAllTokens();
},
};
</script>
272 changes: 272 additions & 0 deletions src/stores/npubcash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { defineStore } from "pinia";
import NDK, { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { useLocalStorage } from "@vueuse/core";
import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency
import { generateSecretKey, getPublicKey } from 'nostr-tools'
import { nip19 } from 'nostr-tools'
import { useWalletStore } from "./wallet";
import { useReceiveTokensStore } from "./receiveTokensStore";
import { notifyApiError, notifyError, notifySuccess, notifyWarning, notify } from "../js/notify";
import { Proof } from "@cashu/cashu-ts";
import token from "../js/token";
import { WalletProof, useMintsStore } from "./mints";
import { useTokensStore } from "../stores/tokens";

type NPCConnection = {
walletPublicKey: string,
walletPrivateKey: string,
}

type NPCBalance = {
error: string,
data: number
}

type NPCClaim = {
error: string,
data: {
token: string,
}
}
type NPCWithdrawl = {
id: number;
claim_ids: number[];
created_at: number;
pubkey: string;
amount: number;
}

type NPCWithdrawals = {
error: string,
data: {
count: number,
withdrawals: Array<NPCWithdrawl>
}
}

const NIP98Kind = 27235;

export const useNPCStore = defineStore("npc", {
state: () => ({
npcEnabled: useLocalStorage<boolean>("cashu.npc.enabled", false),
npcConnections: useLocalStorage<NPCConnection[]>("cashu.npc.connections", []),
npcAddress: useLocalStorage<string>("cashu.npc.address", ""),
npcDomain: useLocalStorage<string>("cashu.npc.domain", "npub.cash"),
baseURL: useLocalStorage<string>("cashu.npc.baseURL", "https://npub.cash"),
ndk: new NDK(),
signer: {} as NDKPrivateKeySigner,
}),
getters: {
},
actions: {
claimAllTokens: async function () {
if (!this.npcEnabled) {
return
}
const receiveStore = useReceiveTokensStore();
const npubCashBalance = await this.getBalance();
console.log("npub.cash balance: " + npubCashBalance);
if (npubCashBalance > 0) {
notifySuccess(`You have ${npubCashBalance} sats on npub.cash`);
const token = await this.getClaim();
if (token) {
// add token to history first
this.addPendingTokenToHistory(token)
receiveStore.receiveData.tokensBase64 = token;
try {
// redeem token automatically
const walletStore = useWalletStore()
await walletStore.redeem()
} catch {
// if it doesn't work, show the receive window
receiveStore.showReceiveTokens = true;
}
// this.storeUnclaimedProofs(token)
// const proofsToClaim = this.getUnclaimedProofs(token)
// const proofsStore = useProofsStore()
// const encodedToken = proofsStore.serializeProofs(proofsToClaim)
// receiveStore.receiveData.tokensBase64 = encodedToken;
// const walletStore = useWalletStore()
// try {
// // try to receive automatically
// await walletStore.redeem()
// } catch {
// // if it doesn't work, show the receive window
// receiveStore.showReceiveTokens = true;
// }
}
}
},
// storeUnclaimedProofs: function (encodedToken: string) {
// const proofs: WalletProof[] = token.getProofs(token.decode(encodedToken))
// const unclaimedProofs = proofs.filter((p) =>
// !this.unclaimedProofs.map((pu) => pu.secret).includes(p.secret)
// );
// this.unclaimedProofs = this.unclaimedProofs.concat(unclaimedProofs)
// },
// getUnclaimedProofs: function (encodedToken: string): WalletProof[] {
// // get all unclaimedProofs from the same mint as encodedToken
// const mintUrl = token.getMint(token.decode(encodedToken))
// const mintsStore = useMintsStore()
// const mint = mintsStore.mints.find((m) => m.url == mintUrl);
// if (mint == undefined) {
// return []
// }
// const mintKeysetIds = mint.keysets.map((k) => k.id)
// const unclaimedProofsFromSameMint = this.unclaimedProofs.filter((p) => mintKeysetIds.includes(p.id))
// console.log(`unclaimed: ${unclaimedProofsFromSameMint.length}`)
// return unclaimedProofsFromSameMint;
// },
tokenAlreadyInHistory: function (tokenStr: string) {
const tokensStore = useTokensStore();
return (
tokensStore.historyTokens.find((t) => t.token === tokenStr) !== undefined
);
},
addPendingTokenToHistory: function (tokenStr: string) {
const tokensStore = useTokensStore();
if (this.tokenAlreadyInHistory(tokenStr)) {
return;
}
const decodedToken = token.decode(tokenStr);
if (decodedToken == undefined) {
throw Error('could not decode token')
}
// get amount from decodedToken.token.proofs[..].amount
const amount = token.getProofs(decodedToken).reduce(
(sum, el) => (sum += el.amount),
0
);

const mintUrl = token.getMint(decodedToken)
const unit = token.getUnit(decodedToken)
tokensStore.addPendingToken({
amount: amount,
serializedProofs: tokenStr,
mint: mintUrl,
unit: unit
});
this.showReceiveTokens = false;
},
generateNPCConnection: async function () {
let conn: NPCConnection
// NOTE: we only support one connection for now
if (!this.npcConnections.length) {
const walletStore = useWalletStore();
const sk = walletStore.seed.slice(0, 32)
const walletPublicKeyHex = getPublicKey(sk) // `pk` is a hex string
const walletPrivateKeyHex = bytesToHex(sk)
// print nsec and npub
console.log('Lightning address for wallet:', nip19.npubEncode(walletPublicKeyHex) + '@npub.cash')
// console.log('nsec:', nip19.nsecEncode(sk))
console.log('npub:', nip19.npubEncode(walletPublicKeyHex))
conn = {
walletPublicKey: walletPublicKeyHex,
walletPrivateKey: walletPrivateKeyHex,
} as NPCConnection;
this.npcConnections = this.npcConnections.concat(conn)
this.npcAddress = nip19.npubEncode(this.npcConnections[0].walletPublicKey) + '@' + this.npcDomain
this.baseURL = `https://${this.npcDomain}`
}
this.signer = new NDKPrivateKeySigner(this.npcConnections[0].walletPrivateKey)
this.ndk = new NDK({ explicitRelayUrls: this.relays, signer: this.signer });
},
generateNip98Event: async function (url: string, method: string, body: string): Promise<string> {
const nip98Event = new NDKEvent(this.ndk);
nip98Event.kind = NIP98Kind;
nip98Event.content = '';
nip98Event.tags = [['u', url], ['method', method]];
// TODO: if body is set, add 'payload' tag with sha256 hash of body
const sig = await nip98Event.sign(this.signer)
const eventString = JSON.stringify(nip98Event.rawEvent());
// encode the eventString to base64
return btoa(eventString)
},
getInfo: async function () {
const authHeader = await this.generateNip98Event(
`${this.baseURL}/api/v1/info`,
"GET"
);
try {
const response = await fetch(`${this.baseURL}/api/v1/info`, {
method: "GET",
headers: {
Authorization: `Nostr ${authHeader}`,
},
})
return response.json()
} catch (e) {
console.error(e)
}
},
getBalance: async function (): Promise<number> {
const authHeader = await this.generateNip98Event(
`${this.baseURL}/api/v1/balance`,
"GET"
);
try {
const response = await fetch(`${this.baseURL}/api/v1/balance`, {
method: "GET",
headers: {
Authorization: `Nostr ${authHeader}`,
},
})
// deserialize the response to NPCBalance
const balance: NPCBalance = await response.json()
if (balance.error) {
return 0
}
return balance.data
} catch (e) {
console.error(e)
return 0
}
},
getClaim: async function (): Promise<string> {
const authHeader = await this.generateNip98Event(
`${this.baseURL}/api/v1/claim`,
"GET"
);
try {
const response = await fetch(`${this.baseURL}/api/v1/claim`, {
method: "GET",
headers: {
Authorization: `Nostr ${authHeader}`,
},
})
// deserialize the response to NPCClaim
const claim: NPCClaim = await response.json()
if (claim.error) {
return ""
}
return claim.data.token
} catch (e) {
console.error(e)
return ""
}
},
// getWithdraw: async function (): Promise<string> {
// const authHeader = await this.generateNip98Event(
// `${this.baseURL}/api/v1/withdrawals`,
// "POST",
// );
// try {
// const response = await fetch(`${this.baseURL}/api/v1/withdraw`, {
// method: "GET",
// headers: {
// Authorization: `Nostr ${authHeader}`,
// },
// })
// // deserialize the response to NPCClaim
// const claim: NPCClaim = await response.json()
// if (claim.error) {
// return ""
// }
// return claim.data.token
// } catch (e) {
// console.error(e)
// return ""
// }
// }
}
});
Loading