From 8300198fcb0ec8339181af95d17ae033d35669d1 Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Thu, 16 Oct 2025 18:58:51 +0100 Subject: [PATCH 1/4] feat: add currency conversion support for swaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements fiat currency support for Submarine and Reverse Submarine swaps to allow users to specify swap amounts in their preferred currency (e.g., £50 GBP) instead of manually converting to satoshis. Key features: - Added currency dropdown to swap dialogs with real-time fiat-to-sats conversion - Stores both original currency amount and converted satoshi amount in database - Displays conversion hints like "50 GBP = 50,000 sats" - Auto swaps remain sats-only to avoid unpredictable behavior with exchange rates - Fetches available currencies and rates from LNbits core API - Swap history displays amounts as "50 GBP (50,000 sats)" Changes: - Added m006_add_currency_support migration for currency and amount_display columns - Updated models with currency and amount_display fields - Enhanced swap dialogs with currency selection and conversion hints - Added Vue.js methods for currency loading, rate fetching, and conversion - Updated table displays to show both fiat and satoshi amounts Closes #53 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- migrations.py | 35 +++++ models.py | 8 ++ .../boltz/_reverseSubmarineSwapDialog.html | 35 +++-- templates/boltz/_submarineSwapDialog.html | 35 +++-- templates/boltz/index.html | 129 ++++++++++++++++-- 5 files changed, 216 insertions(+), 26 deletions(-) diff --git a/migrations.py b/migrations.py index 45f570f..0c60c65 100644 --- a/migrations.py +++ b/migrations.py @@ -155,3 +155,38 @@ 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,56 @@ }) .catch(LNbits.utils.notifyApiError) }, + loadCurrencies() { + LNbits.api + .request('GET', '/api/v1/currencies') + .then(response => { + this.currencies = ['sats', ...response.data] + }) + .catch(err => { + console.error('Failed to load currencies:', err) + this.currencies = ['sats'] + }) + }, + 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 +815,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() From 3da6760df826a333172e39f641ea714fc5fe6646 Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Mon, 22 Dec 2025 20:27:34 +0000 Subject: [PATCH 2/4] fix: split long line in migrations.py for ruff compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- migrations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index 0c60c65..382823c 100644 --- a/migrations.py +++ b/migrations.py @@ -184,7 +184,8 @@ async def m006_add_currency_support(db): # 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" + "UPDATE boltz.submarineswap " + "SET amount_display = amount WHERE amount_display = 0" ) await db.execute( "UPDATE boltz.reverse_submarineswap " From afd00eb00d1f9991c2a53e246d85c72d28c92fda Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Mon, 22 Dec 2025 21:06:52 +0000 Subject: [PATCH 3/4] fix: split another long line in migrations.py for ruff compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- migrations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index 382823c..dcf6f50 100644 --- a/migrations.py +++ b/migrations.py @@ -164,7 +164,8 @@ async def m006_add_currency_support(db): """ # Add currency columns (default to 'sats' for existing swaps) await db.execute( - "ALTER TABLE boltz.submarineswap ADD COLUMN currency TEXT NOT NULL DEFAULT 'sats'" + "ALTER TABLE boltz.submarineswap " + "ADD COLUMN currency TEXT NOT NULL DEFAULT 'sats'" ) await db.execute( "ALTER TABLE boltz.reverse_submarineswap " From ff7164eb9e78d893bee664ba46c9356cb60b5310 Mon Sep 17 00:00:00 2001 From: Ben Weeks Date: Mon, 22 Dec 2025 21:08:29 +0000 Subject: [PATCH 4/4] refactor: use LNbits Core globals for currencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use this.g.allowedCurrencies / this.g.currencies instead of API call. Respects admin currency settings per lnbits/nostrmarket#116. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- templates/boltz/index.html | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/templates/boltz/index.html b/templates/boltz/index.html index 029bcef..09d9024 100644 --- a/templates/boltz/index.html +++ b/templates/boltz/index.html @@ -754,15 +754,12 @@ .catch(LNbits.utils.notifyApiError) }, loadCurrencies() { - LNbits.api - .request('GET', '/api/v1/currencies') - .then(response => { - this.currencies = ['sats', ...response.data] - }) - .catch(err => { - console.error('Failed to load currencies:', err) - this.currencies = ['sats'] - }) + // 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') {