currencyrate: implement currencyrate command.
What changed, and why it matters
This commit adds a new public command called currencyrate to Core Lightning. It lets users ask the node for the current median exchange rate of one bitcoin into a chosen fiat currency. The change is purely a feature addition: it exposes data that the existing currencyrate plugin already collects, without altering security-sensitive logic such as wallet handling, network parsing, or cryptographic checks.
No security action required. Reviewers may optionally confirm that the new RPC respects the plugin's existing access-control model and that the median calculation in the oracle remains unchanged.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch wires a new JSON-RPC method currencyrate into the existing Rust currencyrate plugin. The handler normalizes the supplied ISO-4217 currency code to uppercase, requests the currency from the oracle, and returns the median BTC-to-currency rate. It also adds the corresponding JSON schema, generated msggen schema entry, man page build target, documentation index entry, and integration tests. No existing behavior is removed or weakened.
Changed components
plugins/currencyrate-plugin/src/main.rsdoc/schemas/currencyrate.jsoncontrib/msggen/msggen/schema.jsondoc/Makefiledoc/index.rsttests/test_currencyrate.pyInspect captured patch +162 / −8
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index fced75f1..9323bed1 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -7237,6 +7237,55 @@
}
]
},
+ "currencyrate.json": {
+ "$schema": "../rpc-schema-draft.json",
+ "type": "object",
+ "rpc": "currencyrate",
+ "title": "Command to determine a reasonable rate of conversion for a given currency",
+ "added": "v26.04",
+ "description": [
+ "The **currencyrate** RPC command provides the conversion of one BTC into the given currency",
+ "It uses the median of the available exchange-rate sources for the requested currency."
+ ],
+ "request": {
+ "required": [
+ "currency"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "currency": {
+ "type": "string",
+ "description": [
+ "The ISO-4217 currency code (e.g. USD) to convert to.",
+ "The plugin normalizes this value to uppercase before querying sources."
+ ]
+ }
+ }
+ },
+ "response": {
+ "required": [
+ "rate"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "rate": {
+ "type": "number",
+ "description": [
+ "The median value of one BTC, computed using the median result from the available sources."
+ ]
+ }
+ }
+ },
+ "author": [
+ "daywalker90 is mainly responsible."
+ ],
+ "see_also": [
+ "lightning-listcurrencyrates(7)"
+ ],
+ "resources": [
+ "Main web site: [https://github.com/ElementsProject/lightning](https://github.com/ElementsProject/lightning)"
+ ]
+ },
"datastore.json": {
"$schema": "../rpc-schema-draft.json",
"type": "object",
diff --git a/doc/Makefile b/doc/Makefile
index 24aa4f42..0e180122 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -42,6 +42,7 @@ MARKDOWNPAGES := doc/addgossip.7 \
doc/createinvoice.7 \
doc/createonion.7 \
doc/createrune.7 \
+ doc/currencyrate.7 \
doc/datastore.7 \
doc/datastoreusage.7 \
doc/decode.7 \
diff --git a/doc/index.rst b/doc/index.rst
index 6540ea2b..79baaaf1 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -50,6 +50,7 @@ Core Lightning Documentation
createinvoice <createinvoice.7.md>
createonion <createonion.7.md>
createrune <createrune.7.md>
+ currencyrate <currencyrate.7.md>
datastore <datastore.7.md>
datastoreusage <datastoreusage.7.md>
decode <decode.7.md>
diff --git a/doc/schemas/currencyrate.json b/doc/schemas/currencyrate.json
new file mode 100644
index 00000000..882c7be4
--- /dev/null
+++ b/doc/schemas/currencyrate.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "../rpc-schema-draft.json",
+ "type": "object",
+ "rpc": "currencyrate",
+ "title": "Command to determine a reasonable rate of conversion for a given currency",
+ "added": "v26.04",
+ "description": [
+ "The **currencyrate** RPC command provides the conversion of one BTC into the given currency",
+ "It uses the median of the available exchange-rate sources for the requested currency."
+ ],
+ "request": {
+ "required": [
+ "currency"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "currency": {
+ "type": "string",
+ "description": [
+ "The ISO-4217 currency code (e.g. USD) to convert to.",
+ "The plugin normalizes this value to uppercase before querying sources."
+ ]
+ }
+ }
+ },
+ "response": {
+ "required": [
+ "rate"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "rate": {
+ "type": "number",
+ "description": [
+ "The median value of one BTC, computed using the median result from the available sources."
+ ]
+ }
+ }
+ },
+ "author": [
+ "daywalker90 is mainly responsible."
+ ],
+ "see_also": [
+ "lightning-listcurrencyrates(7)"
+ ],
+ "resources": [
+ "Main web site: [https://github.com/ElementsProject/lightning](https://github.com/ElementsProject/lightning)"
+ ]
+}
diff --git a/plugins/currencyrate-plugin/src/main.rs b/plugins/currencyrate-plugin/src/main.rs
index d642a368..6f140478 100644
--- a/plugins/currencyrate-plugin/src/main.rs
+++ b/plugins/currencyrate-plugin/src/main.rs
@@ -63,6 +63,13 @@ median from currencyrates results",
.description("Returns the BTC price for the currency from every source")
.usage("currency"),
)
+ .rpcmethod_from_builder(
+ RpcMethodBuilder::new("currencyrate", currencyrate)
+ .description(
+ "Provides the conversion of one BTC into the given currency, using the median of the available exchange-rate sources",
+ )
+ .usage("currency"),
+ )
.dynamic()
.configure()
.await?
@@ -138,6 +145,40 @@ async fn currencyconvert(plugin: Plugin<PluginState>, args: Value) -> Result<Val
}
}
+async fn currencyrate(plugin: Plugin<PluginState>, args: Value) -> Result<Value, anyhow::Error> {
+ let currency = match args {
+ Value::Array(values) => {
+ let currency = values
+ .first()
+ .ok_or_else(|| anyhow!("Missing currency"))?
+ .as_str()
+ .ok_or_else(|| anyhow!("currency must be a string"))?
+ .to_owned();
+ currency.to_uppercase()
+ }
+ Value::Object(map) => {
+ let currency = map
+ .get("currency")
+ .ok_or_else(|| anyhow!("Missing currency"))?
+ .as_str()
+ .ok_or_else(|| anyhow!("currency must be a string"))?
+ .to_owned();
+ currency.to_uppercase()
+ }
+ _ => return Err(anyhow!("Arguments must be an array or dictionary")),
+ };
+
+ let oracle = plugin.state().oracle.lock().await;
+ oracle.currency_requested(¤cy).await;
+
+ match oracle.get_median_rate(¤cy).await {
+ Ok(result) => Ok(json!({
+ "rate": result,
+ })),
+ Err(e) => Err(anyhow!("Error converting currency: {e}")),
+ }
+}
+
async fn listcurrencyrates(plugin: Plugin<PluginState>, args: Value) -> Result<Value, anyhow::Error> {
let currency = match args {
Value::Array(values) => {
diff --git a/tests/test_currencyrate.py b/tests/test_currencyrate.py
index 68866735..e4f1795b 100644
--- a/tests/test_currencyrate.py
+++ b/tests/test_currencyrate.py
@@ -11,22 +11,29 @@ from werkzeug.serving import make_server
LOGGER = logging.getLogger(__name__)
-def median_range(amount, rateslist):
- """Return the reasonable median of these rates (similar to what currencyrate does)"""
+def median(rateslist):
rates = [entry["amount"] for entry in rateslist]
rates.sort()
if len(rates) % 2 == 1:
- btc = rates[len(rates) // 2]
+ return rates[len(rates) // 2]
else:
- btc = (rates[len(rates) - 1] + rates[len(rates) - 1]) / 2
+ return (rates[len(rates) - 1] + rates[len(rates) - 1]) / 2
- msats = amount * 100_000_000_000 / btc
+
+def median_conversion(amount, rateslist):
+ msats = amount * 100_000_000_000 / median(rateslist)
# Give it +/- 1%
return range(int(msats * 0.99), int(msats * 1.01))
+def median_rate(rateslist):
+ rate = median(rateslist)
+
+ return range(int(rate * 0.99), int(rate * 1.01))
+
+
def test_apis_batch1(node_factory):
opts = {
"currencyrate-disable-source": ["bitstamp", "coinbase"],
@@ -68,7 +75,9 @@ def test_apis_batch1(node_factory):
assert "msat" in convert
assert convert["msat"] > 0
- assert convert["msat"] in median_range(100, rateslist)
+ assert convert["msat"] in median_conversion(100, rateslist)
+
+ assert int(l1.rpc.currencyrate("usd")['rate']) in median_rate(rateslist)
def test_apis_batch2(node_factory):
@@ -110,7 +119,9 @@ def test_apis_batch2(node_factory):
assert "msat" in convert
assert convert["msat"] > 0
- assert convert["msat"] in median_range(100, rateslist)
+ assert convert["msat"] in median_conversion(100, rateslist)
+
+ assert int(l1.rpc.currencyrate("USD")['rate']) in median_rate(rateslist)
def test_custom_source(node_factory):
@@ -160,7 +171,9 @@ def test_custom_source(node_factory):
assert "msat" in convert
assert convert["msat"] > 0
- assert convert["msat"] in median_range(100, rateslist)
+ assert convert["msat"] in median_conversion(100, rateslist)
+
+ assert int(l1.rpc.currencyrate("USD")['rate']) in median_rate(rateslist)
def test_no_sources(node_factory):
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.