diff --git a/flutter_wallet_example/README.md b/flutter_wallet_example/README.md new file mode 100644 index 00000000..9327b19d --- /dev/null +++ b/flutter_wallet_example/README.md @@ -0,0 +1,26 @@ +# Flutter Wallet Example + +This example shows how to build a simple Flutter wallet application using the Rust SDK. + +The Rust side exposes a small FFI layer compiled as a dynamic library using `cdylib`. The +Flutter application calls into this library using `dart:ffi`. + +The default Ark server is **https://mutinynet.arkade.sh**. The example connects to this +server when initializing the client. + +## Structure + +- `lib/main.dart` – minimal Flutter UI that loads the dynamic library and displays an + off‑chain address from the wallet. +- `native` – Rust crate compiled as a dynamic library. It wraps `ark-client` and provides + FFI bindings. + +## Building + +``` +flutter run +``` + +Ensure you have Rust and Flutter installed. The Rust crate builds as part of the Flutter +project via `cargo`. + diff --git a/flutter_wallet_example/lib/main.dart b/flutter_wallet_example/lib/main.dart new file mode 100644 index 00000000..3ca660b6 --- /dev/null +++ b/flutter_wallet_example/lib/main.dart @@ -0,0 +1,49 @@ +import 'dart:ffi'; +import 'package:flutter/material.dart'; + +// Load the native library. In a real application the path would depend on the +// platform (Android/iOS/macOS). For simplicity this uses `DynamicLibrary.process`. +final DynamicLibrary native = DynamicLibrary.process(); + +typedef _InitClientNative = Void Function(); + +typedef _GetOffchainAddressNative = Pointer Function(); + +final _initClient = native.lookupFunction<_InitClientNative, void Function()>('init_client'); +final _getOffchainAddress = native + .lookupFunction<_GetOffchainAddressNative, Pointer Function()>('get_offchain_address'); + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String address = ''; + + @override + void initState() { + super.initState(); + _initClient(); + final ptr = _getOffchainAddress(); + address = ptr.cast().toDartString(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Ark Flutter Example')), + body: Center( + child: Text(address.isEmpty ? 'Loading...' : address), + ), + ), + ); + } +} diff --git a/flutter_wallet_example/native/Cargo.toml b/flutter_wallet_example/native/Cargo.toml new file mode 100644 index 00000000..d6e90164 --- /dev/null +++ b/flutter_wallet_example/native/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ark_flutter_example" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ark-client = { path = "../../ark-client" } +ark-bdk-wallet = { path = "../../ark-bdk-wallet" } +ark-core = { path = "../../ark-core" } +bitcoin = "0.30" +async-trait = "0.1" +tokio = { version = "1.41", features = ["rt", "macros"] } diff --git a/flutter_wallet_example/native/src/lib.rs b/flutter_wallet_example/native/src/lib.rs new file mode 100644 index 00000000..0af632b8 --- /dev/null +++ b/flutter_wallet_example/native/src/lib.rs @@ -0,0 +1,150 @@ +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::sync::Arc; + +use ark_client::{Client, OfflineClient}; +use ark_core::ArkAddress; +use bitcoin::key::Keypair; +use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use bitcoin::Address; + +struct SimpleBlockchain; + +#[async_trait::async_trait] +impl ark_client::Blockchain for SimpleBlockchain { + async fn find_outpoints( + &self, + _address: &Address, + ) -> Result, ark_client::Error> { + Ok(vec![]) + } + + async fn find_tx( + &self, + _txid: &bitcoin::Txid, + ) -> Result, ark_client::Error> { + Ok(None) + } + + async fn get_output_status( + &self, + _txid: &bitcoin::Txid, + _vout: u32, + ) -> Result { + Ok(ark_client::SpendStatus { spend_txid: None }) + } + + async fn broadcast(&self, _tx: &bitcoin::Transaction) -> Result<(), ark_client::Error> { + Ok(()) + } + + async fn get_fee_rate(&self) -> Result { + Ok(1.0) + } + + async fn broadcast_package( + &self, + _txs: &[&bitcoin::Transaction], + ) -> Result<(), ark_client::Error> { + Ok(()) + } +} + +struct DummyWallet; + +impl ark_client::wallet::OnchainWallet for DummyWallet { + fn get_onchain_address(&self) -> Result { + Ok("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp3whfpx" + .parse() + .unwrap()) + } + + async fn sync(&self) -> Result<(), ark_client::Error> { + Ok(()) + } + + fn balance(&self) -> Result { + Ok(ark_client::wallet::Balance::default()) + } + + fn prepare_send_to_address( + &self, + _address: Address, + _amount: bitcoin::Amount, + _fee_rate: bitcoin::FeeRate, + ) -> Result { + Err(ark_client::Error::ad_hoc("not implemented")) + } + + fn sign( + &self, + _psbt: &mut bitcoin::psbt::PartiallySignedTransaction, + ) -> Result { + Ok(false) + } + + fn select_coins( + &self, + _target_amount: bitcoin::Amount, + ) -> Result { + Err(ark_client::Error::ad_hoc("not implemented")) + } +} + +impl ark_client::wallet::BoardingWallet for DummyWallet { + fn new_boarding_output( + &self, + _server_pk: bitcoin::XOnlyPublicKey, + _exit_delay: bitcoin::Sequence, + _network: bitcoin::Network, + ) -> Result { + Err(ark_client::Error::ad_hoc("not implemented")) + } + + fn get_boarding_outputs(&self) -> Result, ark_client::Error> { + Ok(vec![]) + } + + fn sign_for_pk( + &self, + _pk: &bitcoin::XOnlyPublicKey, + _msg: &bitcoin::secp256k1::Message, + ) -> Result { + Err(ark_client::Error::ad_hoc("not implemented")) + } +} + +static mut CLIENT: Option> = None; + +#[no_mangle] +pub extern "C" fn init_client() { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[1u8; 32]).unwrap(); + let keypair = Keypair::from_secret_key(&secp, &secret_key); + + let blockchain = Arc::new(SimpleBlockchain); + let wallet = Arc::new(DummyWallet); + + let offline = OfflineClient::new( + "flutter".to_string(), + keypair, + blockchain, + wallet, + "https://mutinynet.arkade.sh".to_string(), + ); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let client = rt.block_on(async move { offline.connect().await.unwrap() }); + + unsafe { + CLIENT = Some(client); + } +} + +#[no_mangle] +pub extern "C" fn get_offchain_address() -> *const c_char { + let client = unsafe { CLIENT.as_ref().unwrap() }; + let (addr, _) = client.get_offchain_address().unwrap(); + let s = addr.to_string(); + CString::new(s).unwrap().into_raw() +} diff --git a/flutter_wallet_example/pubspec.yaml b/flutter_wallet_example/pubspec.yaml new file mode 100644 index 00000000..a28b7040 --- /dev/null +++ b/flutter_wallet_example/pubspec.yaml @@ -0,0 +1,14 @@ +name: flutter_wallet_example +description: A simple Flutter wallet example using the Ark Rust SDK +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +flutter: + uses-material-design: true