Skip to content

Commit

Permalink
fix: update wallet_app tutorial (#431)
Browse files Browse the repository at this point in the history
* docs: add step about Android minimun SDK version required by `wallet_kit`

* feat(wallet_kit): add deploy account support

* fix(wallet_kit): refresh accounts ETH balance when wallet is expanded

* wallet_kit: fix analyze issue

* fix(wallet_kit): disable 'Send ETH' button when account is not deployed

* docs: add missing Scaffold in mobile wallet tutorial

* fix(wallet_kit): use `TokenSymbol.name` instead of  hardcoded string for account balances keys

* fix(wallet_kit): disable 'Deploy account' button if ETH account balance is not enough

* feat(wallet_kit): also refresh STRK balance when wallet is expanded

* docs: add info for Android biometric support and devnet minting in mobile-wallet tutorial

* fix(wallet_kit): check if account is deployed when wallet is expanded

* fix(wallet_kit): ensure private key is not null before signer creation

* wallet_kit: apply some code rabbit recommendation

* docs: fix typo
  • Loading branch information
ptisserand authored Jan 8, 2025
1 parent e890801 commit ad394cb
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 41 deletions.
83 changes: 66 additions & 17 deletions docs/examples/mobile-wallet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,51 @@ flutter pub add wallet_kit hive_flutter hooks_riverpod flutter_dotenv
4. Create a `.env` file in the root of your wallet_app project

```bash
ACCOUNT_CLASS_HASH="0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c"
ACCOUNT_CLASS_HASH="0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f"
RPC="http://127.0.0.1:5050/rpc"
```

> If you are building for Android, use `RPC="http://10.0.2.2:5050/rpc"` instead.
> Please note that `ACCOUNT_CLASS_HASH` must match the one used by your version of `starknet-devnet`, it's displayed at startup.
Here is the value for `starknet-devnet 0.2.0`
> ```
> Predeployed accounts using class with hash: 0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f
> ```
> If you are running on another device that the host running `starknet-devnet`, you should use the external IP of your host running and start `starknet-devnet` with `--host 0.0.0.0` argument
5. Add `.env` file in your `pubspec.yaml`
```yaml
assets:
- .env
```
6. Update Android minimun SDK version

`secure_store` package used by `wallet_kit` require Android minimum SDK version set to at least 23, you need to modify `android/app/build.gradle`:
```
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.wallet_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
```

7. Biometric support (optional)

In order to use `Biometric` on Android, your `MainActivity` must inherit from `FlutterFragmentActivity` instead of `FlutterActity`.
You need to modify your `MainActivity.kt` with:
```kotlin
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity()
```

## Let's write some code

Let's start with a simple `main` function in your 'main.dart' file.
Expand Down Expand Up @@ -174,21 +207,24 @@ class HomeScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Layout2(
children: [
SizedBox(height: 32),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
WalletSelector(),
AccountAddress(),
],
),
SizedBox(height: 32),
WalletBody(),
SendEthButton(),
],
return const Scaffold(
body: Layout2(
children: [
SizedBox(height: 32),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
WalletSelector(),
AccountAddress(),
DeployAccountButton(),
],
),
SizedBox(height: 32),
WalletBody(),
SendEthButton(),
],
),
);
}
}
Expand All @@ -215,3 +251,16 @@ class WalletApp extends HookConsumerWidget {
```

Now you can run your app with `flutter run` and see your wallet in action! 💸

---

Deploying an account requires some ETH to pay transaction fees.
With `starknet-devnet`, you can mint some ETH to your account address with the following command:
```shell
curl --silent -H 'Content-type: application/json' \
-X POST http://localhost:5050/mint \
-d '{"address": ""<YOUR_ACCOUNT_ADDRESS>"", "amount": 20000000000000000000, "unit": "WEI"}'
```
```console
{"new_balance":"20000000000000000000","unit":"WEI","tx_hash":"0x9d2d26cef777c50b64475592e0df6e6c6012014e660f97bb37aaf5138aff54"}
```
39 changes: 38 additions & 1 deletion packages/wallet_kit/lib/services/wallet_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,42 @@ class WalletService {
);
return address;
}

static Future<bool> deployAccount({
required SecureStore secureStore,
required Account account,
}) async {
final privateKey = await secureStore.getSecret(
key: privateKeyKey(account.walletId, account.id));
if (privateKey == null) {
throw Exception("Private key not found");
}

s.Signer? signer = s.Signer(privateKey: s.Felt.fromHexString(privateKey));

final provider = WalletKit().provider;

// call data depends on class hash...
final constructorCalldata = [signer.publicKey];
final tx = await s.Account.deployAccount(
signer: signer,
provider: provider,
constructorCalldata: constructorCalldata,
classHash: WalletKit().accountClassHash,
);
signer = null;
final (contractAddress, txHash) = tx.when(
result: (result) =>
(result.contractAddress, result.transactionHash.toHexString()),
error: (error) => throw Exception('${error.code}: ${error.message}'),
);
bool success = await s.waitForAcceptance(
transactionHash: txHash,
provider: provider,
);

return success;
}
}

seedPhraseKey(String walletId) {
Expand All @@ -168,12 +204,13 @@ Future<String> sendEth({
s.Signer? signer = s.Signer(privateKey: privateKey);

final provider = WalletKit().provider;
final chainId = WalletKit().chainId;

final fundingAccount = s.Account(
provider: provider,
signer: signer,
accountAddress: s.Felt.fromHexString(account.address),
chainId: s.StarknetChainId.testNet,
chainId: chainId,
);

final txHash = await fundingAccount.send(
Expand Down
39 changes: 37 additions & 2 deletions packages/wallet_kit/lib/wallet_state/wallet_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Wallets extends _$Wallets with PersistedState<WalletsState> {
@override
String get boxName => 'wallet';

bool _isRefreshing = false;

@override
WalletsState fromJson(Map<String, dynamic> json) =>
WalletsState.fromJson(json);
Expand Down Expand Up @@ -128,6 +130,19 @@ class Wallets extends _$Wallets with PersistedState<WalletsState> {
);
}

deployAccount({
required SecureStore secureStore,
required Account account,
}) async {
final success = await WalletService.deployAccount(
secureStore: secureStore, account: account);
updateSelectedAccountIsDeployed(
walletId: account.walletId,
accountId: account.id,
isDeployed: success,
);
}

updateSelectedAccountIsDeployed({
required String walletId,
required int accountId,
Expand Down Expand Up @@ -171,6 +186,25 @@ class Wallets extends _$Wallets with PersistedState<WalletsState> {
state = state.copyWith(wallets: {}, selected: null);
}

Future<void> refreshAccount(String walletId, int accountId) async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
await refreshEthBalance(walletId, accountId);
await refreshStrkBalance(walletId, accountId);
final isDeployed = await WalletService.isAccountValid(
account: state.wallets[walletId]!.accounts[accountId]!,
);
updateSelectedAccountIsDeployed(
walletId: walletId,
accountId: accountId,
isDeployed: isDeployed,
);
} finally {
_isRefreshing = false;
}
}

refreshEthBalance(String walletId, int accountId) async {
final accountAddress =
state.wallets[walletId]?.accounts[accountId]?.address;
Expand Down Expand Up @@ -198,7 +232,7 @@ class Wallets extends _$Wallets with PersistedState<WalletsState> {
accountId: account.copyWith(
balances: {
...account.balances,
'ETH': double.parse(ethBalance.toStringAsFixed(4)),
TokenSymbol.ETH.name: double.parse(ethBalance.toStringAsFixed(4)),
},
),
},
Expand Down Expand Up @@ -233,7 +267,8 @@ class Wallets extends _$Wallets with PersistedState<WalletsState> {
accountId: account.copyWith(
balances: {
...account.balances,
'STRK': double.parse(strkBalance.toStringAsFixed(4)),
TokenSymbol.STRK.name:
double.parse(strkBalance.toStringAsFixed(4)),
},
),
},
Expand Down
39 changes: 39 additions & 0 deletions packages/wallet_kit/lib/widgets/deploy_account_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../wallet_kit.dart';

class DeployAccountButton extends HookConsumerWidget {
const DeployAccountButton({
super.key,
});

// ignore: constant_identifier_names
static const double MINIMUN_ETH_BALANCE = 0.00001;

@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedAccount = ref.watch(
walletsProvider.select((value) => value.selectedAccount),
);
if (selectedAccount?.isDeployed == false) {
final ethBalance =
selectedAccount!.balances[TokenSymbol.ETH.name] ?? 0.00;
final enoughBalance = ethBalance >= MINIMUN_ETH_BALANCE;
return PrimaryButton(
label: enoughBalance ? 'Deploy account' : 'Not enough ETH',
onPressed: enoughBalance
? () async {
final secureStore = await ref
.read(walletsProvider.notifier)
.getSecureStoreForWallet(context: context);
await ref.read(walletsProvider.notifier).deployAccount(
secureStore: secureStore,
account: selectedAccount,
);
}
: null);
} else {
return const SizedBox.shrink();
}
}
}
1 change: 1 addition & 0 deletions packages/wallet_kit/lib/widgets/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export 'send_eth_button.dart';
export 'account_address.dart';
export 'nft_list.dart';
export 'nft_details.dart';
export 'deploy_account_button.dart';
31 changes: 17 additions & 14 deletions packages/wallet_kit/lib/widgets/send_eth_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ class SendEthButton extends HookConsumerWidget {
final selectedAccount = ref.watch(walletsProvider.select(
(value) => value.selectedAccount,
));
if (selectedAccount == null) {
return const SizedBox.shrink();
}

return PrimaryButton(
label: 'Send',
onPressed: () async {
if (selectedAccount == null) {
throw Exception('Account is required');
}
final password = await showPasswordModal(context);
if (password == null) {
throw Exception('Password is required');
}
await sendEth(
account: selectedAccount,
password: password,
recipientAddress: recipientAddress,
amount: 0.001);
},
onPressed: selectedAccount.isDeployed
? () async {
final password = await showPasswordModal(context);
if (password == null) {
throw Exception('Password is required');
}
await sendEth(
account: selectedAccount,
password: password,
recipientAddress: recipientAddress,
amount: 0.001);
}
: null,
);
}
}
15 changes: 10 additions & 5 deletions packages/wallet_kit/lib/widgets/token_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ class TokenList extends HookConsumerWidget {

useEffect(() {
if (selectedAccount != null) {
ref.read(walletsProvider.notifier).refreshEthBalance(
selectedAccount.walletId,
selectedAccount.id,
);
ref.read(walletsProvider.notifier)
..refreshEthBalance(
selectedAccount.walletId,
selectedAccount.id,
)
..refreshStrkBalance(
selectedAccount.walletId,
selectedAccount.id,
);
}
return;
}, [selectedAccount?.id]);
Expand All @@ -44,7 +49,7 @@ class TokenListItem extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final balance = ref.watch(walletsProvider.select(
(value) => value.selectedAccount?.balances[symbol] ?? 0.00,
(value) => value.selectedAccount?.balances[symbol.name] ?? 0.00,
));

return Padding(
Expand Down
Loading

0 comments on commit ad394cb

Please sign in to comment.