Repository navigation
bitwindow: bid for FreeBank blocks #2410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
029f676
orchestrator: let the BMM engine drive FreeBank
mblowes 7e07d57
bitwindow: bid for FreeBank blocks, under a name
mblowes 3aa7239
orchestrator: retry a FreeBank template on the tip
mblowes 6ea460a
orchestrator: leave FreeBank bids to the engine
mblowes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:bitwindow/utils/freebank_conf.dart'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:get_it/get_it.dart'; | ||
| import 'package:logger/logger.dart'; | ||
| import 'package:sail_ui/pages/sidechains/bmm_tab.dart'; | ||
| import 'package:sail_ui/sail_ui.dart'; | ||
|
|
||
| // The BMM tab scrolls on its own, so the dialog gives it a fixed slot. | ||
| const _bmmTabHeight = 420.0; | ||
|
|
||
| // The tab's control row holds two buttons and three fields, with a 44 px gap | ||
| // between them, and it does not wrap. Below this width Flutter clips the | ||
| // max-bid field and the manual-bid button. | ||
| const _bmmTabWidth = 1160.0; | ||
|
|
||
| /// Bidding for FreeBank blocks from BitWindow: the name the node writes into the | ||
| /// blocks it wins, then the BMM engine's own controls. FreeBank has no app of its | ||
| /// own, so BitWindow is where a user starts it. | ||
| class FreeBankBidDialog extends StatefulWidget { | ||
| const FreeBankBidDialog({super.key}); | ||
|
|
||
| @override | ||
| State<FreeBankBidDialog> createState() => _FreeBankBidDialogState(); | ||
| } | ||
|
|
||
| class _FreeBankBidDialogState extends State<FreeBankBidDialog> { | ||
| final FreeBankRPC _rpc = GetIt.I.get<FreeBankRPC>(); | ||
| final TextEditingController _name = TextEditingController(); | ||
| String? _saved; | ||
| String? _error; | ||
| bool _saving = false; | ||
|
|
||
| bool get _hasName => _saved != null && _saved!.isNotEmpty; | ||
|
|
||
| File get _conf => File(filePath([_rpc.binary.datadirNetwork(), 'freebank.conf'])); | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _load(); | ||
| } | ||
|
|
||
| Future<void> _load() async { | ||
| try { | ||
| final conf = await _conf.exists() ? await _conf.readAsString() : ''; | ||
| final tag = readCoinbaseTag(conf); | ||
| if (!mounted) { | ||
| return; | ||
| } | ||
| setState(() { | ||
| _saved = tag; | ||
| // A name is required before the first bid, so offer one to change. | ||
| _name.text = (tag == null || tag.isEmpty) ? suggestCoinbaseTag() : tag; | ||
| }); | ||
| } catch (e) { | ||
| GetIt.I.get<Logger>().w('FreeBank: could not read freebank.conf: $e'); | ||
| if (!mounted) { | ||
| return; | ||
| } | ||
| setState(() => _error = 'Could not read the name: $e'); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _save() async { | ||
| final problem = coinbaseTagProblem(_name.text); | ||
| if (problem != null) { | ||
| setState(() => _error = problem); | ||
| return; | ||
| } | ||
| setState(() { | ||
| _error = null; | ||
| _saving = true; | ||
| }); | ||
| try { | ||
| final conf = await _conf.exists() ? await _conf.readAsString() : ''; | ||
| await _conf.parent.create(recursive: true); | ||
| await _conf.writeAsString(setCoinbaseTag(conf, _name.text)); | ||
| // freebankd reads the name at startup only. | ||
| await GetIt.I.get<BinaryProvider>().restart(_rpc.binary); | ||
| if (!mounted) { | ||
| return; | ||
| } | ||
| setState(() => _saved = _name.text.trim()); | ||
| } catch (e) { | ||
| if (!mounted) { | ||
| return; | ||
| } | ||
| setState(() => _error = 'Could not save the name: $e'); | ||
| } finally { | ||
| if (mounted) { | ||
| setState(() => _saving = false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _name.dispose(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return SailDialog( | ||
| title: 'Bid for FreeBank blocks', | ||
| subtitle: | ||
| 'For each eCash block, your node offers a small fee to have its next FreeBank block included. ' | ||
| 'A bid is paid from your BitWindow wallet, and only when it wins.', | ||
| error: _error, | ||
| maxWidth: _bmmTabWidth + 80, | ||
| maxHeight: 720, | ||
|
octobocto marked this conversation as resolved.
|
||
| withCloseButton: true, | ||
| child: SailColumn( | ||
| spacing: SailStyleValues.padding16, | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| SailText.primary13('Name on your blocks', bold: true), | ||
| SailRow( | ||
| spacing: SailStyleValues.padding08, | ||
| children: [ | ||
| Expanded( | ||
| child: SailTextField( | ||
| controller: _name, | ||
| hintText: "Your name, or your pool's name", | ||
| enabled: !_saving, | ||
| ), | ||
| ), | ||
| SailButton( | ||
| label: 'Save', | ||
| loading: _saving, | ||
| onPressed: _save, | ||
| ), | ||
| ], | ||
| ), | ||
| SailText.secondary12( | ||
| _hasName | ||
| ? 'Blocks you win show "$_saved" on the explorer. Saving a new name restarts your FreeBank node.' | ||
| : 'Blocks you win show this name on the explorer. Change it if you like, then save. ' | ||
| 'Saving restarts your FreeBank node.', | ||
| ), | ||
| SizedBox( | ||
| height: _bmmTabHeight, | ||
| child: SingleChildScrollView( | ||
| scrollDirection: Axis.horizontal, | ||
| child: SizedBox( | ||
| width: _bmmTabWidth, | ||
| // Every block a bid wins carries the name, so bidding waits for one. | ||
| child: _hasName | ||
| ? const BMMTab() | ||
| : Stack( | ||
| children: [ | ||
| const IgnorePointer(child: Opacity(opacity: 0.35, child: BMMTab())), | ||
| Center(child: SailText.primary15('Save a name above to start bidding.')), | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| /// The one line of freebank.conf that BitWindow edits: `coinbasetag=`, the name | ||
| /// a FreeBank node writes into the coinbase of every block it makes, so block | ||
| /// explorers can credit the block to it. freebankd reads it at startup only. | ||
| library; | ||
|
|
||
| import 'dart:math'; | ||
|
|
||
| const coinbaseTagKey = 'coinbasetag'; | ||
| const coinbaseTagMaxBytes = 64; | ||
|
|
||
| /// A Core-style network header, for example `[regtest]`. Every line after one | ||
| /// belongs to that network only. | ||
| final _sectionHeader = RegExp(r'^\[[^\]]*\]$'); | ||
|
|
||
| /// The network section freebankd reads. BitWindow starts the node with no chain | ||
| /// flag, so it runs its default main chain. | ||
| const activeNetworkSection = 'main'; | ||
|
|
||
| /// The name the node takes, or null when the conf sets none. A `[main]` entry | ||
| /// beats the global part, and any other network section counts for nothing. The | ||
| /// last line of a scope wins, as in Core. | ||
| String? readCoinbaseTag(String conf) { | ||
| String? global; | ||
| String? active; | ||
| String? section; | ||
| for (final line in conf.split('\n')) { | ||
| final trimmed = line.trim(); | ||
| if (_sectionHeader.hasMatch(trimmed)) { | ||
| section = trimmed.substring(1, trimmed.length - 1); | ||
| continue; | ||
| } | ||
| if (!trimmed.startsWith('$coinbaseTagKey=')) { | ||
| continue; | ||
| } | ||
| final value = trimmed.substring(coinbaseTagKey.length + 1).trim(); | ||
| if (section == null) { | ||
| global = value; | ||
| } else if (section == activeNetworkSection) { | ||
| active = value; | ||
| } | ||
| } | ||
| return active ?? global; | ||
| } | ||
|
|
||
| /// Why freebankd would refuse this name, or null if it takes it. It mirrors the | ||
| /// node's own check (1 to 64 printable ASCII bytes), plus the two things the conf | ||
| /// file itself would bend: a `#` starts a comment, and quotes become part of the | ||
| /// name. | ||
| String? coinbaseTagProblem(String name) { | ||
| final tag = name.trim(); | ||
| if (tag.isEmpty) { | ||
| return 'Give a name.'; | ||
| } | ||
| if (tag.length > coinbaseTagMaxBytes) { | ||
| return 'Use at most $coinbaseTagMaxBytes characters.'; | ||
| } | ||
| for (final unit in tag.codeUnits) { | ||
| if (unit < 0x20 || unit > 0x7e) { | ||
| return 'Use plain letters, digits, punctuation and spaces.'; | ||
| } | ||
| } | ||
| if (tag.contains('#')) { | ||
| return 'Leave out #: it starts a comment in freebank.conf.'; | ||
| } | ||
| if (tag.startsWith('"') || tag.startsWith("'") || tag.endsWith('"') || tag.endsWith("'")) { | ||
| return 'Leave out the quotes: they would become part of the name.'; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /// The conf with the name the node takes set to [name]. A `[main]` entry beats | ||
| /// the global part, so the change goes there when one exists. Otherwise the line | ||
| /// goes in the global part, before the first network header. Every other line | ||
| /// stays as it was. | ||
| String setCoinbaseTag(String conf, String name) { | ||
| final line = '$coinbaseTagKey=${name.trim()}'; | ||
| final lines = conf.isEmpty ? <String>[] : conf.split('\n'); | ||
| final target = _tagInActiveSection(lines) ? activeNetworkSection : null; | ||
|
|
||
| final kept = <String>[]; | ||
| var written = false; | ||
| String? section; | ||
| for (final l in lines) { | ||
| final trimmed = l.trim(); | ||
| if (_sectionHeader.hasMatch(trimmed)) { | ||
| if (!written && target == null) { | ||
| kept.add(line); | ||
| written = true; | ||
| } | ||
| section = trimmed.substring(1, trimmed.length - 1); | ||
| } else if (section == target && trimmed.startsWith('$coinbaseTagKey=')) { | ||
|
octobocto marked this conversation as resolved.
|
||
| if (!written) { | ||
| kept.add(line); | ||
| written = true; | ||
| } | ||
| continue; | ||
| } | ||
| kept.add(l); | ||
| } | ||
| if (!written) { | ||
| if (kept.isNotEmpty && kept.last.isEmpty) { | ||
| kept.insert(kept.length - 1, line); | ||
| } else { | ||
| kept.add(line); | ||
|
octobocto marked this conversation as resolved.
|
||
| } | ||
| } | ||
| final out = kept.join('\n'); | ||
| return out.endsWith('\n') ? out : '$out\n'; | ||
| } | ||
|
|
||
| bool _tagInActiveSection(List<String> lines) { | ||
| String? section; | ||
| for (final l in lines) { | ||
| final trimmed = l.trim(); | ||
| if (_sectionHeader.hasMatch(trimmed)) { | ||
| section = trimmed.substring(1, trimmed.length - 1); | ||
| } else if (section == activeNetworkSection && trimmed.startsWith('$coinbaseTagKey=')) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /// A name to offer a user who has none yet: unique enough to spot their own | ||
| /// blocks, it says where they came from, and it carries nothing personal. | ||
| String suggestCoinbaseTag([Random? random]) { | ||
| const letters = 'abcdefghjkmnpqrstuvwxyz23456789'; | ||
| final rng = random ?? Random.secure(); | ||
| final suffix = List.generate(4, (_) => letters[rng.nextInt(letters.length)]).join(); | ||
| return 'bitwindow-$suffix'; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.