Skip to content
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
2 changes: 1 addition & 1 deletion quasar.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ module.exports = configure(function (/* ctx */) {

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
https: true,
https: false, // http so the local http page can reach the local http Hyperborea Mint (no mixed-content)
open: true, // opens browser window automatically
port: 8080,
},
Expand Down
7 changes: 6 additions & 1 deletion src/components/CreateInvoiceDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,13 @@ export default defineComponent({
return `lightning:${request.toUpperCase()}`;
},
showReusableQuote(): boolean {
// On-chain previously showed a "reusable" prior address here, but "Create
// Address" always mints a NEW one — so the user saw two different QR codes for
// a single deposit. The reuse display was never wired into the create button,
// so drop it for on-chain: go straight to the single created-address page
// (the proper deposit page with the copy button). Bolt12 offers still reuse.
return (
(this.isBolt12 || this.isOnchain) &&
this.isBolt12 &&
!this.showAmountInput &&
this.reusableReceiveQuote !== null
);
Expand Down
10 changes: 10 additions & 0 deletions src/components/HistoryTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@

<!-- Amount -->
<div class="text-right">
<!-- Amountless pending receive (e.g. an on-chain board still
confirming): the net isn't known to the wallet until the mint
credits it, so don't render a misleading "+0" — show progress. -->
<div
v-if="transaction.status === 'pending' && !transaction.amount"
class="amount-text text-weight-bold text-grey-6"
>
{{ isOnchainTransaction(transaction) ? "Confirming…" : "—" }}
</div>
<div
v-else
class="amount-text text-weight-bold"
:class="{
'text-amount-positive':
Expand Down
141 changes: 139 additions & 2 deletions src/components/InvoiceDetailDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@
<!-- Content -->
<div class="content-area">
<q-card-section class="q-pa-none">
<div v-if="invoiceData.request" class="row justify-center q-mb-md">
<div
v-if="invoiceData.request && !pjInProgress && !pjMinted"
class="row justify-center q-mb-md"
>
<div
class="col-12 col-sm-11 col-md-8 q-px-md"
style="max-width: 600px"
Expand Down Expand Up @@ -88,6 +91,64 @@
</div>
</div>

<!-- payjoin-board: incoming tx detected (custom on-ramp progress) -->
<div v-else-if="pjInProgress" class="row justify-center q-mb-md">
<div
class="col-12 col-sm-11 col-md-8 q-px-md"
style="max-width: 600px"
>
<div class="payjoin-progress column items-center justify-center">
<div
class="row items-center justify-center text-weight-bold"
style="color: #ec4899; font-size: 1.15rem"
>
<span>Detected incoming payjoin tx</span>
<q-spinner-dots size="30px" color="pink-4" class="q-ml-sm" />
</div>
<a
:href="pjMeta.url"
target="_blank"
rel="noopener"
class="q-mt-md"
style="color: #ec4899; text-decoration: underline"
>
view on block explorer
<q-icon name="open_in_new" size="xs" class="q-ml-xs" />
</a>
<div class="text-grey-6 q-mt-sm" style="font-size: 0.8rem">
{{ pjStatusText }}
</div>
</div>
</div>
</div>

<!-- payjoin-board: ecash issued -->
<div v-else-if="pjMinted" class="row justify-center q-mb-md">
<div
class="col-12 col-sm-11 col-md-8 q-px-md"
style="max-width: 600px"
>
<div class="payjoin-progress column items-center justify-center">
<transition appear enter-active-class="animated tada">
<q-icon
name="check_circle"
color="positive"
style="font-size: 96px"
/>
</transition>
<div
class="text-positive text-weight-bold q-mt-sm"
style="font-size: 1.15rem"
>
Ecash received
</div>
<div class="text-grey-6 q-mt-xs">
boarded into Ark via payjoin
</div>
</div>
</div>
</div>

<q-card-section class="q-pa-sm">
<div
v-if="invoiceData?.mintQuote"
Expand Down Expand Up @@ -137,7 +198,7 @@
style="max-width: 600px"
>
<q-btn
v-if="invoiceData.request"
v-if="invoiceData.request && !pjInProgress && !pjMinted"
class="full-width"
unelevated
size="lg"
Expand Down Expand Up @@ -166,6 +227,11 @@ import { useWorkersStore } from "../stores/workers";
import MeltQuoteInformation from "./MeltQuoteInformation.vue";
import MintQuoteInformation from "./MintQuoteInformation.vue";
import { LightningMethod } from "src/stores/walletTypes";
import {
fetchAddressTxMetadata,
onchainNetwork,
type MempoolTxMetadata,
} from "src/js/onchain";
// type hint for global mixin
declare const windowMixin: any;

Expand All @@ -183,6 +249,8 @@ export default defineComponent({
copyButtonCopied: false,
copyButtonTimeout: null as any,
metadataRefreshTrigger: 0,
pjMeta: null as MempoolTxMetadata | null,
pjPollTimer: null as any,
};
},
computed: {
Expand Down Expand Up @@ -234,15 +302,76 @@ export default defineComponent({
}
return "lightning:" + this.invoiceData.request;
},
// ── payjoin-board on-ramp progress (custom) ──────────────────────────
isPayjoin(): boolean {
return this.isOnchain && (this.invoiceData.request || "").includes("pj=");
},
pjAddress(): string {
const r = (this.invoiceData.request || "").replace(/^bitcoin:/i, "");
return r.split("?")[0];
},
pjMinted(): boolean {
return this.isPayjoin && this.invoiceData.status === "paid";
},
pjInProgress(): boolean {
return (
this.isPayjoin && !!this.pjMeta && this.invoiceData.status !== "paid"
);
},
pjExplorerName(): string {
return onchainNetwork(this.pjAddress) === "mutinynet"
? "mutinynet.com"
: "mempool.space";
},
pjStatusText(): string {
const c = this.pjMeta?.confirmations ?? 0;
const t = this.pjMeta?.confirmationThreshold ?? 1;
if (c >= t)
return `Confirmed ${c}/${t} — boarding into Ark, issuing your ecash…`;
if (c >= 1)
return `Confirming ${c}/${t} block${t === 1 ? "" : "s"}…`;
return "Seen in mempool — waiting for the first confirmation…";
},
},
watch: {
showInvoiceDetails(val: boolean) {
if (val) {
this.metadataRefreshTrigger += 1;
this.pjMeta = null;
this.startPjPoll();
} else {
this.stopPjPoll();
}
},
"invoiceData.status"(val: string) {
if (val === "paid") this.stopPjPoll();
},
},
methods: {
startPjPoll() {
this.stopPjPoll();
if (!this.isPayjoin) return;
this.pollPjOnce();
this.pjPollTimer = setInterval(() => this.pollPjOnce(), 4000);
},
stopPjPoll() {
if (this.pjPollTimer) {
clearInterval(this.pjPollTimer);
this.pjPollTimer = null;
}
},
async pollPjOnce() {
if (!this.isPayjoin || this.invoiceData.status === "paid") {
this.stopPjPoll();
return;
}
try {
const meta = await fetchAddressTxMetadata(this.pjAddress, 1);
if (meta) this.pjMeta = meta;
} catch (e) {
// transient mempool fetch error — keep polling
}
},
onCopyBolt11: async function () {
const request = this.invoiceData?.request;
if (request) {
Expand All @@ -264,6 +393,7 @@ export default defineComponent({
},
},
beforeUnmount() {
this.stopPjPoll();
if (this.copyButtonTimeout) {
clearTimeout(this.copyButtonTimeout);
}
Expand Down Expand Up @@ -338,4 +468,11 @@ export default defineComponent({
.checkmark-icon {
font-size: clamp(100px, 35vw, 200px);
}

/* payjoin-board on-ramp progress panel */
.payjoin-progress {
min-height: 320px;
padding: 24px;
text-align: center;
}
</style>
34 changes: 30 additions & 4 deletions src/components/MeltQuoteInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@
</div>
</div>

<!-- outbound on-chain offboard: view on block explorer (mirrors inbound) -->
<div v-if="hasTxid" class="row justify-center q-mb-md">
<a
:href="onchainTxUrl"
target="_blank"
rel="noopener"
class="view-on-explorer"
>
view on block explorer
<q-icon name="open_in_new" size="xs" class="q-ml-xs" />
</a>
</div>

<div
v-if="hasTxid && (onchainMetadata || loadingOnchainMetadata)"
class="detail-item q-mb-md"
Expand Down Expand Up @@ -401,10 +414,14 @@ export default defineComponent({
const mint = mintStore.mints.find((m: any) => m.url === this.mintUrl);
const methods =
mint?.info?.nuts?.[5]?.methods || mint?.info?.nuts?.["5"]?.methods;
const method = methods?.find(
(m: any) =>
m.method === LightningMethod.Onchain && m.unit === this.quoteUnit
);
// Prefer the exact unit match; fall back to any on-chain method so a unit
// mismatch can't silently collapse the denominator. Melt is the mint's own
// outgoing tx (settles at 1 conf), so 1 is the correct fallback here.
const method =
methods?.find(
(m: any) =>
m.method === LightningMethod.Onchain && m.unit === this.quoteUnit
) || methods?.find((m: any) => m.method === LightningMethod.Onchain);
return Number(method?.options?.confirmations || 1);
},
async loadOnchainMetadata() {
Expand Down Expand Up @@ -557,6 +574,15 @@ export default defineComponent({
justify-content: flex-end;
}

.view-on-explorer {
display: inline-flex;
align-items: center;
color: var(--q-primary);
text-decoration: underline;
font-size: 14px;
font-weight: 600;
}

.chain-status-fade-enter-active,
.chain-status-fade-leave-active {
transition: opacity 0.2s ease;
Expand Down
Loading