solana_kit_mobile_wallet_adapter

pub package docs CI coverage

Flutter plugin for the Solana Mobile Wallet Adapter (MWA) protocol.

This package provides both:

  • dApp-side client flows (launch wallet, establish session, request signatures)
  • wallet-side server flows (receive authorize/sign requests from dApps)

Platform support

Platform dApp (client) Wallet (server)
Android Supported Supported
iOS No-op No-op
Web N/A N/A

Use isMwaSupported() / assertMwaSupported() before invoking MWA APIs.

What this package includes

dApp-side APIs

  • transact() for simple one-call session lifecycle
  • LocalAssociationScenario for explicit same-device control
  • startRemoteScenario() for reflector-based cross-device sessions
  • KitMobileWallet typed wrapper over the protocol wallet interface

wallet-side APIs

  • WalletScenario lifecycle and request routing
  • WalletScenarioCallbacks for authorize/sign/deauthorize handling
  • Typed request objects (AuthorizeDappRequest, SignTransactionsRequest, etc.)
  • MwaDigitalAssetLinksHostApi for Android package verification

Installation

flutter pub add solana_kit_mobile_wallet_adapter

Inside this monorepo, workspace dependency resolution is automatic.

dApp usage

Simple lifecycle (transact)

import 'package:solana_kit_mobile_wallet_adapter/solana_kit_mobile_wallet_adapter.dart';

final auth = await transact((wallet) async {
  final authorizeResult = await wallet.authorize(
    identity: const AppIdentity(name: 'My dApp'),
    chain: 'solana:mainnet',
  );

  await wallet.signTransactions(
    payloads: [base64EncodedTransaction],
  );

  return authorizeResult;
});

Manual local association

final scenario = LocalAssociationScenario();

try {
  final rawWallet = await scenario.start();
  final wallet = wrapWithKitApi(rawWallet);

  final auth = await wallet.authorize(
    identity: const AppIdentity(name: 'My dApp'),
    chain: 'solana:devnet',
  );

  final signed = await wallet.signTransactions(
    payloads: ['base64tx1', 'base64tx2'],
  );
} finally {
  await scenario.close();
}

Remote association (cross-device)

startRemoteScenario resolves once a real reflector ID has been negotiated and a valid association URI is available.

final remote = await startRemoteScenario(
  const RemoteWalletAssociationConfig(
    reflectorHost: 'reflector.example.com',
  ),
);

// Display this as QR for wallet scan.
final uriForQr = remote.associationUri;

try {
  final wallet = await remote.wallet;
  await wallet.getCapabilities();
} finally {
  remote.close();
}

wallet usage

Start a wallet scenario

class MyWalletCallbacks implements WalletScenarioCallbacks {
  @override
  void onAuthorizeRequest(AuthorizeDappRequest request) {
    request.completeWithAuthorize(
      accounts: [
        AuthorizedAccount(
          address: base64PublicKey,
          label: 'Main Account',
        ),
      ],
      authToken: 'issued-token',
    );
  }

  @override
  void onSignTransactionsRequest(SignTransactionsRequest request) {
    final signed = signPayloads(request.payloads);
    request.completeWithSignedPayloads(signed);
  }

  @override
  void onReauthorizeRequest(ReauthorizeDappRequest request) {
    request.completeWithReauthorize(
      accounts: [AuthorizedAccount(address: base64PublicKey)],
      authToken: 'renewed-token',
    );
  }

  @override
  void onScenarioReady() {}

  @override
  void onScenarioServingClients() {}

  @override
  void onScenarioServingComplete() {}

  @override
  void onScenarioComplete() {}

  @override
  void onScenarioError(Object? error) {}

  @override
  void onScenarioTeardownComplete() {}

  @override
  void onSignMessagesRequest(SignMessagesRequest request) {}

  @override
  void onSignAndSendTransactionsRequest(SignAndSendTransactionsRequest request) {}

  @override
  void onDeauthorizedEvent(DeauthorizedEvent event) {}
}

final scenario = WalletScenario(
  walletName: 'My Wallet',
  config: const MobileWalletAdapterConfig(
    maxTransactionsPerSigningRequest: 10,
    optionalFeatures: ['solana:signTransactions'],
  ),
  callbacks: MyWalletCallbacks(),
);

await scenario.start();
final dal = MwaDigitalAssetLinksHostApi();

final callingPackage = await dal.getCallingPackage();
final isVerified = await dal.verifyCallingPackage(
  clientIdentityUri: 'https://example.com',
);

This is useful when wallet policy requires Android app-origin verification before honoring sensitive requests.

Native parity and behavior notes

  • Android wallet-side implementation is backed by Solana Mobile walletlib request/scenario APIs.
  • Request lifecycle is explicit:
    • native request -> Dart callback -> completeWith* -> native resolve/cancel
  • Local/remote transport handling enforces inbound encrypted sequence ordering.
  • Remote association supports reflector protocol negotiation (binary and base64).

Maintenance and CI

  • Android native compile safety is enforced in CI by building a temporary Android Flutter app that depends on this plugin.
  • Local equivalent command:
./scripts/check-mobile-wallet-adapter-android-compile.sh

Architecture

  • Dart: protocol/session handling via solana_kit_mobile_wallet_adapter_protocol
  • Android Kotlin: intent launch + walletlib/DAL host bridges
  • iOS Swift: safe no-op plugin for mixed-platform app compatibility

Manual testing app

A runnable Flutter Android example app is available in example/.

cd packages/solana_kit_mobile_wallet_adapter/example
flutter pub get
flutter run

For emulator/device wallet setup (including Solana's mock MWA wallet), follow:

Example

Use example/main.dart as a runnable starting point for solana_kit_mobile_wallet_adapter.

  • Import path: package:solana_kit_mobile_wallet_adapter/solana_kit_mobile_wallet_adapter.dart
  • This section is centrally maintained with mdt to keep package guidance aligned.
  • After updating shared docs templates, run docs:update from the repo root.

Maintenance

  • Validate docs in CI and locally with docs:check.
  • Keep examples focused on one workflow and reference package README sections for deeper API details.