# Schedule the next run heapq.heappush(next_runs, (time() + n[2], n[1], n[2])) @plugin.init() def init(configuration, options, plugin): plugin.probe_interval = int(options['probe-interval']) plugin.probe_exclusion_duration = int(options['probe-exclusion-duration']) db_filename = 'sqlite:///' + os.path.join(configuration['lightning-dir'], 'probes.db') engine = create_engine(db_filename, echo=True) Base.metadata.create_all(engine) plugin.Session = sessionmaker() plugin.Session.configure(bind=engine) t = threading.Thread(target=schedule, args=[plugin]) t.daemon = True t.start() # Probes that are still pending and need to be checked against. plugin.pending_probes = [] plugin.add_option('probe-interval', '3600', 'How many seconds should we wait between probes?') plugin.add_option( 'probe-exclusion-duration', '1800', 'How many seconds should temporarily failed channels be excluded?') plugin.run()
elif a['type'] == 'ipv4' and best_address['type'] != 'ipv4': best_address = a if best_address: plugin.my_address = info['id'] + '@' + best_address['address'] if best_address['port'] != 9735: plugin.my_address += ':' + str(best_address['port']) else: plugin.my_address = None plugin.log("Plugin summary.py initialized") plugin.add_option( 'summary-currency', 'USD', 'What currency should I look up on btcaverage?' ) plugin.add_option( 'summary-currency-prefix', 'USD $', 'What prefix to use for currency' ) plugin.add_option( 'summary-availability-interval', 300, 'How often in seconds the availability should be calculated.' ) plugin.add_option( 'summary-availability-window', 72,
CONF_FILE_NAME = "watchtower.conf" DEFAULT_CONF = { "DEFAULT_PORT": {"value": 9814, "type": int}, "MAX_RETRIES": {"value": 30, "type": int}, "APPOINTMENTS_FOLDER_NAME": {"value": "appointment_receipts", "type": str, "path": True}, "TOWERS_DB": {"value": "towers", "type": str, "path": True}, "PRIVATE_KEY": {"value": "sk.der", "type": str, "path": True}, } plugin = Plugin() # Adds OPTIONS for c-lighting config plugin.add_option( "watchtower-default-port", DEFAULT_CONF.get("DEFAULT_PORT").get("value"), "default tower API port", "int" ) plugin.add_option( "watchtower-max-retries", DEFAULT_CONF.get("MAX_RETRIES").get("value"), "maximum post retries if the tower is unreachable", "int", ) plugin.add_option( "watchtower-appointments-folder-name", DEFAULT_CONF.get("APPOINTMENTS_FOLDER_NAME").get("value"), "name of the appointments' database within the watchtower data folder", "string", ) plugin.add_option( "watchtower-towers-db",
from pyln.client import Plugin plugin = Plugin() @plugin.method("estimatefees") def getfeerate(plugin, **kwargs): time.sleep(1) return {} @plugin.method("getrawblockbyheight") def getblock(plugin, **kwargs): time.sleep(1) return {} @plugin.method("getchaininfo") def getchaininfo(plugin, **kwargs): time.sleep(1) return {} # We don't use these options, but it allows us to get to the expected failure. plugin.add_option("bitcoin-rpcuser", "", "") plugin.add_option("bitcoin-rpcpassword", "", "") plugin.add_option("bitcoin-rpcport", "", "") plugin.add_option("bitcoin-datadir", "", "") plugin.run()
# just stop here. if amt <= 0: request.set_result({"result": "continue"}) return t = threading.Thread(target=try_rebalance, args=(scid, chan, amt, peer, request)) t.daemon = True t.start() @plugin.init() def init(options, configuration, plugin): plugin.log("jitrebalance.py initializing {}".format(configuration)) plugin.node_id = plugin.rpc.getinfo()['id'] # FIXME: this int() shouldn't be needed: check if this is pyln's or # lightningd's fault. plugin.rebalance_timeout = int(options.get("jitrebalance-try-timeout")) # Set of currently active rebalancings, keyed by their payment_hash plugin.rebalances = {} plugin.add_option( "jitrebalance-try-timeout", 60, "Number of seconds before we stop trying to rebalance a channel.", opt_type="int") plugin.run()
f.write(json.dumps(onion)) plugin.log("Holding onto an incoming htlc for {hold_time} seconds".format( hold_time=plugin.hold_time)) time.sleep(plugin.hold_time) print("Onion written to {}".format(fname)) # Give the tester something to look for plugin.log("htlc_accepted hook called") return {'result': plugin.hold_result} plugin.add_option('hold-time', 10, 'How long should we hold on to HTLCs?', opt_type='int') plugin.add_option( 'hold-result', 'continue', 'How should we continue after holding?', ) @plugin.init() def init(options, configuration, plugin): plugin.log("hold_htlcs.py initializing") plugin.hold_time = options['hold-time'] plugin.hold_result = options['hold-result']
f"imbalance of {int(100 * plugin.imbalance)}%/{int(100 * ( 1 - plugin.imbalance))}%, " f"update_threshold: {int(100 * plugin.update_threshold)}%, " f"update_threshold_abs: {plugin.update_threshold_abs}, " f"enough_liquidity: {plugin.big_enough_liquidity}, " f"deactivate_fuzz: {plugin.deactivate_fuzz}, " f"forward_event_subscription: {plugin.forward_event_subscription}, " f"adjustment_method: {plugin.get_ratio.__name__}, " f"fee_strategy: {plugin.fee_strategy.__name__}, " f"listchannels_by_dst: {plugin.listchannels_by_dst}") plugin.mutex.release() feeadjust(plugin) plugin.add_option( "feeadjuster-deactivate-fuzz", False, "Deactivate update threshold randomization and hysterisis.", "flag" ) plugin.add_option( "feeadjuster-deactivate-fee-update", False, "Deactivate automatic fee updates for forward events.", "flag" ) plugin.add_option( "feeadjuster-threshold", "0.05", "Relative channel balance delta at which to trigger an update. Default 0.05 means 5%. " "Note: it's also fuzzed by 1.5%", "string" )
return plugin.rpc.pay(bolt11) except Exception as e: if (e.error['code'] == 205 # Invoice is for another network or e.error['code'] == -32602 # Invalid bolt11: Unknown chain or e.error['code'] == 205 # Could not find a route ) and ('Invoice is for another network' in e.error['message'] or 'Invalid bolt11: Unknown chain' in e.error['message'] or 'Could not find a route' in e.error['message']): plugin.log('GATEPAY: error paying normally (%s)' % e.error['message']) gatepays = plugin.get_option('gatepay').split(GATEPAY_SPLIT_STRING) if not gatepays: return { 'error': 'Gatepay failed to pay normally and there\'s no gatepay configured.' } for gatepay in gatepays: toreturn = _gatepay_with_gatepay(plugin, bolt11, gatepay) if has_error('gatepay', toreturn): return {'error': toreturn['error']} return toreturn or { 'error': 'Error calling gatepay plugin bolt11 %s' % bolt11 } plugin.add_option('gatepay', '', 'Your most trusted gatepay.') plugin.run()
plugin.erringnodes = int(options.get("rebalance-erringnodes")) plugin.getroute = getroute_switch(options.get("rebalance-getroute")) plugin.rebalanceall_msg = None plugin.log( f"Plugin rebalance initialized with {plugin.fee_base} base / {plugin.fee_ppm} ppm fee " f"cltv_final:{plugin.cltv_final} " f"maxhops:{plugin.maxhops} " f"msatfactor:{plugin.msatfactor} " f"erringnodes:{plugin.erringnodes} " f"getroute:{plugin.getroute.__name__} ") plugin.add_option( "rebalance-getroute", "iterative", "Getroute method for route search can be 'basic' or 'iterative'." "'basic': Tries all routes sequentially. " "'iterative': Tries shorter and bigger routes first.", "string") plugin.add_option( "rebalance-maxhops", "5", "Maximum number of hops for `getroute` call. Set to 0 to disable. " "Note: Two hops are added for own nodes input and output channel. " "Note: Routes with a 8 or more hops have less than 3% success rate.", "string") plugin.add_option( "rebalance-msatfactor", "4", "Will instruct `getroute` call to use higher requested capacity first. " "Note: This will decrease to 1 when no routes can be found.", "string") plugin.add_option(
else: # It returns sat/vB, we want sat/kVB, so multiply everything by 10**3 slow = int(feerates["144"] * 10**3) normal = int(feerates["5"] * 10**3) urgent = int(feerates["3"] * 10**3) very_urgent = int(feerates["2"] * 10**3) return { "opening": normal, "mutual_close": normal, "unilateral_close": very_urgent, "delayed_to_us": normal, "htlc_resolution": urgent, "penalty": urgent, "min_acceptable": slow // 2, "max_acceptable": very_urgent * 10, } plugin.add_option( "sauron-api-endpoint", "", "The URL of the esplora instance to hit (including '/api').") plugin.add_option( "sauron-tor-proxy", "", "Tor's SocksPort address in the form address:port, don't specify the" " protocol. If you didn't modify your torrc you want to put" "'localhost:9050' here.") plugin.run()
#!/usr/bin/env python3 """This plugin is used to check that plugin options are parsed properly. The plugin offers 3 options, one of each supported type. """ from pyln.client import Plugin plugin = Plugin() @plugin.init() def init(configuration, options, plugin): for name, val in options.items(): plugin.log("option {} {} {}".format(name, val, type(val))) plugin.add_option('str_opt', 'i am a string', 'an example string option') plugin.add_option('int_opt', 7, 'an example int type option', opt_type='int') plugin.add_option('bool_opt', True, 'an example bool type option', opt_type='bool') plugin.add_flag_option('flag_opt', 'an example flag type option') plugin.add_option('str_optm', None, 'an example string option', multi=True) plugin.add_option('int_optm', 7, 'an example int type option', opt_type='int', multi=True) plugin.add_option('greeting', 7, 'option _names_ should be unique', opt_type='int', multi=True) plugin.run()
'%Y-%m-%d %H:%M:%S (UTC)') entry['resolved_time'] = forward['resolved_time'] entry['timestamp'] = time_str result.append(entry) return result @plugin.init() def init(options, configuration, plugin): plugin.options['cltv-final']['value'] = plugin.rpc.listconfigs().get( 'cltv-final') plugin.options['fee-base']['value'] = plugin.rpc.listconfigs().get( 'fee-base') plugin.options['fee-per-satoshi']['value'] = plugin.rpc.listconfigs().get( 'fee-per-satoshi') plugin.log("Plugin sendinvoiceless.py initialized") plugin.add_option('cltv-final', 10, 'Number of blocks for final CheckLockTimeVerify expiry') plugin.add_option( 'fee-base', None, 'The routing base fee in msat. Will be derived automatically via rpc.listconfigs()' ) plugin.add_option( 'fee-per-satoshi', None, 'The routing fee ppm. Will be derived automatically via rpc.listconfigs()') plugin.run()
set_proxies(plugin) sourceopts = options['add-source'] # Prior to 0.9.3, 'multi' was unsupported. if type(sourceopts) is not list: sourceopts = [sourceopts] if sourceopts != ['']: for s in sourceopts: parts = s.split(',') sources.append(Source(parts[0], parts[1], parts[2:])) disableopts = options['disable-source'] # Prior to 0.9.3, 'multi' was unsupported. if type(disableopts) is not list: disableopts = [disableopts] if disableopts != ['']: for s in sources[:]: if s.name in disableopts: sources.remove(s) # As a bad example: binance,https://api.binance.com/api/v3/ticker/price?symbol=BTC{currency}T,price plugin.add_option(name='add-source', default='', description='Add source name,urlformat,resultmembers...') plugin.add_option(name='disable-source', default='', description='Disable source by name') # This has an effect only for recent pyln versions (0.9.3+). plugin.options['add-source']['multi'] = True plugin.options['disable-source']['multi'] = True plugin.run()
print("Rejected invoice payment: it was not paid trough the right channel") request.set_result({ "result": "reject", }) return update_channel_creation_status(plugin, plugin.channel_creation, Status.InvoicePaid) print("Accepted invoice payment") request.set_result({ "result": "continue" }) # TODO: automatically connect to node # TODO: get node from API # TODO: default values PLUGIN.add_option( "boltz-api", "", "Boltz API endpoint" ) PLUGIN.add_option( "boltz-node", "", "Public key of the Boltz Lightning node" ) PLUGIN.run()
plugin.log("Plugin helloworld.py initialized") @plugin.subscribe("connect") def on_connect(plugin, id, address, **kwargs): plugin.log("Received connect event for peer {}".format(id)) @plugin.subscribe("disconnect") def on_disconnect(plugin, id, **kwargs): plugin.log("Received disconnect event for peer {}".format(id)) @plugin.subscribe("invoice_payment") def on_payment(plugin, invoice_payment, **kwargs): plugin.log("Received invoice_payment event for label {}, preimage {}," " and amount of {}".format(invoice_payment.get("label"), invoice_payment.get("preimage"), invoice_payment.get("msat"))) @plugin.hook("htlc_accepted") def on_htlc_accepted(onion, htlc, plugin, **kwargs): plugin.log('on_htlc_accepted called') time.sleep(20) return {'result': 'continue'} plugin.add_option('greeting', 'Hello', 'The greeting I should use.') plugin.run()
Percentage defaults to 100, resulting in a full channel. Chunks defaults to 0 (auto-detect). Use 'fill 10' to incease a channels total balance by 10%. """ payload = read_params('fill', scid, percentage, chunks, retry_for, maxfeepercent, exemptfee) return execute(payload) @plugin.method("setbalance") def setbalance(plugin, scid: str, percentage: float = 50, chunks: int = 0, retry_for: int = 60, maxfeepercent: float = 0.5, exemptfee: Millisatoshi = Millisatoshi(5000)): """Brings a channels own liquidity to X percent using circular payments. Percentage defaults to 50, resulting in a balanced channel. Chunks defaults to 0 (auto-detect). Use 'setbalance 100' to fill a channel. Use 'setbalance 0' to drain a channel. """ payload = read_params('setbalance', scid, percentage, chunks, retry_for, maxfeepercent, exemptfee) return execute(payload) @plugin.init() def init(options, configuration, plugin): plugin.options['cltv-final']['value'] = plugin.rpc.listconfigs().get('cltv-final') plugin.log("Plugin drain.py initialized") plugin.add_option('cltv-final', 10, 'Number of blocks for final CheckLockTimeVerify expiry') plugin.run()
os.listdir(".")))) return abort( "Could not find backup.lock in the lightning-dir, have you initialized using the backup-cli utility?" ) d = json.load(open("backup.lock", 'r')) if destination is None or destination == 'null': destination = d['backend_url'] elif destination != d['backend_url']: abort("The destination specified as option does not match the one " "specified in backup.lock. Please check your settings") if not plugin.db_path.startswith('sqlite3'): abort("The backup plugin only works with the sqlite3 database.") plugin.backend = get_backend(destination, require_init=True) for c in plugin.early_writes: apply_write(plugin, c) plugin.add_option( 'backup-destination', None, 'Destination of the database backups (file:///filename/on/another/disk/).') if __name__ == "__main__": # Did we perform the version check of backend versus the first write? plugin.initialized = False plugin.early_writes = [] plugin.run()
#!/usr/bin/env python3 """Simple plugin to allow testing while closing of HTLC is delayed. """ from pyln.client import Plugin import time plugin = Plugin() @plugin.hook('invoice_payment') def on_payment(payment, plugin, **kwargs): time.sleep(float(plugin.get_option('holdtime'))) return {'result': 'continue'} plugin.add_option('holdtime', '10', 'The time to hold invoice for.') plugin.run()
from pyln.client import Plugin plugin = Plugin() @plugin.hook('openchannel') def on_openchannel(openchannel, plugin, **kwargs): plugin.log(repr(openchannel)) mindepth = int(plugin.options['zeroconf-mindepth']['value']) if openchannel['id'] == plugin.options['zeroconf-allow']['value']: plugin.log(f"This peer is in the zeroconf allowlist, setting mindepth={mindepth}") return {'result': 'continue', 'mindepth': mindepth} else: return {'result': 'continue'} plugin.add_option( 'zeroconf-allow', 'A node_id to allow zeroconf channels from', '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f' ) plugin.add_option( 'zeroconf-mindepth', 0, 'Number of confirmations to require from allowlisted peers', ) plugin.run()
if command == "stop": if stop_server(port): return "stopped server on port{}".format(port) else: return "could not stop the server" if command == "restart": stop_server(port) suc = start_server(port) if suc: return "started server successfully on port {}".format(port) else: return "Could not start server on port {}".format(port) plugin.add_option('donation-autostart', 'true', 'Should the donation server start automatically') plugin.add_option('donation-web-port', '33506', 'Which port should the donation server listen to?') @plugin.init() def init(options, configuration, plugin): port = int(options['donation-web-port']) if options['donation-autostart'].lower() in ['true', '1']: start_server(port) plugin.run()
command_not_doc.append(command_name) result = {'path-doc': path, 'commands': command_not_doc} return result def is_documented(plugin, path, command): plugin.log(path) for file in os.listdir(path): if file.endswith(".md"): file_name = os.path.splitext(os.path.basename(file))[0] file_name = file_name.split(".")[0] if "-" not in file_name: continue file_name = file_name.split("-")[1] plugin.log('Final file name: {}'.format(file_name)) plugin.log('Command: {}'.format(command)) if command == file_name: return True return False @plugin.init() def init(options, configuration, plugin, **kwargs): plugin.log("Plugin helloworld.py initialized") plugin.add_option('doc', 'doc', 'The greeting I should use.') plugin.run()
in_payments_fulfilled_gauge, in_msatoshi_offered_gauge, in_msatoshi_fulfilled_gauge, out_payments_offered_gauge, out_payments_fulfilled_gauge, out_msatoshi_offered_gauge, out_msatoshi_fulfilled_gauge, ] @plugin.init() def init(options, configuration, plugin): s = options['prometheus-listen'].rpartition(':') if len(s) != 3 or s[1] != ':': print("Could not parse prometheus-listen address") exit(1) ip, port = s[0], int(s[2]) registry = CollectorRegistry() start_http_server(addr=ip, port=port, registry=registry) registry.register(NodeCollector(plugin.rpc, registry)) registry.register(FundsCollector(plugin.rpc, registry)) registry.register(PeerCollector(plugin.rpc, registry)) registry.register(ChannelsCollector(plugin.rpc, registry)) plugin.add_option('prometheus-listen', '0.0.0.0:9900', 'Address and port to bind to') plugin.run()
"""This plugin is used to check that plugin options are parsed properly. The plugin offers 3 options, one of each supported type. """ from pyln.client import Plugin plugin = Plugin() @plugin.init() def init(configuration, options, plugin): for name, val in options.items(): plugin.log("option {} {} {}".format(name, val, type(val))) plugin.add_option('str_opt', 'i am a string', 'an example string option') plugin.add_option('int_opt', 7, 'an example int type option', opt_type='int') plugin.add_option('bool_opt', True, 'an example bool type option', opt_type='bool') plugin.add_flag_option('flag_opt', 'an example flag type option') plugin.add_option('str_optm', None, 'an example string option', multi=True) plugin.add_option('int_optm', 7, 'an example int type option', opt_type='int', multi=True) plugin.run()
num_channels = min(int(available_funds / plugin.min_capacity_sat), plugin.num_channels - len(channels)) # Each channel will have this capacity channel_capacity = math.floor(available_funds / num_channels) print("I'd like to open {} new channels with {} satoshis each".format( num_channels, channel_capacity)) candidates = plugin.autopilot.find_candidates(num_channels, strategy=Strategy.DIVERSE, percentile=0.5) plugin.autopilot.connect(candidates, available_funds, dryrun=dryrun) plugin.add_option( 'autopilot-percent', '75', 'What percentage of funds should be under the autopilots control?') plugin.add_option('autopilot-num-channels', '10', 'How many channels should the autopilot aim for?') plugin.add_option( 'autopilot-min-channel-size-msat', '100000000', 'Minimum channel size to open.', ) plugin.run()
# Prefer IPv4, otherwise take any to give out address. best_address = None for a in info['address']: if best_address is None: best_address = a elif a['type'] == 'ipv4' and best_address['type'] != 'ipv4': best_address = a if best_address: plugin.my_address = info['id'] + '@' + best_address['address'] if best_address['port'] != 9735: plugin.my_address += ':' + str(best_address['port']) else: plugin.my_address = None plugin.log("Plugin summary.py initialized") plugin.add_option( 'summary-currency', 'USD', 'What currency should I look up on btcaverage?' ) plugin.add_option( 'summary-currency-prefix', 'USD $', 'What prefix to use for currency' ) plugin.run()
config.read(wallet_config_file) app = TerminusApp(config, CLIGHTNING) app.run_app() @plugin.init() def init(options, configuration, plugin, **kwargs): terminus_config_file = plugin.get_option("moneysocket_terminus_config") plugin.log("using config file %s" % os.path.abspath(terminus_config_file)) reactor.callFromThread(run_app, terminus_config_file) DEFAULT_WALLET_CONFIG = "./moneysocket-terminus.conf" plugin.add_option("moneysocket_terminus_config", DEFAULT_WALLET_CONFIG, "config file to obtain settings") def plugin_thread(): plugin.run() reactor.callFromThread(reactor.stop) fmt = '%(asctime)s %(levelname)s: %(filename)s:%(lineno)d: %(message)s' datefmt = '%H:%M:%S' logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.DEBUG) reactor.callInThread(plugin_thread) reactor.run()
notification_type_name, *args, **kwargs) for nt in NOTIFICATION_TYPES: # subscribe to all notifications on = functools.partial(on_notification, str(nt)) on.__annotations__ = {} # needed to please Plugin._coerce_arguments() plugin.add_subscription(str(nt), on) DEFAULT_HIGH_WATER_MARK = 1000 for t in ALL_TYPES: # zmq socket binding option endpoint_opt = t.endpoint_option() endpoint_desc = "Enable publish {} info to ZMQ socket endpoint".format(t) plugin.add_option(endpoint_opt, None, endpoint_desc, opt_type='string') # high water mark option hwm_opt = t.hwm_option() hwm_desc = ("Set publish {} info message high water mark " "(default: {})".format(t, DEFAULT_HIGH_WATER_MARK)) plugin.add_option(hwm_opt, DEFAULT_HIGH_WATER_MARK, hwm_desc, opt_type='int') ############################################################################### def plugin_thread(): plugin.run() reactor.callFromThread(reactor.stop)
# reload since we need to start it to pass through its manifest before we get # any cli options. So we're doomed to get our parent cmdline and parse out the # argument by hand. parent = psutil.Process().parent() cmdline = parent.cmdline() plugin.path = None prefix = '--autoreload-plugin=' for c in cmdline: if c.startswith(prefix): plugin.path = c[len(prefix):] break if plugin.path: plugin.child = ChildPlugin(plugin.path, plugin) # If we can't start on the first attempt we can't inject into the # manifest, no point in continuing. if not plugin.child.start(): raise Exception( "Could not start the plugin under development, can't continue") inject_manifest(plugin, plugin.child.manifest) # Now we can run the actual plugin plugin.add_option( "autoreload-plugin", None, "Path to the plugin that we should be watching and reloading.") plugin.run()
time.sleep(1) # Search for lightningd in my ancestor processes: procs = [p for p in psutil.Process(os.getpid()).parents()] for p in procs: if p.name() != 'lightningd': continue plugin.log("Killing process {name} ({pid})".format(name=p.name(), pid=p.pid)) p.kill() # Sleep forever, just in case the master doesn't die on us... while True: time.sleep(30) plugin.add_option( 'backup-destination', None, 'UNUSED. Kept for backward compatibility only. Please update your configuration to remove this option.' ) if __name__ == "__main__": # Did we perform the first write check? plugin.initialized = False if not os.path.exists("backup.lock"): kill("Could not find backup.lock in the lightning-dir") d = json.load(open("backup.lock", 'r')) destination = d['backend_url'] plugin.backend = get_backend(destination, require_init=True) plugin.run()
@plugin.method("estimatefees") def getfeerate(plugin, **kwargs): feerate_url = "{}/fee-estimates".format(plugin.api_endpoint) feerate_req = requests.get(feerate_url) assert feerate_req.status_code == 200 feerates = json.loads(feerate_req.text) # It renders sat/vB, we want sat/kVB, so multiply everything by 10**3 slow = int(feerates["144"] * 10**3) normal = int(feerates["5"] * 10**3) urgent = int(feerates["3"] * 10**3) very_urgent = int(feerates["2"] * 10**3) return { "opening": normal, "mutual_close": normal, "unilateral_close": very_urgent, "delayed_to_us": normal, "htlc_resolution": urgent, "penalty": urgent, "min_acceptable": slow // 2, "max_acceptable": very_urgent * 10, } plugin.add_option( "sauron-api-endpoint", "", "The URL of the esplora instance to hit (including '/api').") plugin.run()