Skip to content
Merged
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
2 changes: 2 additions & 0 deletions bitwindow/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ Future<(Directory, File, Logger)> init(String arguments) async {
GetIt.I.registerSingleton<CoinShiftRPC>(CoinShiftLive());
GetIt.I.registerSingleton<PhotonRPC>(PhotonLive());
GetIt.I.registerSingleton<TruthcoinRPC>(TruthcoinLive());
GetIt.I.registerSingleton<FreeBankRPC>(FreeBankLive());

registerNodeMode();

Expand Down Expand Up @@ -262,6 +263,7 @@ Future<(Directory, File, Logger)> init(String arguments) async {
GetIt.I.get<PhotonRPC>(),
GetIt.I.get<TruthcoinRPC>(),
GetIt.I.get<CoinShiftRPC>(),
GetIt.I.get<FreeBankRPC>(),
],
),
);
Expand Down
4 changes: 4 additions & 0 deletions bitwindow/lib/pages/sidechains_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin {
Truthcoin() => SidechainType.SIDECHAIN_TYPE_TRUTHCOIN,
Photon() => SidechainType.SIDECHAIN_TYPE_PHOTON,
CoinShift() => SidechainType.SIDECHAIN_TYPE_COINSHIFT,
FreeBank() => SidechainType.SIDECHAIN_TYPE_FREEBANK,
_ => null,
};

Expand Down Expand Up @@ -898,6 +899,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin {
var b when b is Truthcoin => GetIt.I.isRegistered<TruthcoinRPC>() ? GetIt.I.get<TruthcoinRPC>() : null,
var b when b is Photon => GetIt.I.isRegistered<PhotonRPC>() ? GetIt.I.get<PhotonRPC>() : null,
var b when b is CoinShift => GetIt.I.isRegistered<CoinShiftRPC>() ? GetIt.I.get<CoinShiftRPC>() : null,
var b when b is FreeBank => GetIt.I.isRegistered<FreeBankRPC>() ? GetIt.I.get<FreeBankRPC>() : null,
_ => null,
};

Expand Down Expand Up @@ -949,6 +951,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin {
var b when b is Truthcoin => (GetIt.I.isRegistered<TruthcoinRPC>() ? GetIt.I.get<TruthcoinRPC>() : null),
var b when b is Photon => (GetIt.I.isRegistered<PhotonRPC>() ? GetIt.I.get<PhotonRPC>() : null),
var b when b is CoinShift => (GetIt.I.isRegistered<CoinShiftRPC>() ? GetIt.I.get<CoinShiftRPC>() : null),
var b when b is FreeBank => (GetIt.I.isRegistered<FreeBankRPC>() ? GetIt.I.get<FreeBankRPC>() : null),
_ => null,
};
}
Expand Down Expand Up @@ -1645,6 +1648,7 @@ class _DepositModalState extends State<DepositModal> {
ZSide() => GetIt.I.isRegistered<ZSideRPC>() ? GetIt.I.get<ZSideRPC>() : null,
CoinShift() => GetIt.I.isRegistered<CoinShiftRPC>() ? GetIt.I.get<CoinShiftRPC>() : null,
Thunder() => GetIt.I.isRegistered<ThunderRPC>() ? GetIt.I.get<ThunderRPC>() : null,
FreeBank() => GetIt.I.isRegistered<FreeBankRPC>() ? GetIt.I.get<FreeBankRPC>() : null,
_ => null,
};

Expand Down
96 changes: 96 additions & 0 deletions bitwindow/test/sidechains_table_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ class _CoinShiftRPC extends MockCoinShiftRPC {
Future<(double, double)> balance() async => wallet;
}

class _Orchestrator implements OrchestratorRPC {
final balanceReads = <BinaryType>[];
GetSidechainBalanceResponse balance = GetSidechainBalanceResponse();

@override
Future<GetSidechainBalanceResponse> getSidechainBalance(BinaryType sidechain) async {
balanceReads.add(sidechain);
return balance;
}

@override
Future<GetBinaryVersionResponse> getBinaryVersion(String name, {bool forceBackend = false}) async =>
GetBinaryVersionResponse(version: '$name 1.0');

@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

Thunder _thunder({bool downloaded = true, bool updateAvailable = false}) {
final base = Thunder();
return base.copyWith(
Expand All @@ -116,6 +134,22 @@ Thunder _thunder({bool downloaded = true, bool updateAvailable = false}) {
);
}

FreeBank _freeBank() {
final base = FreeBank();
return base.copyWith(
metadata: base.metadata.copyWith(
remoteTimestamp: null,
downloadedTimestamp: DateTime(2026, 1),
binaryPath: File('/tmp/freebankd'),
updateable: true,
),
);
}

final Finder _settingsButton = find.byWidgetPredicate(
(widget) => widget is SailButton && widget.icon == SailSVGAsset.settings,
);

Finder _button(String label) => find.byWidgetPredicate((widget) => widget is SailButton && widget.label == label);

final Finder _updateButton = find.byWidgetPredicate(
Expand Down Expand Up @@ -268,6 +302,68 @@ void main() {
expect(find.text('—'), findsOneWidget);
});

// FreeBank reads all its state through the orchestrator.
group('FreeBank', () {
late _Orchestrator orchestrator;
late FreeBankRPC freeBankRPC;

Future<void> setUpFreeBank() async {
GetIt.I.registerSingleton<BinaryProvider>(_Binaries([_freeBank()]));
orchestrator = _Orchestrator();
GetIt.I.registerSingleton<OrchestratorRPC>(orchestrator);
freeBankRPC = FreeBankLive();
GetIt.I.registerSingleton<FreeBankRPC>(freeBankRPC);
await GetIt.I.unregister<BalanceProvider>();
balances = BalanceProvider(connections: [thunderRPC, coinShiftRPC, freeBankRPC]);
GetIt.I.registerSingleton<BalanceProvider>(balances);
GetIt.I.get<SidechainProvider>().sidechains[130] = SidechainOverview(
ListSidechainsResponse_Sidechain(title: 'FreeBank', slot: 130, balanceSatoshi: Int64(300000000)),
[],
[],
);
}

testWidgets('the row has a settings button that opens the chain settings', (tester) async {
await setUpFreeBank();
await pumpTable(tester);

expect(_settingsButton, findsOneWidget);
await tester.tap(_settingsButton);
await tester.pump(kDoubleTapTimeout);
await tester.pump(const Duration(milliseconds: 500));

expect(find.byType(ChainSettingsModal), findsOneWidget);
expect(find.descendant(of: find.byType(ChainSettingsModal), matching: find.text('FreeBank')), findsOneWidget);
expect(find.textContaining('version freebank 1.0'), findsOneWidget);
expect(tester.takeException(), isNull);
});

testWidgets('a stopped FreeBank offers Start and reads no balance', (tester) async {
await setUpFreeBank();
await balances.fetch();
await pumpTable(tester);

expect(_buttonWidget(tester, 'Start').variant, ButtonVariant.primary);
expect(_button('Stop'), findsNothing);
expect(_settingsButton, findsOneWidget);
expect(orchestrator.balanceReads, isEmpty);
expect(tester.takeException(), isNull);
});

testWidgets('a running FreeBank offers Stop and shows the balance the orchestrator reads', (tester) async {
await setUpFreeBank();
orchestrator.balance = GetSidechainBalanceResponse(confirmedSats: Int64(250000000));
freeBankRPC.connected = true;
await balances.fetch();
await pumpTable(tester);

expect(_buttonWidget(tester, 'Stop').variant, ButtonVariant.outline);
expect(_tooltip('Connected'), findsOneWidget);
expect(orchestrator.balanceReads, contains(BinaryType.BINARY_TYPE_FREEBANK));
expect(find.text(GetIt.I.get<FormatterProvider>().formatBTC(2.5)), findsOneWidget);
});
});

testWidgets('a chain that is not downloaded offers a primary Download', (tester) async {
setUpChain(_thunder(downloaded: false));
await pumpTable(tester);
Expand Down
4 changes: 2 additions & 2 deletions sidechain-orchestrator/api/explorer_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type broadcastResult struct {

func (h *ExplorerHandler) broadcastSource(chain string) (source, error) {
chain = strings.TrimSpace(chain)
if chain != "bitnames" && chain != "bitassets" && chain != "freebank" {
if chain != "bitnames" && chain != "bitassets" {
return source{}, connect.NewError(connect.CodeInvalidArgument,
fmt.Errorf("transaction rebroadcast supports BitNames, BitAssets and FreeBank"))
fmt.Errorf("transaction rebroadcast supports BitNames and BitAssets"))
}
src, err := h.sourceOf(chain)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion sidechain-orchestrator/api/explorer_broadcast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const broadcastTestJSON = `{
}`

func TestExplorerBroadcastKeepsSignedJSON(t *testing.T) {
for _, chain := range []string{"bitnames", "bitassets", "freebank"} {
for _, chain := range []string{"bitnames", "bitassets"} {
t.Run(chain, func(t *testing.T) {
node := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Expand Down
41 changes: 22 additions & 19 deletions sidechain-orchestrator/api/freebank_dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -13,8 +15,6 @@ import (
orchestrator "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator"
"github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/config"
pb "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/gen/orchestrator/v1"
"github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/sidechain"
bitassetssvc "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/sidechain/bitassets"
)

// GetSyncStatus shows FreeBank with its own type.
Expand All @@ -24,34 +24,37 @@ func TestSidechainTypeFromNameKnowsFreebank(t *testing.T) {
assert.Equal(t, pb.SidechainType_SIDECHAIN_TYPE_UNSPECIFIED, sidechainTypeFromName("nosuchchain"))
}

// FreeBank has no "balance" RPC, so its balance comes from bitcoin_balance.
func TestFreebankBalanceUsesBitcoinBalanceMethod(t *testing.T) {
var gotMethod string
// FreeBank keeps the one wallet its node created and predates getbalances, so
// its balance is getwalletinfo, asked at the root endpoint with the cookie.
func TestFreebankBalanceReadsItsOwnWallet(t *testing.T) {
config.SetHomeDir(t.TempDir())
t.Cleanup(func() { config.SetHomeDir("") })
datadir := config.FreebankDirs.DatadirNetwork(config.NetworkRegtest, "")
require.NoError(t, os.MkdirAll(datadir, 0o700))
require.NoError(t, os.WriteFile(filepath.Join(datadir, ".cookie"), []byte("__cookie__:secret"), 0o600))

var gotPath, gotMethod, gotUser string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID uint64 `json:"id"`
Method string `json:"method"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
gotMethod = req.Method
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"total_sats":300,"available_sats":125}}`))
gotPath, gotMethod = r.URL.Path, req.Method
gotUser, _, _ = r.BasicAuth()
_, _ = w.Write([]byte(`{"result":{"balance":1.0,"unconfirmed_balance":0.25,"immature_balance":0.5},"error":null}`))
}))
defer srv.Close()
host, port := hostPort(t, srv)

bh := bitassetssvc.NewHandler(sidechain.NewJSONRPCProxy(host, port))
h := &Handler{
orch: &orchestrator.Orchestrator{Network: string(config.NetworkRegtest)},
sidechainBalances: map[string]SidechainBalanceFunc{"freebank": bh.WalletBalance},
}

h := &Handler{orch: &orchestrator.Orchestrator{Network: string(config.NetworkRegtest)}}
confirmed, pending, err := h.fetchSidechainBalance(
context.Background(),
orchestrator.BinaryConfig{Name: "freebank", Host: host, Port: port},
orchestrator.BinaryConfig{Name: "freebank", Host: host, Port: port, IsBitcoinCore: true},
)
require.NoError(t, err)
assert.Equal(t, "bitcoin_balance", gotMethod)
assert.Equal(t, int64(125), confirmed)
assert.Equal(t, int64(175), pending)
assert.Equal(t, "/", gotPath)
assert.Equal(t, "getwalletinfo", gotMethod)
assert.Equal(t, "__cookie__", gotUser)
assert.Equal(t, int64(100_000_000), confirmed)
assert.Equal(t, int64(75_000_000), pending)
}
19 changes: 11 additions & 8 deletions sidechain-orchestrator/api/sidechain_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,21 @@ func TestCoreForkAnswersOnlyWhatItDrives(t *testing.T) {
assert.Contains(t, err.Error(), "proposes no withdrawal bundle")
}

// FreeBank is a BitAssets fork, so it does BMM and proposes withdrawal bundles.
func TestFreebankAnswersEveryInterfaceLikeBitassets(t *testing.T) {
cfg := orchestrator.BinaryConfig{Name: "freebank", DisplayName: "FreeBank", Port: 6130}
// FreeBank blind merge mines with its own refreshbmm ticker and settles
// withdrawals on its own paths, so it is a Node and nothing more.
func TestFreebankAnswersNeitherBMMNorWithdrawals(t *testing.T) {
cfg := orchestrator.BinaryConfig{Name: "freebank", DisplayName: "FreeBank", Port: 8454, IsBitcoinCore: true}
node, err := sidechainNode(cfg, config.NetworkRegtest)
require.NoError(t, err)

bmm, err := bmmNode(cfg, config.NetworkRegtest)
require.NoError(t, err)
assert.NotNil(t, bmm)
_, err = bmmNode(cfg, config.NetworkRegtest)
require.Error(t, err)
assert.Contains(t, err.Error(), "FreeBank")
assert.Contains(t, err.Error(), "produces its own blocks")

_, proposesBundles := node.(sidechain.WithdrawalNode)
assert.True(t, proposesBundles)
_, err = withdrawalNode(node, cfg.DisplayName)
require.Error(t, err)
assert.Contains(t, err.Error(), "proposes no withdrawal bundle")
}

// A chain with no directory config names itself in the failure, rather than
Expand Down
12 changes: 6 additions & 6 deletions sidechain-orchestrator/chains_config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 23,
"version": 24,
"binaries": {
"bitcoincore": {
"type": "core",
Expand Down Expand Up @@ -837,9 +837,9 @@
"version": "latest",
"description": "Credit-creation drivechain (Scottish free-banking lineage).",
"repo_url": "https://github.com/mbdrivechains/freebank",
"port": 6130,
"port": 8454,
"chain_layer": 2,
"is_bitcoin_core": false,
"is_bitcoin_core": true,
"color": "orange",
"health_check": {
"type": "tcp"
Expand All @@ -849,9 +849,9 @@
"directories": {
"binary": {
"default": {
"linux": "freebank",
"macos": "freebank",
"windows": "freebank"
"linux": ".freebank",
"macos": "FreeBank",
"windows": "FreeBank"
}
},
"flutter_frontend": {}
Expand Down
5 changes: 0 additions & 5 deletions sidechain-orchestrator/cmd/drivechaind/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,11 +633,6 @@ func run(cctx *cli.Context) error {
path, bitassetsHandler := bitassetsrpc.NewBitAssetsServiceHandler(h, connect.WithInterceptors(authIC))
mux.Handle(path, bitassetsHandler)
log.Info().Str("sidechain", name).Int("port", cfg.Port).Msg("registered sidechain RPC service")
case "freebank":
// FreeBank is a BitAssets fork and uses the same wallet RPC.
h := bitassetssvc.NewHandler(proxy)
handler.SetSidechainBalance(name, h.WalletBalance)
log.Info().Str("sidechain", name).Int("port", cfg.Port).Msg("registered sidechain balance reader")
case "photon":
h := photonsvc.NewHandler(proxy)
handler.SetSidechainBalance(name, h.WalletBalance)
Expand Down
8 changes: 4 additions & 4 deletions sidechain-orchestrator/config/binaries.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ func (b BinaryDirConfig) GetBlockchainDataPaths(networkDir string, network Netwo
case "bitwindowd":
return GetExistingFilesInDir(networkDir, []string{"bitdrive", "bitwindow.db"}, log)

case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift", "freebankd":
case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift":
return GetExistingFilesInDir(networkDir, SidechainChainDataNames, log)

default:
Expand Down Expand Up @@ -538,7 +538,7 @@ func (b BinaryDirConfig) GetWalletPaths(networkDir string, network Network, log
paths = append(paths, legacyWallet)
}

case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift", "freebankd":
case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift":
paths = append(paths, GetExistingFilesInDir(networkDir, []string{"wallet.mdb"}, log)...)

case "bitwindowd":
Expand Down Expand Up @@ -633,7 +633,7 @@ func (b BinaryDirConfig) GetLogPaths(networkDir string, log zerolog.Logger) []st
logfile.Name, "debug.log", "drivechaind.log",
}, log)...)

case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift", "freebankd":
case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift":
paths = append(paths, GetExistingFilesInDir(networkDir, []string{"logs"}, log)...)
}

Expand Down Expand Up @@ -662,7 +662,7 @@ func (b BinaryDirConfig) LogPath(networkDir string) string {
case "bip300301-enforcer":
return findLatestEnforcerLog(networkDir)

case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift", "freebankd":
case "thunder", "bitnames", "bitassets", "thunder-orchard", "truthcoin", "photon", "coinshift":
return findLatestDirVersionedLog(networkDir)

default:
Expand Down
Loading
Loading