Para provides Cosmos blockchain signing capabilities in Flutter applications through a secure bridge-based architecture. The integration supports message signing and transaction signing for major Cosmos networks including Cosmos Hub, Osmosis, Juno, and Stargaze.

Signing-Only Integration: This integration focuses on cryptographic signing operations. Transaction broadcasting and on-chain interactions require additional implementation using Cosmos RPC endpoints.

Prerequisites

Architecture

Para’s Cosmos integration uses a bridge-based architecture for enhanced security:

  • Flutter App β†’ Para Bridge β†’ CosmJS Signers β†’ Cosmos Networks
  • All signing operations are performed through a secure web bridge
  • Compatible with CosmJS v0.33.1 for maximum ecosystem compatibility
  • Supports both Proto (Direct) and Amino signing methods

Dependencies

The Para Flutter SDK includes built-in Cosmos support. No additional dependencies are required beyond the standard Para SDK.

dependencies:
  para: ^1.4.0-dev.1  # Or latest version

Supported Networks

Para currently supports 6 Cosmos networks with pre-configured settings:

NetworkChain IDPrefixDefault Denom
Cosmos Hubcosmoshub-4cosmosuatom
Osmosisosmosis-1osmouosmo
Junojuno-1junoujuno
Stargazestargaze-1starsustars
Provider Testnetprovidercosmosuatom
Osmosis Testnetosmo-test-5osmouosmo

Basic Usage

Initialization

import 'package:para/para.dart';

// Initialize the Cosmos signer with required parameters
final cosmosSigner = ParaCosmosSigner(
  para: para, // Your Para instance (required)
  chainId: 'cosmoshub-4', // Chain ID (default: cosmoshub-4)  
  rpcUrl: 'https://cosmos-rpc.publicnode.com', // RPC endpoint (optional - required for getBalance())
  prefix: 'cosmos', // Address prefix (default: cosmos)
);

// Select a wallet for signing operations (required before any operations)
await cosmosSigner.selectWallet(walletId);

Wallet Operations

try {
  // Get the Cosmos address (bech32 format)
  final address = await cosmosSigner.getAddress();
  print('Cosmos address: $address'); // cosmos1...

  // Check balance (queries via RPC)
  final balance = await cosmosSigner.getBalance();
  print('Balance: $balance uatom');

  // Specify custom denom for multi-token chains
  final osmoBalance = await cosmosSigner.getBalance(denom: 'uosmo');
  print('OSMO Balance: $osmoBalance uosmo');
} on CosmosSignerException catch (e) {
  print('Operation failed: ${e.message}');
}

## Message Signing

### Sign Arbitrary Messages

