Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update wallet_app tutorial #431

Merged
merged 14 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Comment on lines +207 to +213
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove hardcoded private key.

Good change to use dynamic chainId from WalletKit. However, there's a hardcoded private key (0x1) which should be removed from production code.

The commented-out code suggests using PasswordStore - consider implementing this instead of the hardcoded value.

);

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
Loading