diff --git a/migrations.py b/migrations.py index 45f570f..dcf6f50 100644 --- a/migrations.py +++ b/migrations.py @@ -155,3 +155,40 @@ async def m005_fix_settings_table_drop_mempool(db): await db.execute("DROP TABLE boltz.settings") # NOTE using `boltz.settings` for the RENAME TO clause will not work in sqlite await db.execute("ALTER TABLE boltz.settings_backup RENAME TO settings") + + +async def m006_add_currency_support(db): + """ + Add currency support for fiat conversion in swaps. + Stores the currency used and the original display amount entered by user. + """ + # Add currency columns (default to 'sats' for existing swaps) + await db.execute( + "ALTER TABLE boltz.submarineswap " + "ADD COLUMN currency TEXT NOT NULL DEFAULT 'sats'" + ) + await db.execute( + "ALTER TABLE boltz.reverse_submarineswap " + "ADD COLUMN currency TEXT NOT NULL DEFAULT 'sats'" + ) + + # Add amount_display columns (stores original fiat amount entered by user) + # Using REAL type to store decimal values like 234.56 for fiat + await db.execute( + "ALTER TABLE boltz.submarineswap " + "ADD COLUMN amount_display REAL NOT NULL DEFAULT 0" + ) + await db.execute( + "ALTER TABLE boltz.reverse_submarineswap " + "ADD COLUMN amount_display REAL NOT NULL DEFAULT 0" + ) + + # For existing rows, set amount_display = amount (they're all in sats) + await db.execute( + "UPDATE boltz.submarineswap " + "SET amount_display = amount WHERE amount_display = 0" + ) + await db.execute( + "UPDATE boltz.reverse_submarineswap " + "SET amount_display = amount WHERE amount_display = 0" + ) diff --git a/models.py b/models.py index f65b0dd..643475c 100644 --- a/models.py +++ b/models.py @@ -30,6 +30,8 @@ class SubmarineSwap(BaseModel): bip21: str redeem_script: str blinding_key: str | None = None + currency: str = "sats" + amount_display: float = 0 class CreateSubmarineSwap(BaseModel): @@ -40,6 +42,8 @@ class CreateSubmarineSwap(BaseModel): direction: str = Query("receive") feerate: bool = Query(None) feerate_value: int | None = Query(None) + currency: str = Query("sats") + amount_display: float = Query(0) class ReverseSubmarineSwap(BaseModel): @@ -63,6 +67,8 @@ class ReverseSubmarineSwap(BaseModel): timeout_block_height: int redeem_script: str blinding_key: str | None = None + currency: str = "sats" + amount_display: float = 0 class CreateReverseSubmarineSwap(BaseModel): @@ -74,6 +80,8 @@ class CreateReverseSubmarineSwap(BaseModel): onchain_address: str = Query(...) feerate: bool = Query(None) feerate_value: int | None = Query(None) + currency: str = Query("sats") + amount_display: float = Query(0) class AutoReverseSubmarineSwap(BaseModel): diff --git a/templates/boltz/_reverseSubmarineSwapDialog.html b/templates/boltz/_reverseSubmarineSwapDialog.html index af7d3f3..5d1443d 100644 --- a/templates/boltz/_reverseSubmarineSwapDialog.html +++ b/templates/boltz/_reverseSubmarineSwapDialog.html @@ -26,15 +26,32 @@ > - +
+
+ +
+
+ +
+
- +
+
+ +
+
+ +
+
{ + if (row.currency && row.currency !== 'sats') { + return `${row.amount_display} ${row.currency} (${val.toLocaleString()} sats)` + } + return `${val.toLocaleString()} sats` + } }, { name: 'direction', @@ -312,7 +322,13 @@ name: 'amount', align: 'left', label: 'Amount', - field: 'amount' + field: 'amount', + format: (val, row) => { + if (row.currency && row.currency !== 'sats') { + return `${row.amount_display} ${row.currency} (${val.toLocaleString()} sats)` + } + return `${val.toLocaleString()} sats` + } }, { name: 'direction', @@ -523,7 +539,8 @@ data: { asset: 'BTC/BTC', direction: 'receive', - feerate: false + feerate: false, + currency: 'sats' } } }, @@ -538,7 +555,8 @@ asset: 'BTC/BTC', direction: 'send', instant_settlement: true, - feerate: false + feerate: false, + currency: 'sats' } } }, @@ -566,7 +584,23 @@ id: this.reverseSubmarineSwapDialog.data.wallet }) let data = this.reverseSubmarineSwapDialog.data - this.createReverseSubmarineSwap(wallet, data) + + // Store original amount entered by user + const amount_display = data.amount + const currency = data.currency || 'sats' + + // Convert to sats for the API + const amount_sats = this.convertFiatToSats(data.amount, currency) + + // Prepare data for API + const apiData = { + ...data, + amount: amount_sats, // Always send sats to API + amount_display: amount_display, // Original amount entered + currency: currency + } + + this.createReverseSubmarineSwap(wallet, apiData) }, sendAutoReverseSubmarineSwapFormData() { let wallet = _.findWhere(this.g.user.wallets, { @@ -580,7 +614,23 @@ id: this.submarineSwapDialog.data.wallet }) let data = this.submarineSwapDialog.data - this.createSubmarineSwap(wallet, data) + + // Store original amount entered by user + const amount_display = data.amount + const currency = data.currency || 'sats' + + // Convert to sats for the API + const amount_sats = this.convertFiatToSats(data.amount, currency) + + // Prepare data for API + const apiData = { + ...data, + amount: amount_sats, // Always send sats to API + amount_display: amount_display, // Original amount entered + currency: currency + } + + this.createSubmarineSwap(wallet, apiData) }, exportSubmarineSwapCSV() { LNbits.utils.exportCSV( @@ -703,6 +753,53 @@ }) .catch(LNbits.utils.notifyApiError) }, + loadCurrencies() { + // Use LNbits Core global currencies (respects admin settings) + const coreCurrencies = + this.g.allowedCurrencies.length > 0 + ? this.g.allowedCurrencies + : this.g.currencies + this.currencies = ['sats', ...coreCurrencies] + }, + updateFiatRate(currency) { + if (currency && currency !== 'sats') { + LNbits.api + .request('GET', '/api/v1/rate/' + currency, null) + .then(response => { + this.fiatRates[currency] = response.data.rate + }) + .catch(err => { + console.error(`Failed to get rate for ${currency}:`, err) + }) + } + }, + getAmountHint(swapType) { + const dialog = + swapType === 'submarine' + ? this.submarineSwapDialog + : this.reverseSubmarineSwapDialog + const amount = dialog.data.amount + const currency = dialog.data.currency + + if (!amount || !currency || currency === 'sats') { + return 'Enter the amount you want to convert' + } + + const rate = this.fiatRates[currency] + if (!rate) { + return `Loading ${currency} exchange rate...` + } + + const sats = Math.round(amount * rate) + return `${amount} ${currency} = ${sats.toLocaleString()} sats (actual conversion calculated at time of swap)` + }, + convertFiatToSats(amount, currency) { + if (!currency || currency === 'sats') { + return Math.round(amount) + } + const rate = this.fiatRates[currency] + return rate ? Math.round(amount * rate) : 0 + }, getBoltzConfig() { LNbits.api .request('GET', '/boltz/api/v1/swap/boltz') @@ -715,7 +812,20 @@ }) } }, + watch: { + 'submarineSwapDialog.data.currency'(newCurrency) { + if (newCurrency) { + this.updateFiatRate(newCurrency) + } + }, + 'reverseSubmarineSwapDialog.data.currency'(newCurrency) { + if (newCurrency) { + this.updateFiatRate(newCurrency) + } + } + }, created() { + this.loadCurrencies() this.getBoltzConfig() this.getSubmarineSwap() this.getReverseSubmarineSwap()