1111
1212LOGGER = logging .getLogger (__name__ )
1313
14+ ALL_RESOURCES = [
15+ "bitstamp" ,
16+ "coinbase" ,
17+ "coingecko" ,
18+ "kraken" ,
19+ "blockchain.info" ,
20+ "coindesk" ,
21+ "binance" ,
22+ ]
23+
1424
1525def median (rateslist ):
1626 rates = [entry ["amount" ] for entry in rateslist ]
@@ -127,15 +137,7 @@ def test_apis_batch2(node_factory):
127137
128138def test_custom_source (node_factory ):
129139 opts = {
130- "currencyrate-disable-source" : [
131- "bitstamp" ,
132- "coinbase" ,
133- "coingecko" ,
134- "kraken" ,
135- "blockchain.info" ,
136- "coindesk" ,
137- "binance" ,
138- ],
140+ "currencyrate-disable-source" : ALL_RESOURCES ,
139141 "currencyrate-add-source" : [
140142 r"my-coingecko,https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies={currency_lc},bitcoin,{currency_lc}" ,
141143 r"my-kraken,https://api.kraken.com/0/public/Ticker?pair=XXBTZ{currency},result,XXBTZ{currency},c,0" ,
@@ -179,15 +181,7 @@ def test_custom_source(node_factory):
179181
180182def test_no_sources (node_factory ):
181183 opts = {
182- "currencyrate-disable-source" : [
183- "bitstamp" ,
184- "coinbase" ,
185- "coingecko" ,
186- "kraken" ,
187- "blockchain.info" ,
188- "coindesk" ,
189- "binance" ,
190- ],
184+ "currencyrate-disable-source" : ALL_RESOURCES ,
191185 }
192186 l1 = node_factory .get_node (options = opts )
193187
@@ -239,6 +233,9 @@ def fake_rateserver():
239233 "fast" : 100_000_000 ,
240234 "slow" : 50_000_000 ,
241235 "slow_delay" : 1 ,
236+ "too_high" : 50_000.0116 ,
237+ "too_low" : 50_000.0114 ,
238+ "midpoint" : 50_000.0115 ,
242239 }
243240
244241 @app .get ("/fast" )
@@ -250,6 +247,18 @@ def slow():
250247 time .sleep (state ["slow_delay" ])
251248 return jsonify ({"price" : state ["slow" ]})
252249
250+ @app .get ("/too_high" )
251+ def too_high ():
252+ return jsonify ({"price" : state ["too_high" ]})
253+
254+ @app .get ("/too_low" )
255+ def too_low ():
256+ return jsonify ({"price" : state ["too_low" ]})
257+
258+ @app .get ("/midpoint" )
259+ def midpoint ():
260+ return jsonify ({"price" : state ["midpoint" ]})
261+
253262 srv = _ServerThread (app )
254263 srv .start ()
255264 try :
@@ -265,15 +274,7 @@ def slow():
265274def test_cached_median (node_factory , fake_rateserver ):
266275 """This should use the median of available sources"""
267276 opts = {
268- "currencyrate-disable-source" : [
269- "bitstamp" ,
270- "coinbase" ,
271- "coingecko" ,
272- "kraken" ,
273- "blockchain.info" ,
274- "coindesk" ,
275- "binance" ,
276- ],
277+ "currencyrate-disable-source" : ALL_RESOURCES ,
277278 "currencyrate-add-source" : [
278279 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
279280 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -302,15 +303,7 @@ def test_cached_median(node_factory, fake_rateserver):
302303
303304def test_bkpr_listaccountevents_currencyrate (node_factory , fake_rateserver ):
304305 opts = {
305- "currencyrate-disable-source" : [
306- "bitstamp" ,
307- "coinbase" ,
308- "coingecko" ,
309- "kraken" ,
310- "blockchain.info" ,
311- "coindesk" ,
312- "binance" ,
313- ],
306+ "currencyrate-disable-source" : ALL_RESOURCES ,
314307 "currencyrate-add-source" : [
315308 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
316309 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -333,15 +326,7 @@ def test_bkpr_listaccountevents_currencyrate(node_factory, fake_rateserver):
333326def test_bkpr_listaccountevents_realtime (node_factory , fake_rateserver ):
334327 """Make sure we don't wait for bkpr command to look up rates!"""
335328 opts = {
336- "currencyrate-disable-source" : [
337- "bitstamp" ,
338- "coinbase" ,
339- "coingecko" ,
340- "kraken" ,
341- "blockchain.info" ,
342- "coindesk" ,
343- "binance" ,
344- ],
329+ "currencyrate-disable-source" : ALL_RESOURCES ,
345330 "currencyrate-add-source" : [
346331 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
347332 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -372,15 +357,7 @@ def test_bkpr_listaccountevents_realtime(node_factory, fake_rateserver):
372357
373358def test_bkpr_currency_dynamic (node_factory , fake_rateserver ):
374359 opts = {
375- "currencyrate-disable-source" : [
376- "bitstamp" ,
377- "coinbase" ,
378- "coingecko" ,
379- "kraken" ,
380- "blockchain.info" ,
381- "coindesk" ,
382- "binance" ,
383- ],
360+ "currencyrate-disable-source" : ALL_RESOURCES ,
384361 "currencyrate-add-source" : [
385362 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
386363 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -434,15 +411,7 @@ def test_bkpr_currency_dynamic(node_factory, fake_rateserver):
434411
435412def test_bkpr_currencyrate_persisted (node_factory , fake_rateserver ):
436413 opts = {
437- "currencyrate-disable-source" : [
438- "bitstamp" ,
439- "coinbase" ,
440- "coingecko" ,
441- "kraken" ,
442- "blockchain.info" ,
443- "coindesk" ,
444- "binance" ,
445- ],
414+ "currencyrate-disable-source" : ALL_RESOURCES ,
446415 "currencyrate-add-source" : [
447416 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
448417 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -504,15 +473,7 @@ def test_bkpr_currencyrate_persisted(node_factory, fake_rateserver):
504473
505474def test_bkpr_currencyrate_warns_for_old_events (node_factory , fake_rateserver ):
506475 opts = {
507- "currencyrate-disable-source" : [
508- "bitstamp" ,
509- "coinbase" ,
510- "coingecko" ,
511- "kraken" ,
512- "blockchain.info" ,
513- "coindesk" ,
514- "binance" ,
515- ],
476+ "currencyrate-disable-source" : ALL_RESOURCES ,
516477 "currencyrate-add-source" : [
517478 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
518479 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -568,15 +529,7 @@ def test_bkpr_currencyrate_warns_for_old_events(node_factory, fake_rateserver):
568529
569530def test_bkpr_currencyrate_ranges (node_factory , fake_rateserver ):
570531 opts = {
571- "currencyrate-disable-source" : [
572- "bitstamp" ,
573- "coinbase" ,
574- "coingecko" ,
575- "kraken" ,
576- "blockchain.info" ,
577- "coindesk" ,
578- "binance" ,
579- ],
532+ "currencyrate-disable-source" : ALL_RESOURCES ,
580533 "currencyrate-add-source" : [
581534 f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
582535 f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
@@ -628,3 +581,99 @@ def test_bkpr_currencyrate_ranges(node_factory, fake_rateserver):
628581 # We will load them fine on restart, too.
629582 l1 .restart ()
630583 assert l1 .rpc .bkpr_listaccountevents () == events
584+
585+
586+ def test_currencyrate_rounding (node_factory , fake_rateserver ):
587+ """Test currencyrate returns at most 3 decimal places (ISO 4217)."""
588+
589+ cases = [
590+ ("too_high" , f"{ fake_rateserver ['url' ]} /too_high" , 50_000.012 ),
591+ ("too_low" , f"{ fake_rateserver ['url' ]} /too_low" , 50_000.011 ),
592+ ("midpoint" , f"{ fake_rateserver ['url' ]} /midpoint" , 50_000.012 ),
593+ ]
594+
595+ for source_name , url , expected in cases :
596+ opts = {
597+ "currencyrate-disable-source" : ALL_RESOURCES ,
598+ "currencyrate-add-source" : [f"{ source_name } ,{ url } ,price" ],
599+ }
600+ l1 = node_factory .get_node (options = opts )
601+
602+ result = l1 .rpc .currencyrate ("USD" )
603+ LOGGER .info ("currencyrate rounding [%s]: %s" , source_name , result )
604+ rate = result ["rate" ]
605+
606+ decimal_places = len (f"{ rate :.10f} " .split ("." )[1 ].rstrip ("0" ))
607+ assert decimal_places <= 3 , (
608+ f"[{ source_name } ] Expected at most 3 decimal places, "
609+ f"got { rate !r} ({ decimal_places } decimal places)"
610+ )
611+ assert abs (rate - expected ) < 1e-9 , (
612+ f"[{ source_name } ] Expected { expected } after rounding, got { rate !r} "
613+ )
614+
615+ l1 .stop ()
616+
617+
618+ def test_currencyrate_source (node_factory , fake_rateserver ):
619+ """Test currencyrate with a source argument returns that source's rate."""
620+
621+ opts = {
622+ "currencyrate-disable-source" : ALL_RESOURCES ,
623+ "currencyrate-add-source" : [
624+ f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
625+ f"slow,{ fake_rateserver ['url' ]} /slow,price" ,
626+ ],
627+ }
628+ l1 = node_factory .get_node (options = opts )
629+
630+ result_fast = l1 .rpc .call ("currencyrate" , ["USD" , "fast" ])
631+ LOGGER .info ("currencyrate fast: %s" , result_fast )
632+ assert result_fast ["rate" ] == float (fake_rateserver ["state" ]["fast" ])
633+
634+ result_slow = l1 .rpc .call ("currencyrate" , ["USD" , "slow" ])
635+ LOGGER .info ("currencyrate slow: %s" , result_slow )
636+ assert result_slow ["rate" ] == float (fake_rateserver ["state" ]["slow" ])
637+
638+ expected_median = (
639+ fake_rateserver ["state" ]["fast" ] + fake_rateserver ["state" ]["slow" ]
640+ ) / 2
641+ result_median = l1 .rpc .call ("currencyrate" , ["USD" ])
642+ LOGGER .info ("currencyrate median: %s" , result_median )
643+ assert result_median ["rate" ] == float (expected_median )
644+
645+
646+ def test_currencyrate_unknown_source (node_factory , fake_rateserver ):
647+ """Test currencyrate with a non-existent source name returns an error."""
648+
649+ opts = {
650+ "currencyrate-disable-source" : ALL_RESOURCES ,
651+ "currencyrate-add-source" : [
652+ f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
653+ ],
654+ }
655+ l1 = node_factory .get_node (options = opts )
656+
657+ with pytest .raises (RpcError , match = "Unknown source `nonexistent`" ):
658+ l1 .rpc .call ("currencyrate" , ["USD" , "nonexistent" ])
659+
660+
661+ def test_currencyrate_too_many_args (node_factory , fake_rateserver ):
662+ """Test that all three RPC calls reject extra positional arguments."""
663+
664+ opts = {
665+ "currencyrate-disable-source" : ALL_RESOURCES ,
666+ "currencyrate-add-source" : [
667+ f"fast,{ fake_rateserver ['url' ]} /fast,price" ,
668+ ],
669+ }
670+ l1 = node_factory .get_node (options = opts )
671+
672+ with pytest .raises (RpcError , match = "Too many arguments" ):
673+ l1 .rpc .call ("currencyrate" , ["USD" , "fast" , "extra_arg" ])
674+
675+ with pytest .raises (RpcError , match = "Too many arguments" ):
676+ l1 .rpc .call ("listcurrencyrates" , ["USD" , "extra_arg" ])
677+
678+ with pytest .raises (RpcError , match = "Too many arguments" ):
679+ l1 .rpc .call ("currencyconvert" , [100 , "USD" , "extra_arg" ])
0 commit comments