```dart
import 'dart:convert';

try {
  final message = "Hello Cosmos from Para!";
  
  // Sign the message (automatically hashed with SHA256)
  final signature = await cosmosSigner.signMessage(message);
  
  print('Message: $message');
  // The signature is returned as Uint8List - encode to base64 for display/storage
  print('Signature: ${base64Encode(signature)}');
  
  // The raw signature bytes can be used directly for verification
} on CosmosSignerException catch (e) {
  print('Signing failed: ${e.message}');
  // Handle specific error types (see Error Handling section)
}

Transaction Signing

Para supports both Proto (Direct) and Amino signing methods for Cosmos transactions. Both methods require a properly formatted SignDoc encoded as base64.

What is a SignDoc? A SignDoc is a structured representation of a transaction that needs to be signed. It contains the transaction body, auth info, chain ID, and account details. You’ll need to use a Cosmos SDK library to properly construct SignDocs. Learn more in the Cosmos SDK documentation.

Transaction Signing Only: Para provides cryptographic signatures for transactions. You’ll need to use a Cosmos SDK library or RPC client to construct the SignDoc and broadcast the signed transaction.

Proto/Direct Signing

import 'dart:convert';

try {
  // You need to construct a proper SignDoc using Cosmos SDK
  // This example shows the expected structure
  final signDoc = {
    'bodyBytes': base64Encode(utf8.encode('your_transaction_body_protobuf')),
    'authInfoBytes': base64Encode(utf8.encode('your_auth_info_protobuf')),
    'chainId': 'cosmoshub-4',
    'accountNumber': '12345',
  };
  
  // Convert the entire SignDoc to base64
  final signDocBase64 = base64Encode(utf8.encode(jsonEncode(signDoc)));
  
  final result = await cosmosSigner.signDirect(
    signDocBase64: signDocBase64,
  );
  
  print('Proto signing successful:');
  print('Signature: ${result['signature']}');
  // Note: The result is a Map<String, dynamic> containing the signature data
  
  // Use the signature to construct and broadcast your transaction
} on CosmosSignerException catch (e) {
  print('Proto signing failed: ${e.message}');
}

Amino Signing

import 'dart:convert';

try {
  // Create standard Amino sign doc structure
  final aminoSignDoc = {
    'chain_id': 'cosmoshub-4',
    'account_number': '12345',
    'sequence': '0',
    'fee': {
      'amount': [{'denom': 'uatom', 'amount': '5000'}],
      'gas': '200000'
    },
    'msgs': [
      {
        'type': 'cosmos-sdk/MsgSend',
        'value': {
          'from_address': 'cosmos1sender...',
          'to_address': 'cosmos1recipient...',
          'amount': [{'denom': 'uatom', 'amount': '1000000'}]
        }
      }
    ],
    'memo': 'Sent via Para'
  };
  
  // Convert to base64 for signing
  final signDocBase64 = base64Encode(utf8.encode(jsonEncode(aminoSignDoc)));
  
  final result = await cosmosSigner.signAmino(
    signDocBase64: signDocBase64,
  );
  
  print('Amino signing successful:');
  print('Signature: ${result['signature']}');
  // Note: The result is a Map<String, dynamic> containing the signature data
  
  // Use the signature to construct and broadcast your transaction
} on CosmosSignerException catch (e) {
  print('Amino signing failed: ${e.message}');
}

Network Configuration

Para supports the following Cosmos networks with pre-configured settings. You can initialize the signer for any supported network:

final cosmosSigner = ParaCosmosSigner(
  para: para,
  chainId: 'cosmoshub-4',
  prefix: 'cosmos',
  rpcUrl: 'https://cosmos-rpc.publicnode.com', // Optional
);

Limited Network Support: Para currently supports only the 6 networks listed above. Custom chains cannot be added through the SDK - support for additional Cosmos chains requires platform-level configuration by the Para team. Contact support if you need a specific chain added.

Error Handling

Para provides comprehensive error handling with 11 specific error types to help you handle different failure scenarios:

try {
  await cosmosSigner.selectWallet(walletId);
  final result = await cosmosSigner.signDirect(signDocBase64: signDoc);
} on CosmosSignerException catch (e) {
  switch (e.type) {
    case CosmosSignerErrorType.missingWalletId:
      print('No wallet selected - call selectWallet() first');
      break;
    case CosmosSignerErrorType.invalidWalletType:
      print('Selected wallet is not a Cosmos wallet');
      break;
    case CosmosSignerErrorType.invalidWalletAddress:
      print('Wallet address is invalid or unavailable');
      break;
    case CosmosSignerErrorType.bridgeError:
      print('Bridge communication error: ${e.message}');
      break;
    case CosmosSignerErrorType.signingFailed:
      print('Generic signing operation failed: ${e.message}');
      break;
    case CosmosSignerErrorType.protoSigningFailed:
      print('Proto/Direct signing failed: ${e.message}');
      break;
    case CosmosSignerErrorType.aminoSigningFailed:
      print('Amino signing failed: ${e.message}');
      break;
    case CosmosSignerErrorType.networkError:
      print('Network/RPC error: ${e.message}');
      break;
    case CosmosSignerErrorType.invalidSignDoc:
      print('SignDoc format is invalid: ${e.message}');
      break;
    case CosmosSignerErrorType.invalidTransaction:
      print('Transaction format is invalid: ${e.message}');
      break;
    default:
      print('Unknown error: ${e.message}');
  }
  
  // Access the underlying error for debugging
  if (e.underlyingError != null) {
    print('Underlying error: ${e.underlyingError}');
  }
}

Common Error Scenarios

Complete Example

Here’s a practical example showing Cosmos integration with proper error handling:

import 'package:flutter/material.dart';
import 'package:para/para.dart';
import 'dart:convert';

class CosmosWalletView extends StatefulWidget {
  final Para para;
  final String walletId;

  const CosmosWalletView({
    Key? key,
    required this.para,
    required this.walletId,
  }) : super(key: key);

  @override
  _CosmosWalletViewState createState() => _CosmosWalletViewState();
}

class _CosmosWalletViewState extends State<CosmosWalletView> {
  late ParaCosmosSigner signer;
  String? address;
  String? balance;
  String message = 'Hello Cosmos from Para!';
  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    _initializeSigner();
  }

  Future<void> _initializeSigner() async {
    // Initialize signer for Cosmos Hub
    signer = ParaCosmosSigner(
      para: widget.para,
      chainId: 'cosmoshub-4',
      prefix: 'cosmos',
      rpcUrl: 'https://cosmos-rpc.publicnode.com',
    );

    await _loadWalletInfo();
  }

  Future<void> _loadWalletInfo() async {
    setState(() => isLoading = true);
    
    try {
      // Select the wallet first
      await signer.selectWallet(widget.walletId);
      
      // Get address and balance
      final addr = await signer.getAddress();
      final bal = await signer.getBalance();
      
      setState(() {
        address = addr;
        balance = bal;
      });
    } on CosmosSignerException catch (e) {
      _showError('Wallet Setup Failed', 'Error: ${e.type.name}\n${e.message}');
    } catch (e) {
      _showError('Unexpected Error', e.toString());
    } finally {
      setState(() => isLoading = false);
    }
  }

  Future<void> _signMessage() async {
    if (message.isEmpty) {
      _showError('Error', 'Please enter a message to sign');
      return;
    }

    setState(() => isLoading = true);
    
    try {
      final signature = await signer.signMessage(message);
      final signatureB64 = base64Encode(signature);
      
      _showSuccess(
        'Message Signed Successfully',
        'Message: $message\n\nSignature: ${signatureB64.substring(0, 32)}...'
      );
    } on CosmosSignerException catch (e) {
      _showError('Signing Failed', 'Error: ${e.type.name}\n${e.message}');
    } finally {
      setState(() => isLoading = false);
    }
  }

  Future<void> _signTestTransaction() async {
    setState(() => isLoading = true);
    
    try {
      // Create a sample Amino sign doc for demonstration
      // In production, you'd construct this from actual transaction data
      final aminoSignDoc = {
        'chain_id': 'cosmoshub-4',
        'account_number': '123456',
        'sequence': '0',
        'fee': {
          'amount': [{'denom': 'uatom', 'amount': '5000'}],
          'gas': '200000'
        },
        'msgs': [
          {
            'type': 'cosmos-sdk/MsgSend',
            'value': {
              'from_address': address ?? '',
              'to_address': 'cosmos1demo...', // Demo address
              'amount': [{'denom': 'uatom', 'amount': '1000000'}]
            }
          }
        ],
        'memo': 'Demo transaction via Para'
      };
      
      final signDocBase64 = base64Encode(utf8.encode(jsonEncode(aminoSignDoc)));
      final result = await signer.signAmino(signDocBase64: signDocBase64);
      
      _showSuccess(
        'Transaction Signed',
        'Chain: cosmoshub-4\nAmount: 1 ATOM\nSignature: ${result['signature']?.substring(0, 32)}...\n\nNote: This is a demo signature only - transaction not broadcast'
      );
    } on CosmosSignerException catch (e) {
      _showError('Transaction Signing Failed', 'Error: ${e.type.name}\n${e.message}');
    } finally {
      setState(() => isLoading = false);
    }
  }

  void _showError(String title, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showSuccess(String title, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: SingleChildScrollView(child: Text(message)),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Para Cosmos Demo'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _loadWalletInfo,
          ),
        ],
      ),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Card(
                    child: Padding(
                      padding: EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('Cosmos Wallet', 
                               style: Theme.of(context).textTheme.titleLarge),
                          Divider(),
                          Text('Network: Cosmos Hub (cosmoshub-4)'),
                          SizedBox(height: 8),
                          Text('Address: ${address ?? 'Loading...'}',
                               style: TextStyle(fontFamily: 'monospace')),
                          SizedBox(height: 8),
                          Text('Balance: ${balance ?? 'Loading...'} Β΅ATOM'),
                        ],
                      ),
                    ),
                  ),
                  SizedBox(height: 16),
                  Card(
                    child: Padding(
                      padding: EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('Message Signing',
                               style: Theme.of(context).textTheme.titleMedium),
                          SizedBox(height: 8),
                          TextField(
                            decoration: InputDecoration(
                              labelText: 'Message to sign',
                              border: OutlineInputBorder(),
                            ),
                            controller: TextEditingController(text: message),
                            onChanged: (value) => message = value,
                          ),
                          SizedBox(height: 8),
                          ElevatedButton(
                            onPressed: _signMessage,
                            child: Text('Sign Message'),
                          ),
                        ],
                      ),
                    ),
                  ),
                  SizedBox(height: 16),
                  Card(
                    child: Padding(
                      padding: EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('Transaction Signing',
                               style: Theme.of(context).textTheme.titleMedium),
                          SizedBox(height: 8),
                          Text('Demo: Sign a 1 ATOM transfer transaction'),
                          SizedBox(height: 8),
                          ElevatedButton(
                            onPressed: _signTestTransaction,
                            child: Text('Sign Demo Transaction'),
                          ),
                          SizedBox(height: 8),
                          Text('Note: This only demonstrates signing. To broadcast transactions, use a Cosmos RPC client.',
                               style: Theme.of(context).textTheme.bodySmall),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
    );
  }
}

Limitations

Important Limitations: Understanding these constraints is crucial for successful integration.

Best Practices

Testing

Para provides E2E tests for Cosmos functionality in the examples repository:

# Navigate to the Flutter example directory
cd examples-hub/mobile/with-flutter

# Test 10: Cosmos wallet operations (address generation, balance queries)
dart run test_e2e/tool/run_single_test.dart 10

# Test 11: Cosmos message signing (signMessage functionality)
dart run test_e2e/tool/run_single_test.dart 11

# Run all E2E tests including Cosmos, EVM, and Solana tests
dart run test_e2e/tool/run_tests.dart

Manual Testing Checklist

Test the following scenarios to ensure proper integration:

Next Steps