ceres_mpc 0.1.1 copy "ceres_mpc: ^0.1.1" to clipboard
ceres_mpc: ^0.1.1 copied to clipboard

Ceres MPC SDK — two-party ECDSA keygen, recovery, signing

example/lib/main.dart

/// ceres_mpc SDK usage examples.
///
/// Demonstrates keygen, recovery, sign, backup, and error handling flows.
/// Uses [MockMpcTransport] to run without a real server.
///
/// For production usage with a real server, see [http_transport_example.dart].
// ignore_for_file: implementation_imports, invalid_use_of_internal_member
library;

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:ceres_mpc/ceres_mpc.dart';
import 'package:ceres_mpc/src/bridge/mpc_engine.dart';
import 'package:ceres_mpc/src/rust/frb_generated.dart';

import 'http_transport_example.dart';
import 'mock_engine.dart';
import 'mock_transport.dart';
import 'ws_transport_example.dart';

/// Set to true to use mock engine (no Rust crypto, for UI testing).
/// Set to false to use real Rust engine (requires real server).
const _useMockEngine = true;

enum ExampleTransportMode {
  mock('Mock', 'Local demo transport with no backend required.'),
  http('HTTP', 'Classic JSON-RPC over HTTP. Requires a real MPC backend.'),
  websocket('WebSocket', 'Persistent JSON-RPC over WebSocket with reconnect support.');

  const ExampleTransportMode(this.label, this.description);

  final String label;
  final String description;
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!_useMockEngine) {
    await RustLib.init();
  }
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ceres_mpc Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const ExampleHomePage(),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  final _logs = <String>[];
  late MpcClient _client;
  late MpcEngine _engine;
  late MpcTransport _transport;
  final _httpUrlController = TextEditingController(text: 'http://127.0.0.1:3000/rpc');
  final _wsUrlController = TextEditingController(text: 'ws://127.0.0.1:3000/ws');
  ExampleTransportMode _transportMode = ExampleTransportMode.mock;

  // ── Stored state from keygen (used by sign/recovery/export) ──
  KeygenResult? _lastKeygen;
  String? _currentLocalShare; // updated after keygen or recovery
  String? _backupEnvelope; // created after keygen via deriveBackupEnvelope
  bool _exported = false;

  bool get _hasKey => _lastKeygen != null && _currentLocalShare != null;

  @override
  void initState() {
    super.initState();
    _rebuildClient(logChange: false);
  }

  @override
  void dispose() {
    _httpUrlController.dispose();
    _wsUrlController.dispose();
    unawaited(_disposeTransport(_transport));
    super.dispose();
  }

  void _log(String message) {
    debugPrint(message);
    setState(() => _logs.add(message));
  }

  void _clearLogs() {
    setState(() => _logs.clear());
  }

  Future<void> _switchTransport(ExampleTransportMode mode) async {
    if (_transportMode == mode) return;
    await _disposeTransport(_transport);
    setState(() {
      _transportMode = mode;
      _lastKeygen = null;
      _currentLocalShare = null;
      _backupEnvelope = null;
      _exported = false;
    });
    _rebuildClient();
  }

  void _rebuildClient({bool logChange = true}) {
    _transport = _createTransport();
    _engine = _useMockEngine ? MockMpcEngine() : MpcEngine(RustLib.instance.api);
    _client = MpcClient(engine: _engine, transport: _transport);
    if (logChange) {
      _log('Transport switched to ${_transportMode.label}.');
      _log(_transportMode.description);
      if (_transportMode == ExampleTransportMode.mock) {
        _log('Mock mode stays fully runnable without a backend.');
      } else {
        _log('Update the endpoint below and connect to a real MPC server.');
      }
    }
  }

  MpcTransport _createTransport() {
    return switch (_transportMode) {
      ExampleTransportMode.mock => MockMpcTransport(),
      ExampleTransportMode.http => HttpMpcTransport(rpcUrl: _httpUrlController.text.trim()),
      ExampleTransportMode.websocket => WebSocketMpcTransport(wsUrl: _wsUrlController.text.trim()),
    };
  }

  Future<void> _disposeTransport(MpcTransport transport) async {
    if (transport is WebSocketMpcTransport) {
      await transport.close();
    }
  }

  Widget _buildTransportCard() {
    final endpointController = switch (_transportMode) {
      ExampleTransportMode.http => _httpUrlController,
      ExampleTransportMode.websocket => _wsUrlController,
      ExampleTransportMode.mock => null,
    };

    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Transport Mode', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 12),
            SegmentedButton<ExampleTransportMode>(
              segments: ExampleTransportMode.values.map((mode) => ButtonSegment<ExampleTransportMode>(value: mode, label: Text(mode.label))).toList(),
              selected: {_transportMode},
              onSelectionChanged: (selection) {
                unawaited(_switchTransport(selection.first));
              },
            ),
            const SizedBox(height: 12),
            Text(_transportMode.description),
            if (endpointController != null) ...[
              const SizedBox(height: 12),
              TextField(
                controller: endpointController,
                decoration: InputDecoration(labelText: _transportMode == ExampleTransportMode.http ? 'HTTP RPC URL' : 'WebSocket URL', border: const OutlineInputBorder()),
                onSubmitted: (_) => _rebuildClient(),
              ),
              const SizedBox(height: 8),
              Text('Tip: switching transport only changes the injected constructor argument.', style: Theme.of(context).textTheme.bodySmall),
            ] else ...[
              const SizedBox(height: 8),
              Text('Mock mode uses in-memory server behavior so the example remains runnable offline.', style: Theme.of(context).textTheme.bodySmall),
            ],
            const SizedBox(height: 12),
            SelectableText(_transportSnippet(), style: Theme.of(context).textTheme.bodySmall?.copyWith(fontFamily: 'monospace')),
          ],
        ),
      ),
    );
  }

  String _transportSnippet() {
    return switch (_transportMode) {
      ExampleTransportMode.mock =>
        '''
final client = MpcClient(
  engine: MockMpcEngine(),
  transport: MockMpcTransport(),
);''',
      ExampleTransportMode.http =>
        '''
final client = MpcClient(
  engine: MpcEngine(RustLib.instance.api),
  transport: HttpMpcTransport(
    rpcUrl: '${_httpUrlController.text.trim()}',
  ),
);''',
      ExampleTransportMode.websocket =>
        '''
final client = MpcClient(
  engine: MpcEngine(RustLib.instance.api),
  transport: WebSocketMpcTransport(
    wsUrl: '${_wsUrlController.text.trim()}',
  ),
);''',
    };
  }

  // ── Example 1: Keygen ───────────────────────────────────────────

  /// Full keygen flow. After completion you get:
  /// - address: EVM address derived from group public key
  /// - publicKey: hex-encoded uncompressed secp256k1 public key
  /// - localEncryptedShare: device key share (store in secure storage!)
  Future<void> _runKeygen() async {
    _clearLogs();
    _log('=== Keygen Example ===');
    _log('Starting keygen...');

    try {
      final result = await _client.keygen();

      setState(() {
        _lastKeygen = result;
        _currentLocalShare = result.localEncryptedShare;
        _exported = false;
      });

      _log('Keygen successful!');
      _log('  address: ${result.address}');
      _log('  publicKey: ${result.publicKey.substring(0, 20)}...');
      _log('  mpcKeyId: ${result.mpcKeyId}');
      _log('  rotationVersion: ${result.rotationVersion}');
      _log('  localEncryptedShare: ${result.localEncryptedShare.length} chars');
      _log('');

      // Auto-create backup envelope (in real app, prompt user for secret)
      _log('Creating backup envelope...');
      final envelope = await _engine.deriveBackupEnvelope(result.localEncryptedShare, 'demo_backup_secret_123', DateTime.now().toUtc().toIso8601String());
      final envelopeJson = '{"version":"${envelope.version}","algorithm":"${envelope.algorithm}","created_at":"${envelope.createdAt}","payload":"${envelope.payload}"}';
      setState(() => _backupEnvelope = envelopeJson);
      _log('Backup envelope created (${envelopeJson.length} chars)');
      _log('');
      _log('Ready: Sign / Recovery / Export buttons are now active.');
    } on MpcProtocolException catch (e) {
      _log('Protocol error: ${e.message} (round: ${e.round})');
    } on MpcTransportException catch (e) {
      _log('Transport error: ${e.message} (method: ${e.method})');
    } catch (e) {
      _log('Unexpected error: $e');
    }
  }

  // ── Example 2: Recovery ─────────────────────────────────────────

  /// Recovery flow. Requires:
  /// - mpcKeyId: from the original keygen
  /// - encryptedBackupShare: the backup envelope stored by user
  /// - userBackupSecret: user's backup password/secret
  ///
  /// After recovery:
  /// - New localEncryptedShare (rotated, old one invalidated)
  /// - Same address as before
  /// - rotationVersion incremented
  Future<void> _runRecovery() async {
    _clearLogs();
    _log('=== Recovery Example ===');

    if (_lastKeygen == null || _backupEnvelope == null) {
      _log('Run keygen first (creates key + backup envelope).');
      return;
    }

    _log('Starting recovery for ${_lastKeygen!.mpcKeyId}...');
    _log('  Using backup envelope from keygen step');

    try {
      final result = await _client.recover(mpcKeyId: _lastKeygen!.mpcKeyId, encryptedBackupShare: _backupEnvelope!, userBackupSecret: 'demo_backup_secret_123', currentRotationVersion: _lastKeygen!.rotationVersion);

      // Update local share (old one is now invalid after rotation)
      setState(() => _currentLocalShare = result.localEncryptedShare);

      _log('Recovery successful!');
      _log('  address: ${result.address}');
      _log('  rotationVersion: ${result.rotationVersion}');
      _log('  mpcKeyId: ${result.mpcKeyId}');
      _log('  localEncryptedShare updated (old one invalidated)');
      _log('');
      _log('Address unchanged after recovery ✓');
    } on MpcProtocolException catch (e) {
      _log('Protocol error: ${e.message}');
    } on MpcTransportException catch (e) {
      _log('Transport error: ${e.message}');
    } catch (e) {
      _log('Unexpected error: $e');
    }
  }

  // ── Example 3: Sign ─────────────────────────────────────────────

  /// Sign flow. Requires:
  /// - mpcKeyId: identifies which key pair to use
  /// - messageHash: keccak256 hash of the unsigned transaction
  /// - localEncryptedShare: device key share from keygen/recovery
  ///
  /// Returns (r, s, recid) to assemble an ECDSA signature.
  Future<void> _runSign() async {
    _clearLogs();
    _log('=== Sign Example ===');

    if (_lastKeygen == null || _currentLocalShare == null) {
      _log('Run keygen first.');
      return;
    }
    if (_exported) {
      _log('Key already exported — signing disabled.');
      return;
    }

    final msgHash = 'aabbccdd' * 8; // 64-char hex hash (demo)
    _log('Signing transaction...');
    _log('  messageHash: ${msgHash.substring(0, 16)}...');
    _log('  using localShare from ${_currentLocalShare == _lastKeygen!.localEncryptedShare ? "keygen" : "recovery"}');

    try {
      final result = await _client.sign(mpcKeyId: _lastKeygen!.mpcKeyId, messageHash: msgHash, localEncryptedShare: _currentLocalShare!);

      _log('Signing successful!');
      _log('  r: ${result.r.substring(0, 20)}...');
      _log('  s: ${result.s.substring(0, 20)}...');
      _log('  recid: ${result.recid}');
      _log('');
      _log('Next steps:');
      _log('  1. Assemble signed transaction with (r, s, recid)');
      _log('  2. Broadcast to EVM chain');
    } on MpcProtocolException catch (e) {
      _log('Protocol error: ${e.message}');
    } on MpcTransportException catch (e) {
      _log('Transport error: ${e.message}');
    } catch (e) {
      _log('Unexpected error: $e');
    }
  }

  // ── Example 4: Export Private Key ────────────────────────────────

  /// Export MPC wallet to a standard wallet.
  /// This reconstructs the full private key from both party shares.
  ///
  /// WARNING: After export, the MPC key pair should be considered compromised.
  /// The server marks the key as "exported" and disables further MPC operations.
  Future<void> _runExport() async {
    _clearLogs();
    _log('=== Export Private Key Example ===');

    if (_lastKeygen == null || _currentLocalShare == null) {
      _log('Run keygen first.');
      return;
    }
    if (_exported) {
      _log('Key already exported.');
      return;
    }

    _log('Requesting key export for ${_lastKeygen!.mpcKeyId}...');
    _log('WARNING: This will compromise the MPC key pair!');
    _log('');

    try {
      final result = await _client.exportPrivateKey(mpcKeyId: _lastKeygen!.mpcKeyId, localEncryptedShare: _currentLocalShare!);

      setState(() => _exported = true);

      _log('Export successful!');
      _log('  address: ${result.address}');
      _log('  privateKey: ${result.privateKey.substring(0, 10)}...[REDACTED]');
      _log('');
      _log('MPC key pair is now compromised.');
      _log('Sign / Recovery buttons disabled.');
    } on MpcProtocolException catch (e) {
      _log('Protocol error: ${e.message}');
    } on MpcTransportException catch (e) {
      _log('Transport error: ${e.message}');
    } catch (e) {
      _log('Unexpected error: $e');
    }
  }

  // ── Example 5: Error Handling ───────────────────────────────────

  /// Demonstrates how to handle different error types.
  Future<void> _runErrorDemo() async {
    _clearLogs();
    _log('=== Error Handling Example ===');
    _log('');

    // Example: catching specific exceptions
    _log('MpcProtocolException:');
    _log('  Thrown when Rust-side protocol returns error status.');
    _log('  Contains: message, round (which round failed).');
    _log('  Example: invalid server proof, verification failed.');
    _log('');
    _log('MpcTransportException:');
    _log('  Thrown when network communication fails.');
    _log('  Contains: message, endpoint, cause (original error).');
    _log('  Example: timeout, connection refused, 500 error.');
    _log('');

    _log('Pattern:');
    _log('  try {');
    _log('    await client.keygen();');
    _log('  } on MpcProtocolException catch (e) {');
    _log('    // Crypto error: show user, maybe retry');
    _log('    log("Protocol failed at round \${e.round}");');
    _log('  } on MpcTransportException catch (e) {');
    _log('    // Network error: retry with backoff');
    _log('    log("Network failed: \${e.method}");');
    _log('  }');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ceres_mpc Example')),
      body: Column(
        children: [
          _buildTransportCard(),
          // Action buttons
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                ElevatedButton(onPressed: _runKeygen, child: const Text('1. Keygen')),
                ElevatedButton(onPressed: _hasKey && !_exported ? _runSign : null, child: const Text('2. Sign')),
                ElevatedButton(onPressed: _hasKey && !_exported && _backupEnvelope != null ? _runRecovery : null, child: const Text('3. Recovery')),
                ElevatedButton(
                  onPressed: _hasKey && !_exported ? _runExport : null,
                  style: ElevatedButton.styleFrom(backgroundColor: _hasKey && !_exported ? Colors.orange : null, foregroundColor: _hasKey && !_exported ? Colors.white : null),
                  child: const Text('4. Export'),
                ),
                OutlinedButton(onPressed: _runErrorDemo, child: const Text('Error Handling')),
              ],
            ),
          ),
          const Divider(),
          // Log output
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: _logs.length,
              itemBuilder: (context, index) {
                return Text(_logs[index], style: const TextStyle(fontFamily: 'monospace', fontSize: 13));
              },
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
79
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Ceres MPC SDK — two-party ECDSA keygen, recovery, signing

Repository (GitHub)
View/report issues

Topics

#mpc #crypto #wallet #ecdsa

License

MIT (license)

Dependencies

collection, flutter, flutter_rust_bridge

More

Packages that depend on ceres_mpc

Packages that implement ceres_mpc