tresori_sdk 1.0.0
tresori_sdk: ^1.0.0 copied to clipboard
A comprehensive interface for blockchain wallet management, supporting Self-Custodial, Custodial, and MPC wallet operations.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tresori_sdk/tresori.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TreSori Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
),
home: const WalletDemoPage(),
);
}
}
class WalletDemoPage extends StatefulWidget {
const WalletDemoPage({super.key});
@override
State<WalletDemoPage> createState() => _WalletDemoPageState();
}
enum LogType { info, success, error, warning }
class LogEntry {
final DateTime timestamp;
final String message;
final LogType type;
LogEntry(this.message, {this.type = LogType.info})
: timestamp = DateTime.now();
}
class _WalletDemoPageState extends State<WalletDemoPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final _apiKeyController = TextEditingController();
final _loggerController = ScrollController();
final List<LogEntry> _logs = [];
bool _autoScroll = true;
bool _showLogs = false;
bool _isInitialized = false;
bool _isLoading = false;
String _loadingMessage = '';
// Common User ID
final _userIdController = TextEditingController();
// ============ SELF CUSTODIAL STATE ============
Chain? _selfCustodialChain;
String? _currentMnemonic;
String? _currentPrivateKey;
String? _currentAddress;
final _selfCustodialRpcController = TextEditingController();
final _selfCustodialToController = TextEditingController();
final _selfCustodialAmountController = TextEditingController();
final _selfCustodialContractAddressController = TextEditingController();
final _selfCustodialFunctionController = TextEditingController();
final _selfCustodialAbiController = TextEditingController();
final _selfCustodialParamsController = TextEditingController();
// ============ CUSTODIAL STATE ============
Chain? _custodialChain;
String? _custodialAddress;
final _custodialToController = TextEditingController();
final _custodialAmountController = TextEditingController();
final _custodialTokenAddressController = TextEditingController();
final _custodialContractAddressController = TextEditingController();
final _custodialFunctionController = TextEditingController();
final _custodialAbiController = TextEditingController();
final _custodialParamsController = TextEditingController();
// ============ MPC STATE ============
Chain? _mpcChain;
String? _mpcAddress;
String? _mpcUserShard;
String? _mpcBalance;
String? _mpcPublicKey;
bool _mpcIsNewUser = false;
final _emailController = TextEditingController();
final _otpController = TextEditingController();
final _phoneController = TextEditingController();
final _countryCodeController = TextEditingController(text: '+1');
final _mpcToController = TextEditingController();
final _mpcAmountController = TextEditingController();
final _mpcTokenAddressController = TextEditingController();
final _mpcUserShardsController = TextEditingController();
final _mpcContractAddressController = TextEditingController();
final _mpcFunctionController = TextEditingController();
final _mpcAbiController = TextEditingController();
final _mpcParamsController = TextEditingController();
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
_apiKeyController.dispose();
_userIdController.dispose();
_loggerController.dispose();
// Self Custodial
_selfCustodialRpcController.dispose();
_selfCustodialToController.dispose();
_selfCustodialAmountController.dispose();
_selfCustodialContractAddressController.dispose();
_selfCustodialFunctionController.dispose();
_selfCustodialAbiController.dispose();
_selfCustodialParamsController.dispose();
// Custodial
_custodialToController.dispose();
_custodialAmountController.dispose();
_custodialTokenAddressController.dispose();
_custodialContractAddressController.dispose();
_custodialFunctionController.dispose();
_custodialAbiController.dispose();
_custodialParamsController.dispose();
// MPC
_emailController.dispose();
_otpController.dispose();
_phoneController.dispose();
_countryCodeController.dispose();
_mpcToController.dispose();
_mpcAmountController.dispose();
_mpcTokenAddressController.dispose();
_mpcUserShardsController.dispose();
_mpcContractAddressController.dispose();
_mpcFunctionController.dispose();
_mpcAbiController.dispose();
_mpcParamsController.dispose();
super.dispose();
}
void _log(String message, {LogType type = LogType.info}) {
setState(() {
_logs.add(LogEntry(message, type: type));
});
if (_autoScroll) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_loggerController.hasClients) {
_loggerController.animateTo(
_loggerController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
}
// ============ LOADING HELPER ============
Future<T?> _runWithLoading<T>(
String message,
Future<T> Function() operation,
) async {
setState(() {
_isLoading = true;
_loadingMessage = message;
});
try {
return await operation();
} finally {
setState(() {
_isLoading = false;
_loadingMessage = '';
});
}
}
// ============ INITIALIZATION ============
Future<void> _initialize() async {
await _runWithLoading('Initializing SDK...', () async {
try {
_log('Initializing SDK...');
await TreSori().initialize(_apiKeyController.text);
setState(() {
_isInitialized = true;
if (chain.all.isNotEmpty) {
_selfCustodialChain = chain.all.first;
_custodialChain = chain.all.first;
_mpcChain = chain.all.first;
}
});
_log(
'Initialized with API Key: ${_apiKeyController.text}',
type: LogType.success,
);
_log(
'Loaded ${chain.all.length} chains: ${chain.all.map((c) => '${c.blockchain}-${c.network}').join(', ')}',
type: LogType.info,
);
} catch (e) {
_log('Error initializing: $e', type: LogType.error);
}
});
}
// ============ SELF CUSTODIAL METHODS ============
Future<void> _createSelfCustodialWallet() async {
if (_selfCustodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Creating wallet...', () async {
try {
final mnemonic = TreSori().generateMnemonic();
final result = await TreSori().createWallet(
chain: _selfCustodialChain!,
mnemonic: mnemonic,
userId: _userIdController.text,
);
setState(() {
_currentMnemonic = result['mnemonic'];
_currentPrivateKey = result['privateKey'];
_currentAddress = result['address'];
});
_log(
'Wallet Created:\nAddress: $_currentAddress\nPrivate Key: $_currentPrivateKey\nMnemonic: $_currentMnemonic',
type: LogType.success,
);
} catch (e) {
_log('Error creating wallet: $e', type: LogType.error);
}
});
}
void _importFromMnemonic() {
showDialog(
context: context,
builder: (context) {
final controller = TextEditingController();
return AlertDialog(
title: const Text('Import from Mnemonic'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'Enter 12-word mnemonic',
),
maxLines: 3,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_doImportMnemonic(controller.text);
},
child: const Text('Import'),
),
],
);
},
);
}
void _doImportMnemonic(String mnemonic) {
try {
final result = TreSori().importWalletFromMnemonic(mnemonic);
setState(() {
_currentMnemonic = mnemonic;
_currentPrivateKey = result['privateKey'];
_currentAddress = result['address'];
});
_log(
'Wallet Imported (Mnemonic):\nAddress: $_currentAddress',
type: LogType.success,
);
} catch (e) {
_log('Error importing wallet: $e', type: LogType.error);
}
}
void _importFromPrivateKey() {
showDialog(
context: context,
builder: (context) {
final controller = TextEditingController();
return AlertDialog(
title: const Text('Import from Private Key'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'Enter Private Key (hex)',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_doImportPrivateKey(controller.text);
},
child: const Text('Import'),
),
],
);
},
);
}
void _doImportPrivateKey(String privateKey) {
try {
final result = TreSori().importWalletFromPrivateKey(privateKey);
setState(() {
_currentMnemonic = null;
_currentPrivateKey = result['privateKey'];
_currentAddress = result['address'];
});
_log(
'Wallet Imported (Private Key):\nAddress: $_currentAddress',
type: LogType.success,
);
} catch (e) {
_log('Error importing wallet: $e', type: LogType.error);
}
}
Future<void> _transferSelfCustodialTokens() async {
if (_currentPrivateKey == null) {
_log('Error: No wallet loaded.', type: LogType.error);
return;
}
if (_selfCustodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Transferring tokens...', () async {
try {
final amount = EtherAmount.fromBigInt(
EtherUnit.wei,
BigInt.tryParse(_selfCustodialAmountController.text) ?? BigInt.zero,
);
_log('Transferring tokens...');
final result = await TreSori().transferTokens(
chain: _selfCustodialChain!,
privateKey: _currentPrivateKey!,
toAddress: _selfCustodialToController.text,
amount: amount,
rpcUrl: _selfCustodialRpcController.text,
);
_log('Transfer Successful. TX Hash: $result', type: LogType.success);
} catch (e) {
_log('Error transferring tokens: $e', type: LogType.error);
}
});
}
Future<void> _signSelfCustodialContract() async {
if (_currentPrivateKey == null) {
_log('Error: No wallet loaded.', type: LogType.error);
return;
}
if (_selfCustodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Signing contract...', () async {
try {
final abiString = _selfCustodialAbiController.text;
final abi = ContractAbi.fromJson(abiString, 'Contract');
_log('Signing Contract Transaction...');
final result = await TreSori().signContractTransaction(
chain: _selfCustodialChain!,
privateKey: _currentPrivateKey!,
rpcUrl: _selfCustodialRpcController.text,
transaction: Transaction.callContract(
contract: DeployedContract(
abi,
EthereumAddress.fromHex(
_selfCustodialContractAddressController.text,
),
),
function: abi.functions.firstWhere(
(f) => f.name == _selfCustodialFunctionController.text,
),
parameters: [
EthereumAddress.fromHex(
'0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9',
),
BigInt.from(1000000000000000000),
],
),
);
_log('TX Hash: $result', type: LogType.success);
} catch (e) {
_log('Error signing contract: $e', type: LogType.error);
}
});
}
Future<void> _getSelfCustodialBalance() async {
if (_currentAddress == null) {
_log('Error: No wallet address.', type: LogType.error);
return;
}
if (_selfCustodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Fetching balance...', () async {
try {
_log('Fetching balance...');
final result = await TreSori().getWalletBalance(
address: _currentAddress!,
chain: _selfCustodialChain!,
);
_log('Balance: ${result.toString()}', type: LogType.success);
_showBalanceDialog(
'Self Custodial Wallet Balance',
_currentAddress!,
_selfCustodialChain!,
result,
);
} catch (e) {
_log('Error fetching balance: $e', type: LogType.error);
}
});
}
// ============ CUSTODIAL METHODS ============
Future<void> _createCustodialWallet() async {
if (_custodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Creating custodial wallet...', () async {
try {
_log('Creating Custodial Wallet...');
final result = await TreSori().createCustodialWallet(
chain: _custodialChain!,
userId: _userIdController.text,
);
// Try multiple possible response structures
final address =
result['result']?['address'] ??
result['address'] ??
result['walletAddress'] ??
result['data']?['address'] ??
result['data']?['walletAddress'];
setState(() {
_custodialAddress = address?.toString();
});
_log('Custodial Wallet Created: $result', type: LogType.success);
if (_custodialAddress != null) {
_log('Address: $_custodialAddress', type: LogType.info);
}
} catch (e) {
_log('Error creating custodial wallet: $e', type: LogType.error);
}
});
}
Future<void> _transferCustodialTokens() async {
if (_custodialAddress == null) {
_log('Error: No custodial wallet loaded.', type: LogType.error);
return;
}
if (_custodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Transferring tokens...', () async {
try {
final amount = _custodialAmountController.text;
final tokenAddress = _custodialTokenAddressController.text.trim();
_log('Transferring Custodial Tokens...');
final result = await TreSori().transferCustodialTokens(
fromAddress: _custodialAddress!,
toAddress: _custodialToController.text,
amount: amount,
chain: _custodialChain!,
tokenAddress: tokenAddress,
);
_log('Transfer Successful: $result', type: LogType.success);
} catch (e) {
_log('Error transferring custodial tokens: $e', type: LogType.error);
}
});
}
Future<void> _signCustodialContract() async {
if (_custodialAddress == null) {
_log('Error: No custodial wallet loaded.', type: LogType.error);
return;
}
if (_custodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Signing contract...', () async {
try {
final abiString = _custodialAbiController.text;
final abiList = jsonDecode(abiString) as List;
final paramsString = _custodialParamsController.text;
// To ensure that string quotes in JSON are double quotes (")
// (single quotes are invalid in JSON), we replace all single quotes with double quotes before decoding
final params =
paramsString.isNotEmpty
? jsonDecode(paramsString.replaceAll("'", '"')) as List
: [];
_log('Signing Custodial Contract Transaction...');
final result = await TreSori().writeCustodialSmartContractTransaction(
contractAddress: _custodialContractAddressController.text,
functionName: _custodialFunctionController.text,
parameters: params,
contractAbi: abiList,
fromAddress: _custodialAddress!,
chain: _custodialChain!,
);
_log('TX: $result', type: LogType.success);
} catch (e) {
_log('Error signing custodial contract: $e', type: LogType.error);
}
});
}
Future<void> _getCustodialBalance() async {
if (_custodialAddress == null) {
_log('Error: No custodial wallet address.', type: LogType.error);
return;
}
if (_custodialChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Fetching balance...', () async {
try {
_log('Fetching custodial balance...');
final result = await TreSori().getWalletBalance(
address: _custodialAddress!,
chain: _custodialChain!,
);
_log('Balance: ${result.toString()}', type: LogType.success);
_showBalanceDialog(
'Custodial Wallet Balance',
_custodialAddress!,
_custodialChain!,
result,
);
} catch (e) {
_log('Error fetching custodial balance: $e', type: LogType.error);
}
});
}
// ============ MPC METHODS ============
Future<void> _sendEmailVerification() async {
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Sending verification...', () async {
try {
_log('Sending Email Verification...');
final result = await TreSori().sendEmailVerificationRequest(
chain: _mpcChain!,
email: _emailController.text,
userId: _userIdController.text,
isNewUser: _mpcIsNewUser,
);
_log('Email Request Sent: $result', type: LogType.success);
} catch (e) {
_log('Error sending email verification: $e', type: LogType.error);
}
});
}
Future<void> _verifyEmail() async {
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Verifying email...', () async {
try {
_log('Verifying Email OTP...');
final result = await TreSori().verifyEmail(
chain: _mpcChain!,
email: _emailController.text,
userId: _userIdController.text,
otp: _otpController.text,
isNewUser: _mpcIsNewUser,
);
// Extract MPC wallet details from response
// Handle both new user response (from create-wallet) and existing user response (from verify)
final resultData = result['result'] ?? result['data'] ?? result;
// For existing users, wallet info is nested in status
final statusData = resultData['status'];
final dataSource = statusData is Map ? statusData : resultData;
final address =
dataSource['walletAddress'] ??
dataSource['wallet_address'] ??
dataSource['address'] ??
resultData['address'] ??
result['walletAddress'];
final userShard =
dataSource['userShard'] ??
resultData['userShard'] ??
result['userShard'];
final balance =
dataSource['balance'] ?? resultData['balance'] ?? result['balance'];
final publicKey =
dataSource['publicKey'] ??
resultData['publicKey'] ??
result['publicKey'];
setState(() {
if (address != null) _mpcAddress = address.toString();
if (userShard != null) _mpcUserShard = userShard.toString();
if (balance != null) _mpcBalance = balance.toString();
if (publicKey != null) _mpcPublicKey = publicKey.toString();
});
if (_mpcAddress != null) {
_log('MPC Wallet Address: $_mpcAddress', type: LogType.info);
}
if (_mpcUserShard != null) {
_log('User Shard: $_mpcUserShard', type: LogType.info);
}
if (_mpcBalance != null) {
_log('Balance: $_mpcBalance', type: LogType.info);
}
if (_mpcPublicKey != null) {
_log('Public Key: $_mpcPublicKey', type: LogType.info);
}
_log('Email Verified: $result', type: LogType.success);
} catch (e) {
_log('Error verifying email: $e', type: LogType.error);
}
});
}
Future<void> _sendPhoneVerification() async {
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Sending verification...', () async {
try {
_log('Sending Phone Verification...');
final result = await TreSori().sendPhoneVerificationCode(
chain: _mpcChain!,
countryCode: _countryCodeController.text,
phone: _phoneController.text,
userId: _userIdController.text,
isNewUser: _mpcIsNewUser,
);
_log('Phone Request Sent: $result', type: LogType.success);
} catch (e) {
_log('Error sending phone verification: $e', type: LogType.error);
}
});
}
Future<void> _verifyPhone() async {
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Verifying phone...', () async {
try {
_log('Verifying Phone OTP...');
final result = await TreSori().verifyPhone(
chain: _mpcChain!,
countryCode: _countryCodeController.text,
phone: _phoneController.text,
userId: _userIdController.text,
otp: _otpController.text,
isNewUser: _mpcIsNewUser,
);
// Extract MPC wallet details from response
// Handle both new user response (from create-wallet) and existing user response (from verify)
final resultData = result['result'] ?? result['data'] ?? result;
// For existing users, wallet info is nested in status
final statusData = resultData['status'];
final dataSource = statusData is Map ? statusData : resultData;
final address =
dataSource['walletAddress'] ??
dataSource['wallet_address'] ??
dataSource['address'] ??
resultData['address'] ??
result['walletAddress'];
final userShard =
dataSource['userShard'] ??
resultData['userShard'] ??
result['userShard'];
final balance =
dataSource['balance'] ?? resultData['balance'] ?? result['balance'];
final publicKey =
dataSource['publicKey'] ??
resultData['publicKey'] ??
result['publicKey'];
setState(() {
if (address != null) _mpcAddress = address.toString();
if (userShard != null) _mpcUserShard = userShard.toString();
if (balance != null) _mpcBalance = balance.toString();
if (publicKey != null) _mpcPublicKey = publicKey.toString();
});
if (_mpcAddress != null) {
_log('MPC Wallet Address: $_mpcAddress', type: LogType.info);
}
if (_mpcUserShard != null) {
_log('User Shard: $_mpcUserShard', type: LogType.info);
}
if (_mpcBalance != null) {
_log('Balance: $_mpcBalance', type: LogType.info);
}
if (_mpcPublicKey != null) {
_log('Public Key: $_mpcPublicKey', type: LogType.info);
}
_log('Phone Verified: $result', type: LogType.success);
} catch (e) {
_log('Error verifying phone: $e', type: LogType.error);
}
});
}
Future<void> _getMpcBalance() async {
if (_mpcAddress == null) {
_log('Error: No MPC wallet address.', type: LogType.error);
return;
}
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
await _runWithLoading('Fetching balance...', () async {
try {
_log('Fetching MPC wallet balance...');
final result = await TreSori().getWalletBalance(
address: _mpcAddress!,
chain: _mpcChain!,
);
_log('Balance: ${result.toString()}', type: LogType.success);
_showBalanceDialog(
'MPC Wallet Balance',
_mpcAddress!,
_mpcChain!,
result,
);
} catch (e) {
_log('Error fetching MPC balance: $e', type: LogType.error);
}
});
}
Future<void> _transferMpcTokens() async {
if (_mpcAddress == null) {
_log('Error: No MPC wallet address.', type: LogType.error);
return;
}
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
if (_mpcUserShard == null && _mpcUserShardsController.text.isEmpty) {
_log('Error: No user shard available.', type: LogType.error);
return;
}
await _runWithLoading('Transferring MPC tokens...', () async {
try {
final userShard =
_mpcUserShardsController.text.isNotEmpty
? _mpcUserShardsController.text
: _mpcUserShard!;
final tokenAddress = _mpcTokenAddressController.text.trim();
_log('Transferring MPC Tokens...');
final result = await TreSori().transferMpcTokens(
fromAddress: _mpcAddress!,
toAddress: _mpcToController.text,
amount: _mpcAmountController.text,
chain: _mpcChain!,
userShards: userShard,
userIdentity: 'email',
tokenAddress: tokenAddress,
);
_log('Transfer Successful: $result', type: LogType.success);
} catch (e) {
_log('Error transferring MPC tokens: $e', type: LogType.error);
}
});
}
Future<void> _signMpcContract() async {
if (_mpcAddress == null) {
_log('Error: No MPC wallet address.', type: LogType.error);
return;
}
if (_mpcChain == null) {
_log('Error: No chain selected.', type: LogType.error);
return;
}
if (_mpcUserShard == null && _mpcUserShardsController.text.isEmpty) {
_log('Error: No user shard available.', type: LogType.error);
return;
}
await _runWithLoading('Signing MPC contract...', () async {
try {
final abiString = _mpcAbiController.text;
final abiList = jsonDecode(abiString) as List;
final paramsString = _mpcParamsController.text;
final params =
paramsString.isNotEmpty
? jsonDecode(paramsString.replaceAll("'", '"')) as List
: [];
final userShard =
_mpcUserShardsController.text.isNotEmpty
? _mpcUserShardsController.text
: _mpcUserShard!;
_log('Signing MPC Contract Transaction...');
final result = await TreSori().writeMpcSmartContractTransaction(
contractAddress: _mpcContractAddressController.text,
functionName: _mpcFunctionController.text,
parameters: params,
contractAbi: abiList,
fromAddress: _mpcAddress!,
chain: _mpcChain!,
userShards: userShard,
userIdentity: 'email',
);
_log('TX: $result', type: LogType.success);
} catch (e) {
_log('Error signing MPC contract: $e', type: LogType.error);
}
});
}
// ============ UI HELPERS ============
void _showBalanceDialog(
String title,
String address,
Chain selectedChain,
Map<String, dynamic> result,
) {
if (!mounted) return;
// Parse balance from various response structures
final balance =
result['result'] ?? result['balance'] ?? result['data']?['balance'];
final balanceStr = balance?.toString() ?? 'N/A';
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDialogLabel('Address:'),
SelectableText(
address,
style: const TextStyle(fontSize: 12, fontFamily: 'Courier'),
),
const SizedBox(height: 12),
_buildDialogLabel('Chain:'),
Text(
_getChainDisplayName(selectedChain),
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 12),
_buildDialogLabel('Balance:'),
Text(
balanceStr,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
Widget _buildDialogLabel(String text) {
return Text(
text,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildLoadingOverlay({required Widget child}) {
return Stack(
children: [
child,
if (_isLoading)
Container(
color: Colors.black.withOpacity(0.5),
child: Center(
child: Card(
margin: const EdgeInsets.all(32),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
_loadingMessage,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
],
);
}
Widget _buildInfoTile(
String label,
String? value, {
bool isSensitive = false,
}) {
if (value == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.grey.shade700,
),
),
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label copied to clipboard'),
duration: const Duration(seconds: 1),
),
);
},
child: const Padding(
padding: EdgeInsets.all(4.0),
child: Icon(Icons.copy, size: 16, color: Colors.blue),
),
),
],
),
const SizedBox(height: 4),
SelectableText(
value,
style: const TextStyle(fontSize: 13, fontFamily: 'Courier'),
),
],
),
);
}
Widget _buildChainDropdown({
required Chain? value,
required ValueChanged<Chain?> onChanged,
String label = 'Select Chain',
}) {
if (!_isInitialized) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Initialize SDK first to load chains',
style: TextStyle(color: Colors.grey),
),
);
}
return DropdownButtonFormField<Chain>(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
value: value,
isExpanded: true,
onChanged: onChanged,
items:
(chain.all as List<Chain>)
.map(
(c) => DropdownMenuItem(
value: c,
child: Text(_getChainDisplayName(c)),
),
)
.toList(),
);
}
String _getChainDisplayName(Chain c) => '${c.blockchain} - ${c.network}';
String _formatTime(DateTime dt) {
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
// ============ TAB CONTENT BUILDERS ============
Widget _buildSelfCustodialTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Chain Selection
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Chain Configuration',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_buildChainDropdown(
value: _selfCustodialChain,
onChanged:
(val) => setState(() => _selfCustodialChain = val),
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialRpcController,
decoration: const InputDecoration(
labelText: 'RPC URL',
border: OutlineInputBorder(),
hintText: 'https://rpc-amoy.polygon.technology',
),
),
],
),
),
),
const SizedBox(height: 16),
// Wallet Info
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Wallet Management',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
if (_currentAddress != null) ...[
_buildInfoTile('Address', _currentAddress),
_buildInfoTile(
'Private Key',
_currentPrivateKey,
isSensitive: true,
),
if (_currentMnemonic != null)
_buildInfoTile(
'Mnemonic',
_currentMnemonic,
isSensitive: true,
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized ? _getSelfCustodialBalance : null,
icon: const Icon(Icons.account_balance_wallet),
label: const Text('Get Balance'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
),
const SizedBox(height: 12),
],
Wrap(
spacing: 10,
runSpacing: 10,
children: [
ElevatedButton.icon(
onPressed:
_isInitialized ? _createSelfCustodialWallet : null,
icon: const Icon(Icons.add),
label: const Text('Create New'),
),
OutlinedButton.icon(
onPressed: _isInitialized ? _importFromMnemonic : null,
icon: const Icon(Icons.key),
label: const Text('Import Mnemonic'),
),
OutlinedButton.icon(
onPressed:
_isInitialized ? _importFromPrivateKey : null,
icon: const Icon(Icons.vpn_key),
label: const Text('Import PK'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Transfer Tokens
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Transfer Tokens',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialToController,
decoration: const InputDecoration(
labelText: 'To Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialAmountController,
decoration: const InputDecoration(
labelText: 'Amount (Wei)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _currentPrivateKey != null
? _transferSelfCustodialTokens
: null,
icon: const Icon(Icons.send),
label: const Text('Transfer'),
),
),
],
),
),
),
const SizedBox(height: 16),
// Contract Interaction
Card(
child: ExpansionTile(
title: const Text(
'Contract Interaction',
style: TextStyle(fontWeight: FontWeight.bold),
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _selfCustodialContractAddressController,
decoration: const InputDecoration(
labelText: 'Contract Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialFunctionController,
decoration: const InputDecoration(
labelText: 'Function Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialAbiController,
decoration: const InputDecoration(
labelText: 'ABI JSON',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 12),
TextField(
controller: _selfCustodialParamsController,
decoration: const InputDecoration(
labelText: 'Params JSON Array ([])',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _currentPrivateKey != null
? _signSelfCustodialContract
: null,
icon: const Icon(Icons.edit_document),
label: const Text('Sign Contract Transaction'),
),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildCustodialTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Chain Selection
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Chain Configuration',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_buildChainDropdown(
value: _custodialChain,
onChanged: (val) => setState(() => _custodialChain = val),
),
],
),
),
),
const SizedBox(height: 16),
// Wallet Management
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.wallet, color: Colors.blue),
const SizedBox(width: 8),
const Text(
'Wallet Management',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (_custodialAddress != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 14,
color: Colors.green.shade700,
),
const SizedBox(width: 4),
Text(
'Wallet Created',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
if (_custodialAddress != null) ...[
_buildInfoTile('Custodial Address', _custodialAddress),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isInitialized ? _getCustodialBalance : null,
icon: const Icon(Icons.account_balance_wallet),
label: const Text('Get Balance'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
),
const SizedBox(height: 12),
] else ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600),
const SizedBox(width: 12),
const Expanded(
child: Text(
'No custodial wallet created yet. Create one to see the address and balance.',
style: TextStyle(color: Colors.grey),
),
),
],
),
),
const SizedBox(height: 12),
],
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isInitialized ? _createCustodialWallet : null,
icon: const Icon(Icons.add),
label: Text(
_custodialAddress != null
? 'Create Another Wallet'
: 'Create Custodial Wallet',
),
),
),
],
),
),
),
const SizedBox(height: 16),
// Transfer Tokens
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Transfer Tokens',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _custodialToController,
decoration: const InputDecoration(
labelText: 'To Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _custodialAmountController,
decoration: const InputDecoration(
labelText: 'Amount',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 12),
TextField(
controller: _custodialTokenAddressController,
decoration: const InputDecoration(
labelText: 'Token Address (Optional)',
border: OutlineInputBorder(),
hintText: 'Leave empty for native token',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _custodialAddress != null
? _transferCustodialTokens
: null,
icon: const Icon(Icons.send),
label: const Text('Transfer'),
),
),
],
),
),
),
const SizedBox(height: 16),
// Contract Interaction
Card(
child: ExpansionTile(
title: const Text(
'Contract Interaction',
style: TextStyle(fontWeight: FontWeight.bold),
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _custodialContractAddressController,
decoration: const InputDecoration(
labelText: 'Contract Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _custodialFunctionController,
decoration: const InputDecoration(
labelText: 'Function Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _custodialAbiController,
decoration: const InputDecoration(
labelText: 'ABI JSON',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 12),
TextField(
controller: _custodialParamsController,
decoration: const InputDecoration(
labelText: 'Params JSON Array ([])',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _custodialAddress != null
? _signCustodialContract
: null,
icon: const Icon(Icons.edit_document),
label: const Text('Write Contract Transaction'),
),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildMpcTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Chain Selection
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Chain Configuration',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_buildChainDropdown(
value: _mpcChain,
onChanged: (val) => setState(() => _mpcChain = val),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Is New User', style: TextStyle(fontSize: 14)),
Switch(
value: _mpcIsNewUser,
onChanged: (val) => setState(() => _mpcIsNewUser = val),
activeColor: Colors.purple,
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Wallet Info
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.security, color: Colors.purple),
const SizedBox(width: 8),
const Text(
'MPC Wallet',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (_mpcAddress != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 14,
color: Colors.green.shade700,
),
const SizedBox(width: 4),
Text(
'Wallet Created',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
if (_mpcAddress != null) ...[
_buildInfoTile('MPC Address', _mpcAddress),
if (_mpcUserShard != null)
_buildInfoTile('User Shard', _mpcUserShard),
if (_mpcBalance != null)
_buildInfoTile('Balance', _mpcBalance),
if (_mpcPublicKey != null)
_buildInfoTile('Public Key', _mpcPublicKey),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isInitialized ? _getMpcBalance : null,
icon: const Icon(Icons.account_balance_wallet),
label: const Text('Get Balance'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
),
] else
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600),
const SizedBox(width: 12),
const Expanded(
child: Text(
'No MPC wallet created yet. Verify your email or phone below to create one.',
style: TextStyle(color: Colors.grey),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Email Verification
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Email Verification',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed:
_isInitialized ? _sendEmailVerification : null,
child: const Text('Send OTP'),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _otpController,
decoration: const InputDecoration(
labelText: 'OTP',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _isInitialized ? _verifyEmail : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Verify'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Phone Verification
// Card(
// child: Padding(
// padding: const EdgeInsets.all(16),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const Text(
// 'Phone Verification',
// style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
// ),
// const SizedBox(height: 12),
// Row(
// children: [
// SizedBox(
// width: 80,
// child: TextField(
// controller: _countryCodeController,
// decoration: const InputDecoration(
// labelText: 'Code',
// border: OutlineInputBorder(),
// ),
// ),
// ),
// const SizedBox(width: 8),
// Expanded(
// child: TextField(
// controller: _phoneController,
// decoration: const InputDecoration(
// labelText: 'Phone',
// border: OutlineInputBorder(),
// ),
// keyboardType: TextInputType.phone,
// ),
// ),
// const SizedBox(width: 8),
// ElevatedButton(
// onPressed:
// _isInitialized ? _sendPhoneVerification : null,
// child: const Text('Send OTP'),
// ),
// ],
// ),
// const SizedBox(height: 12),
// Row(
// children: [
// Expanded(
// child: TextField(
// controller: _otpController,
// decoration: const InputDecoration(
// labelText: 'OTP',
// border: OutlineInputBorder(),
// ),
// keyboardType: TextInputType.number,
// ),
// ),
// const SizedBox(width: 8),
// ElevatedButton(
// onPressed: _isInitialized ? _verifyPhone : null,
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.green,
// foregroundColor: Colors.white,
// ),
// child: const Text('Verify'),
// ),
// ],
// ),
// ],
// ),
// ),
// ),
// const SizedBox(height: 16),
// Transfer Tokens (MPC)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Transfer Tokens',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _mpcToController,
decoration: const InputDecoration(
labelText: 'To Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _mpcAmountController,
decoration: const InputDecoration(
labelText: 'Amount',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 12),
TextField(
controller: _mpcTokenAddressController,
decoration: const InputDecoration(
labelText: 'Token Address (Optional)',
border: OutlineInputBorder(),
hintText: 'Leave empty for native token',
),
),
const SizedBox(height: 12),
TextField(
controller: _mpcUserShardsController,
decoration: InputDecoration(
labelText: 'User Shards',
border: const OutlineInputBorder(),
hintText:
_mpcUserShard != null
? 'Auto-filled from wallet'
: null,
),
),
if (_mpcUserShard != null &&
_mpcUserShardsController.text.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Will use saved user shard if left empty',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _mpcAddress != null
? _transferMpcTokens
: null,
icon: const Icon(Icons.send),
label: const Text('Transfer'),
),
),
],
),
),
),
const SizedBox(height: 16),
// Smart Contract (MPC)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Smart Contract Interaction',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _mpcContractAddressController,
decoration: const InputDecoration(
labelText: 'Contract Address',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _mpcFunctionController,
decoration: const InputDecoration(
labelText: 'Function Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _mpcAbiController,
decoration: const InputDecoration(
labelText: 'Contract ABI (JSON)',
border: OutlineInputBorder(),
hintText: '[{"name":"...","type":"function",...}]',
),
maxLines: 3,
),
const SizedBox(height: 12),
TextField(
controller: _mpcParamsController,
decoration: const InputDecoration(
labelText: 'Parameters (JSON Array)',
border: OutlineInputBorder(),
hintText: '["param1", 123, true]',
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_isInitialized && _mpcAddress != null
? _signMpcContract
: null,
icon: const Icon(Icons.edit_document),
label: const Text('Write Contract'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
),
),
],
),
),
),
],
),
);
}
Widget _buildLogger() {
return Container(
height: 250,
decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
border: Border(top: BorderSide(color: Colors.grey.shade800)),
),
child: Column(
children: [
// Toolbar
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: const Color(0xFF2D2D2D),
child: Row(
children: [
const Icon(Icons.terminal, color: Colors.white70, size: 16),
const SizedBox(width: 8),
const Text(
'Logs',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Row(
children: [
Transform.scale(
scale: 0.8,
child: Checkbox(
value: _autoScroll,
onChanged: (v) => setState(() => _autoScroll = v!),
fillColor: WidgetStateProperty.all(Colors.blue),
visualDensity: VisualDensity.compact,
),
),
const Text(
'Auto-scroll',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
const SizedBox(width: 8),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(
Icons.copy_all,
color: Colors.white70,
size: 18,
),
tooltip: 'Copy All',
onPressed: () {
final text = _logs
.map((l) => '[${l.timestamp}] ${l.message}')
.join('\n');
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('All logs copied'),
duration: Duration(seconds: 1),
),
);
},
),
const SizedBox(width: 16),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(
Icons.delete_outline,
color: Colors.white70,
size: 18,
),
tooltip: 'Clear Logs',
onPressed: () => setState(() => _logs.clear()),
),
const SizedBox(width: 8),
],
),
),
// Log List
Expanded(
child: ListView.builder(
controller: _loggerController,
padding: const EdgeInsets.all(8),
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
Color color;
IconData icon;
switch (log.type) {
case LogType.error:
color = Colors.redAccent;
icon = Icons.error_outline;
break;
case LogType.success:
color = Colors.greenAccent;
icon = Icons.check_circle_outline;
break;
case LogType.warning:
color = Colors.orangeAccent;
icon = Icons.warning_amber_rounded;
break;
case LogType.info:
color = Colors.lightBlueAccent;
icon = Icons.info_outline;
break;
}
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Icon(icon, color: color, size: 14),
),
const SizedBox(width: 8),
Text(
_formatTime(log.timestamp),
style: TextStyle(
color: Colors.grey.shade500,
fontFamily: 'Courier',
fontSize: 12,
),
),
const SizedBox(width: 8),
Expanded(
child: SelectableText(
log.message,
style: TextStyle(
color: color,
fontFamily: 'Courier',
fontSize: 13,
),
),
),
const SizedBox(width: 4),
InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: log.message));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Log copied'),
duration: Duration(milliseconds: 500),
),
);
},
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Icon(
Icons.copy,
size: 12,
color: Colors.grey.shade700,
),
),
),
],
),
);
},
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return _buildLoadingOverlay(
child: Scaffold(
appBar: AppBar(
title: const Text('TreSori Demo'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Self Custodial'),
Tab(text: 'Custodial'),
Tab(text: 'MPC'),
],
),
),
actions: [
IconButton(
icon: Icon(_showLogs ? Icons.visibility_off : Icons.visibility),
tooltip: _showLogs ? 'Hide Logs' : 'Show Logs',
onPressed: () => setState(() => _showLogs = !_showLogs),
),
],
),
body: Column(
children: [
// Common Initialization Section
Container(
color: Colors.grey.shade50,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Icons.settings, size: 20),
const SizedBox(width: 8),
const Text(
'Initialization',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 16),
if (_isInitialized)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 14,
color: Colors.green.shade700,
),
const SizedBox(width: 4),
Text(
'SDK Initialized',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _apiKeyController,
decoration: const InputDecoration(
labelText: 'API Key',
border: OutlineInputBorder(),
isDense: true,
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _userIdController,
decoration: const InputDecoration(
labelText: 'User ID',
border: OutlineInputBorder(),
isDense: true,
),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _initialize,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
),
child: const Text('Initialize'),
),
],
),
],
),
),
const Divider(height: 1),
// Tab Content
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildSelfCustodialTab(),
_buildCustodialTab(),
_buildMpcTab(),
],
),
),
// Logger
if (_showLogs) _buildLogger(),
],
),
),
);
}
}