Solana Wallet New Implementation (#2011)

* Feat: Implement Solana wallet using on_chain

* v4.23.0 release candidate (#1974)

* v4.23.0 release candidate

* - Fix restoring zano from QR
- Fix Zano confirmations count
- Fix birdpay
- Fix balance display

* Fix Zano assets showing amount before they are added

* - handle fetching token data while the API is busy
- potential fix for duplicate transactions

* fix receive confirmations, maybe

* revert onChangeWallet cleanup

* Fix confirmations not updating

* improve zano wallet opening, fix CI commands and messages on slack (#1979)

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Cache wallet when creating/restoring as well

* - hardcode Trocador Maximum limit for Zano temporarily
- Configure Cake Zano node to use SSL

* reformatting [skip ci]

* revert to non-ssl

* update build numbers [skip ci]

* disable zano for desktop [skip ci]

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>

* CW-711 passphrase for XMR/WOWcreation (#1992)

* add monero passphrase
add wownero passphrase
add passphrase to seed screen

* obscure passphrase by default
disable passphrase create for zano

* Update lib/view_model/wallet_keys_view_model.dart [skip ci]

* Update lib/src/screens/wallet_keys/wallet_keys_page.dart [skip ci]

* Update lib/view_model/advanced_privacy_settings_view_model.dart

* dynamic passphrase icon

* fix polyseed not being encrypted by passphrase

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* show Zano keys properly in the keys tab (#2004)

* fix: Switch private key hex encoding

* Modified existing implementation to use older version of packages

* fix: Fetch direct transaction history amounts instead of decimals, and add Create Account Instructions to Transaction History List

* fix: Remove Create Account entries in Transaction History and disable activating token accounts of selected tokens

* feat: Add passphrase support to Solana

* fix: Issues with transaction amount and dissappearing transaction history items (very annoying bug)

* fix: Issue with flipping transactions and incorrect transaction status

* PR Review fixes

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
David Adegoke 2025-03-14 15:42:17 +01:00 committed by GitHub
parent 1c29be7993
commit 1b5be705f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 716 additions and 561 deletions

View file

@ -26,7 +26,7 @@ class DefaultSPLTokens {
decimal: 5,
mint: 'Bonk',
iconPath: 'assets/images/bonk_icon.png',
enabled: true,
enabled: false,
),
SPLToken(
name: 'Raydium',
@ -35,7 +35,7 @@ class DefaultSPLTokens {
decimal: 6,
mint: 'ray',
iconPath: 'assets/images/ray_icon.png',
enabled: true,
enabled: false,
),
SPLToken(
name: 'Wrapped Ethereum (Sollet)',

View file

@ -1,9 +1,8 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:solana/encoder.dart';
class PendingSolanaTransaction with PendingTransaction {
final double amount;
final SignedTx signedTransaction;
final String serializedTransaction;
final String destinationAddress;
final Function sendTransaction;
final double fee;
@ -11,7 +10,7 @@ class PendingSolanaTransaction with PendingTransaction {
PendingSolanaTransaction({
required this.fee,
required this.amount,
required this.signedTransaction,
required this.serializedTransaction,
required this.destinationAddress,
required this.sendTransaction,
});
@ -36,7 +35,7 @@ class PendingSolanaTransaction with PendingTransaction {
String get feeFormatted => fee.toString();
@override
String get hex => signedTransaction.encode();
String get hex => serializedTransaction;
@override
String get id => '';

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,9 @@ class SolanaTransactionInfo extends TransactionInfo {
@override
String amountFormatted() {
String stringBalance = solAmount.toString();
if (stringBalance.toString().length >= 12) {
stringBalance = stringBalance.substring(0, 12);
}
return '$stringBalance $tokenSymbol';
}

View file

@ -30,9 +30,9 @@ import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/base58.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:on_chain/solana/solana.dart' hide Store;
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
part 'solana_wallet.g.dart';
@ -77,14 +77,6 @@ abstract class SolanaWalletBase
final String? _hexPrivateKey;
final EncryptionFileUtils encryptionFileUtils;
// The Solana WalletPair
Ed25519HDKeyPair? _walletKeyPair;
Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
// To access the privateKey bytes.
Ed25519HDKeyPairData? _keyPairData;
late final SolanaWalletClient _client;
@observable
@ -108,29 +100,23 @@ abstract class SolanaWalletBase
final Completer<SharedPreferences> _sharedPrefs = Completer();
@override
Ed25519HDKeyPairData get keys {
if (_keyPairData == null) {
return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
}
Object get keys => throw UnimplementedError("keys");
return _keyPairData!;
}
late final SolanaPrivateKey _solanaPrivateKey;
late final SolanaPublicKey _solanaPublicKey;
SolanaPublicKey get solanaPublicKey => _solanaPublicKey;
SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey;
String get solanaAddress => _solanaPublicKey.toAddress().address;
@override
String? get seed => _mnemonic;
@override
String get privateKey {
final privateKeyBytes = _keyPairData!.bytes;
final publicKeyBytes = _keyPairData!.publicKey.bytes;
final encodedBytes = privateKeyBytes + publicKeyBytes;
final privateKey = base58encode(encodedBytes);
return privateKey;
}
String get privateKey => _solanaPrivateKey.seedHex();
@override
WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey);
@ -140,35 +126,47 @@ abstract class SolanaWalletBase
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
// Create WalletPair using either the mnemonic or the privateKey
_walletKeyPair = await getWalletPair(
// Create the privatekey using either the mnemonic or the privateKey
_solanaPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
passphrase: passphrase,
);
// Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
_keyPairData = await _walletKeyPair!.extract();
// Extract the public key and wallet address
_solanaPublicKey = _solanaPrivateKey.publicKey();
walletInfo.address = _walletKeyPair!.address;
walletInfo.address = _solanaPublicKey.toAddress().address;
await walletAddresses.init();
await transactionHistory.init();
await save();
}
Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
Future<SolanaPrivateKey> getPrivateKey({
String? mnemonic,
String? privateKey,
String? passphrase,
}) async {
assert(mnemonic != null || privateKey != null);
if (mnemonic != null) {
return Wallet.fromMnemonic(mnemonic, account: 0, change: 0);
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
// Derive a Solana private key from the seed
final bip44 = Bip44.fromSeed(seed, Bip44Coins.solana);
final childKey = bip44.deriveDefaultPath.change(Bip44Changes.chainExt);
return SolanaPrivateKey.fromSeed(childKey.privateKey.raw);
}
try {
final privateKeyBytes = base58decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList());
final keypairBytes = Base58Decoder.decode(privateKey!);
return SolanaPrivateKey.fromSeed(keypairBytes);
} catch (_) {
final privateKeyBytes = HEX.decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
return SolanaPrivateKey.fromBytes(privateKeyBytes);
}
}
@ -206,7 +204,8 @@ abstract class SolanaWalletBase
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed);
printV(estimatedFee.toString());
} catch (e) {
estimatedFee = 0.0;
}
@ -274,7 +273,7 @@ abstract class SolanaWalletBase
tokenMint: tokenMint,
tokenTitle: transactionCurrency.title,
inputAmount: totalAmount,
ownerKeypair: _walletKeyPair!,
ownerPrivateKey: _solanaPrivateKey,
tokenDecimals: transactionCurrency.decimals,
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
@ -291,9 +290,7 @@ abstract class SolanaWalletBase
/// Fetches the native SOL transactions linked to the wallet Public Key
Future<void> _updateNativeSOLTransactions() async {
final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
final transactions = await _client.fetchTransactions(address);
final transactions = await _client.fetchTransactions(_solanaPublicKey.toAddress());
await _addTransactionsToTransactionHistory(transactions);
}
@ -308,10 +305,10 @@ abstract class SolanaWalletBase
for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
token.symbol,
token.decimal,
_walletKeyPair!,
mintAddress: token.mintAddress,
splTokenSymbol: token.symbol,
splTokenDecimal: token.decimal,
privateKey: _solanaPrivateKey,
);
// splTokenTransactions.addAll(tokenTxs);
@ -387,6 +384,7 @@ abstract class SolanaWalletBase
'mnemonic': _mnemonic,
'private_key': _hexPrivateKey,
'balance': balance[currency]!.toJSON(),
'passphrase': passphrase,
});
static Future<SolanaWallet> open({
@ -414,8 +412,9 @@ abstract class SolanaWalletBase
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final passphrase = data['passphrase'] as String?;
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey);
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
@ -428,6 +427,7 @@ abstract class SolanaWalletBase
return SolanaWallet(
walletInfo: walletInfo,
password: password,
passphrase: keysData.passphrase,
mnemonic: keysData.mnemonic,
privateKey: keysData.privateKey,
initialBalance: balance,
@ -442,7 +442,7 @@ abstract class SolanaWalletBase
}
Future<SolanaBalance> _fetchSOLBalance() async {
final balance = await _client.getBalance(_walletKeyPair!.address);
final balance = await _client.getBalance(solanaAddress);
return SolanaBalance(balance);
}
@ -451,10 +451,9 @@ abstract class SolanaWalletBase
for (var token in splTokensBox.values) {
if (token.enabled) {
try {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} catch (e) {
printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
@ -482,10 +481,9 @@ abstract class SolanaWalletBase
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} else {
@ -507,37 +505,10 @@ abstract class SolanaWalletBase
}
Future<SPLToken?> getSPLToken(String mintAddress) async {
// Convert SPL token mint address to public key
final Ed25519HDPublicKey mintPublicKey;
try {
mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
} catch (_) {
return null;
}
// Fetch token's metadata account
try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
return null;
}
String? iconPath;
try {
iconPath = await _client.getIconImageFromTokenUri(token.uri);
} catch (_) {}
String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), '');
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: filteredTokenSymbol,
mintAddress: mintAddress,
iconPath: iconPath,
);
} catch (e) {
return await _client.fetchSPLTokenInfo(mintAddress);
} catch (e, s) {
printV('Error fetching token: ${e.toString()}, ${s.toString()}');
return null;
}
}
@ -582,7 +553,7 @@ abstract class SolanaWalletBase
final messageBytes = utf8.encode(message);
// Sign the message bytes with the wallet's private key
final signature = (await _walletKeyPair!.sign(messageBytes)).toString();
final signature = (_solanaPrivateKey.sign(messageBytes)).toString();
return HEX.encode(utf8.encode(signature)).toUpperCase();
}
@ -596,7 +567,7 @@ abstract class SolanaWalletBase
final base58EncodedPublicKeyString = match.group(2)!;
final sigBytes = bytesString.split(', ').map(int.parse).toList();
List<int> pubKeyBytes = base58decode(base58EncodedPublicKeyString);
List<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
return [sigBytes, pubKeyBytes];
} else {
@ -619,19 +590,18 @@ abstract class SolanaWalletBase
}
// make sure the address derived from the public key provided matches the one we expect
final pub = Ed25519HDPublicKey(pubKeyBytes);
if (address != pub.toBase58()) {
final pub = SolanaPublicKey.fromBytes(pubKeyBytes);
if (address != pub.toAddress().address) {
return false;
}
return await verifySignature(
return pub.verify(
message: messageBytes,
signature: sigBytes,
publicKey: Ed25519HDPublicKey(pubKeyBytes),
);
}
SolanaClient? get solanaClient => _client.getSolanaClient;
SolanaRPC? get solanaProvider => _client.getSolanaProvider;
@override
String get password => _password;

View file

@ -33,6 +33,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
walletInfo: credentials.walletInfo!,
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -118,6 +119,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);

View file

@ -1,7 +1,6 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
import 'package:solana/metaplex.dart';
part 'spl_token.g.dart';
@ -55,7 +54,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
required String mint,
required String symbol,
required String mintAddress,
String? iconPath
String? iconPath,
}) {
return SPLToken(
name: name,
@ -117,31 +116,3 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
@override
int get hashCode => mintAddress.hashCode;
}
class NFT extends SPLToken {
final ImageInfo? imageInfo;
NFT(
String mint,
String name,
String symbol,
String mintAddress,
int decimal,
String iconPath,
this.imageInfo,
) : super(
name: name,
symbol: symbol,
mintAddress: mintAddress,
decimal: decimal,
mint: mint,
iconPath: iconPath,
);
}
class ImageInfo {
final String uri;
final OffChainMetadata? data;
const ImageInfo(this.uri, this.data);
}

View file

@ -11,7 +11,6 @@ environment:
dependencies:
flutter:
sdk: flutter
solana: ^0.31.0+1
cw_core:
path: ../cw_core
http: ^1.1.0
@ -21,6 +20,14 @@ dependencies:
shared_preferences: ^2.0.15
bip32: ^2.0.0
hex: ^0.2.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
dev_dependencies:
flutter_test: