diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue index 071ca4e7..bca39049 100644 --- a/packages/app/src/components/chat/ChatMessage.vue +++ b/packages/app/src/components/chat/ChatMessage.vue @@ -231,6 +231,30 @@ /> +
+ +
+ +
+ +
+ +
+ +
+
{ return detectBitcoinAddresses(props.message.content) }) +const bolt11Invoices = computed(() => { + if (isUser.value) return [] + return detectBolt11Invoices(props.message.content) +}) + +const bolt12Offers = computed(() => { + if (isUser.value) return [] + return detectBolt12Offers(props.message.content) +}) + +const detectedTxIds = computed(() => { + if (isUser.value) return [] + return detectTxIds(props.message.content) +}) + const isCodeResponse = computed(() => !isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code' ) diff --git a/packages/app/src/components/renderers/Bolt11InvoiceCard.vue b/packages/app/src/components/renderers/Bolt11InvoiceCard.vue new file mode 100644 index 00000000..6e89adeb --- /dev/null +++ b/packages/app/src/components/renderers/Bolt11InvoiceCard.vue @@ -0,0 +1,130 @@ + + + diff --git a/packages/app/src/components/renderers/Bolt12OfferCard.vue b/packages/app/src/components/renderers/Bolt12OfferCard.vue new file mode 100644 index 00000000..facf0d2a --- /dev/null +++ b/packages/app/src/components/renderers/Bolt12OfferCard.vue @@ -0,0 +1,72 @@ + + + diff --git a/packages/app/src/components/renderers/FedimintEcashCard.vue b/packages/app/src/components/renderers/FedimintEcashCard.vue new file mode 100644 index 00000000..1e9992f3 --- /dev/null +++ b/packages/app/src/components/renderers/FedimintEcashCard.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/app/src/components/renderers/MempoolTxCard.vue b/packages/app/src/components/renderers/MempoolTxCard.vue new file mode 100644 index 00000000..7b22e818 --- /dev/null +++ b/packages/app/src/components/renderers/MempoolTxCard.vue @@ -0,0 +1,114 @@ + + + diff --git a/packages/app/src/composables/useBitcoinPrice.ts b/packages/app/src/composables/useBitcoinPrice.ts new file mode 100644 index 00000000..039ddbcf --- /dev/null +++ b/packages/app/src/composables/useBitcoinPrice.ts @@ -0,0 +1,70 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue' + +const price = ref(null) +const currency = ref('USD') +const lastUpdated = ref(null) +const isLoading = ref(false) + +let refreshTimer: ReturnType | null = null +let initialized = false + +async function fetchPrice() { + isLoading.value = true + try { + const res = await fetch('https://mempool.space/api/v1/prices') + if (!res.ok) throw new Error('Failed to fetch price') + const data = await res.json() + price.value = data.USD ?? null + lastUpdated.value = Date.now() + } catch { + // Keep existing price on error + } finally { + isLoading.value = false + } +} + +export function useBitcoinPrice() { + function formatPrice(usd: number | null): string { + if (usd === null) return '...' + return '$' + usd.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }) + } + + function satsToUsd(sats: number): string { + if (!price.value) return '...' + const usd = (sats / 100000000) * price.value + if (usd < 0.01) return '<$0.01' + return '$' + usd.toFixed(2) + } + + function usdToSats(usd: number): number | null { + if (!price.value) return null + return Math.round((usd / price.value) * 100000000) + } + + onMounted(() => { + if (!initialized) { + initialized = true + fetchPrice() + refreshTimer = setInterval(fetchPrice, 60000) // 60s refresh + } + }) + + onBeforeUnmount(() => { + if (refreshTimer) { + clearInterval(refreshTimer) + refreshTimer = null + initialized = false + } + }) + + return { + price, + currency, + lastUpdated, + isLoading, + formatPrice, + satsToUsd, + usdToSats, + fetchPrice, + } +} diff --git a/packages/app/src/composables/useLnurlAuth.ts b/packages/app/src/composables/useLnurlAuth.ts new file mode 100644 index 00000000..e6e76146 --- /dev/null +++ b/packages/app/src/composables/useLnurlAuth.ts @@ -0,0 +1,55 @@ +import { ref } from 'vue' + +const lnurlAuthUrl = ref(null) +const isAuthenticated = ref(false) +const lightningIdentity = ref(null) + +const STORAGE_KEY = 'aiui-lnurl-auth-identity' + +function loadIdentity() { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + lightningIdentity.value = stored + isAuthenticated.value = true + } + } catch { /* ignore */ } +} + +loadIdentity() + +export function useLnurlAuth() { + function generateLnurlAuthUrl(): string { + // Generate a challenge for LNURL-auth + // In production, this would come from the server + const challenge = Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + + const url = `lnurl-auth://auth?tag=login&k1=${challenge}` + lnurlAuthUrl.value = url + return url + } + + function setIdentity(pubkey: string) { + lightningIdentity.value = pubkey + isAuthenticated.value = true + localStorage.setItem(STORAGE_KEY, pubkey) + } + + function clearIdentity() { + lightningIdentity.value = null + isAuthenticated.value = false + localStorage.removeItem(STORAGE_KEY) + lnurlAuthUrl.value = null + } + + return { + lnurlAuthUrl, + isAuthenticated, + lightningIdentity, + generateLnurlAuthUrl, + setIdentity, + clearIdentity, + } +} diff --git a/packages/app/src/composables/useNWC.ts b/packages/app/src/composables/useNWC.ts new file mode 100644 index 00000000..44e8f686 --- /dev/null +++ b/packages/app/src/composables/useNWC.ts @@ -0,0 +1,89 @@ +import { ref } from 'vue' + +const STORAGE_KEY = 'aiui-nwc-connection' + +export interface NWCConnection { + relayUrl: string + walletPubkey: string + secret: string +} + +const isConnected = ref(false) +const balance = ref(null) +const connectionString = ref(null) + +function loadConnection(): NWCConnection | null { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + connectionString.value = stored + isConnected.value = true + return parseConnectionString(stored) + } + } catch { /* ignore */ } + return null +} + +function parseConnectionString(str: string): NWCConnection | null { + // Format: nostr+walletconnect://pubkey?relay=wss://...&secret=hex + try { + const url = new URL(str) + const walletPubkey = url.hostname || url.pathname.replace('//', '') + const relayUrl = url.searchParams.get('relay') ?? '' + const secret = url.searchParams.get('secret') ?? '' + if (!walletPubkey || !relayUrl || !secret) return null + return { relayUrl, walletPubkey, secret } + } catch { + return null + } +} + +export function useNWC() { + function connect(nwcString: string): boolean { + const parsed = parseConnectionString(nwcString) + if (!parsed) return false + + localStorage.setItem(STORAGE_KEY, nwcString) + connectionString.value = nwcString + isConnected.value = true + return true + } + + function disconnect() { + localStorage.removeItem(STORAGE_KEY) + connectionString.value = null + isConnected.value = false + balance.value = null + } + + async function getBalance(): Promise { + const conn = loadConnection() + if (!conn) return null + + // NIP-47: Send get_balance request via relay + // This is a simplified version — full implementation needs NIP-47 event signing + return null + } + + async function payInvoice(bolt11: string): Promise<{ success: boolean; preimage?: string; error?: string }> { + const conn = loadConnection() + if (!conn) return { success: false, error: 'Not connected' } + + // NIP-47: Send pay_invoice request via relay + // Full implementation requires NIP-47 event creation and signing with the secret + return { success: false, error: 'NWC pay requires NIP-47 event signing' } + } + + // Load on init + loadConnection() + + return { + isConnected, + balance, + connectionString, + connect, + disconnect, + getBalance, + payInvoice, + } +}