mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
feat: await trusted mint management actions
This commit is contained in:
@@ -0,0 +1,152 @@
|
|||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import {
|
||||||
|
fetchMintAddress,
|
||||||
|
fetchPayRequest,
|
||||||
|
lightningAddressUsername,
|
||||||
|
mintAddressUrl,
|
||||||
|
resolveMintInput,
|
||||||
|
serverOf,
|
||||||
|
} from 'lnurlcash-kit';
|
||||||
|
|
||||||
|
import { mintAddressCacheInfo } from '@/lnurlcash/trustedMints';
|
||||||
|
import { useMintsStore } from '@/stores/mints';
|
||||||
|
import { useWalletStore } from '@/stores/wallet';
|
||||||
|
|
||||||
|
export const useManageMintsPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const mints = useMintsStore();
|
||||||
|
const wallet = useWalletStore();
|
||||||
|
const toast = (type: 'positive' | 'negative' | 'warning' | 'info', message: string): void => {
|
||||||
|
if (typeof $q.notify === 'function') {
|
||||||
|
$q.notify({ type, message, position: 'top', timeout: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const fingerprint = (pubkey: string): string =>
|
||||||
|
pubkey.length > 18 ? `${pubkey.slice(0, 10)}…${pubkey.slice(-8)}` : pubkey;
|
||||||
|
const balanceAt = (server: string): string =>
|
||||||
|
(wallet.balanceByMintSats.get(server) ?? 0).toLocaleString(undefined, {
|
||||||
|
maximumFractionDigits: 3,
|
||||||
|
});
|
||||||
|
const suggestions = computed(() =>
|
||||||
|
mints.PUBLIC_MINTS.filter(
|
||||||
|
(address) => !mints.mints.some((mint) => mint.server === address.replace(/^@/, '')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const banner = ref('');
|
||||||
|
const addServer = ref('');
|
||||||
|
const addPubkey = ref('');
|
||||||
|
const discovering = ref('');
|
||||||
|
const confirmingRemove = ref(false);
|
||||||
|
const removeTarget = ref('');
|
||||||
|
const trustResult = (result: string, server: string): void => {
|
||||||
|
if (result === 'rekey-pending') {
|
||||||
|
toast('warning', `${server} advertised a different signing key - review it above.`);
|
||||||
|
} else if (result === 'unchanged') {
|
||||||
|
toast('info', `${server} is already trusted.`);
|
||||||
|
} else {
|
||||||
|
toast('positive', `${server} is now trusted.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const trustManual = async (): Promise<void> => {
|
||||||
|
banner.value = '';
|
||||||
|
try {
|
||||||
|
const result = await mints.trust(addServer.value, addPubkey.value);
|
||||||
|
trustResult(result, addServer.value.trim());
|
||||||
|
addServer.value = '';
|
||||||
|
addPubkey.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
banner.value = error instanceof Error ? error.message : 'Could not trust that mint.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const trustSuggestion = async (address: string): Promise<void> => {
|
||||||
|
if (discovering.value) return;
|
||||||
|
banner.value = '';
|
||||||
|
discovering.value = address;
|
||||||
|
try {
|
||||||
|
const url = resolveMintInput(address);
|
||||||
|
if (!url) throw new Error('That mint address cannot be resolved.');
|
||||||
|
let nodeInfo = null;
|
||||||
|
let payUrl = url;
|
||||||
|
const addressUrl = mintAddressUrl(url);
|
||||||
|
if (addressUrl) {
|
||||||
|
try {
|
||||||
|
nodeInfo = await fetchMintAddress(addressUrl);
|
||||||
|
payUrl = nodeInfo.payLink;
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const info = await fetchPayRequest(payUrl);
|
||||||
|
const announcedKey = nodeInfo?.nodePubkey ?? info.mintPubkey;
|
||||||
|
if (!announcedKey) {
|
||||||
|
throw new Error("This mint didn't announce its signing key - add it manually instead.");
|
||||||
|
}
|
||||||
|
const server = serverOf(payUrl);
|
||||||
|
const result = await mints.trust(
|
||||||
|
server,
|
||||||
|
announcedKey,
|
||||||
|
mintAddressCacheInfo(nodeInfo, lightningAddressUsername(payUrl)),
|
||||||
|
);
|
||||||
|
trustResult(result, server);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Could not reach that mint.';
|
||||||
|
banner.value = `Could not add ${address}: ${message}`;
|
||||||
|
} finally {
|
||||||
|
discovering.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const askRemove = (server: string): void => {
|
||||||
|
banner.value = '';
|
||||||
|
removeTarget.value = server;
|
||||||
|
confirmingRemove.value = true;
|
||||||
|
};
|
||||||
|
const doRemove = async (): Promise<void> => {
|
||||||
|
confirmingRemove.value = false;
|
||||||
|
try {
|
||||||
|
await mints.remove(removeTarget.value);
|
||||||
|
if (mints.defaultMint === removeTarget.value) mints.setDefaultMint(null);
|
||||||
|
toast('positive', `${removeTarget.value} removed.`);
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) throw error;
|
||||||
|
banner.value = `${removeTarget.value} can't be removed while you hold notes from it - move or spend them first.`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const confirmRekey = async (server: string): Promise<void> => {
|
||||||
|
banner.value = '';
|
||||||
|
try {
|
||||||
|
await mints.confirmRekey(server);
|
||||||
|
} catch (error) {
|
||||||
|
banner.value = error instanceof Error ? error.message : 'Could not confirm that signing key.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dismissRekey = async (server: string): Promise<void> => {
|
||||||
|
banner.value = '';
|
||||||
|
try {
|
||||||
|
await mints.dismissRekey(server);
|
||||||
|
} catch (error) {
|
||||||
|
banner.value = error instanceof Error ? error.message : 'Could not dismiss that signing key.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
addPubkey,
|
||||||
|
addServer,
|
||||||
|
askRemove,
|
||||||
|
balanceAt,
|
||||||
|
banner,
|
||||||
|
confirmingRemove,
|
||||||
|
confirmRekey,
|
||||||
|
discovering,
|
||||||
|
dismissRekey,
|
||||||
|
doRemove,
|
||||||
|
fingerprint,
|
||||||
|
mints,
|
||||||
|
removeTarget,
|
||||||
|
router,
|
||||||
|
suggestions,
|
||||||
|
trustManual,
|
||||||
|
trustSuggestion,
|
||||||
|
};
|
||||||
|
};
|
||||||
+22
-147
@@ -36,15 +36,9 @@
|
|||||||
color="warning"
|
color="warning"
|
||||||
text-color="dark"
|
text-color="dark"
|
||||||
label="Confirm new key"
|
label="Confirm new key"
|
||||||
@click="mints.confirmRekey(mint.server)"
|
@click="confirmRekey(mint.server)"
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
color="grey-5"
|
|
||||||
label="Dismiss"
|
|
||||||
@click="mints.dismissRekey(mint.server)"
|
|
||||||
/>
|
/>
|
||||||
|
<q-btn flat dense color="grey-5" label="Dismiss" @click="dismissRekey(mint.server)" />
|
||||||
</div>
|
</div>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -195,146 +189,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { useManageMintsPage } from '@/composables/useManageMintsPage';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useQuasar } from 'quasar';
|
|
||||||
import {
|
|
||||||
fetchMintAddress,
|
|
||||||
fetchPayRequest,
|
|
||||||
lightningAddressUsername,
|
|
||||||
mintAddressUrl,
|
|
||||||
resolveMintInput,
|
|
||||||
serverOf,
|
|
||||||
} from 'lnurlcash-kit';
|
|
||||||
|
|
||||||
import { mintAddressCacheInfo } from '@/lnurlcash/trustedMints';
|
const {
|
||||||
import { useMintsStore } from '@/stores/mints';
|
addPubkey,
|
||||||
import { useWalletStore } from '@/stores/wallet';
|
addServer,
|
||||||
|
askRemove,
|
||||||
const router = useRouter();
|
balanceAt,
|
||||||
const $q = useQuasar();
|
banner,
|
||||||
const mints = useMintsStore();
|
confirmingRemove,
|
||||||
const wallet = useWalletStore();
|
confirmRekey,
|
||||||
|
discovering,
|
||||||
const toast = (type: 'positive' | 'negative' | 'warning' | 'info', message: string): void => {
|
dismissRekey,
|
||||||
// guarded: the Notify plugin registration lives in quasar.config, outside
|
doRemove,
|
||||||
// this component's control - a missing registration must not break a flow
|
fingerprint,
|
||||||
if (typeof $q.notify === 'function') {
|
mints,
|
||||||
$q.notify({ type, message, position: 'top', timeout: 3000 });
|
removeTarget,
|
||||||
}
|
router,
|
||||||
};
|
suggestions,
|
||||||
|
trustManual,
|
||||||
const fingerprint = (pubkey: string): string =>
|
trustSuggestion,
|
||||||
pubkey.length > 18 ? `${pubkey.slice(0, 10)}…${pubkey.slice(-8)}` : pubkey;
|
} = useManageMintsPage();
|
||||||
|
|
||||||
const balanceAt = (server: string): string =>
|
|
||||||
(wallet.balanceByMintSats.get(server) ?? 0).toLocaleString(undefined, {
|
|
||||||
maximumFractionDigits: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- suggestions (PUBLIC_MINTS not yet trusted) ----
|
|
||||||
const suggestions = computed(() =>
|
|
||||||
mints.PUBLIC_MINTS.filter(
|
|
||||||
(address) => !mints.mints.some((m) => m.server === address.replace(/^@/, '')),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const banner = ref('');
|
|
||||||
|
|
||||||
// ---- manual add ----
|
|
||||||
const addServer = ref('');
|
|
||||||
const addPubkey = ref('');
|
|
||||||
|
|
||||||
const trustResult = (result: string, server: string): void => {
|
|
||||||
if (result === 'rekey-pending') {
|
|
||||||
toast('warning', `${server} advertised a different signing key - review it above.`);
|
|
||||||
} else if (result === 'unchanged') {
|
|
||||||
toast('info', `${server} is already trusted.`);
|
|
||||||
} else {
|
|
||||||
toast('positive', `${server} is now trusted.`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trustManual = () => {
|
|
||||||
banner.value = '';
|
|
||||||
try {
|
|
||||||
const result = mints.trust(addServer.value, addPubkey.value);
|
|
||||||
trustResult(result, addServer.value.trim());
|
|
||||||
addServer.value = '';
|
|
||||||
addPubkey.value = '';
|
|
||||||
} catch (err) {
|
|
||||||
banner.value = err instanceof Error ? err.message : 'Could not trust that mint.';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- one-tap suggestion: discover the mint's signing key live, then trust ----
|
|
||||||
const discovering = ref('');
|
|
||||||
|
|
||||||
const trustSuggestion = async (address: string) => {
|
|
||||||
if (discovering.value) return;
|
|
||||||
banner.value = '';
|
|
||||||
discovering.value = address;
|
|
||||||
try {
|
|
||||||
const url = resolveMintInput(address);
|
|
||||||
if (!url) throw new Error('That mint address cannot be resolved.');
|
|
||||||
// best-effort mint-address discovery, same shape-narrowing as the
|
|
||||||
// receive flow - its payLink is authoritative when present
|
|
||||||
let nodeInfo = null;
|
|
||||||
let payUrl = url;
|
|
||||||
const addressUrl = mintAddressUrl(url);
|
|
||||||
if (addressUrl) {
|
|
||||||
try {
|
|
||||||
nodeInfo = await fetchMintAddress(addressUrl);
|
|
||||||
payUrl = nodeInfo.payLink;
|
|
||||||
} catch {
|
|
||||||
// no mint-address support here - proceed with just the guess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const info = await fetchPayRequest(payUrl);
|
|
||||||
// the signing key is announced on the mint-address (lnurlw discovery)
|
|
||||||
// response, not the payRequest: per the reference mint (lnurl-mint) a
|
|
||||||
// wallet paying the mint invoice recovers the node id from the invoice
|
|
||||||
// itself, so LUD-25 advertises mintPubkey on the withdraw side only
|
|
||||||
const announcedKey = nodeInfo?.nodePubkey ?? info.mintPubkey;
|
|
||||||
if (!announcedKey) {
|
|
||||||
throw new Error("This mint didn't announce its signing key - add it manually instead.");
|
|
||||||
}
|
|
||||||
const server = serverOf(payUrl);
|
|
||||||
const result = mints.trust(
|
|
||||||
server,
|
|
||||||
announcedKey,
|
|
||||||
mintAddressCacheInfo(nodeInfo, lightningAddressUsername(payUrl)),
|
|
||||||
);
|
|
||||||
trustResult(result, server);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : 'Could not reach that mint.';
|
|
||||||
banner.value = `Could not add ${address}: ${message}`;
|
|
||||||
} finally {
|
|
||||||
discovering.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- remove ----
|
|
||||||
const confirmingRemove = ref(false);
|
|
||||||
const removeTarget = ref('');
|
|
||||||
|
|
||||||
const askRemove = (server: string) => {
|
|
||||||
banner.value = '';
|
|
||||||
removeTarget.value = server;
|
|
||||||
confirmingRemove.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const doRemove = () => {
|
|
||||||
confirmingRemove.value = false;
|
|
||||||
try {
|
|
||||||
mints.remove(removeTarget.value);
|
|
||||||
if (mints.defaultMint === removeTarget.value) mints.setDefaultMint(null);
|
|
||||||
toast('positive', `${removeTarget.value} removed.`);
|
|
||||||
} catch {
|
|
||||||
// a mint you hold notes from is locked against removal - say why
|
|
||||||
banner.value = `${removeTarget.value} can't be removed while you hold notes from it - move or spend them first.`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user