mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
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:
parent
1c29be7993
commit
1b5be705f6
19 changed files with 716 additions and 561 deletions
|
@ -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)',
|
||||
|
|
|
@ -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
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue