Skip to content

Multisig lounge frontend #785

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
130 changes: 125 additions & 5 deletions bitwindow/lib/pages/wallet_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:sail_ui/providers/balance_provider.dart';
import 'package:sail_ui/rpcs/bitwindow_api.dart';
import 'package:sail_ui/sail_ui.dart';
import 'package:stacked/stacked.dart';
import 'package:bitwindow/widgets/create_multisig_modal.dart';

@RoutePage()
class WalletPage extends StatelessWidget {
Expand Down Expand Up @@ -2460,11 +2461,130 @@ class MultisigLoungeTab extends StatelessWidget {
return SailCard(
title: 'Multisig Lounge',
subtitle: 'Create and manage multi-signature wallets',
child: Center(
child: SailText.primary15(
'Multisig functionality coming soon...',
color: context.sailTheme.colors.textTertiary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: SailText.primary13('Lounges'),
),
SailSpacing(SailStyleValues.padding08),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: SailTable(
getRowId: (index) => 'empty$index',
headerBuilder: (context) => [
const SailTableHeaderCell(name: 'Name'),
const SailTableHeaderCell(name: 'ID'),
const SailTableHeaderCell(name: 'Total Keys'),
const SailTableHeaderCell(name: 'Keys Required'),
const SailTableHeaderCell(name: 'Participants'),
],
rowBuilder: (context, row, selected) {
return [
const SailTableCell(value: 'Multisig functionality coming soon...'),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
];
},
rowCount: 1,
columnWidths: const [-1, -1, -1, -1, -1],
drawGrid: true,
),
),
const SizedBox(width: SailStyleValues.padding16),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SailButton(
label: 'Create New Lounge',
onPressed: () async {
showDialog(
context: context,
builder: (context) => const CreateMultisigModal(),
);
},
variant: ButtonVariant.primary,
),
],
),
),
],
),
),
SailSpacing(SailStyleValues.padding32),
Center(
child: SailText.primary13('Transaction History'),
),
SailSpacing(SailStyleValues.padding08),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: SailTable(
getRowId: (index) => 'tx_empty$index',
headerBuilder: (context) => [
const SailTableHeaderCell(name: 'Lounge'),
const SailTableHeaderCell(name: 'MuTxid'),
const SailTableHeaderCell(name: 'Status'),
const SailTableHeaderCell(name: 'Action'),
const SailTableHeaderCell(name: 'Bitcoin Txid'),
],
rowBuilder: (context, row, selected) {
return [
const SailTableCell(value: 'Multisig functionality coming soon...'),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
const SailTableCell(value: ''),
];
},
rowCount: 1,
columnWidths: const [-1, -1, -1, -1, -1],
drawGrid: true,
),
),
const SizedBox(width: SailStyleValues.padding16),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: SailText.primary13('Transaction Tools'),
),
SailSpacing(SailStyleValues.padding08),
SailButton(
label: 'Create Transaction',
onPressed: null,
variant: ButtonVariant.secondary,
),
SailSpacing(SailStyleValues.padding08),
SailButton(
label: 'Sign and Send',
onPressed: null,
variant: ButtonVariant.secondary,
),
SailSpacing(SailStyleValues.padding08),
SailButton(
label: 'Finalize and Broadcast',
onPressed: null,
variant: ButtonVariant.secondary,
),
],
),
),
],
),
),
],
),
);
}
Expand Down
133 changes: 126 additions & 7 deletions bitwindow/lib/providers/bitdrive_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ class BitDriveProvider extends ChangeNotifier {
static const String AUTH_KEY_PATH = "m/44'/0'/0'/$BITDRIVE_DERIVATION_INDEX/1";
static const int AUTH_TAG_SIZE = 8; // 8 bytes auth tag

// File type flags (encoded in first byte of metadata)
static const int FLAG_STANDARD = 0; // 0000 0000
static const int FLAG_ENCRYPTED = 1; // 0000 0001
static const int FLAG_MULTISIG = 2; // 0000 0010
// Combined flags
static const int FLAG_ENCRYPTED_MULTISIG = 3; // 0000 0011 (ENCRYPTED | MULTISIG)

BitDriveProvider() {
// Listen for blockchain sync status changes
blockchainProvider.addListener(_onSyncStatusChanged);
Expand Down Expand Up @@ -270,7 +277,9 @@ class BitDriveProvider extends ChangeNotifier {
if (metadataBytes.length != 9) continue;

final metadata = ByteData.view(Uint8List.fromList(metadataBytes).buffer);
final isEncrypted = metadata.getUint8(0) == 1;
final flagByte = metadata.getUint8(0);
final isEncrypted = (flagByte & FLAG_ENCRYPTED) != 0;
final isMultisig = (flagByte & FLAG_MULTISIG) != 0;
final timestamp = metadata.getUint32(1);
final fileType = utf8.decode(metadataBytes.sublist(5, 9)).trim();

Expand All @@ -283,6 +292,107 @@ class BitDriveProvider extends ChangeNotifier {
final content = isEncrypted ? await _decryptContent(contentBytes, timestamp, fileType) : contentBytes;

await file.writeAsBytes(content);

// Process multisig configuration files using the dedicated flag
if (isMultisig && fileType == 'txt' || fileType == 'jsn' || fileType == 'jso') {
try {
final contentStr = utf8.decode(content);
final json = jsonDecode(contentStr);

// Check if this is a valid multisig configuration
if (json is Map && json.containsKey('p') && json.containsKey('name') && json.containsKey('keys')) {
log.i('BitDrive: Found multisig configuration (flagged)');

// Ensure multisig directory exists
final multisigDir = path.join(_bitdriveDir!, 'multisig');
final dir = Directory(multisigDir);
if (!await dir.exists()) {
await dir.create(recursive: true);
}

// Write multisig configuration to multisig directory
final p = json['p'];
final name = json['name'];

// Add entry to config file
final configFile = File(path.join(multisigDir, 'multisig.conf'));
if (!await configFile.exists()) {
await configFile.writeAsString('');
}

// Append the configuration if it doesn't already exist
final configContent = await configFile.readAsString();
if (!configContent.contains('P$p=')) {
await configFile.writeAsString(
'P$p="$name"\n',
mode: FileMode.append,
);

// Store keys
if (json['keys'] is List) {
final keysFile = File(path.join(multisigDir, 'P$p.keys'));
final keysList = (json['keys'] as List).cast<Map<String, dynamic>>();
final keysContent = keysList.map((k) => '${k['name']}=${k['publicKey']}').join('\n');
await keysFile.writeAsString(keysContent);
}

log.i('BitDrive: Restored multisig configuration P$p');
}
}
} catch (e) {
log.w('BitDrive: Error processing multisig file: $e');
// Continue if there's an error parsing
}
}
// For backward compatibility, also check unflagged txt files for JSON structure
else if (!isMultisig && fileType == 'txt') {
try {
final contentStr = utf8.decode(content);
if (contentStr.trim().startsWith('{') && contentStr.trim().endsWith('}')) {
final json = jsonDecode(contentStr);

// Check if this is a valid multisig configuration
if (json is Map && json.containsKey('p') && json.containsKey('name') && json.containsKey('keys')) {
log.i('BitDrive: Found multisig configuration (unflagged)');

// Process multisig config (same code as above)
final multisigDir = path.join(_bitdriveDir!, 'multisig');
final dir = Directory(multisigDir);
if (!await dir.exists()) {
await dir.create(recursive: true);
}

final p = json['p'];
final name = json['name'];

final configFile = File(path.join(multisigDir, 'multisig.conf'));
if (!await configFile.exists()) {
await configFile.writeAsString('');
}

final configContent = await configFile.readAsString();
if (!configContent.contains('P$p=')) {
await configFile.writeAsString(
'P$p="$name"\n',
mode: FileMode.append,
);

if (json['keys'] is List) {
final keysFile = File(path.join(multisigDir, 'P$p.keys'));
final keysList = (json['keys'] as List).cast<Map<String, dynamic>>();
final keysContent = keysList.map((k) => '${k['name']}=${k['publicKey']}').join('\n');
await keysFile.writeAsString(keysContent);
}

log.i('BitDrive: Restored multisig configuration P$p');
}
}
}
} catch (e) {
// Not a JSON file or not a multisig configuration - ignore
}
}

restoredCount++;
} catch (e) {
log.e('BitDrive: Error processing tx ${tx.txid}: $e');
Expand Down Expand Up @@ -312,15 +422,15 @@ class BitDriveProvider extends ChangeNotifier {
notifyListeners();
}

Future<void> setTextContent(String content) async {
Future<void> setTextContent(String content, {bool isMultisig = false}) async {
if (content.length > 1024 * 1024) {
error = 'Text size must be less than 1MB';
notifyListeners();
return;
}
textContent = content;
fileContent = null;
fileName = 'text.txt';
fileName = isMultisig ? 'multisig.json' : 'text.txt';
mimeType = 'text/plain';
error = null;
notifyListeners();
Expand All @@ -336,7 +446,7 @@ class BitDriveProvider extends ChangeNotifier {
notifyListeners();
}

Future<void> store() async {
Future<void> store({bool isMultisig = false}) async {
if (_isFetching) return;
_isFetching = true;
error = null;
Expand All @@ -352,7 +462,12 @@ class BitDriveProvider extends ChangeNotifier {
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final fileType = _detectFileType(content);

metadata.setUint8(0, shouldEncrypt ? 1 : 0);
// Set flag byte - encode both encryption and multisig flags
int flagByte = FLAG_STANDARD;
if (shouldEncrypt) flagByte |= FLAG_ENCRYPTED;
if (isMultisig) flagByte |= FLAG_MULTISIG;

metadata.setUint8(0, flagByte);
metadata.setUint32(1, timestamp);

final typeBytes = utf8.encode(fileType.padRight(4, ' '));
Expand Down Expand Up @@ -423,7 +538,9 @@ class BitDriveProvider extends ChangeNotifier {
}

final metadata = ByteData.view(Uint8List.fromList(metadataBytes).buffer);
final isEncrypted = metadata.getUint8(0) == 1;
final flagByte = metadata.getUint8(0);
final isEncrypted = (flagByte & FLAG_ENCRYPTED) != 0;
final isMultisig = (flagByte & FLAG_MULTISIG) != 0;
final timestamp = metadata.getUint32(1);
final fileType = utf8.decode(metadataBytes.sublist(5, 9)).trim();

Expand Down Expand Up @@ -533,7 +650,9 @@ class BitDriveProvider extends ChangeNotifier {
if (metadataBytes.length != 9) continue;

final metadata = ByteData.view(Uint8List.fromList(metadataBytes).buffer);
final isEncrypted = metadata.getUint8(0) == 1;
final flagByte = metadata.getUint8(0);
final isEncrypted = (flagByte & FLAG_ENCRYPTED) != 0;
final isMultisig = (flagByte & FLAG_MULTISIG) != 0;
final timestamp = metadata.getUint32(1);
final fileType = utf8.decode(metadataBytes.sublist(5, 9)).trim();

Expand Down
Loading
Loading