Skip to content
Open
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
37 changes: 37 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
8 changes: 8 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
35 changes: 26 additions & 9 deletions templates/boltz/_reverseSubmarineSwapDialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,32 @@
>
</q-select>

<q-input
filled
dense
emit-value
:label="amountLabel(reverseSubmarineSwapDialog.data.asset)"
v-model.trim="reverseSubmarineSwapDialog.data.amount"
type="number"
hint="Enter the amount you want to send (e.g., 100000 sats)"
></q-input>
<div class="row">
<div class="col q-pr-sm">
<q-input
filled
dense
emit-value
v-model.number="reverseSubmarineSwapDialog.data.amount"
type="number"
label="Amount *"
:min="reverseSubmarineSwapDialog.data.currency === 'sats' ? '1' : '0.01'"
:step="reverseSubmarineSwapDialog.data.currency === 'sats' ? '1' : '0.01'"
:hint="getAmountHint('reverse')"
></q-input>
</div>
<div class="col-auto" style="width: 100px">
<q-select
filled
dense
v-model="reverseSubmarineSwapDialog.data.currency"
:options="currencies"
:display-value="reverseSubmarineSwapDialog.data.currency || 'sats'"
label="Currency"
@update:model-value="updateFiatRate"
></q-select>
</div>
</div>
<div class="row">
<div class="col">
<q-option-group
Expand Down
35 changes: 26 additions & 9 deletions templates/boltz/_submarineSwapDialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,32 @@
>
</q-select>

<q-input
filled
dense
emit-value
v-model.trim="submarineSwapDialog.data.amount"
:label="amountLabel(submarineSwapDialog.data.asset)"
type="number"
hint="Enter the amount you want to convert (e.g., 100000 sats)"
></q-input>
<div class="row">
<div class="col q-pr-sm">
<q-input
filled
dense
emit-value
v-model.number="submarineSwapDialog.data.amount"
type="number"
label="Amount *"
:min="submarineSwapDialog.data.currency === 'sats' ? '1' : '0.01'"
:step="submarineSwapDialog.data.currency === 'sats' ? '1' : '0.01'"
:hint="getAmountHint('submarine')"
></q-input>
</div>
<div class="col-auto" style="width: 100px">
<q-select
filled
dense
v-model="submarineSwapDialog.data.currency"
:options="currencies"
:display-value="submarineSwapDialog.data.currency || 'sats'"
label="Currency"
@update:model-value="updateFiatRate"
></q-select>
</div>
</div>
<div class="row">
<div class="col">
<q-option-group
Expand Down
126 changes: 118 additions & 8 deletions templates/boltz/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@
{value: 'receive', label: 'Receive specified Amount'}
],
assetOptions: ['BTC/BTC', 'L-BTC/BTC'],
currencies: [],
fiatRates: {},
submarineSwapDialog: {
show: false,
data: {
asset: 'BTC/BTC',
direction: 'receive',
feerate: false
feerate: false,
currency: 'sats'
}
},
reverseSubmarineSwapDialog: {
Expand All @@ -70,7 +73,8 @@
asset: 'BTC/BTC',
direction: 'send',
instant_settlement: true,
feerate: false
feerate: false,
currency: 'sats'
}
},
autoReverseSubmarineSwapDialog: {
Expand Down Expand Up @@ -240,7 +244,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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -523,7 +539,8 @@
data: {
asset: 'BTC/BTC',
direction: 'receive',
feerate: false
feerate: false,
currency: 'sats'
}
}
},
Expand All @@ -538,7 +555,8 @@
asset: 'BTC/BTC',
direction: 'send',
instant_settlement: true,
feerate: false
feerate: false,
currency: 'sats'
}
}
},
Expand Down Expand Up @@ -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, {
Expand All @@ -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(
Expand Down Expand Up @@ -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)
})
}
},
Comment on lines +764 to +775

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like we have this.g.allowedCurrencies, is there better way to reference exchange rates @dni ?

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')
Expand All @@ -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()
Expand Down