- Ecash
+ {{
+ sendData.historyAmount && sendData.historyAmount < 0
+ ? "Sent"
+ : "Received"
+ }}
+ Ecash
@@ -259,6 +264,11 @@
{{ displayUnit }}
+
+
+ Fees: {{ formatCurrency(paidFees, tokenUnit) }}
+
+
+
+ {{
+ ndefSupported ? "Flash to NFC card" : "NDEF unsupported"
+ }}
+
+
+
+
Delete from history
- Close
@@ -380,7 +425,12 @@ import { mapActions, mapState, mapWritableState } from "pinia";
import ChooseMint from "components/ChooseMint.vue";
import { UR, UREncoder } from "@gandlaf21/bc-ur";
import SendPaymentRequest from "./SendPaymentRequest.vue";
-
+import {
+ notifyError,
+ notifySuccess,
+ notify,
+ notifyWarning,
+} from "src/js/notify.ts";
export default defineComponent({
name: "SendTokenDialog",
mixins: [windowMixin],
@@ -414,6 +464,8 @@ export default defineComponent({
framentInervalSlow: 500,
fragmentSpeedLabel: "F",
isV4Token: false,
+ scanningCard: false,
+ ndefSupported: "NDEFReader" in globalThis,
};
},
computed: {
@@ -439,6 +491,7 @@ export default defineComponent({
...mapState(useSettingsStore, [
"checkSentTokens",
"includeFeesInSendAmount",
+ "nfcEncoding",
]),
...mapState(useWorkersStore, ["tokenWorkerRunning"]),
// TOKEN METHODS
@@ -458,6 +511,9 @@ export default defineComponent({
let mint = token.getMint(token.decode(this.sendData.tokensBase64));
return mint;
},
+ paidFees: function () {
+ return this.sumProofs - Math.abs(this.sendData.historyAmount);
+ },
displayMemo: function () {
return token.getMemo(token.decode(this.sendData.tokensBase64));
},
@@ -648,6 +704,104 @@ export default defineComponent({
this.showDeleteDialog = false;
this.clearAllWorkers();
},
+ writeTokensToCard: function () {
+ if (!this.scanningCard) {
+ try {
+ this.ndef = new NDEFReader();
+ this.controller = new AbortController();
+ const signal = this.controller.signal;
+ this.ndef
+ .scan({ signal })
+ .then(() => {
+ console.log("> Scan started");
+
+ this.ndef.onreadingerror = (error) => {
+ console.error(`Cannot read NDEF data! ${error}`);
+ notifyError("Cannot read data from the NFC tag");
+ this.controller.abort();
+ this.scanningCard = false;
+ };
+
+ this.ndef.onreading = ({ message, serialNumber }) => {
+ console.log(`Read card ${serialNumber}`);
+ this.controller.abort();
+ this.scanningCard = false;
+ try {
+ let records = [];
+ switch (this.nfcEncoding) {
+ case "text":
+ records = [
+ {
+ recordType: "text",
+ data: `${this.sendData.tokensBase64}`,
+ },
+ ];
+ break;
+ case "weburl":
+ records = [
+ {
+ recordType: "url",
+ data: `${window.location}#token=${this.sendData.tokensBase64}`,
+ },
+ ];
+ break;
+ case "binary":
+ throw new Error("Binary encoding not supported yet");
+ /*
+ const data = null;
+ records = [
+ {
+ recordType: "mime",
+ mediaType: "application/octet-stream",
+ data: data,
+ },
+ ];
+ break;
+ */
+ default:
+ throw new Error(
+ `Unknown NFC encoding: ${this.nfcEncoding}`
+ );
+ }
+ this.ndef
+ .write({ records: records }, { overwrite: true })
+ .then(() => {
+ console.log("Successfully flashed tokens to card!");
+ notifySuccess("Successfully flashed tokens to card!");
+ this.showSendTokens = false;
+ })
+ .catch((err) => {
+ console.error(
+ `NFC write failed: The card may not have enough capacity (needed ${records[0].data.length} bytes).`
+ );
+ notifyError(
+ `NFC write failed: The card may not have enough capacity (needed ${records[0].data.length} bytes).`
+ );
+ });
+ } catch (err) {
+ console.error(`NFC error: ${err.message}`);
+ notifyError(`NFC error: ${err.message}`);
+ }
+ };
+ this.scanningCard = true;
+ })
+ .catch((error) => {
+ console.error(`NFC error: ${error.message}`);
+ notifyError(`NFC error: ${error.message}`);
+ this.scanningCard = false;
+ });
+ notifyWarning("This will overwrite your card!");
+ } catch (error) {
+ console.error(`NFC error: ${error.message}`);
+ notifyError(`NFC error: ${error.message}`);
+ this.scanningCard = false;
+ }
+ }
+ },
+ closeCardScanner: function () {
+ this.controller.abort();
+ this.scanningCard = false;
+ },
lockTokens: async function () {
let sendAmount = this.sendData.amount;
// if unit is USD, multiply by 100
@@ -704,8 +858,9 @@ export default defineComponent({
// update UI
this.sendData.tokens = sendProofs;
-
this.sendData.tokensBase64 = this.serializeProofs(sendProofs);
+ this.sendData.historyAmount = -this.sendData.amount;
+
this.addPendingToken({
amount: -sendAmount,
serializedProofs: this.sendData.tokensBase64,
diff --git a/src/components/SettingsView.vue b/src/components/SettingsView.vue
index 273600d0..75d3dd4b 100644
--- a/src/components/SettingsView.vue
+++ b/src/components/SettingsView.vue
@@ -533,6 +533,91 @@
+
+
+
+
+
+ WebNFC
+
+ Choose the encoding for writing to NFC cards
+
+
+
+
+
+
+
+
+ Text
+ Store token in plain text
+
+
+
+
+
+
+
+ URL
+
+ Store URL to this wallet with token
+
+
+
+
+
+
+
+
@@ -1062,6 +1147,7 @@ export default defineComponent({
nip46Token: "",
nip07SignerAvailable: false,
newRelay: "",
+ ndefSupported: "NDEFReader" in globalThis,
};
},
computed: {
@@ -1069,6 +1155,7 @@ export default defineComponent({
"getBitcoinPrice",
"checkSentTokens",
"useWebsockets",
+ "nfcEncoding",
]),
...mapState(useP2PKStore, ["p2pkKeys"]),
...mapWritableState(useP2PKStore, ["showP2PKDialog"]),
diff --git a/src/pages/WalletPage.vue b/src/pages/WalletPage.vue
index 7f94f311..d87c442c 100644
--- a/src/pages/WalletPage.vue
+++ b/src/pages/WalletPage.vue
@@ -215,9 +215,8 @@ import { useNPCStore } from "src/stores/npubcash";
import { useNostrStore } from "src/stores/nostr";
import { usePRStore } from "src/stores/payment-request";
import { useStorageStore } from "src/stores/storage";
-
import ReceiveTokenDialog from "src/components/ReceiveTokenDialog.vue";
-import { notifyError, notifySuccess, notify } from "../js/notify";
+import { notifyError, notify } from "../js/notify";
export default {
mixins: [windowMixin],
diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts
index 6f0ce8a7..da05adb3 100644
--- a/src/stores/nostr.ts
+++ b/src/stores/nostr.ts
@@ -403,7 +403,7 @@ export const useNostrStore = defineStore("nostr", {
const receiveStore = useReceiveTokensStore();
const prStore = usePRStore();
const sendTokensStore = useSendTokensStore();
-
+ const tokensStore = useTokensStore();
const proofs = payload.proofs;
const mint = payload.mint;
const unit = payload.unit;
@@ -415,7 +415,7 @@ export const useNostrStore = defineStore("nostr", {
const tokenStr = getEncodedTokenV4(token);
- const tokenInHistory = this.tokenAlreadyInHistory(tokenStr);
+ const tokenInHistory = tokensStore.tokenAlreadyInHistory(tokenStr);
if (tokenInHistory && tokenInHistory.amount > 0) {
console.log("### incoming token already in history");
return;
@@ -453,18 +453,14 @@ export const useNostrStore = defineStore("nostr", {
await this.addPendingTokenToHistory(tokenStr);
}
},
- tokenAlreadyInHistory: function (tokenStr: string): HistoryToken | undefined {
- const tokensStore = useTokensStore();
- return tokensStore.historyTokens.find((t) => t.token === tokenStr);
- },
addPendingTokenToHistory: function (tokenStr: string, verbose = true) {
const receiveStore = useReceiveTokensStore();
- if (this.tokenAlreadyInHistory(tokenStr)) {
+ const tokensStore = useTokensStore();
+ if (tokensStore.tokenAlreadyInHistory(tokenStr)) {
notifySuccess("Ecash already in history");
receiveStore.showReceiveTokens = false;
return;
}
- const tokensStore = useTokensStore();
const decodedToken = token.decode(tokenStr);
if (decodedToken == undefined) {
throw Error('could not decode token')
diff --git a/src/stores/receiveTokensStore.js b/src/stores/receiveTokensStore.js
index 15f01592..f8665c98 100644
--- a/src/stores/receiveTokensStore.js
+++ b/src/stores/receiveTokensStore.js
@@ -4,6 +4,8 @@ import { useUiStore } from "./ui";
import { useP2PKStore } from "./p2pk";
import { useWalletStore } from "./wallet";
import token from "src/js/token";
+import { useTokensStore } from "./tokens";
+import { notifyError, notifySuccess, notify } from "../js/notify";
export const useReceiveTokensStore = defineStore("receiveTokensStore", {
state: () => ({
@@ -12,6 +14,7 @@ export const useReceiveTokensStore = defineStore("receiveTokensStore", {
tokensBase64: "",
p2pkPrivateKey: "",
},
+ scanningCard: false,
}),
actions: {
decodeToken: function (encoded_token) {
@@ -31,8 +34,6 @@ export const useReceiveTokensStore = defineStore("receiveTokensStore", {
const mintStore = useMintsStore();
const walletStore = useWalletStore();
const receiveStore = useReceiveTokensStore();
- const uIStore = useUiStore();
- receiveStore.showReceiveTokens = false;
console.log("### receive tokens", receiveStore.receiveData.tokensBase64);
if (receiveStore.receiveData.tokensBase64.length == 0) {
@@ -56,6 +57,7 @@ export const useReceiveTokensStore = defineStore("receiveTokensStore", {
}
// redeem the token
await walletStore.redeem(receiveStore.receiveData.tokensBase64);
+ receiveStore.showReceiveTokens = false;
},
receiveIfDecodes: async function () {
try {
@@ -69,5 +71,100 @@ export const useReceiveTokensStore = defineStore("receiveTokensStore", {
return false;
}
},
+ toggleScanner: function () {
+ const receiveStore = useReceiveTokensStore();
+ const tokenStore = useTokensStore();
+ if (this.scanningCard === false) {
+ try {
+ this.ndef = new window.NDEFReader();
+ this.controller = new AbortController();
+ const signal = this.controller.signal;
+ this.ndef
+ .scan({ signal })
+ .then(() => {
+ console.log("> Scan started");
+
+ this.ndef.addEventListener("readingerror", () => {
+ console.error("Cannot read data from the NFC tag.");
+ notifyError("Cannot read data from the NFC tag.");
+ this.controller.abort();
+ this.scanningCard = false;
+ });
+
+ this.ndef.addEventListener(
+ "reading",
+ ({ message, serialNumber }) => {
+ try {
+ const record = message.records[0];
+ const recordType = record.recordType;
+ let tokenStr = "";
+ switch (recordType) {
+ case "text":
+ const text = new TextDecoder().decode(record.data);
+ if (!text.startsWith("cashu")) {
+ throw new Error(
+ "text does not contain a cashu token"
+ );
+ }
+ tokenStr = text;
+ break;
+ case "url":
+ const url = new TextDecoder().decode(record.data);
+ const i = url.indexOf("#token=cashu");
+ if (i === -1) {
+ throw new Error("URL does not contain a cashu token");
+ }
+ tokenStr = url.substring(i + 7);
+ break;
+ case "mime":
+ if (record.mediaType !== "application/octet-stream") {
+ throw new Error("binary data expected");
+ }
+ const data = new Uint8Array(record.data.buffer);
+ const prefix = String.fromCharCode(...data.slice(0, 4));
+ if (prefix !== "craw") {
+ throw new Error(
+ "binary data does not contain a cashu token"
+ );
+ }
+ // TODO: decode the binary token from data
+ throw new Error(
+ "binary token parsing not implemented yet"
+ );
+ break;
+ default:
+ throw new Error(`unsupported recordType ${recordType}`);
+ }
+ const historyToken =
+ tokenStore.tokenAlreadyInHistory(tokenStr);
+ if (!historyToken || historyToken.status === "pending") {
+ receiveStore.receiveData.tokensBase64 = tokenStr;
+ receiveStore.showReceiveTokens = true;
+ } else {
+ notify("Token already in history.");
+ }
+ } catch (err) {
+ console.error(`Something went wrong! ${err}`);
+ notifyError(`Something went wrong! ${err}`);
+ }
+ this.controller.abort();
+ this.scanningCard = false;
+ }
+ );
+ this.scanningCard = true;
+ })
+ .catch((error) => {
+ console.error(`Scan error: ${error.message}`);
+ notifyError(`Scan error: ${error.message}`);
+ });
+ } catch (error) {
+ console.error(`NFC error: ${error.message}`);
+ notifyError(`NFC error: ${error.message}`);
+ }
+ } else {
+ this.controller.abort();
+ this.scanningCard = false;
+ }
+ },
},
});
diff --git a/src/stores/sendTokensStore.ts b/src/stores/sendTokensStore.ts
index 8f94c817..9a384f2e 100644
--- a/src/stores/sendTokensStore.ts
+++ b/src/stores/sendTokensStore.ts
@@ -1,5 +1,6 @@
import { defineStore } from "pinia";
import { decodePaymentRequest, PaymentRequest } from "@cashu/cashu-ts";
+import { HistoryToken } from "./tokens";
export const useSendTokensStore = defineStore("sendTokensStore", {
state: () => ({
@@ -7,28 +8,34 @@ export const useSendTokensStore = defineStore("sendTokensStore", {
showLockInput: false,
sendData: {
amount: null,
+ historyAmount: null,
memo: "",
tokens: "",
tokensBase64: "",
p2pkPubkey: "",
paymentRequest: undefined,
+ historyToken: undefined,
} as {
amount: number | null;
+ historyAmount: number | null;
memo: string;
tokens: string;
tokensBase64: string;
p2pkPubkey: string;
paymentRequest?: PaymentRequest;
+ historyToken: HistoryToken | undefined;
},
}),
actions: {
clearSendData() {
this.sendData.amount = null;
+ this.sendData.historyAmount = null;
this.sendData.memo = "";
this.sendData.tokens = "";
this.sendData.tokensBase64 = "";
this.sendData.p2pkPubkey = "";
this.sendData.paymentRequest = undefined;
+ this.sendData.historyToken = undefined;
}
},
});
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 33392d7b..66ebcfab 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -1,16 +1,39 @@
import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core";
-const defaultNostrRelays = ["wss://relay.damus.io", "wss://relay.8333.space/", "wss://nos.lol"]
+const defaultNostrRelays = [
+ "wss://relay.damus.io",
+ "wss://relay.8333.space/",
+ "wss://nos.lol",
+];
export const useSettingsStore = defineStore("settings", {
state: () => {
return {
- getBitcoinPrice: useLocalStorage("cashu.settings.getBitcoinPrice", false),
- checkSentTokens: useLocalStorage("cashu.settings.checkSentTokens", true),
- useWebsockets: useLocalStorage("cashu.settings.useWebsockets", false),
- defaultNostrRelays: useLocalStorage("cashu.settings.defaultNostrRelays", defaultNostrRelays),
- includeFeesInSendAmount: useLocalStorage("cashu.settings.includeFeesInSendAmount", false),
- }
- }
+ getBitcoinPrice: useLocalStorage(
+ "cashu.settings.getBitcoinPrice",
+ false
+ ),
+ checkSentTokens: useLocalStorage(
+ "cashu.settings.checkSentTokens",
+ true
+ ),
+ useWebsockets: useLocalStorage(
+ "cashu.settings.useWebsockets",
+ false
+ ),
+ defaultNostrRelays: useLocalStorage(
+ "cashu.settings.defaultNostrRelays",
+ defaultNostrRelays
+ ),
+ includeFeesInSendAmount: useLocalStorage(
+ "cashu.settings.includeFeesInSendAmount",
+ false
+ ),
+ nfcEncoding: useLocalStorage(
+ "cashu.settings.nfcEncoding",
+ "weburl"
+ ),
+ };
+ },
});
diff --git a/src/stores/tokens.ts b/src/stores/tokens.ts
index 4e704a5a..e583bca7 100644
--- a/src/stores/tokens.ts
+++ b/src/stores/tokens.ts
@@ -2,6 +2,7 @@ import { useLocalStorage } from "@vueuse/core";
import { date } from "quasar";
import { defineStore } from "pinia";
import { PaymentRequest, Proof, Token } from "@cashu/cashu-ts";
+import token from "src/js/token";
/**
* The tokens store handles everything related to tokens and proofs
@@ -121,7 +122,10 @@ export const useTokensStore = defineStore("tokens", {
if (index >= 0) {
this.historyTokens.splice(index, 1);
}
- }
+ },
+ tokenAlreadyInHistory(tokenStr: string): HistoryToken | undefined {
+ return this.historyTokens.find((t) => t.token === tokenStr);
+ },
},
});