diff --git a/src/components/SendTokenDialog.vue b/src/components/SendTokenDialog.vue
index c90f9fa52..31afc22db 100644
--- a/src/components/SendTokenDialog.vue
+++ b/src/components/SendTokenDialog.vue
@@ -77,6 +77,14 @@
style="max-width: 600px"
>
+
+ {{ paymentRequestMintWarning }}
+
@@ -395,6 +403,38 @@ export default defineComponent({
this.isValidPubkey(this.sendData.p2pkPubkey)
);
},
+ // NUT-18's `m` field is mandatory today (cashu-ts `PaymentRequest.mints`
+ // is a plain `string[]`). This constraint helper wraps that so when the
+ // spec change at https://github.com/cashubtc/nuts/pull/381 lands we can
+ // also return `{ kind: "preferred", allowed }` without touching callers:
+ // the button disable + warning logic just keys off `kind`.
+ paymentRequestMintConstraint(): {
+ kind: "mandatory";
+ allowed: string[];
+ } | null {
+ const allowed =
+ this.sendData.paymentRequest?.mints?.filter((m): m is string => !!m) ??
+ [];
+ if (allowed.length === 0) return null;
+ return { kind: "mandatory", allowed };
+ },
+ selectedMintViolatesRequest(): boolean {
+ const c = this.paymentRequestMintConstraint;
+ if (!c) return false;
+ if (!this.activeMintUrl) return true;
+ return !c.allowed.includes(this.activeMintUrl);
+ },
+ paymentRequestMintWarning(): string {
+ // Only block (and warn) for mandatory constraints. Preferred-mint
+ // copy can be added later as a separate i18n key without reshaping
+ // this method.
+ const c = this.paymentRequestMintConstraint;
+ if (!c || c.kind !== "mandatory") return "";
+ if (!this.selectedMintViolatesRequest) return "";
+ return this.$t(
+ "SendTokenDialog.errors.mint_not_allowed_by_request"
+ ) as string;
+ },
paymentRequestButtonDisabled(): boolean {
if (!this.sendData.paymentRequest) {
return true;
@@ -409,10 +449,24 @@ export default defineComponent({
if (this.globalMutexLock) {
return true;
}
+ const c = this.paymentRequestMintConstraint;
+ if (c && c.kind === "mandatory" && this.selectedMintViolatesRequest) {
+ return true;
+ }
return false;
},
},
watch: {
+ activeMintUrl: function (newUrl, oldUrl) {
+ // When the user switches mint inside the Pay-PaymentRequest sheet, any
+ // proofs we already prepared belong to the previous mint (and embed
+ // that mint URL in the serialized token). Drop them so the next pay
+ // attempt rebuilds from the newly-selected mint.
+ if (!this.showSendTokens) return;
+ if (!this.sendData.paymentRequest) return;
+ if (!newUrl || !oldUrl || newUrl === oldUrl) return;
+ useSendTokensStore().invalidatePreparedPaymentRequestToken();
+ },
showSendTokens: function (val) {
if (val) {
this.$nextTick(() => {
diff --git a/src/i18n/cs-CZ/index.ts b/src/i18n/cs-CZ/index.ts
index ef6aa2d26..3f3f64bf0 100644
--- a/src/i18n/cs-CZ/index.ts
+++ b/src/i18n/cs-CZ/index.ts
@@ -1050,6 +1050,8 @@ export default {
errors: {
amount_required: "Nejprve zadejte částku.",
serialization_failed: "Nepodařilo se připravit ecash token.",
+ mint_not_allowed_by_request:
+ "Vybraný mint není tímto platebním požadavkem přijímán.",
},
},
diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts
index d81129da5..e7cecaba9 100644
--- a/src/i18n/en-US/index.ts
+++ b/src/i18n/en-US/index.ts
@@ -1040,6 +1040,8 @@ export default {
errors: {
amount_required: "Enter an amount first.",
serialization_failed: "Could not prepare ecash token.",
+ mint_not_allowed_by_request:
+ "The selected mint is not accepted by this payment request.",
},
},
SendPaymentRequest: {
diff --git a/src/i18n/pt-BR/index.ts b/src/i18n/pt-BR/index.ts
index 265bfce50..2656a369b 100644
--- a/src/i18n/pt-BR/index.ts
+++ b/src/i18n/pt-BR/index.ts
@@ -1047,6 +1047,8 @@ export default {
errors: {
amount_required: "Insira um valor primeiro.",
serialization_failed: "Não foi possível preparar o token ecash.",
+ mint_not_allowed_by_request:
+ "O mint selecionado não é aceito por esta solicitação de pagamento.",
},
},
SendPaymentRequest: {
diff --git a/src/stores/proofs.ts b/src/stores/proofs.ts
index ff95e963f..dcbfa8c05 100644
--- a/src/stores/proofs.ts
+++ b/src/stores/proofs.ts
@@ -39,8 +39,13 @@ export const useProofsStore = defineStore("proofs", {
},
});
- // Function to update activeProofs
- const updateActiveProofs = async () => {
+ // Filter the live in-memory proofs ref instead of re-querying cashuDb.
+ // The liveQuery above keeps `proofs.value` in sync with the table, so
+ // doing this synchronously lets activeProofs update in the same tick
+ // as activeMintUrl/activeUnit — otherwise downstream getters like
+ // activeBalance briefly read the old mint's proofs and the UI flashes
+ // an "insufficient balance" warning before the async fetch resolves.
+ const updateActiveProofs = () => {
const mintStore = useMintsStore();
const currentMint = mintStore.mints.find(
(m) => m.url === mintStore.activeMintUrl
@@ -58,15 +63,10 @@ export const useProofsStore = defineStore("proofs", {
return;
}
- const keysetIds = unitKeysets.map((k) => k.id);
- const activeProofs = await cashuDb.proofs
- .where("id")
- .anyOf(keysetIds)
- .toArray()
- .then((proofs) => {
- return coerceWalletProofs(proofs).filter((p) => !p.reserved);
- });
- mintStore.activeProofs = activeProofs;
+ const keysetIds = new Set(unitKeysets.map((k) => k.id));
+ mintStore.activeProofs = proofs.value.filter(
+ (p) => keysetIds.has(p.id) && !p.reserved
+ );
};
return {
diff --git a/src/stores/sendTokensStore.ts b/src/stores/sendTokensStore.ts
index 9a0249e45..afd27d416 100644
--- a/src/stores/sendTokensStore.ts
+++ b/src/stores/sendTokensStore.ts
@@ -37,5 +37,18 @@ export const useSendTokensStore = defineStore("sendTokensStore", {
this.sendData.paymentRequest = undefined;
this.sendData.historyToken = undefined;
},
+ // Drops any token/proofs that were prepared for the current PaymentRequest
+ // so the next pay attempt rebuilds them from the currently-active mint.
+ // No-op when no PR is active (regular send flows keep their tokensBase64
+ // until the dialog closes).
+ invalidatePreparedPaymentRequestToken(): boolean {
+ if (!this.sendData.paymentRequest) return false;
+ if (!this.sendData.tokensBase64) return false;
+ this.sendData.tokens = "";
+ this.sendData.tokensBase64 = "";
+ this.sendData.historyToken = undefined;
+ this.sendData.historyAmount = null;
+ return true;
+ },
},
});
diff --git a/test/vitest/__tests__/sendTokensStore.test.ts b/test/vitest/__tests__/sendTokensStore.test.ts
new file mode 100644
index 000000000..0c6867ef8
--- /dev/null
+++ b/test/vitest/__tests__/sendTokensStore.test.ts
@@ -0,0 +1,62 @@
+import { test, describe, expect, beforeEach } from "vitest";
+import { setActivePinia, createPinia } from "pinia";
+import { useSendTokensStore } from "src/stores/sendTokensStore";
+
+beforeEach(() => {
+ setActivePinia(createPinia());
+});
+
+describe("sendTokensStore.invalidatePreparedPaymentRequestToken", () => {
+ test("no-op when there is no active payment request", () => {
+ const store = useSendTokensStore();
+ store.sendData.tokensBase64 = "cashuB-xxxxxx";
+ store.sendData.tokens = "stale-proofs";
+ store.sendData.historyToken = { id: "h1" } as any;
+ store.sendData.historyAmount = -42;
+
+ const changed = store.invalidatePreparedPaymentRequestToken();
+
+ expect(changed).toBe(false);
+ // Regular send flows must keep their cached token until the dialog closes.
+ expect(store.sendData.tokensBase64).toBe("cashuB-xxxxxx");
+ expect(store.sendData.tokens).toBe("stale-proofs");
+ expect(store.sendData.historyToken).toEqual({ id: "h1" });
+ expect(store.sendData.historyAmount).toBe(-42);
+ });
+
+ test("no-op when there is no cached token to invalidate", () => {
+ const store = useSendTokensStore();
+ store.sendData.paymentRequest = { id: "pr1" } as any;
+
+ const changed = store.invalidatePreparedPaymentRequestToken();
+
+ expect(changed).toBe(false);
+ expect(store.sendData.paymentRequest).toEqual({ id: "pr1" });
+ });
+
+ test("clears cached PR token state so the next pay attempt rebuilds", () => {
+ const store = useSendTokensStore();
+ store.sendData.paymentRequest = {
+ id: "pr1",
+ mints: ["https://trusted.example"],
+ } as any;
+ store.sendData.tokensBase64 = "cashuB-from-wrong-mint";
+ store.sendData.tokens = "stale-proofs";
+ store.sendData.historyToken = { id: "h1" } as any;
+ store.sendData.historyAmount = -42;
+
+ const changed = store.invalidatePreparedPaymentRequestToken();
+
+ expect(changed).toBe(true);
+ expect(store.sendData.tokensBase64).toBe("");
+ expect(store.sendData.tokens).toBe("");
+ expect(store.sendData.historyToken).toBeUndefined();
+ expect(store.sendData.historyAmount).toBeNull();
+ // The payment request itself must stay intact – only the prepared
+ // proofs/token are invalidated.
+ expect(store.sendData.paymentRequest).toEqual({
+ id: "pr1",
+ mints: ["https://trusted.example"],
+ });
+ });
+});